/* CheckGO web client — one stylesheet, layered once:
   tokens → base → components → patterns → views → responsive → preferences.
   Each media query exists exactly once, in the last two sections. */

/* ===========================================================================
   Tokens
   =========================================================================== */

:root {
    /* Brand */
    --color-primary: #1B8C6A;
    --color-primary-dark: #0F6D52;
    --color-primary-tint: #D1FAE5;
    --color-primary-wash: rgba(27, 140, 106, 0.08);
    --color-primary-line: #CEE6DA;

    /* Surfaces */
    --color-bg: #F8FAFC;
    --color-surface: #FFFFFF;
    --color-surface-sunken: #F1F5F9;
    --color-border: #E2E8F0;
    --color-border-strong: #CBD5E1;

    /* Text */
    --text-primary: #0F172A;
    --text-secondary: #475569;
    --text-tertiary: #64748B;

    /* Status — one red family: danger fills, -dark is deep text, risk-* are badge pairs */
    --color-success: #10B981;
    --color-danger: #EF4444;
    --color-danger-bg: #FEF2F2;
    --color-danger-dark: #B91C1C;
    --color-warning: #F59E0B;
    --color-warning-bg: #FFFBEB;
    --color-info: #3B82F6;
    --color-info-bg: #EFF6FF;
    --color-info-dark: #1D4ED8;
    --color-info-line: #BFDBFE;

    /* Risk badges */
    --risk-high-bg: #FEE2E2;
    --risk-high-fg: #DC2626;
    --risk-mid-bg: #FEF3C7;
    --risk-mid-fg: #D97706;
    --risk-low-bg: #D1FAE5;
    --risk-low-fg: #059669;

    /* Spacing — 8px grid */
    --space-xs: 4px;
    --space-sm: 8px;
    --space-md: 12px;
    --space-lg: 16px;
    --space-xl: 24px;
    --space-2xl: 32px;
    --space-3xl: 48px;

    --radius-sm: 8px;
    --radius-md: 12px;
    --radius-lg: 16px;
    --radius-xl: 24px;
    --radius-full: 9999px;

    /* Type scale — the one ladder every view uses (docs/UI-REBUILD-PLAN §2.1). */
    --font-display: 1.75rem;
    --font-title: 1.25rem;
    --font-heading: 1.0625rem;
    --font-body: 0.9375rem;
    --font-body-sm: 0.84375rem;
    --font-caption: 0.75rem;
    --leading-tight: 1.45;
    --leading-normal: 1.5;

    /* Bigger surfaces read as thicker: more spread, deeper shadow. */
    --shadow-chip: 0 1px 2px rgba(15, 23, 42, 0.06);
    --shadow-card: 0 2px 8px rgba(15, 23, 42, 0.025);
    --shadow-raised: 0 1px 3px rgba(15, 23, 42, 0.06), 0 10px 24px rgba(15, 23, 42, 0.08);

    /* Critically damped settle — no overshoot on ordinary UI. */
    --ease-settle: cubic-bezier(0.32, 0.72, 0, 1);

    --nav-height: 52px;
    --tab-height: 56px;

    color-scheme: light;
}

/* ===========================================================================
   Base — reset, page frame, view stage, focus and press feedback
   =========================================================================== */

*,
*::before,
*::after {
    box-sizing: border-box;
}

/* Any explicit `display` below would otherwise beat the UA's `[hidden]` rule
   and leave hidden chrome on screen. */
[hidden] {
    display: none !important;
}

html {
    /* Spacing and layout are in rem so a larger browser text size scales the
       whole interface instead of overflowing a fixed-pixel frame. */
    font: 100%/1.55 -apple-system, BlinkMacSystemFont, 'HarmonyOS Sans SC',
        'PingFang SC', 'Noto Sans CJK SC', 'Source Han Sans SC', 'Helvetica Neue',
        Arial, sans-serif;
    -webkit-text-size-adjust: 100%;
}

body {
    margin: 0;
    background: var(--color-bg);
    color: var(--text-primary);
    overflow-wrap: anywhere;
    /* Chrome floats over the content, so the page owns the scroll. */
    overscroll-behavior-y: none;
}

.app-shell {
    min-height: 100dvh;
    max-width: 46rem;
    margin: 0 auto;
    position: relative;
}

/* Shared footer block: the AI disclaimer sits right above the ICP filing
   number, centred at the bottom of the site as the law wants it, one stacked
   pair on every page. The extra bottom padding keeps it clear of the fixed
   tab bar. */
.site-footer {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-lg) var(--space-lg)
        calc(var(--tab-height) + env(safe-area-inset-bottom) + var(--space-lg));
}

/* The public security badge leads the numbers, so the line is a centred flex
   row; two filing numbers plus the badge can exceed a narrow phone, wrap. */
.site-footer .beian-row {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 6px;
}

.site-footer .beian-badge {
    width: auto;
    height: 16px;
}

.site-footer a {
    color: var(--text-tertiary);
    font-size: var(--font-caption);
    text-decoration: none;
}

