/* =====================================================================
   HAULAT — app design system  (assets/css/app.css)

   A dashboard in the modern-fintech idiom: a quiet near-black field with
   one soft corner glow, content held in large rounded elevated cards,
   and dense readable list rows carrying the actual work. Chrome recedes;
   the data is the interface.

   Haulat's departure from that reference is the hue. Where those apps
   glow blue, this glows oxblood — the same #6E0B12 → #D71921 the landing
   page is built from — so signing in feels like walking further into the
   same building rather than into a different one.

   Dark is the default and gets the design attention. Light is a first-
   class alternative, not an afterthought: every token below is declared
   twice and nothing is hard-coded past this block.

   Load order:  index.css  →  app.css
   ===================================================================== */

/* =====================================================================
   1 · RESET
   ===================================================================== */
*, *::before, *::after { box-sizing: border-box; }

/* THE hidden ATTRIBUTE ACTUALLY HIDES.

   `[hidden]` is display:none in the UA stylesheet, at the lowest
   possible specificity, so ANY display declaration on the element beats
   it. `.pic-busy { display: grid }` with `hidden` in the markup left two
   upload spinners on screen from first paint, on a page where nothing
   had been chosen.

   This is not specific to that component. Nothing in this stylesheet
   declared `[hidden]` at all, which means every current and future
   `display:` rule on an element that PHP or JS toggles with `hidden`
   was the same bug waiting. One line, at the top, with !important
   because the whole point is to outrank a later declaration. */
[hidden] { display: none !important; }

html.app {
    -webkit-text-size-adjust: 100%;
    scroll-behavior: smooth;
}
html.app body {
    margin: 0;
    min-height: 100svh;
    font-family: var(--f-body);
    font-size: 15px;
    line-height: 1.5;
    color: var(--txt);
    background: var(--bg);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    overflow-x: hidden;
}
:where(html.app a) { color: inherit; text-decoration: none; }
:where(html.app button, html.app input, html.app select, html.app textarea) {
    font: inherit; color: inherit;
}
/* Every reset below is wrapped in :where() so it carries ZERO specificity.
   Without it `html.app button { background: none }` (0,1,1) out-ranks
   `.capture-ring` and `.pill` (0,1,0) and silently strips the fill off
   every button in the app — which is exactly what it did the first time. */
:where(html.app button) { background: none; border: 0; padding: 0; cursor: pointer; }
:where(html.app img, html.app svg) { display: block; max-width: 100%; }
:where(html.app ul, html.app ol) { margin: 0; padding: 0; list-style: none; }
:where(html.app h1, html.app h2, html.app h3, html.app h4) { margin: 0; font-weight: 800; line-height: 1.15; }
:where(html.app h5, html.app h6) { margin: 0; font-weight: 800; line-height: 1.2; }

/* ---- EVERY HEADING IS AKIRA ----------------------------------------
   index.css already styles bare h1–h6, but the app is a different
   document tree with its own components, and relying on an element
   selector from the landing stylesheet to reach into it made the rule
   invisible — nothing said it here, so any component that set its own
   font-family quietly won and nobody noticed which ones had.

   Stated once, in one place, at real specificity. Components that need
   the body face (.section-title, the small uppercase eyebrows) override
   deliberately below and now have to say so.

   NOT wrapped in :where(): this is a decision, not a reset.

   The TYPEFACE only — no text-transform. index.css uppercases landing
   headings, and copying that here would have uppercased the things the
   app puts in an <h1> that the landing page never does: somebody's
   display name on their own link-in-bio page, the "Welcome, Mei" on
   onboarding, a store name. Shouting a person's name back at them is
   not a brand decision, it is a bug. Components that want uppercase
   ask for it. */
html.app :is(h1, h2, h3, h4, h5, h6) {
    font-family: var(--f-display);
    letter-spacing: -.01em;
}
/* Akira carries no CJK glyphs — see the note in index.css. Latin in
   Akira and Han in a substituted face is two typefaces in one line. */
html[lang="zh-Hant"].app :is(h1, h2, h3, h4, h5, h6),
html[lang="ja"].app :is(h1, h2, h3, h4, h5, h6) {
    font-family: var(--f-body);
    font-weight: 800;
    letter-spacing: 0;
    text-transform: none;
}

html.app ::-webkit-scrollbar { width: 10px; height: 10px; }
html.app ::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 99px; }
html.app ::-webkit-scrollbar-track { background: transparent; }

/* =====================================================================
   2 · TOKENS
   ===================================================================== */
html.app {
    /* brand — lifted verbatim from index.css */
    --h-red:   #D71921;
    --h-deep:  #6E0B12;
    --h-mid:   #B01620;
    --h-pink:  #FFD9DC;

    --f-display: 'Akira', 'Inter', sans-serif;
    --f-body:    'Inter', 'PingFang TC', 'Hiragino Sans', 'Microsoft JhengHei', -apple-system, sans-serif;
    --f-mono:    'JetBrains Mono', ui-monospace, monospace;

    /* geometry — the large radii are what make it read as this family */
    --r-card:  26px;
    --r-panel: 20px;
    --r-input: 14px;
    --r-pill:  999px;
    --r-sm:    12px;

    --rail-w:      248px;
    --rail-w-icons: 92px;
    --topbar-h:    76px;
    --tabbar-h:    76px;
    --gutter:      24px;
    --safe-b: env(safe-area-inset-bottom, 0px);

    --ease: cubic-bezier(.22, 1, .36, 1);
    --t1: .14s; --t2: .26s; --t3: .42s;

    /* glow, resolved from data-bg below */
    --glow: #6E0B12;
}

/* ---- DARK (default) ---- */
html.app,
html.app[data-resolved="dark"] {
    --bg:        #0A0507;
    --bg-2:      #060304;
    /* The card must read as lifted off the field. At #150C0F against a
       #0A0507 ground the delta was too small to see through the glow —
       the whole layout flattened into one dark rectangle. */
    --surface:   #1B1114;      /* the content card */
    --surface-2: #261A1E;      /* dropdowns, sheets, inputs */
    --surface-3: #332327;      /* hover on surface-2 */
    --raise:     rgba(255,255,255,.045);   /* row hover */
    --raise-2:   rgba(255,255,255,.085);   /* active nav */

    --txt:       #FFFFFF;
    --txt-2:     #B3A2A6;      /* secondary — warm grey, not blue-grey */
    --txt-3:     #7C6B6F;      /* tertiary / disabled */

    --line:      rgba(255,255,255,.07);
    --line-2:    rgba(255,255,255,.14);

    --accent:    #E8323A;
    --accent-txt:#FFFFFF;
    --on-accent: #FFFFFF;

    --pos:       #35C77E;
    --warn:      #E0A63A;

    --shadow-1: 0 2px 8px rgba(0,0,0,.35);
    --shadow-2: 0 18px 48px rgba(0,0,0,.55), 0 2px 8px rgba(0,0,0,.4);
    --glow-a: .70;
    --glow-b: .34;

    /* ---- GLASS — the landing page's button material, tokenised ------
       index.css builds every CTA out of exactly three things: a low-alpha
       white fill, a 1px white hairline, and a backdrop blur. Those three
       numbers are the entire brand signature, so they live here as tokens
       and every button in the app is assembled from them rather than from
       a solid surface colour. Changing the feel of the whole app is now a
       four-line edit. */
    --glass:       rgba(255,255,255,.10);
    --glass-hover: rgba(255,255,255,.20);
    --glass-line:  rgba(255,255,255,.30);
    --glass-line-2:rgba(255,255,255,.50);
    --glass-blur:  blur(20px) saturate(1.25);
    --glass-inset: inset 0 1px 0 rgba(255,255,255,.16);
    /* the hairline that separates rail from content — fades at both ends
       so it reads as an outline on a pane of glass, not a table divider */
    --edge: rgba(255,255,255,.16);
}

/* ---- LIGHT ---- */
html.app[data-resolved="light"] {
    --bg:        #F4EAEC;
    --bg-2:      #FBF6F7;
    --surface:   #FFFFFF;
    --surface-2: #FFFFFF;
    --surface-3: #F3EBED;
    --raise:     rgba(20,5,7,.035);
    --raise-2:   rgba(20,5,7,.07);

    --txt:       #16070A;
    --txt-2:     #6F5C60;
    --txt-3:     #9C8A8E;

    --line:      rgba(20,5,7,.08);
    --line-2:    rgba(20,5,7,.14);

    --accent:    #C4141C;
    --accent-txt:#FFFFFF;
    --on-accent: #FFFFFF;

    --pos:       #128A55;
    --warn:      #A9700F;

    --shadow-1: 0 1px 3px rgba(20,5,7,.06);
    --shadow-2: 0 16px 44px rgba(20,5,7,.13), 0 2px 6px rgba(20,5,7,.06);
    --glow-a: .55;
    --glow-b: .35;

    /* Glass in light mode has to be smoked, not frosted. A white fill with
       a white hairline on a #F4EAEC field is literally invisible — the
       material has to invert to hold the same shape and the same weight. */
    --glass:       rgba(20,5,7,.045);
    --glass-hover: rgba(20,5,7,.085);
    --glass-line:  rgba(20,5,7,.16);
    --glass-line-2:rgba(20,5,7,.26);
    --glass-blur:  blur(20px) saturate(1.15);
    --glass-inset: inset 0 1px 0 rgba(255,255,255,.55);
    --edge: rgba(20,5,7,.12);
}