/* View stage — the router animates children in and out of here. No viewport
   min-height: the stage is only as tall as its content, so the footer hugs
   the page instead of sitting a screen of air below it. */

.view-stage {
    position: relative;
}

.view {
    padding: calc(var(--nav-height) + env(safe-area-inset-top) + var(--space-lg))
        var(--space-lg)
        calc(var(--tab-height) + env(safe-area-inset-bottom) + var(--space-2xl));
    display: flex;
    flex-direction: column;
    gap: var(--space-lg);
}

.view.is-fullscreen {
    padding-bottom: calc(env(safe-area-inset-bottom) + var(--space-2xl));
}

/* The router focuses the incoming view (tabindex -1) so keyboard and
   screen-reader users land on the new page; it must not draw a ring. */
.view:focus {
    outline: none;
}

:where(button, a, [role='button'], input):focus-visible {
    outline: 3px solid var(--color-primary-dark);
    outline-offset: 4px;
}

/* Every tappable surface answers on press-down, never only on release. */
:where(.button, .tappable, .segment, .upload-zone, .nav-back) {
    touch-action: manipulation;
    transition: transform 120ms ease-out, background-color 120ms ease-out;
}
:where(.button, .tappable, .segment, .upload-zone, .nav-back):not(:disabled):not([aria-disabled='true']):active {
    transform: scale(0.98);
}

/* ===========================================================================
   Components — chrome, type, buttons, cards, badges, fields, states
   ---------------------------------------------------------------------------
   Chrome: translucent material, content scrolls underneath
   =========================================================================== */

.nav-bar {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 20;
    height: calc(var(--nav-height) + env(safe-area-inset-top));
    padding-top: env(safe-area-inset-top);
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(248, 250, 252, 0.72);
    backdrop-filter: blur(20px) saturate(180%);
    -webkit-backdrop-filter: blur(20px) saturate(180%);
}

/* A scroll edge effect rather than a hard 1px divider: the separation only
   appears where content actually slides under the bar. */
.nav-bar::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: -12px;
    height: 12px;
    background: linear-gradient(rgba(248, 250, 252, 0.72), rgba(248, 250, 252, 0));
    opacity: 0;
    transition: opacity 180ms var(--ease-settle);
    pointer-events: none;
}

.nav-bar[data-scrolled='true']::after {
    opacity: 1;
}

.nav-title {
    margin: 0;
    /* Vibrancy over a translucent surface: heavier weight and a touch more
       tracking keep the label legible as content passes behind it. */
    font-size: 1.0625rem;
    font-weight: 600;
    letter-spacing: 0.01em;
    color: var(--text-primary);
}

.nav-back {
    position: absolute;
    left: var(--space-sm);
    width: 2.75rem;
    height: 2.75rem;
    display: grid;
    place-items: center;
    border: 0;
    padding: 0;
    background: transparent;
    color: var(--color-primary);
    cursor: pointer;
    border-radius: var(--radius-full);
}

.nav-back-glyph {
    width: 0.625rem;
    height: 0.625rem;
    border-left: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(45deg);
    margin-left: 3px;
}

/* Desktop-only brand block; the phone nav keeps just the title. */
.nav-brand {
    display: none;
    color: var(--text-primary);
    text-decoration: none;
    align-items: center;
    gap: var(--space-sm);
    font-weight: 700;
    font-size: 1.125rem;
}
.nav-brand img { object-fit: contain; border-radius: var(--radius-sm); }
.nav-brand small { display: block; font-size: 0.75rem; font-weight: 400; color: var(--text-secondary); }

/* Account avatar: mirrored nav-back on phones (absolute so the title stays
   centred); the desktop media query puts it in the row, right of the tab
   pill. The popover card anchors to the nav bar and opens under it. */
.nav-avatar {
    position: absolute;
    right: var(--space-sm);
    width: 2.5rem;
    height: 2.5rem;
    padding: 0;
    display: grid;
    place-items: center;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-full);
    background: var(--color-surface);
    color: var(--text-tertiary);
    cursor: pointer;
}
/* Glyph and photo share the one grid cell so the photo overlays the
   placeholder instead of stacking under it; the photo is later in the DOM
   and paints on top. The photo rounds itself — the button no longer crops
   overflow, so the member badge is free to straddle the ring. */
.nav-avatar-img { grid-area: 1 / 1; width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
.nav-avatar-glyph { grid-area: 1 / 1; width: 1.375rem; height: 1.375rem; }

/* Lifetime membership on the avatar: a gold ring plus a crown badge centred
   on the ring's bottom-right edge, half over the photo and half over the
   page, so it reads as a seal on the ring instead of a speck inside it. */
.nav-avatar-member {
    border-color: #d4a017;
    box-shadow: 0 0 0 2px rgba(212, 160, 23, 0.35);
}
.nav-avatar-crown {
    grid-area: 1 / 1;
    align-self: end;
    justify-self: end;
    /* Nudge past the corner so the badge centre lands on the ring edge. */
    margin: 0 -0.25rem -0.25rem 0;
    width: 1.125rem;
    height: 1.125rem;
    padding: 2px;
    border-radius: 50%;
    background: var(--color-surface);
    color: #d4a017;
}

.nav-account-card {
    position: absolute;
    top: calc(100% + var(--space-sm));
    right: var(--space-sm);
    width: min(17rem, calc(100vw - 2 * var(--space-md)));
    z-index: 30;
}

.tab-bar {
    position: fixed;
    bottom: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 100%;
    max-width: 46rem;
    z-index: 20;
    display: flex;
    height: calc(var(--tab-height) + env(safe-area-inset-bottom));
    padding-bottom: env(safe-area-inset-bottom);
    background: rgba(255, 255, 255, 0.72);
    backdrop-filter: blur(20px) saturate(180%);
    -webkit-backdrop-filter: blur(20px) saturate(180%);
    /* Bright top edge — light catching the material. */
    border-top: 1px solid rgba(255, 255, 255, 0.6);
    box-shadow: 0 -1px 0 rgba(15, 23, 42, 0.04);
}

.tab-item {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    text-decoration: none;
    color: var(--text-tertiary);
    font-size: 0.75rem;
    letter-spacing: 0.02em;
    -webkit-tap-highlight-color: transparent;
}

.tab-item[aria-current='page'] {
    color: var(--color-primary);
}

.tab-glyph {
    width: 1.375rem;
    height: 1.375rem;
}

/* ---- Type ---------------------------------------------------------------- */

.display-title {
    margin: 0;
    font-size: clamp(1.5rem, 6vw, 1.875rem);
    /* Tight leading and negative tracking as the type grows. */
    line-height: 1.15;
    letter-spacing: -0.02em;
    font-weight: 700;
}

.section-title {
    margin: 0;
    font-size: 1rem;
    font-weight: 600;
    letter-spacing: 0.01em;
    color: var(--text-primary);
}

.body-text {
    margin: 0;
    font-size: 0.9375rem;
    line-height: 1.65;
    color: var(--text-secondary);
}

.muted-text {
    margin: 0;
    font-size: 0.8125rem;
    line-height: 1.6;
    /* Small type wants slightly positive tracking to stay legible. */
    letter-spacing: 0.01em;
    color: var(--text-tertiary);
}

.legal-note {
    margin: 0;
    text-align: center;
    font-size: 0.75rem;
    letter-spacing: 0.01em;
    color: var(--text-tertiary);
}

/* The profile hero is one card: identity left, purchase right on wide rows;
   phones stack the two halves. When the purchase half hides (unlimited
   accounts) the card falls back to a single column. Compound selector on
   purpose: the .card base further down sets display:flex and would otherwise
   win the cascade, leaving the two-column template dead. */
.card.profile-hero {
    display: grid;
    gap: var(--space-xl);
}

.eyebrow {
    margin: 0;
    color: var(--color-primary-dark);
    font-size: 0.8125rem;
    font-weight: 600;
    letter-spacing: 0.08em;
}

.flow-status { margin: 0; font-size: 0.875rem; color: var(--text-secondary); }
.flow-status:empty { display: none; }

/* ---- Buttons --------------------------------------------------------------- */

.button {
    appearance: none;
    border: 0;
    width: 100%;
    padding: 0.875rem var(--space-lg);
    border-radius: var(--radius-md);
    text-align: center;
    font-size: 1rem;
    font-weight: 600;
    letter-spacing: 0.01em;
    font-family: inherit;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    background: var(--color-primary);
    color: #fff;
    box-shadow: var(--shadow-chip);
}

.button:disabled {
    background: var(--color-surface-sunken);
    color: var(--text-tertiary);
    box-shadow: none;
    cursor: not-allowed;
}

.button-secondary {
    background: var(--color-surface);
    color: var(--color-primary);
    box-shadow: inset 0 0 0 1px var(--color-primary-tint);
}

.button-quiet {
    background: transparent;
    color: var(--text-tertiary);
    box-shadow: none;
    font-weight: 500;
    font-size: 0.875rem;
}

.button-danger {
    background: transparent;
    color: var(--color-danger);
    box-shadow: inset 0 0 0 1px rgba(239, 68, 68, 0.25);
}

.button-inline {
    width: auto;
    padding: 0.5rem var(--space-lg);
    font-size: 0.875rem;
}

/* ---- Cards ----------------------------------------------------------------- */

.card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-card);
    padding: var(--space-lg);
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
}

.card-hero {
    background: linear-gradient(135deg, var(--color-primary-wash), var(--color-surface));
    align-items: center;
    text-align: center;
    gap: var(--space-sm);
    padding: var(--space-xl) var(--space-lg);
}

/* Login has no card above it, so its hero carries the top breathing room. */
.card-hero-standalone {
    padding-top: var(--space-2xl);
}

/* ---- Badges ---------------------------------------------------------------- */

.badge {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    padding: 3px 0.625rem;
    border-radius: var(--radius-full);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    background: var(--color-surface-sunken);
    color: var(--text-tertiary);
    white-space: nowrap;
}