/* ---- background hue variants ---- */
html.app[data-bg="oxblood"] { --glow: #6E0B12; }
html.app[data-bg="ember"]   { --glow: #7A2B08; }
html.app[data-bg="forest"]  { --glow: #0B3A24; }
html.app[data-bg="teal"]    { --glow: #06333C; }
html.app[data-bg="violet"]  { --glow: #3A0B45; }
html.app[data-bg="ink"]     { --glow: #141414; }

html.app[data-resolved="light"][data-bg="oxblood"] { --glow: #F0CDD3; }
html.app[data-resolved="light"][data-bg="ember"]   { --glow: #F6DECD; }
html.app[data-resolved="light"][data-bg="forest"]  { --glow: #D2EADF; }
html.app[data-resolved="light"][data-bg="teal"]    { --glow: #D0E8EC; }
html.app[data-resolved="light"][data-bg="violet"]  { --glow: #E6D6EE; }
html.app[data-resolved="light"][data-bg="ink"]     { --glow: #E4E4E6; }

/* =====================================================================
   3 · THE FIELD
   One soft glow anchored top-left, a fainter one bottom-right to stop
   the far corner going dead. Fixed, non-interactive, and cheap: two
   radial gradients, no animation, nothing for a mid-tier phone to drop
   frames on.
   ===================================================================== */
.field {
    position: fixed; inset: 0; z-index: 0; pointer-events: none;
    background:
        radial-gradient(120% 90% at 0% 0%,
            color-mix(in srgb, var(--glow) calc(var(--glow-a) * 100%), transparent) 0%,
            transparent 62%),
        radial-gradient(90% 80% at 100% 100%,
            color-mix(in srgb, var(--glow) calc(var(--glow-b) * 100%), transparent) 0%,
            transparent 58%),
        var(--bg);
}
/* Fallback for engines without color-mix — plain flat field, still correct. */
@supports not (background: color-mix(in srgb, red 50%, blue)) {
    .field { background: var(--bg); }
}

/* ---- THE DOTTED MATRIX ----------------------------------------------
   The landing page's retained device, now carried across every signed-in
   screen. Same lattice, same 38px pitch, same alphas — it is the one
   texture that says Haulat without a logo in it, and leaving it at the
   door meant the app read as a generic dashboard the moment you signed
   in.

   On ::after rather than a new element, so all three layouts inherit it
   from the one <div class="field"> they already render — no markup
   change, nothing to forget on a fourth layout later.

   Sits ABOVE the glow and below .shell (z-index 1), which is what keeps
   it a texture on the field rather than a screen over the content. */
.field::after {
    content: ''; position: absolute; inset: 0; pointer-events: none;
    background-image: radial-gradient(rgba(255, 255, 255, .12) 1.4px, transparent 1.4px);
    background-size: 38px 38px;
}
html.app[data-resolved="light"] .field::after {
    background-image: radial-gradient(rgba(30, 4, 8, .10) 1.4px, transparent 1.4px);
}

/* =====================================================================
   4 · SHELL
   ===================================================================== */
.shell {
    position: relative; z-index: 1;
    min-height: 100svh;
    display: grid;
    grid-template-columns: var(--rail-w) minmax(0, 1fr);
}
html.app[data-nav="icons"] .shell { grid-template-columns: var(--rail-w-icons) minmax(0, 1fr); }

/* ---- left rail ----
   The rail is a pane of glass standing beside the content, not a panel
   painted onto it. Its only edge is a hairline that fades out at the top
   and bottom, so the eye reads "these are two surfaces" without a hard
   divider ever being drawn. The blur is what sells it: the field's glow
   goes soft behind the rail and stays sharp beside it. */
.rail {
    position: sticky; top: 0; align-self: start;
    height: 100svh;
    display: flex; flex-direction: column; gap: 6px;
    padding: 22px 16px 20px;
    overflow-y: auto; overscroll-behavior: contain;

    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    background: linear-gradient(to right,
        color-mix(in srgb, var(--glass) 45%, transparent),
        var(--glass));
}
/* The vertical outline. A pseudo-element rather than border-right because
   a border cannot fade — and a hairline that runs the full 100svh at even
   opacity reads as a table rule, which is the thing we removed. */
.rail::after {
    content: ''; position: absolute; top: 0; right: 0; bottom: 0;
    width: 1px; pointer-events: none;
    background: linear-gradient(to bottom,
        transparent 0%,
        var(--edge) 12%,
        var(--edge) 88%,
        transparent 100%);
}
@supports not (backdrop-filter: blur(1px)) {
    .rail { background: var(--glass); }
}
.rail-brand {
    display: flex; align-items: center; gap: 12px;
    padding: 4px 8px 22px;
    min-height: 40px;
}
.rail-mark {
    flex: 0 0 auto; width: 40px; height: 40px; border-radius: 12px;
    background: var(--txt); color: var(--bg);
    display: grid; place-items: center;
    box-shadow: var(--shadow-1);
}
.rail-mark img { width: 22px; filter: none; }
html.app[data-resolved="dark"] .rail-mark img { filter: none; }
.rail-word {
    font-family: var(--f-display); font-size: 17px; letter-spacing: .04em;
    text-transform: uppercase; color: var(--txt); white-space: nowrap;
}

.rail-item {
    display: flex; align-items: center; gap: 14px;
    padding: 12px 14px; border-radius: 16px;
    color: var(--txt-2); font-weight: 600; font-size: 14.5px;
    white-space: nowrap;
    transition: background var(--t1) ease, color var(--t1) ease;
}
.rail-item svg { flex: 0 0 auto; width: 22px; height: 22px; }
.rail-item:hover { background: var(--raise); color: var(--txt); }
.rail-item.on   { background: var(--raise-2); color: var(--txt); }
.rail-spacer { flex: 1; }

/* icons-only rail: the label becomes a caption under the glyph, which is
   how the reference does it — a tooltip would need a hover the touch
   user never performs. */
html.app[data-nav="icons"] .rail { align-items: center; padding-inline: 10px; }
html.app[data-nav="icons"] .rail-brand { padding: 4px 0 22px; }
html.app[data-nav="icons"] .rail-word { display: none; }
html.app[data-nav="icons"] .rail > .rail-item {
    flex-direction: column; gap: 5px; width: 68px; padding: 11px 4px;
    font-size: 10.5px; font-weight: 600; letter-spacing: .01em;
}

/* ---- top bar ----
   No fill and no shadow. The old `linear-gradient(to bottom, var(--bg))`
   painted an opaque black band across the top of the content column that
   stopped dead at the rail — which is precisely the seam it was supposed
   to hide. The bar now carries nothing but a blur: content passing under
   it on scroll goes soft instead of disappearing behind a slab, and the
   rail and the content column read as one continuous surface. */
.topbar {
    position: sticky; top: 0; z-index: 30;
    display: flex; align-items: center; gap: 16px;
    min-height: var(--topbar-h);
    padding: 14px var(--gutter) 10px;
    background: transparent;
}
/* The blur lives on a pseudo-element, NOT on .topbar itself. A mask applied
   to the bar would fade the bottom of its own title text along with the
   blur; on a ::before it only ever touches the backdrop. */
.topbar::before {
    content: ''; position: absolute; inset: 0; z-index: -1; pointer-events: none;
    backdrop-filter: blur(22px) saturate(1.2);
    -webkit-backdrop-filter: blur(22px) saturate(1.2);

    /* THE RAMP.

       This was a single stop at 78%, which is not a fade — it is a cut
       with 22% of softening on it. Scrolling a list under it, you could
       see the exact horizontal line where blurred became sharp, and the
       line did not move, so it read as an edge on the glass rather than
       as the glass ending.

       Five stops on an ease-out curve instead: full strength through the
       top half where the title sits, then falling away with the steepest
       drop in the middle so nothing lands on a constant slope. There is
       no boundary left to find.

       A gradient MASK, not opacity on the bar — masking .topbar itself
       would fade the bottom of its own title along with the backdrop. */
    --topbar-fade: linear-gradient(to bottom,
        #000 0%, #000 46%,
        rgba(0,0,0,.86) 62%,
        rgba(0,0,0,.55) 76%,
        rgba(0,0,0,.22) 89%,
        transparent 100%);
    mask-image: var(--topbar-fade);
    -webkit-mask-image: var(--topbar-fade);
}
/* Where backdrop-filter is unavailable the bar would let text collide with
   text on scroll, so — and only there — fall back to the flat field. */
@supports not (backdrop-filter: blur(1px)) {
    .topbar { background: var(--bg); }
}
.topbar-title {
    flex: 1; min-width: 0;
    font-family: var(--f-display); font-size: clamp(22px, 2.4vw, 30px);
    letter-spacing: -.01em; text-transform: uppercase; color: var(--txt);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The wordmark is a phone-only substitute for the page title — see the
   comment in views/layout/app.php. Hidden here, shown in the ≤767px
   block, so desktop and tablet are untouched. */
.topbar-mark { display: none; }
.topbar-mark img {
    height: 18px; width: auto;
    /* haulat-logo.svg is authored black. Dark mode inverts it to white;
       light mode leaves it alone. A second SVG file would drift from the
       first the day somebody redraws the mark. */
    filter: brightness(0) invert(1);
}
html.app[data-resolved="light"] .topbar-mark img { filter: none; }
html[lang="zh-Hant"].app .topbar-title,
html[lang="ja"].app .topbar-title {
    font-family: var(--f-body); font-weight: 800; letter-spacing: 0; text-transform: none;
}
.topbar-actions { display: flex; align-items: center; gap: 10px; }

/* Fill, border and blur come from the glass block in section 7 — an
   icon button is a round glass button and nothing else. Only geometry
   is set here. */
.icon-btn {
    position: relative;
    width: 40px; height: 40px; border-radius: 50%;
    display: grid; place-items: center;
    transition: background var(--t1) ease, border-color var(--t1) ease, color var(--t1) ease;
}
.icon-btn svg { width: 20px; height: 20px; }
/* open/active flips to the solid, same pairing as .pill.on */
.icon-btn.on { background: var(--txt); border-color: var(--txt); color: var(--bg); box-shadow: none; }
.icon-btn.on:hover { background: var(--txt); border-color: var(--txt); color: var(--bg); }

.avatar-btn { width: 38px; height: 38px; border-radius: 50%; overflow: hidden; flex: 0 0 auto; }
.avatar-btn img { width: 100%; height: 100%; object-fit: cover; }
.avatar-fallback {
    width: 100%; height: 100%; display: grid; place-items: center;
    background: var(--surface-2); color: var(--txt-2); font-weight: 700; font-size: 14px;
}

/* ---- main ---- */
.main {
    padding: 0 var(--gutter) 56px;
    max-width: 1320px;
}
.main--narrow { max-width: 860px; }

/* =====================================================================
   5 · CARDS
   The single most characteristic element: a large, softly elevated,
   heavily rounded slab that holds a whole screen's worth of rows.
   ===================================================================== */
.card {
    background: var(--surface);
    border-radius: var(--r-card);
    box-shadow: var(--shadow-1);
    overflow: hidden;
}
html.app[data-resolved="light"] .card { box-shadow: var(--shadow-1); }
.card + .card { margin-top: 16px; }
.card-pad { padding: 22px var(--gutter); }

.card-head {
    display: flex; align-items: flex-start; gap: 16px;
    padding: 24px var(--gutter) 18px;
    flex-wrap: wrap;
}
.card-head-main { flex: 1; min-width: 0; }
.card-title {
    font-family: var(--f-display); font-size: clamp(24px, 2.6vw, 32px);
    letter-spacing: -.015em; color: var(--txt); display: flex; align-items: center; gap: 8px;
}
html[lang="zh-Hant"].app .card-title, html[lang="ja"].app .card-title {
    font-family: var(--f-body); font-weight: 800; letter-spacing: 0;
}
.card-sub {
    display: flex; align-items: center; gap: 7px;
    margin-top: 4px; color: var(--txt-2); font-size: 14px;
}
.card-sub img { width: 18px; height: 18px; border-radius: 50%; }

.section-title {
    padding: 24px var(--gutter) 10px;
    font-family: var(--f-body); font-size: 13px; font-weight: 700;
    letter-spacing: .09em; text-transform: uppercase; color: var(--txt-3);
}
html[lang="zh-Hant"].app .section-title, html[lang="ja"].app .section-title {
    letter-spacing: .02em; text-transform: none; font-size: 13.5px;
}


/* =====================================================================
   FIGURE GRID
   A row of two to four labelled numbers inside a card. Used by the
   wallet and by the admin ledger, which is why it lives here rather
   than in either one's page file.
   ===================================================================== */
/* The three secondary numbers. Equal weight between them on purpose:
   held money is not a footnote to available money, it is the other half
   of the same fact. */
/* The 1px-gap technique — coloured container showing through as
   dividers — leaves a visible dark rectangle wherever the auto-fit grid
   ends on a half-empty row, which it does on a phone the moment there is
   an odd number of facts. Outlines instead: they overlap harmlessly, they
   do not affect layout, and an empty cell simply is not there. */
.wallet-split {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
    gap: 0; margin-bottom: 20px;
    background: var(--surface); border-radius: var(--r-panel); overflow: hidden;
}
.wallet-split > div {
    padding: 14px 16px;
    outline: 1px solid var(--line); outline-offset: -1px;
}
.wallet-split-k {
    display: block; margin-bottom: 5px;
    font-size: 11px; letter-spacing: .12em; text-transform: uppercase; color: var(--txt-3);
}
.wallet-split-v {
    display: block; font-family: var(--f-mono); font-size: 17px; font-weight: 700;
    font-variant-numeric: tabular-nums;
}
.wallet-split-v--pos { color: var(--pos); }
@media (max-width: 767px) {
    .wallet-split { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* =====================================================================
   6 · LIST ROWS
   Full-bleed hover, no dividers, 44px leading avatar, right-aligned
   trailing value. This is the workhorse — finds, bounties, matches,
   transactions and settings all render through it.
   ===================================================================== */
.rows { padding: 4px 0 8px; }

.row {
    display: flex; align-items: center; gap: 16px;
    padding: 13px var(--gutter);
    width: 100%; text-align: left;
    transition: background var(--t1) ease;
}
.row:hover { background: var(--raise); }
.row:active { background: var(--raise-2); }

.row-fig {
    flex: 0 0 auto; position: relative;
    width: 44px; height: 44px; border-radius: 50%;
    overflow: hidden; background: var(--surface-2);
    display: grid; place-items: center; color: var(--txt-2);
}
.row-fig img { width: 100%; height: 100%; object-fit: cover; }
.row-fig svg { width: 20px; height: 20px; }
.row-fig--square { border-radius: 12px; width: 46px; height: 58px; }

/* the small state pip a merchant logo carries in the reference */
.row-fig-pip {
    position: absolute; right: -1px; bottom: -1px;
    width: 17px; height: 17px; border-radius: 50%;
    background: var(--surface); display: grid; place-items: center;
    border: 2px solid var(--surface);
}
.row-fig-pip svg { width: 11px; height: 11px; }

.row-body { flex: 1; min-width: 0; }
/* These are spans (an <a class="row"> cannot legally contain a <div>), so
   they need to be told to stack. */
.row-title, .row-meta, .row-amt, .row-sub { display: block; }
/* TWO LINES, NOT ONE.

   These clamped to a single line with an ellipsis, which is right for a
   name and wrong for a sentence. The alignment audit found 27 strings
   cut mid-word across the app, and they were not edge cases: the privacy
   explanations on the profile screen, the support greeting in Chats,
   every notification title long enough to matter, a hunter's bio in
   People, and most bounty titles in the wallet. "Your bounty is live:
   Undercover 2003 scab deni…" is the half without the piece in it.

   A line clamp instead of nowrap keeps the list rhythm, because the row
   still cannot grow without bound, while giving a real sentence the room
   to be read. Nothing here needed to be shorter. It needed a second
   line.

   overflow-wrap: anywhere so a long URL or handle breaks instead of
   forcing the row wider than the card. */
.row-title, .row-meta {
    display: -webkit-box; -webkit-box-orient: vertical;
    overflow: hidden; overflow-wrap: anywhere;
}
.row-title {
    font-size: 15.5px; font-weight: 600; color: var(--txt);
    line-height: 1.35; -webkit-line-clamp: 2;
}
.row-meta {
    margin-top: 2px; font-size: 13.5px; color: var(--txt-2);
    line-height: 1.4; -webkit-line-clamp: 2;
}
/* Where a row IS a single short label and the clamp only costs layout
   work, opt back out. */
.row--tight .row-title, .row--tight .row-meta {
    display: block; white-space: nowrap; text-overflow: ellipsis;
}
/* And where the row is genuinely explaining something, two lines is
   still a cut. A settings switch whose description stops halfway is the
   same bug as a title that does, one screen further in. */
.row--prose .row-title, .row--prose .row-meta {
    display: block; -webkit-line-clamp: none; white-space: normal;
}
.row-tail { flex: 0 0 auto; text-align: right; }
.row-amt { font-size: 15.5px; font-weight: 600; color: var(--txt); white-space: nowrap; }
.row-amt--muted { color: var(--txt-2); font-weight: 500; }
.row-amt--pos   { color: var(--pos); }
.row-amt--void  { color: var(--txt-3); text-decoration: line-through; }
.row-sub { margin-top: 2px; font-size: 13px; color: var(--txt-3); white-space: nowrap; }
.row-chev { flex: 0 0 auto; color: var(--txt-3); }
.row-chev svg { width: 18px; height: 18px; }

.row-more {
    display: block; width: 100%;
    padding: 16px var(--gutter) 20px; text-align: center;
    font-size: 14.5px; font-weight: 600; color: var(--txt);
}
.row-more:hover { background: var(--raise); }

/* =====================================================================
   7 · BUTTONS

   The landing page has exactly two buttons and the app now has the same
   two. There is no third.

       GLASS   translucent fill · 1px hairline · backdrop blur
       SOLID   white fill · near-black text

   Everything clickable in the product resolves to one of them — .btn,
   .pill, .icon-btn, the tab strips, the file pickers. That is the whole
   point: a shopper who arrives from hau.lat should not be able to tell
   where the marketing site ended and the app began, and the only way to
   guarantee that is to refuse to invent new button materials here.

   Note the red is gone from the primary CTA. The landing page's primary
   action is white-on-black and its glass is the secondary; matching it
   means the accent red stops being a button colour and goes back to
   being what it actually is — the state colour for capture, LOCKED and
   destructive confirmations. Buttons no longer compete with it.
   ===================================================================== */

/* ---- the glass material, defined once ---- */
.u-glass,
.pill,
.btn--glass,
.btn--ghost,
.icon-btn {
    background: var(--glass);
    border: 1px solid var(--glass-line);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    box-shadow: var(--glass-inset);
    color: var(--txt);
}
.u-glass:hover,
.pill:hover,
.btn--glass:hover,
.btn--ghost:hover,
.icon-btn:hover {
    background: var(--glass-hover);
    border-color: var(--glass-line-2);
    color: var(--txt);
}

.pill {
    display: inline-flex; align-items: center; justify-content: center; gap: 8px;
    height: 44px; padding: 0 18px; border-radius: var(--r-pill);
    font-size: 14px; font-weight: 600; white-space: nowrap;
    transition: background var(--t1) ease, border-color var(--t1) ease, transform var(--t1) ease;
}
.pill:active { transform: scale(.975); }
.pill svg { width: 17px; height: 17px; flex: 0 0 auto; }
/* selected pill flips to the solid — same pairing as the landing hero */
.pill.on { background: var(--txt); border-color: var(--txt); color: var(--bg); box-shadow: none; }
.pill.on:hover { background: var(--txt); border-color: var(--txt); color: var(--bg); opacity: .9; }

.btn {
    display: inline-flex; align-items: center; justify-content: center; gap: 9px;
    min-height: 50px; padding: 0 24px; border-radius: var(--r-input);
    font-size: 15px; font-weight: 700; white-space: nowrap;
    transition: background var(--t1) ease, border-color var(--t1) ease,
                transform var(--t1) ease, opacity var(--t1) ease;
    /* An unqualified .btn is glass. A button with no modifier should be
       the quiet one, never the loud one. */
    background: var(--glass);
    border: 1px solid var(--glass-line);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    box-shadow: var(--glass-inset);
    color: var(--txt);
}
.btn:hover  { background: var(--glass-hover); border-color: var(--glass-line-2); }
.btn:active { transform: scale(.985); }
.btn svg { width: 18px; height: 18px; }

/* ---- SOLID · white fill, near-black text (index.css .btn-primary) ---- */
/* FROSTED, not flat.

   These were opaque #FFFFFF — the only chrome in the product with no
   blur behind it. Next to a header pill, a footer pill, a card and a tab
   bar that are all glass, a flat white rectangle read as a button
   borrowed from somewhere else.

   Now it is the same material as the landing page's header pill: a fill,
   a hairline, a backdrop blur. The difference from .btn (which is glass
   at .10) is only the ALPHA — this one is opaque enough to carry dark
   text, that one is not. One material, two weights, rather than two
   materials.

   .82 is a floor, not a taste. Below roughly .78 the field's glow starts
   showing through hard enough that #111 text drops under 4.5:1 where a
   bright blob sits behind the button — and the blob MOVES, so it would
   fail intermittently and never in a screenshot. */
.btn--primary,
.btn--solid {
    background: rgba(255, 255, 255, .82); color: #111111;
    border: 1px solid rgba(255, 255, 255, .55);
    backdrop-filter: blur(20px) saturate(1.25);
    -webkit-backdrop-filter: blur(20px) saturate(1.25);
    box-shadow: 0 10px 30px rgba(30, 4, 8, .30), inset 0 1px 0 rgba(255, 255, 255, .65);
}
.btn--primary:hover,
.btn--solid:hover {
    background: rgba(255, 255, 255, .93);
    border-color: rgba(255, 255, 255, .8); color: #111111;
}
/* Where blur is unavailable the fill has nothing behind it to frost, so
   the translucency is only a contrast risk with no payoff. Go opaque. */
@supports not (backdrop-filter: blur(1px)) {
    .btn--primary, .btn--solid { background: #FFFFFF; }
}

/* Light mode inverts it: white-on-white is not a button. Same material,
   same alpha, the other end of the ramp — smoked glass rather than
   frosted, carrying white text. */
html.app[data-resolved="light"] .btn--primary,
html.app[data-resolved="light"] .btn--solid {
    background: rgba(22, 7, 10, .84); color: #FFFFFF;
    border-color: rgba(22, 7, 10, .5);
    box-shadow: 0 8px 22px rgba(20, 5, 7, .18), inset 0 1px 0 rgba(255, 255, 255, .12);
}
html.app[data-resolved="light"] .btn--primary:hover,
html.app[data-resolved="light"] .btn--solid:hover {
    background: rgba(28, 12, 16, .94); border-color: rgba(22, 7, 10, .7);
}
@supports not (backdrop-filter: blur(1px)) {
    html.app[data-resolved="light"] .btn--primary,
    html.app[data-resolved="light"] .btn--solid { background: #16070A; }
}

/* ---- DANGER · the one place red stays a button ---- */
.btn--danger { background: var(--accent); border-color: var(--accent); color: var(--on-accent); box-shadow: none; }
.btn--danger:hover { background: color-mix(in srgb, var(--accent) 86%, #000); border-color: transparent; }

.btn--full { width: 100%; }
.btn--sm { min-height: 44px; padding: 0 16px; font-size: 14px; }
.btn[disabled], .btn[aria-disabled="true"] { opacity: .45; pointer-events: none; }

/* Where a glass button sits directly on the elevated card rather than on
   the field, there is no glow behind it to blur — so it needs a touch
   more fill to stay visible. */
.card .btn, .card .pill, .sheet .btn, .menu .btn { --glass: rgba(255,255,255,.075); }
html.app[data-resolved="light"] .card .btn,
html.app[data-resolved="light"] .card .pill { --glass: rgba(20,5,7,.05); }

/* =====================================================================
   8 · CHIPS
   LOCKED solid, CLOSE outlined-light, OPEN outlined — the same grammar
   the OG cards use, so a shared link and the app agree.
   ===================================================================== */
.chip {
    display: inline-flex; align-items: center; gap: 6px;
    height: 26px; padding: 0 11px; border-radius: var(--r-pill);
    font-size: 11.5px; font-weight: 700; letter-spacing: .04em;
    text-transform: uppercase; white-space: nowrap;
    border: 1px solid transparent;
}
html[lang="zh-Hant"].app .chip, html[lang="ja"].app .chip {
    text-transform: none; letter-spacing: 0; font-size: 12px;
}
.chip::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.chip--locked     { background: var(--accent); color: var(--on-accent); }
.chip--close      { background: var(--txt); color: var(--bg); }
.chip--open       { border-color: var(--line-2); color: var(--txt); }
.chip--processing { background: var(--raise-2); color: var(--txt-2); }
.chip--processing::before { animation: pip 1.1s ease-in-out infinite; }
.chip--failed     { background: var(--raise); color: var(--txt-3); }
@keyframes pip { 0%,100% { opacity: 1; } 50% { opacity: .2; } }

/* ---- bounty lifecycle ----
   The colour ranks urgency, not category. "submitted" is amber because
   it is the only state in the whole machine where somebody is waiting on
   YOU and a 48-hour clock is running; everything else can be read at
   leisure. Terminal states are deliberately quiet. */
.chip--claimed    { border-color: var(--line-2); color: var(--txt-2); }
.chip--submitted  { background: var(--warn); color: #1A1204; }
.chip--resolved,
.chip--accepted   { background: var(--pos); color: #04120B; }
.chip--expired,
.chip--cancelled,
.chip--refunded,
.chip--rejected   { background: var(--raise); color: var(--txt-3); }

.badge {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 20px; height: 20px; padding: 0 6px; border-radius: var(--r-pill);
    background: var(--accent); color: var(--on-accent);
    font-size: 11px; font-weight: 800;
}

/* =====================================================================
   9 · DROPDOWN MENUS
   ===================================================================== */
.menu-wrap { position: relative; }
.menu {
    position: absolute; right: 0; top: calc(100% + 10px); z-index: 60;
    min-width: 262px; padding: 10px;
    background: var(--surface-2); border-radius: var(--r-panel);
    box-shadow: var(--shadow-2);
    opacity: 0; visibility: hidden;
    transform: translateY(-8px) scale(.97); transform-origin: top right;
    transition: opacity var(--t2) ease, transform var(--t2) var(--ease), visibility var(--t2);
}
html.app[data-resolved="light"] .menu { border: 1px solid var(--line); }
.menu.open { opacity: 1; visibility: visible; transform: none; }

.menu-head { display: flex; align-items: flex-start; gap: 12px; padding: 12px 12px 14px; }
.menu-head-name { font-family: var(--f-display); font-size: 17px; letter-spacing: -.005em; color: var(--txt); }
html[lang="zh-Hant"].app .menu-head-name, html[lang="ja"].app .menu-head-name { font-family: var(--f-body); font-weight: 800; }
.menu-head-sub { margin-top: 2px; font-size: 13.5px; color: var(--txt-2); }
.menu-head img { width: 46px; height: 46px; border-radius: 50%; object-fit: cover; }

.menu-item {
    display: flex; align-items: center; gap: 13px;
    padding: 11px 12px; border-radius: 13px; width: 100%;
    font-size: 14.5px; font-weight: 500; color: var(--txt);
    transition: background var(--t1) ease;
}
.menu-item:hover { background: var(--raise); }
.menu-item svg { width: 19px; height: 19px; color: var(--txt-2); flex: 0 0 auto; }
.menu-sep { height: 1px; margin: 8px 12px; background: var(--line); }
.menu-foot { padding: 10px 12px 6px; text-align: center; font-size: 12px; color: var(--txt-3); }

/* ---- the bell ----
   A dot, not a number. The count of unread notifications is not
   information anybody acts on — "there is something" and "there is
   nothing" are the only two states that change behaviour, and a badge
   reading 14 mostly communicates that Haulat has been nagging. */
.icon-dot {
    position: absolute; top: 6px; right: 6px;
    width: 8px; height: 8px; border-radius: 50%;
    background: var(--accent);
    box-shadow: 0 0 0 2px var(--bg);
}

.menu--bell { min-width: 330px; padding: 14px; }
.menu--bell .menu-label { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.menu--bell .menu-label a { font-size: 13px; font-weight: 600; color: var(--txt-3); }
.menu--bell .menu-label a:hover { color: var(--txt); text-decoration: underline; }

.bell-list { display: flex; flex-direction: column; gap: 2px; max-height: 60svh; overflow-y: auto; }
.bell-loading, .bell-empty {
    margin: 0; padding: 20px 12px 22px; text-align: center;
    font-size: 13.5px; color: var(--txt-3);
}
.bell-item {
    display: block; padding: 11px 12px; border-radius: 13px;
    transition: background var(--t1) ease;
}
.bell-item:hover { background: var(--raise); }
.bell-text {
    display: block; font-size: 14px; line-height: 1.45; color: var(--txt);
    /* Two lines, then clip. A dropdown row that grows to five lines
       because somebody titled their bounty in a paragraph pushes every
       other notification off the screen. */
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
    overflow: hidden;
}
.bell-when { display: block; margin-top: 3px; font-size: 12px; color: var(--txt-3); }
.bell-item.is-unread .bell-text { font-weight: 600; }
.bell-item.is-unread { position: relative; }
.bell-item.is-unread::before {
    content: ''; position: absolute; left: 3px; top: 50%; transform: translateY(-50%);
    width: 5px; height: 5px; border-radius: 50%; background: var(--accent);
}

/* the same marker on the full page */
.row--fresh { position: relative; }
.row--fresh::before {
    content: ''; position: absolute; left: 0; top: 50%; transform: translateY(-50%);
    width: 3px; height: 22px; border-radius: 0 3px 3px 0; background: var(--accent);
}

/* the apps grid */
.menu--apps { min-width: 330px; padding: 16px; }
.menu-label {
    padding: 2px 4px 12px; font-size: 14px; font-weight: 600; color: var(--txt-2);
}
.apps-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
.app-tile {
    display: flex; flex-direction: column; align-items: center; gap: 8px;
    padding: 10px 4px 12px; border-radius: 14px;
    font-size: 11.5px; font-weight: 600; color: var(--txt-2); text-align: center;
    transition: background var(--t1) ease, color var(--t1) ease;
}
.app-tile:hover { background: var(--raise); color: var(--txt); }
.app-tile-mark {
    width: 54px; height: 54px; border-radius: 16px;
    display: grid; place-items: center;
    box-shadow: var(--shadow-1);
}
.app-tile-mark svg, .app-tile-mark img { width: 26px; height: 26px; }
.app-tile.on .app-tile-mark { outline: 2px solid var(--txt); outline-offset: 2px; }
.app-tile.on { color: var(--txt); }

/* =====================================================================
   10 · SIDE SHEET
   Desktop: a column beside the list. Mobile: a bottom sheet. Same markup.
   ===================================================================== */
.layout-split { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; align-items: start; }
.layout-split.has-sheet { grid-template-columns: minmax(0, 1fr) 420px; }

.sheet { position: relative; }
.sheet-close {
    position: absolute; top: 18px; left: 18px; z-index: 2;
    width: 34px; height: 34px; border-radius: 50%;
    display: grid; place-items: center; color: var(--txt-2);
}
.sheet-close:hover { background: var(--raise-2); color: var(--txt); }
.sheet-body { padding: 60px var(--gutter) 24px; }
.sheet-amt { font-family: var(--f-display); font-size: 30px; letter-spacing: -.02em; color: var(--txt); }
.sheet-name { margin-top: 6px; font-size: 16px; font-weight: 600; }
.sheet-date { margin-top: 2px; font-size: 14px; color: var(--txt-2); }
.sheet-media { margin: 18px 0; border-radius: var(--r-panel); overflow: hidden; background: var(--surface-2); }
.sheet-media img { width: 100%; aspect-ratio: 4 / 5; object-fit: cover; }

.kv { background: var(--surface-2); border-radius: var(--r-panel); overflow: hidden; }
html.app[data-resolved="light"] .kv { background: var(--surface-3); }
.kv + .kv { margin-top: 12px; }
.kv-row {
    display: flex; align-items: center; justify-content: space-between; gap: 16px;
    padding: 14px 18px; font-size: 14.5px; width: 100%;
}
.kv-row + .kv-row { border-top: 1px solid var(--line); }
.kv-k { color: var(--txt-2); }
.kv-v { font-weight: 600; text-align: right; }
.kv-v--link { color: var(--accent); font-weight: 600; }

/* =====================================================================
   11 · GRIDS
   ===================================================================== */
.tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 14px; padding: 4px var(--gutter) var(--gutter); }
.tile { display: block; border-radius: 18px; overflow: hidden; background: var(--surface-2); position: relative; transition: transform var(--t1) var(--ease); }
.tile:hover { transform: translateY(-2px); }
.tile img { width: 100%; aspect-ratio: 3 / 4; object-fit: cover; }
.tile::after {
    content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 42%;
    background: linear-gradient(to top, rgba(0,0,0,.78), transparent);
    pointer-events: none;
}
.tile-foot {
    position: absolute; z-index: 1; left: 10px; right: 10px; bottom: 10px;
    display: flex; align-items: center; justify-content: space-between; gap: 8px;
}
/* On a tile the chip always sits on a photo, so OPEN cannot stay a bare
   outline — it needs its own ground to be legible. */
.tile-foot .chip--open { background: rgba(0,0,0,.55); border-color: rgba(255,255,255,.6); color: #fff; }
.tile-foot .chip--close { background: #fff; color: #111; }

.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 14px; }
.stat { background: var(--surface); border-radius: var(--r-panel); padding: 20px; box-shadow: var(--shadow-1); }
.stat-n { font-family: var(--f-display); font-size: 30px; letter-spacing: -.02em; color: var(--txt); }
.stat-l { margin-top: 6px; font-size: 13px; color: var(--txt-2); }

/* =====================================================================
   12 · FORMS
   ===================================================================== */
.field-group { margin-bottom: 16px; }
.field-group > label {
    display: block; margin-bottom: 7px;
    font-size: 13px; font-weight: 600; color: var(--txt-2);
}
.input {
    width: 100%; min-height: 54px; padding: 15px 17px;
    border-radius: var(--r-input);
    background: var(--surface-2); color: var(--txt);
    border: 1px solid transparent;
    font-size: 16px;            /* 16px stops iOS zooming on focus */
    transition: border-color var(--t1) ease, background var(--t1) ease;
    -webkit-appearance: none; appearance: none;
}
html.app[data-resolved="light"] .input { background: var(--surface-3); }
.input::placeholder { color: var(--txt-3); }
.input:focus { outline: none; border-color: var(--accent); background: var(--surface-3); }
.input[aria-invalid="true"] { border-color: var(--accent); }

/* ---- PASSWORD REVEAL ---------------------------------------------
   The eye sits inside the field's right edge and the input reserves the
   space for it, so the icon never lands on top of a long password. Both
   glyphs are in the DOM and one is hidden, rather than swapping an SVG
   on click: no reflow, no flash, and the icon cannot fail to appear
   because a second file had not loaded yet.

   44px square. It is the one control on this screen somebody taps with
   a thumb while holding the phone in the other hand. */
.pw-wrap { position: relative; display: block; }
.pw-wrap .input { padding-right: 54px; }
.pw-eye {
    position: absolute; top: 50%; right: 5px; transform: translateY(-50%);
    width: 44px; height: 44px; border-radius: 12px;
    display: grid; place-items: center;
    color: var(--txt-3); background: none; border: 0; cursor: pointer;
    transition: color var(--t1) ease, background var(--t1) ease;
}
.pw-eye:hover { color: var(--txt); background: var(--raise); }
.pw-eye:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.pw-eye svg { width: 21px; height: 21px; }

/* Closed eye by default: the password is hidden, so the icon shows what
   pressing it would DO. Pressed means the password is visible. */
.pw-eye .pw-open { display: none; }
.pw-eye[aria-pressed="true"] .pw-open { display: block; }
.pw-eye[aria-pressed="true"] .pw-shut { display: none; }
.pw-eye[aria-pressed="true"] { color: var(--txt); }

/* Chromium and Edge draw their OWN reveal control on password inputs.
   Left alone there would be two eyes side by side, one of which we do
   not style and cannot label. */
.pw-wrap .input::-ms-reveal,
.pw-wrap .input::-ms-clear { display: none; }
select.input {
    background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                      linear-gradient(135deg, currentColor 50%, transparent 50%);
    background-position: calc(100% - 20px) 50%, calc(100% - 14px) 50%;
    background-size: 6px 6px, 6px 6px;
    background-repeat: no-repeat;
    padding-right: 44px;
}
.field-err { display: block; margin-top: 7px; font-size: 13px; color: var(--accent); font-weight: 600; }
html.app[data-resolved="dark"] .field-err { color: var(--h-pink); }
.field-hint { display: block; margin-top: 7px; font-size: 13px; color: var(--txt-3); }

/* ---- TAP TARGETS ------------------------------------------------
   An alignment audit across every screen at three widths found the same
   handful of controls under 44px on all eighty of them, because they
   live in the shell: the phone wordmark (18px tall), the "See all" link
   in the notification panel (19px), and the avatar button (38px).

   The fix is an invisible expanded hit area rather than a bigger box.
   Growing the avatar to 44px would change the top bar to satisfy a
   number; growing what a finger can hit costs nothing visually and is
   what the number is actually about.

   Only for ISOLATED controls. Two of these side by side would overlap
   and the second would swallow the first, so anything in a cluster gets
   a genuinely larger box instead. */
.tap44 { position: relative; }
.tap44::after {
    content: ''; position: absolute;
    left: 50%; top: 50%; transform: translate(-50%, -50%);
    width: max(100%, 44px); height: max(100%, 44px);
}

/* ---- CAPTURE ----------------------------------------------------
   Haulat's own element: the thing you press to start a find. Promoted
   here from s-find.css when the unsuccessful-find screen started
   offering the same control, because a retry has to be one tap from the
   miss rather than a navigation back to the home screen. */
.capture {
    display: flex; flex-direction: column; align-items: center; gap: 18px;
    padding: 40px 24px 44px; text-align: center;
}
.capture-h { font-family: var(--f-display); font-size: clamp(24px, 3.4vw, 34px); letter-spacing: -.015em; }
html[lang="zh-Hant"].app .capture-h, html[lang="ja"].app .capture-h { font-family: var(--f-body); font-weight: 800; }
.capture-p { margin: 0; max-width: 34ch; color: var(--txt-2); font-size: 14.5px; }
.capture-ring {
    width: 116px; height: 116px; border-radius: 50%;
    display: grid; place-items: center; margin: 6px 0;
    background: var(--accent); color: var(--on-accent);
    box-shadow: 0 14px 40px color-mix(in srgb, var(--accent) 45%, transparent);
    transition: transform var(--t2) var(--ease), box-shadow var(--t2) ease;
}
.capture-ring:hover  { transform: scale(1.04); }
.capture-ring:active { transform: scale(.96); }
.capture-ring svg { width: 42px; height: 42px; }
.capture-alt { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }

/* The retry variant. Smaller, because on this screen the capture is the
   recovery from a miss rather than the front door, and there is a result
   card and a bounty card below it that both need to be visible. */
.capture--compact { gap: 14px; padding: 26px 24px 30px; }
.capture--compact .capture-h { font-size: clamp(21px, 2.8vw, 27px); }
.capture--compact .capture-ring { width: 92px; height: 92px; margin: 2px 0; }
.capture--compact .capture-ring svg { width: 36px; height: 36px; }

/* ---- SWITCH -----------------------------------------------------
   A real switch rather than a checkbox: the whole ROW is the label, so
   the whole row is the target — which on a phone is the difference
   between a setting people change and one they give up on.

   Promoted here from s-profile.css when Settings › Privacy needed the
   same control. Two copies of a control is how the two screens end up
   with switches that animate differently, and nobody notices until one
   of them is the screen where a person turns their profile off.

   The <input> is the state and stays in the accessibility tree — it is
   moved off-screen, never display:none, so it is still focusable,
   announced, and posted with the form. */
.sw { position: absolute; opacity: 0; width: 0; height: 0; }
/* The input is absolutely positioned, so SOMETHING has to contain it or
   it resolves against whatever ancestor happens to be positioned — and
   a focused zero-size input parked at the top of the page makes keyboard
   focus scroll the list to somewhere the switch is not. */
.row:has(.sw) { position: relative; }
.sw-track {
    flex: 0 0 auto; position: relative;
    width: 48px; height: 28px; border-radius: 999px;
    background: var(--raise-2); border: 1px solid var(--line);
    transition: background var(--t1) ease, border-color var(--t1) ease;
}
.sw-track i {
    position: absolute; top: 3px; left: 3px;
    width: 20px; height: 20px; border-radius: 50%;
    background: var(--txt-2);
    transition: transform var(--t2) var(--ease), background var(--t1) ease;
}
.sw:checked ~ .sw-track { background: var(--accent); border-color: var(--accent); }
.sw:checked ~ .sw-track i { transform: translateX(20px); background: #fff; }
.sw:focus-visible ~ .sw-track { outline: 2px solid var(--accent); outline-offset: 3px; }
/* A switch that cannot be changed has to LOOK like it cannot, or the
   explanation next to it reads as an excuse. */
.sw[disabled] ~ .sw-track { opacity: .45; }
.row--locked { cursor: default; }

/* segmented control */
.seg {
    display: flex; gap: 4px; padding: 4px;
    background: var(--surface-2); border-radius: var(--r-pill);
    overflow-x: auto; scrollbar-width: none;
}
html.app[data-resolved="light"] .seg { background: var(--surface-3); }
.seg::-webkit-scrollbar { display: none; }
.seg a, .seg button {
    flex: 1 0 auto; text-align: center; white-space: nowrap;
    padding: 10px 18px; border-radius: var(--r-pill);
    font-size: 14px; font-weight: 600; color: var(--txt-2);
    transition: background var(--t1) ease, color var(--t1) ease;
}
.seg a:hover, .seg button:hover { color: var(--txt); }
.seg .on { background: var(--txt); color: var(--bg); }

/* =====================================================================
   13 · EMPTY / SKELETON / FLASH
   ===================================================================== */
.empty {
    display: flex; flex-direction: column; align-items: center; gap: 14px;
    text-align: center; padding: 56px 24px 60px;
}
.empty-mark {
    width: 56px; height: 56px; border-radius: 50%;
    display: grid; place-items: center;
    background: var(--surface-2); color: var(--txt-3);
}
html.app[data-resolved="light"] .empty-mark { background: var(--surface-3); }
.empty p { margin: 0; max-width: 36ch; color: var(--txt-2); font-size: 14.5px; }

.skel { background: var(--surface-2); border-radius: 12px; position: relative; overflow: hidden; }
.skel::after {
    content: ''; position: absolute; inset: 0;
    background: linear-gradient(90deg, transparent, var(--raise-2), transparent);
    animation: shimmer 1.5s infinite;
}
@keyframes shimmer { from { transform: translateX(-100%); } to { transform: translateX(100%); } }

.flash-stack {
    position: fixed; z-index: 90; left: 50%; transform: translateX(-50%);
    top: calc(env(safe-area-inset-top, 0px) + 14px);
    width: min(440px, calc(100vw - 28px));
    display: flex; flex-direction: column; gap: 8px; pointer-events: none;
}
.flash {
    padding: 14px 18px; border-radius: var(--r-input);
    background: var(--surface-2); color: var(--txt);
    box-shadow: var(--shadow-2); font-size: 14px; font-weight: 600;
    pointer-events: auto; animation: drop .34s var(--ease) both;
}
html.app[data-resolved="light"] .flash { border: 1px solid var(--line); }
.flash--error   { background: var(--accent); color: var(--on-accent); }
.flash--success { background: var(--pos); color: #04120B; }
@keyframes drop { from { opacity: 0; transform: translateY(-12px); } to { opacity: 1; transform: none; } }

/* =====================================================================
   15 · AUTH + PUBLIC
   ===================================================================== */
.auth-shell { min-height: 100svh; display: flex; flex-direction: column; position: relative; z-index: 1; }
.auth-top { padding: 26px 28px; }
.auth-top img { height: 20px; }
.auth-mid { flex: 1; display: grid; place-items: center; padding: 8px 20px 60px; }
.auth-box { width: 100%; max-width: 400px; }
.auth-h { font-family: var(--f-display); font-size: 30px; letter-spacing: -.015em; text-align: center; }
html[lang="zh-Hant"].app .auth-h, html[lang="ja"].app .auth-h { font-family: var(--f-body); font-weight: 800; }
.auth-p { margin: 10px 0 26px; text-align: center; color: var(--txt-2); font-size: 14.5px; }
.auth-foot { padding: 18px 28px calc(18px + var(--safe-b)); display: flex; gap: 22px; font-size: 13px; color: var(--txt-3); }
.auth-foot a:hover { color: var(--txt-2); }

.pipeline { padding: 4px 0 0; }
.pipeline li {
    display: flex; align-items: center; gap: 13px; padding: 12px 0;
    font-size: 14px; color: var(--txt-3);
    transition: color var(--t2) ease;
}
.pipeline li + li { border-top: 1px solid var(--line); }
.pipeline li::before {
    content: counter(step); counter-increment: step;
    flex: 0 0 auto; width: 22px; height: 22px; border-radius: 50%;
    display: grid; place-items: center; font-size: 11px; font-weight: 700;
    border: 1px solid var(--line-2); color: var(--txt-3);
    transition: background var(--t2) ease, color var(--t2) ease, border-color var(--t2) ease;
}
.pipeline { counter-reset: step; }
.pipeline li.on { color: var(--txt); }
.pipeline li.on::before { background: var(--raise-2); border-color: var(--txt-2); color: var(--txt); }
.pipeline li.done { color: var(--txt-2); }
.pipeline li.done::before { content: '✓'; background: var(--txt); border-color: var(--txt); color: var(--bg); }

/* =====================================================================
   17 · MOBILE
   The rail becomes a bottom bar; the content card goes edge-to-edge.
   ===================================================================== */
.tabbar { display: none; }

@media (max-width: 1023px) {
    html.app .shell { grid-template-columns: var(--rail-w-icons) minmax(0, 1fr); }
    html.app[data-nav="full"] .rail { align-items: center; padding-inline: 10px; }
    html.app[data-nav="full"] .rail-word { display: none; }
    html.app[data-nav="full"] .rail > .rail-item {
        flex-direction: column; gap: 5px; width: 68px; padding: 11px 4px;
        font-size: 10.5px;
    }
    html.app .layout-split.has-sheet { grid-template-columns: minmax(0, 1fr); }
    html.app { --gutter: 20px; }
}

@media (max-width: 767px) {
    html.app { --gutter: 16px; --r-card: 22px; --topbar-h: 62px; }
    html.app body { padding-bottom: calc(var(--tabbar-h) + var(--safe-b)); }

    html.app .shell,
    html.app[data-nav="icons"] .shell,
    html.app[data-nav="full"] .shell { grid-template-columns: minmax(0, 1fr); }
    html.app .rail { display: none; }

    .topbar { padding: 12px var(--gutter) 8px; }
    /* Wordmark replaces the page title on a phone. */
    html.app .topbar-mark { display: block; }
    html.app .topbar-text { display: none; }

    .main { padding-inline: 12px; }
    .card-head { padding: 20px var(--gutter) 14px; }
    .row { padding-inline: var(--gutter); gap: 13px; }
    .row-fig { width: 40px; height: 40px; }
    .sheet-body { padding: 54px var(--gutter) 20px; }

    /* bottom bar */
    /* ---- THE PILL ----
       The bottom bar is a floating pill, in the idiom of the reference
       app: it does not span the screen, it hovers above it. Two things
       follow from that shape and both are deliberate.

       It is narrower than the viewport, so the content behind it stays
       visible at the edges and the bar reads as an object rather than as
       the bottom of the window. And because it floats, it can shrink —
       scrolling down takes it to 80%, which gives back a strip of screen
       without ever removing the navigation.

       The capture button is bigger than its neighbours and breaks the
       pill's top edge. That is Haulat's one departure from the reference
       and it earns it: pressing that button IS the product. */
    .tabbar {
        display: block; position: fixed; z-index: 50;
        left: 0; right: 0; bottom: 0;
        padding: 0 12px calc(10px + var(--safe-b));
        background: none; box-shadow: none;
        pointer-events: none;
        transition: transform var(--t2) var(--ease);
    }
    .tabbar-inner {
        pointer-events: auto;
        display: grid; grid-template-columns: repeat(5, 1fr); align-items: center;
        width: min(420px, 100%); margin: 0 auto;
        padding: 7px 8px;
        border-radius: 999px;

        background: color-mix(in srgb, var(--surface) 86%, transparent);
        border: 1px solid var(--edge);
        backdrop-filter: blur(28px) saturate(1.35);
        -webkit-backdrop-filter: blur(28px) saturate(1.35);
        box-shadow: 0 12px 34px rgba(0,0,0,.34);

        /* Shrink from the bottom edge, so the pill pulls down toward the
           screen edge rather than floating away from the content. */
        transform-origin: bottom center;
        transition: transform var(--t2) var(--ease), opacity var(--t2) ease;
    }
    html[data-resolved="light"] .tabbar-inner { box-shadow: 0 12px 30px rgba(110,11,18,.14); }
    @supports not (backdrop-filter: blur(1px)) {
        .tabbar-inner { background: var(--surface); }
    }

    /* Scrolling down: 20% smaller. Not hidden — navigation that
       disappears makes people scroll back up to find it, which is the
       opposite of the space you just saved. */
    html.nav-down .tabbar-inner { transform: scale(.8); }

    /* ---- ICONS ONLY ----
       The labels are gone from the pill. Five 9.5px words inside a
       420px-wide bar were set at a size nobody reads and, translated,
       either wrapped or clipped — "收藏" fits, "お気に入り" does not. The
       icons are the same five the rail uses, and the destinations are
       the five a person visits daily.

       The text is REMOVED VISUALLY, not deleted. .sr-only keeps it in
       the accessibility tree, so every tab still has a real name for a
       screen reader and for voice control ("tap Vault") — a bar of five
       unlabelled <a>s wrapping an <svg> is five links called nothing. */
    .tab {
        display: flex; flex-direction: column; align-items: center; gap: 4px;
        padding: 8px 2px; min-height: 46px; justify-content: center;
        color: var(--txt-3);
        transition: color var(--t1) ease;
    }
    .tab > span {
        position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
        overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
    }
    /* The icon grows into the space the label gave up, so the tap target
       stays the full 46px rather than shrinking with the text. */
    .tab svg { width: 25px; height: 25px; }
    .tab.on { color: var(--txt); }
    /* The active item gets a dot, not just a colour. This mattered more
       than it did with labels: colour is now the ONLY other signal, and
       colour alone is not one people all receive. */
    .tab.on::after {
        content: ''; width: 4px; height: 4px; border-radius: 50%;
        background: var(--accent); flex: none;
    }
    .tab-capture {
        width: 52px; height: 52px; margin: -14px auto 0; border-radius: 50%;
        display: grid; place-items: center;
        background: var(--accent); color: var(--on-accent);
        box-shadow: 0 6px 18px color-mix(in srgb, var(--accent) 46%, transparent);
        transition: transform var(--t1) var(--ease);
    }
    .tab-capture:active { transform: scale(.93); }
    .tab-capture svg { width: 24px; height: 24px; }

    /* ---- THE HEADER ----
       Slides away on scroll down, returns on scroll up or at the top,
       and only carries its blur once something has actually scrolled
       under it. A permanently frosted bar over a page that has not moved
       is a smudge with nothing behind it. */
    html.app .topbar {
        transition: transform var(--t2) var(--ease), opacity var(--t2) ease;
    }
    html.nav-down .topbar {
        transform: translateY(-100%);
        opacity: 0;
        pointer-events: none;
    }
    html.app .topbar::before { opacity: 0; transition: opacity var(--t2) ease; }
    html.is-scrolled .topbar::before { opacity: 1; }

    .mode-art { width: 108px; height: 74px; }
    .nav-art  { width: 124px; height: 74px; }
    .menu { position: fixed; left: 12px; right: 12px; top: auto; bottom: calc(var(--tabbar-h) + var(--safe-b) + 12px); min-width: 0; transform-origin: bottom center; }
    .menu--apps { min-width: 0; }
    .flash-stack { top: auto; bottom: calc(var(--tabbar-h) + var(--safe-b) + 14px); }
}

/* =====================================================================
   18 · ACCESSIBILITY
   ===================================================================== */
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
    outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 8px;
}
.sr-only {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.skip-link {
    position: absolute; left: 14px; top: -60px; z-index: 100;
    padding: 12px 18px; border-radius: var(--r-input);
    background: var(--txt); color: var(--bg); font-weight: 700; font-size: 13px;
    transition: top var(--t1) ease;
}
.skip-link:focus { top: 14px; }

@media (prefers-reduced-motion: reduce) {
    html.app { --t1: 0s; --t2: 0s; --t3: 0s; }
    html.app { scroll-behavior: auto; }
    .skel::after, .chip--processing::before, .flash { animation: none; }
}
@media (prefers-contrast: more) {
    html.app { --txt-2: var(--txt); --line: var(--line-2); }
}

/* utilities used sparingly by views */
.u-row   { display: flex; align-items: center; gap: 10px; }
.u-grow  { flex: 1; min-width: 0; }
.u-wrap  { flex-wrap: wrap; }
.u-mt    { margin-top: 16px; }
.u-mb    { margin-bottom: 16px; }
.u-muted { color: var(--txt-2); }
.u-small { font-size: 13.5px; }
.u-center{ text-align: center; }
.u-mono  { font-family: var(--f-mono); font-size: 13px; letter-spacing: .02em; }

/* =====================================================================
   19 · PLAN TAG
   A tier badge inside a menu row. Lives here rather than in a page file
   because the profile menu is part of the shell, on every screen.
   ===================================================================== */
.menu-tag {
    margin-left: auto;
    padding: 2px 8px; border-radius: 999px;
    background: var(--raise-2); color: var(--txt-3);
    font-size: 10.5px; font-weight: 800; letter-spacing: .04em;
}

/* An unread count in the profile menu. Same shape as the plan tag next
   to it, different weight — a number here is news, a plan name is not. */
.menu-tag--n {
    background: var(--txt); color: var(--bg);
    font-variant-numeric: tabular-nums; font-weight: 800;
}

/* =====================================================================
   REPORT

   A <details> rather than a dialog, and that is on purpose. This is the
   control somebody reaches for when they are angry about a fake, which
   is the worst possible moment to depend on a script having loaded. It
   opens, it posts, it works with JavaScript switched off.

   IT OPENS IN FLOW, NOT AS A POPOVER, and that took three attempts.

     1. absolute, anchored to the control. Clipped by the card it sits
        in, because cards round their corners and therefore hide their
        overflow. The submit button was unreachable on the bounty page.
     2. fixed, anchored to the viewport. Worse. A fixed element takes its
        containing block from the nearest ancestor with a transform,
        filter or backdrop-filter, and this app puts backdrop-filter on
        rows and bars, so `left: 12px; right: 12px` resolved against a
        27px wrapper. The panel came out 30px wide, off the right edge of
        the screen, and being fixed, no scrolling could ever reach it.
     3. in flow. The disclosure expands and pushes the page down, which
        is what <details> does by default and what it is good at. It
        cannot be clipped, it cannot escape the viewport, and it needs no
        positioning at all.

   The cost is that opening it shifts the layout. For a control somebody
   presses once, that is a fair price for one that always works.

   It lives in app.css rather than a page file because it appears on five
   different screens across both sides of the product.
   ===================================================================== */
.rpt { display: inline-block; }

/* Open, it takes the whole line so the panel has room. In the flex rows
   this control lives in, that means claiming a row of its own. */
.rpt[open] { display: block; width: 100%; flex: 1 0 100%; }

.rpt > summary {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 6px 10px; border-radius: var(--r-pill);
    font-size: 12.5px; font-weight: 600; color: var(--txt-3);
    cursor: pointer; list-style: none;
}
.rpt > summary::-webkit-details-marker { display: none; }
.rpt > summary:hover { color: var(--txt-2); background: var(--surface-2); }
.rpt > summary svg { width: 15px; height: 15px; }
.rpt[data-filed] > summary { color: var(--txt-3); }
.rpt--icon > summary { padding: 6px; }

/* Closed means not laid out. A closed <details> hides its content, but
   the panel still had a layout box: 30px wide, 460px tall, jammed
   against the right edge. The alignment audit found it as twelve
   un-tappable buttons on a screen where nothing was open. */
.rpt:not([open]) .rpt-panel { display: none; }

.rpt-panel {
    width: min(320px, 100%);
    margin-top: 8px; padding: 14px;
    border-radius: var(--r-lg);
    background: var(--surface-2); border: 1px solid var(--line);
}
html.app[data-resolved="light"] .rpt-panel { background: var(--surface-3); }

.rpt-q { margin: 0 0 8px; font-size: 13px; font-weight: 700; }
.rpt-r {
    display: flex; align-items: center; gap: 9px;
    padding: 7px 4px; font-size: 13.5px; cursor: pointer;
    min-height: 34px;
}
.rpt-r input { width: 16px; height: 16px; accent-color: var(--accent); flex: 0 0 auto; }
.rpt-detail { margin-top: 8px; min-height: 62px; resize: vertical; font-size: 13.5px; }
.rpt-form .btn { width: 100%; margin-top: 10px; }
.rpt-note, .rpt-said { margin: 8px 0 0; font-size: 11.5px; color: var(--txt-3); line-height: 1.45; }
.rpt-said { margin: 0; font-size: 13px; color: var(--txt-2); }

/* A transparent wrapper that lets a control sit beside a row that is
   itself an <a>. A <details> inside an anchor is invalid HTML and eats
   the anchor's click, so the two are siblings and this positions them. */
/* The row is an <a>; a <details> inside an anchor is invalid HTML and
   eats the anchor's own click. So the two are siblings, stacked, with
   the control on its own line under the row rather than floating over
   it: in flow it can expand without being clipped by the card. */
.row-host { display: block; }
.row-host-act {
    display: flex; justify-content: flex-end;
    padding: 0 var(--gutter) 6px;
    margin-top: -4px;
}
.row-host-act .rpt[open] { flex: 1 1 100%; }
.row-host-act .rpt-panel { margin-left: auto; }

/* In a chat the control lives next to the bubble and only appears on
   hover or focus, because a report button beside every line somebody
   ever said to you is an accusation waiting in the furniture. It stays
   visible once opened, and it is always reachable by keyboard. */
.ch-msg > .rpt { align-self: center; opacity: 0; transition: opacity var(--t1) ease; }
.ch-msg:hover > .rpt,
.ch-msg > .rpt:focus-within,
.ch-msg > .rpt[open] { opacity: 1; }
@media (hover: none) {
    /* No hover on a phone, so it is simply always there but quiet. */
    .ch-msg > .rpt { opacity: .45; }
}