.badge.risk-high { background: var(--risk-high-bg); color: var(--risk-high-fg); }
.badge.risk-medium { background: var(--risk-mid-bg); color: var(--risk-mid-fg); }
.badge.risk-low { background: var(--risk-low-bg); color: var(--risk-low-fg); }
.badge.status-processing { background: var(--color-primary-wash); color: var(--color-primary); }
.badge.status-failed { background: var(--risk-high-bg); color: var(--risk-high-fg); }
.badge.status-completed { background: var(--risk-low-bg); color: var(--risk-low-fg); }
/* No verdict yet (pending task, no result, unknown level): neutral, same as
   the mini program's secondary-page mapping. */
.badge.risk-none,
.badge.risk-unknown,
.badge.status-pending,
.badge.status-unknown { background: var(--color-surface-sunken); color: var(--text-secondary); }

/* ---- Score hero (D-B) -------------------------------------------------------
   Port of miniapp .score-hero (rpx halved): the ring is the report's anchor;
   result and share render the identical shape, ring colour follows the three
   risk bands. The conic stop is data — ui.js scoreHero sets it as a style
   property. */

.score-hero { display: flex; align-items: center; gap: var(--space-lg); }
.score-hero.score-good { --ring-color: var(--color-primary); }
.score-hero.score-mid { --ring-color: var(--color-warning); }
.score-hero.score-bad { --ring-color: var(--color-danger); }
.score-ring { position: relative; flex: none; width: 96px; height: 96px; }
.score-ring-fill { position: absolute; inset: 0; border-radius: 50%; }
.score-ring-hole {
    position: absolute;
    inset: 7px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: var(--color-surface);
    box-shadow: inset 0 0 0 1px var(--color-border);
}
.score-value { font-size: var(--font-display); font-weight: 700; line-height: 1.1; letter-spacing: -0.02em; color: var(--text-primary); font-variant-numeric: tabular-nums; }
.score-caption { margin-top: 1px; font-size: var(--font-caption); color: var(--text-tertiary); }
.score-copy { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--space-sm); }
.score-level { font-size: var(--font-heading); font-weight: 700; letter-spacing: -0.01em; color: var(--text-primary); }
.score-counts { display: flex; gap: var(--space-xs); }
.badge.count-high { background: var(--risk-high-bg); color: var(--risk-high-fg); }
.badge.count-mid { background: var(--risk-mid-bg); color: var(--risk-mid-fg); }
.badge.count-low { background: var(--risk-low-bg); color: var(--risk-low-fg); }
.score-summary { font-size: var(--font-body-sm); line-height: var(--leading-normal); color: var(--text-secondary); overflow-wrap: break-word; }

/* ---- Form fields ----------------------------------------------------------- */

.field {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
}

.input {
    width: 100%;
    padding: 0.8125rem var(--space-md);
    border-radius: var(--radius-md);
    border: 1px solid var(--color-border);
    background: var(--color-surface);
    font: inherit;
    /* 16px minimum: anything smaller makes iOS Safari zoom on focus. */
    font-size: 1rem;
    color: var(--text-primary);
    transition: border-color 160ms var(--ease-settle),
        box-shadow 160ms var(--ease-settle);
}

.input:focus {
    outline: none;
    border-color: var(--color-primary);
    box-shadow: 0 0 0 3px var(--color-primary-wash);
}

.input-group {
    display: flex;
    gap: var(--space-sm);
    align-items: stretch;
}

.input-group .input {
    flex: 1;
    min-width: 0;
}

.field-error {
    font-size: 0.8125rem;
    color: var(--color-danger);
    min-height: 1.2em;
}

/* Inline validation, not on submit: the message appears next to the field. */
.field-error:empty {
    min-height: 0;
}

/* ---- Segmented choice (party role) ------------------------------------------ */

.segmented {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-md);
}

.segment {
    appearance: none;
    border: 2px solid transparent;
    background: var(--color-surface-sunken);
    border-radius: var(--radius-md);
    padding: var(--space-md);
    font-family: inherit;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    display: flex;
    flex-direction: column;
    gap: 2px;
    align-items: center;
    transition: background-color 200ms var(--ease-settle),
        border-color 200ms var(--ease-settle);
}

.segment-label {
    font-size: 1.0625rem;
    font-weight: 600;
    color: var(--text-primary);
}

.segment-desc {
    font-size: 0.75rem;
    color: var(--text-tertiary);
}

/* Selection reads as colour plus a border — no check glyph. */
.segment[aria-pressed='true'] {
    background: var(--color-primary-tint);
    border-color: var(--color-primary);
}

.segment:disabled,
.upload-zone[aria-disabled='true'] { cursor: default; }

/* ---- Upload zone ------------------------------------------------------------ */

.upload-zone {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-sm);
    min-height: 224px;
    padding: var(--space-2xl) var(--space-lg);
    border: 2px dashed var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-bg);
    text-align: center;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: border-color 200ms var(--ease-settle),
        background-color 200ms var(--ease-settle);
}

.upload-zone[data-dragging='true'] {
    border-color: var(--color-primary);
    background: var(--color-primary-wash);
}

.upload-zone[data-has-file='true'] {
    border-style: solid;
    border-color: var(--color-primary-tint);
}

.upload-zone .body-text {
    color: var(--color-primary-dark);
    font-weight: 600;
    font-size: 1.125rem;
}

.file-name {
    font-size: 1.0625rem;
    font-weight: 600;
    letter-spacing: -0.01em;
    color: var(--color-primary);
    word-break: break-all;
}

/* The real file input stays in the DOM for keyboard and assistive access but
   is never drawn; the upload zone is what the user actually sees and clicks.
   A class rather than an inline style, because the page's CSP allows no
   inline style attributes. */
.file-input {
    display: none;
}

/* ---- Progress, spinner, empty state ------------------------------------------ */

.progress-track {
    height: 4px;
    border-radius: var(--radius-full);
    background: var(--color-surface-sunken);
    overflow: hidden;
}

.progress-fill {
    height: 100%;
    border-radius: var(--radius-full);
    background: var(--color-primary);
    /* Width is driven frame by frame from JS, so no CSS transition here:
       a transition would fight the spring and lag the real progress. */
    width: 0;
}

.spinner {
    width: 1.25rem;
    height: 1.25rem;
    border-radius: 50%;
    border: 2px solid var(--color-primary-tint);
    border-top-color: var(--color-primary);
    animation: spin 720ms linear infinite;
}

@keyframes spin {
    to { transform: rotate(360deg); }
}

/* First-screen loading holds the shape of the rows it will become, so the
   swap to real content does not jump the layout. Same sweep as the mini
   program's .cg-skeleton. */
.skeleton {
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
    padding: var(--space-sm) 0;
}

.skeleton-line {
    height: 0.875rem;
    border-radius: var(--radius-sm);
    background: linear-gradient(90deg, var(--color-surface-sunken) 25%, var(--color-border) 50%, var(--color-surface-sunken) 75%);
    background-size: 400% 100%;
    animation: skeleton-sweep 1.4s ease-in-out infinite;
}

.skeleton-line-wide { width: 68%; }
.skeleton-line-narrow { width: 40%; }

@keyframes skeleton-sweep {
    from { background-position: 100% 0; }
    to { background-position: 0 0; }
}

.empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-2xl) var(--space-lg);
    text-align: center;
}

/* ---- Toasts -------------------------------------------------------------------- */

.toast-stack {
    position: fixed;
    left: 50%;
    bottom: calc(var(--tab-height) + env(safe-area-inset-bottom) + var(--space-xl));
    transform: translateX(-50%);
    z-index: 40;
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    align-items: center;
    pointer-events: none;
    width: max-content;
    max-width: min(24rem, calc(100vw - 2rem));
}

.toast {
    padding: 0.625rem var(--space-lg);
    border-radius: var(--radius-full);
    background: rgba(15, 23, 42, 0.86);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    color: #fff;
    font-size: 0.875rem;
    letter-spacing: 0.01em;
    text-align: center;
    box-shadow: var(--shadow-raised);
    animation: toast-enter 160ms ease-out;
}

@keyframes toast-enter { from { opacity: 0; transform: translateY(8px); } }

/* ===========================================================================
   Patterns — rows, lists and compositions reused across views
   =========================================================================== */

.row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-md);
}

.row-stack {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    min-width: 0;
}

.row-start {
    justify-content: flex-start;
}

.row-tight {
    gap: var(--space-sm);
}

.row-stack-centered {
    align-items: center;
}

.list {
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
    padding: 0;
    margin: 0;
    list-style: none;
}

/* A tappable row is its own surface: it presses, so it must look pressable. */
.tappable {
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    user-select: none;
    text-decoration: none;
    color: inherit;
}

.link-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-md);
    padding: var(--space-md) 0;
    border-bottom: 1px solid var(--color-border);
    font-size: 0.9375rem;
}

.link-row:last-child {
    border-bottom: 0;
}

/* .button is full-width for standalone primary actions; inside a row it must
   size to its label or it squeezes the flex:1 text column to nothing. */
.link-row .button {
    width: auto;
    flex: none;
}

/* A row that cannot be opened must not look like one that can: no pointer,
   and the text steps down to the muted colour. */
div.link-row {
    cursor: default;
    color: var(--text-secondary);
}

.chevron {
    flex: none;
    width: 0.5rem;
    height: 0.5rem;
    border-top: 2px solid var(--text-tertiary);
    border-right: 2px solid var(--text-tertiary);
    transform: rotate(45deg);
}

/* QR payment panel (CG-039) */

/* Purchase option cards — same shape as the mini program tier cards:
   each carries its own price; selection is a hairline upgrade, no shadow. */
.purchase-heading {
    display: flex;
    align-items: baseline;
    gap: var(--space-md);
}

.tier-options {
    display: flex;
    gap: var(--space-sm);
}

.tier-card {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-xs);
    min-height: 44px;
    padding: var(--space-md);
    font-family: inherit;
    text-align: left;
    cursor: pointer;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    transition: border-color 120ms var(--ease-settle), background-color 120ms var(--ease-settle);
}

.tier-card-active {
    border-color: var(--color-primary);
    background: var(--color-primary-wash);
}

.tier-card-label {
    font-size: var(--font-heading);
    font-weight: 600;
    color: var(--text-primary);
}

.tier-card-price-row {
    display: flex;
    align-items: baseline;
    gap: var(--space-xs);
}

.tier-card-amount {
    font-size: var(--font-title);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--text-primary);
}

.tier-card-original {
    font-size: var(--font-caption);
    color: var(--text-tertiary);
    text-decoration: line-through;
}

.tier-card-note {
    font-size: var(--font-caption);
    color: var(--text-secondary);
    /* The lifetime note is two lines in the mini program ('每月 5 次\n报告保留
       90 天'); the break is part of the copy. */
    white-space: pre-line;
}

/* Two channel buttons share one row instead of stacking full-width. */
.qr-actions {
    display: flex;
    gap: var(--space-sm);
}

.qr-actions .button {
    flex: 1;
    padding-inline: var(--space-sm);
    font-size: 0.9375rem;
}

.qr-panel {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-sm);
    text-align: center;
    padding: var(--space-sm) 0;
}

/* The QR SVG carries its own white background; the explicit white here keeps
   the code scannable even if the page theme is dark. */
.qr-image {
    width: 100%;
    max-width: 240px;
    aspect-ratio: 1;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: #fff;
}

/* Risk report */

.risk-item {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    padding: var(--space-lg) 0;
    border-bottom: 1px solid var(--color-border);
}

.risk-item:last-child {
    border-bottom: 0;
    padding-bottom: 0;
}

.risk-title {
    margin: 0;
    font-size: 1rem;
    font-weight: 600;
    letter-spacing: -0.01em;
}

.risk-location {
    font-size: 0.8125rem;
    color: var(--text-tertiary);
}

.risk-suggestion {
    padding: var(--space-md);
    border-radius: var(--radius-sm);
    background: var(--color-primary-wash);
    font-size: 0.875rem;
    line-height: 1.6;
    color: var(--text-secondary);
}

/* Invitation steps */

.invite-steps { margin: 0; padding-left: 1.5rem; color: var(--text-secondary); font-size: 0.9375rem; }
.invite-steps li { padding: 0.5rem 0 0.5rem 0.25rem; }
.invite-steps li::marker { color: var(--color-primary-dark); font-weight: 600; }

/* ===========================================================================
   Views — home, login, detect. Layout intent is an explicit class on the
   view element (set in JS), never a :has() probe into its content.
   =========================================================================== */

/* ---- Home ------------------------------------------------------------------- */

.home-hero {
    align-items: flex-start;
    text-align: left;
    gap: var(--space-lg);
    background: var(--color-primary-wash);
    border-color: var(--color-primary-line);
}

.home-hero .display-title { max-width: 12em; line-height: 1.3; }

.hero-action { width: auto; min-width: 192px; margin-top: var(--space-sm); }

.workflow-list { list-style: none; counter-reset: step; padding: 0; margin: var(--space-sm) 0 0; display: grid; gap: var(--space-xl); }

/* About page checklist, mirroring the mini program's ✓ feature rows. */
.feature-list { list-style: none; padding: 0; margin: var(--space-sm) 0 0; display: grid; gap: var(--space-sm); }
.feature-list li::before { content: '✓ '; color: var(--color-primary); font-weight: 600; }
.workflow-list li { counter-increment: step; position: relative; padding-left: var(--space-3xl); }
.workflow-list li::before { content: '0' counter(step); position: absolute; left: 0; top: 0; color: var(--color-primary-dark); font-size: 0.8125rem; font-weight: 600; background: var(--color-primary-wash); border-radius: var(--radius-sm); padding: var(--space-sm); }
.workflow-list strong { font-size: 0.9375rem; font-weight: 600; }
.workflow-list p { margin-top: var(--space-xs); }

/* ---- Login ------------------------------------------------------------------- */

.login-logo { width: 112px; height: 112px; object-fit: contain; border-radius: var(--radius-lg); }
.login-view .card-hero { background: transparent; gap: var(--space-lg); }
.login-view .display-title { white-space: pre-line; line-height: 1.35; }
.login-methods { display: grid; gap: var(--space-lg); min-width: 0; }
.login-methods .card { gap: var(--space-lg); position: relative; }

/* The WeChat entry hangs half off the phone card's corner: one login
   surface, with the scan as a detour instead of a parallel card. */
.wechat-qr-badge {
    position: absolute;
    top: 0;
    right: 0;
    transform: translate(50%, -50%);
    width: 3rem;
    height: 3rem;
    display: grid;
    place-items: center;
    padding: 0;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-full);
    background: var(--color-surface);
    box-shadow: var(--shadow-card);
    color: var(--color-primary-dark);
}
.wechat-qr-badge svg { width: 1.25rem; height: 1.25rem; }

/* The badge swaps the card between the phone form and the scan pane; both
   panes are flex columns mirroring the card's own gap. The explicit hidden
   rules win over the display set above, which the [hidden] attribute alone
   would lose to. */
.login-pane { display: flex; flex-direction: column; gap: var(--space-lg); }
.login-pane[hidden], .wechat-qr-badge[hidden] { display: none; }

/* The scan pane embeds WeChat's qrconnect page in SDK + stylelite mode. The
   lite template is a code-only panel; the frame is sized to its tallest
   state — the 160px QR plus the switch link beneath it, measured from
   WeChat's own stylesheet. The quick-login state is shorter and leaves
   transparent air at the frame's bottom: WeChat reserves it inside the
   cross-origin frame, so it cannot be cropped from here. No scaling, no
   cropping: every state fits, so there is neither an inner scrollbar nor
   dead chrome. */
.qr-frame {
    display: block;
    width: 15rem;
    height: 12.75rem;
    margin-inline: auto;
    border: 0;
    background: transparent;
}
/* The scan hint sits between two centred neighbours (frame above, back
   button below); left-aligned it reads as a layout mistake. */
.qr-status {
    text-align: center;
}

/* Login hides the tab bar, so the footer and the view stop reserving room
   for it and the disclaimer + filing pair lands right under the content. */
.app-shell:has(.login-view) .site-footer {
    padding-top: 0;
    padding-bottom: calc(env(safe-area-inset-bottom) + var(--space-lg));
}
.login-view { padding-bottom: var(--space-lg); }

/* Active opt-in to the legal docs before login, mirroring the mini program.
   Sits inside the login card under the code field, so it aligns left with
   the inputs instead of centering across the page. */
.login-agree {
    display: flex;
    align-items: flex-start;
    gap: var(--space-sm);
    font-size: var(--font-body-sm);
}
.login-agree p { margin: 0; }
.agree-checkbox { flex: none; margin-top: 0.25em; accent-color: var(--color-primary); }
.agree-link { color: var(--color-primary-dark); text-decoration: none; }

/* ---- Legal docs (用户协议 / 隐私政策) --------------------------------------- */

/* Long-form legal copy: one card, dense blocks, emphasised clauses get the
   brand wash so the "read these especially" paragraphs stand out. */
.legal-doc { gap: var(--space-md); }
.legal-meta { white-space: pre-line; font-size: var(--font-caption); }
.legal-h2 { margin-top: var(--space-md); }
.legal-h3 { font-size: var(--font-body); font-weight: 600; color: var(--text-primary); }
.legal-em {
    background: var(--color-primary-wash);
    border-radius: var(--radius-md);
    padding: var(--space-sm) var(--space-md);
}
/* The doc tables are four-column walls of text; they render as labelled
   cell stacks instead, one bordered block per row. */
.legal-table { display: flex; flex-direction: column; gap: var(--space-sm); }
.legal-table-row {
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    padding: var(--space-xs) var(--space-md);
}
.legal-cell {
    display: grid;
    grid-template-columns: 7em 1fr;
    gap: var(--space-sm);
    padding: var(--space-sm) 0;
}
.legal-cell + .legal-cell { border-top: 1px solid var(--color-border); }
.legal-cell-label { color: var(--text-tertiary); font-size: var(--font-caption); }
.legal-cell-text { color: var(--text-secondary); font-size: 0.875rem; line-height: 1.6; }

/* ---- Detect -------------------------------------------------------------------- */

/* The hero is a page header here, not a card: the upload zone below is the
   visual anchor, so the hero drops its card chrome. */
.detect-view > .card-hero {
    background: transparent;
    box-shadow: none;
    border: 0;
    padding-inline: 0;
}

/* Enterprise-analysis teaser, mirroring the mini program's feature card:
   icon left, title + description right. */
.feature-card {
    flex-direction: row;
    align-items: center;
    gap: var(--space-md);
}

.feature-icon {
    width: 2rem;
    height: 2rem;
    flex: none;
}

.feature-copy {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
}

.feature-title {
    margin: 0;
    font-size: 0.9375rem;
    font-weight: 600;
}

.feature-desc {
    margin: 0;
    font-size: var(--font-caption);
    color: var(--text-tertiary);
}

/* ===========================================================================
   Responsive — one block per breakpoint
   =========================================================================== */

@media (hover: hover) and (pointer: fine) {
    .button:not(:disabled):hover { filter: brightness(0.96); }
    .link-row:hover { background: var(--color-primary-wash); }
}

/* Desktop workspace, with a single-column reading order on phones. */
@media (min-width: 768px) {
    :root { --nav-height: 72px; }
    .app-shell { max-width: 72rem; }
    .view { max-width: 60rem; margin-inline: auto; padding-top: calc(var(--nav-height) + 2.5rem); padding-bottom: calc(env(safe-area-inset-bottom) + var(--space-2xl)); gap: var(--space-xl); }
    /* The tab bar lives in the nav row up top here, so the footer stops
       reserving its height at the bottom of the page. */
    .site-footer { padding-bottom: calc(env(safe-area-inset-bottom) + var(--space-lg)); }
    .card { padding: var(--space-xl); }
    .card-hero { align-items: flex-start; text-align: left; padding: var(--space-2xl); }
    .profile-hero { grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); align-items: start; }
    .profile-hero:has(.profile-purchase[hidden]) { grid-template-columns: 1fr; }
    .display-title { font-size: 2rem; }
    .nav-bar { justify-content: flex-start; gap: var(--space-xl); padding-inline: max(var(--space-xl), calc((100vw - 72rem) / 2 + var(--space-xl))); border-bottom: 1px solid var(--color-border); }
    /* The title only earns its place next to a back button; on tab roots the
       brand already says "CheckGO". Keys off the hidden state the router sets,
       via the fixed sibling order in index.html. */
    .nav-back[hidden] ~ .nav-title { display: none; }
    .nav-back { position: static; order: 1; }
    .nav-title { order: 2; font-size: 0.875rem; color: var(--text-secondary); }
    .nav-brand { display: flex; }
    .tab-bar {
        top: calc((var(--nav-height) - 44px) / 2);
        bottom: auto;
        left: auto;
        right: max(var(--space-xl), calc((100vw - 72rem) / 2 + var(--space-xl)));
        transform: none;
        width: 264px;
        height: 44px;
        padding: 0;
        border: 1px solid var(--color-border);
        border-radius: var(--radius-full);
        box-shadow: none;
    }
    .tab-item { flex-direction: row; gap: var(--space-sm); border-radius: var(--radius-full); font-size: 0.875rem; }
    .tab-item[aria-current='page'] { background: var(--color-primary-wash); font-weight: 600; }
    .tab-glyph { width: 1rem; height: 1rem; }
    /* The avatar sits right of the pill ("我的右边"); the pill only shifts
       when the avatar is actually shown, so a signed-out row stays flush. */
    .nav-avatar { position: static; order: 3; margin-left: auto; }
    .nav-account-card { right: max(var(--space-xl), calc((100vw - 72rem) / 2 + var(--space-xl))); }
    body.has-nav-avatar .tab-bar { right: calc(max(var(--space-xl), calc((100vw - 72rem) / 2 + var(--space-xl))) + 3.5rem); }
    .toast-stack { bottom: var(--space-2xl); }

    /* Home: pitch and workflow side by side, the rest full-width. */
    .home-view { max-width: 72rem; display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); align-items: stretch; }
    .home-view > .home-recent { grid-column: 1 / -1; }
    .home-hero { padding: 2.5rem; justify-content: center; }
    .home-hero .display-title { font-size: 2.5rem; }
    .workflow-card { justify-content: center; padding: var(--space-2xl); }

    /* Login: pitch left, methods right. */
    .login-view { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: center; padding-top: max(calc(var(--nav-height) + 2.5rem), 16vh); gap: var(--space-3xl); }
    .login-view .card-hero { padding: 0; }
    .login-view .display-title { font-size: 2.5rem; }

    /* Detect: the two step cards share the row; everything else spans. */
    .detect-view { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); }
    .detect-view > .card-hero,
    .detect-view > .feature-card,
    .detect-view > .button { grid-column: 1 / -1; }
    .detect-view > .button { width: 20rem; justify-self: center; }
}

@media (min-width: 768px) and (max-width: 1023px) {
    .nav-title { display: none; }
    .home-hero { padding: var(--space-xl); }
    .home-hero .display-title { font-size: 2rem; }
}

/* ===========================================================================
   Preferences — each accessibility axis degrades on its own
   =========================================================================== */

@media (prefers-reduced-transparency: reduce) {
    .nav-bar { background: var(--color-bg); backdrop-filter: none; -webkit-backdrop-filter: none; }
    .tab-bar { background: var(--color-surface); backdrop-filter: none; -webkit-backdrop-filter: none; }
    .toast { background: #0F172A; backdrop-filter: none; -webkit-backdrop-filter: none; }
}

@media (prefers-contrast: more) {
    .card { box-shadow: none; border: 1px solid var(--text-tertiary); }
    .input { border-color: var(--text-secondary); }
    .nav-bar,
    .tab-bar { background: var(--color-surface); backdrop-filter: none; -webkit-backdrop-filter: none; }
    .muted-text,
    .legal-note { color: var(--text-secondary); }
}

/* Reduced motion keeps the feedback and drops the travel: no slides, no
   springs, no overshoot — a short cross-fade carries the same meaning. */
@media (prefers-reduced-motion: reduce) {
    .spinner { animation-duration: 1600ms; }
    .skeleton-line { animation: none; }
    .segment,
    .upload-zone,
    .input { transition-duration: 1ms; }
    .toast { animation: none; }
    :where(.button, .tappable, .segment, .upload-zone, .nav-back):active { transform: none !important; }
}
