/* ============================================================================
   LoreLoom — Shared Design Tokens (foundation layer)
   Linked FIRST on every page, before styles.css / landing.css / pages.css.
   Purpose: one canonical source for breakpoints, spacing, fluid type, radius,
   and touch targets. Existing per-file :root palettes remain; this layer is
   additive and provides the shared scales the responsive refactor builds on.
   ============================================================================ */

:root {
    /* --- Canonical breakpoints -----------------------------------------------
       CSS custom properties can't drive @media queries, so these are the
       DOCUMENTED anchor values every stylesheet must use. Snap any ad-hoc
       breakpoint to the nearest anchor.
         --bp-sm  480px  small phone (portrait)
         --bp-md  768px  phone <-> tablet boundary  (primary mobile anchor)
         --bp-lg  1024px tablet <-> desktop          (sidebar becomes persistent)
         --bp-xl  1280px large desktop
       Reference values (not consumed by @media, kept for tooling/JS): */
    --bp-sm: 480px;
    --bp-md: 768px;
    --bp-lg: 1024px;
    --bp-xl: 1280px;

    /* --- Spacing scale (4px base) --------------------------------------------
       Replaces hardcoded padding/margin/gap values throughout the app. */
    --space-0: 0;
    --space-1: 4px;
    --space-2: 8px;
    --space-3: 12px;
    --space-4: 16px;
    --space-5: 24px;
    --space-6: 32px;
    --space-7: 40px;
    --space-8: 48px;
    --space-9: 64px;

    /* --- Fluid type scale (clamp: min, preferred-vw, max) --------------------
       Headings/body scale with viewport instead of per-breakpoint overrides. */
    --fs-caption: clamp(0.72rem, 0.68rem + 0.2vw, 0.8rem);
    --fs-body: clamp(0.9rem, 0.86rem + 0.25vw, 1rem);
    --fs-lead: clamp(1rem, 0.94rem + 0.35vw, 1.15rem);
    --fs-h3: clamp(1.15rem, 1rem + 0.7vw, 1.5rem);
    --fs-h2: clamp(1.4rem, 1.15rem + 1.2vw, 2rem);
    --fs-h1: clamp(1.7rem, 1.25rem + 2.2vw, 2.75rem);
    --fs-display: clamp(2rem, 1.3rem + 3.4vw, 3.75rem);

    /* --- Font families -------------------------------------------------------
       These live HERE, not in landing.css, because tokens.css is the only
       stylesheet all 19 pages link. Before this, --font-display/--font-body were
       defined in landing.css:26 only, so the three pages that load styles.css
       instead (dashboard, discover, create_character) had NO family tokens at
       all — which is why styles.css hardcoded 'Outfit' in 49 places.

       --font-heading was referenced in 9 pages.css rules and 2 footer.css rules
       and DEFINED NOWHERE. An undefined var() in font-family is invalid at
       computed-value time, and for an inherited property that means `inherit` —
       it does NOT fall back to an earlier valid declaration. So
       `.page-wrap h1 { font-family: var(--font-heading) }` beat
       `h1,h2,h3 { font-family: var(--font-display) }` on specificity and then
       evaporated, and every heading on the 15 content pages rendered in body
       Inter. Measured on about.html and blog.html: h1 -> Inter, while the footer
       wordmark (which wrote the fallback defensively, as
       `var(--font-heading, var(--font-display))`) -> Outfit. The fork was
       visible inside a single page.

       --font-heading points AT --font-display rather than repeating its value:
       two tokens holding the same literal is a fork waiting to happen. Change
       the display face in one place here and all three page groups follow.

       Fraunces, not Outfit: Outfit is a geometric sans a shade away from Inter,
       so the old "display" face and the body face read as one voice at a glance.
       Of the four faces considered, three failed a measured constraint —
       Gambetta is not on Google Fonts (HTTP 400), Instrument Serif ships weight
       400 only and these headings ask for 600/700/800/900 so it would have been
       synthetically emboldened, and Bricolage Grotesque is another grotesque
       sans. Fraunces is variable 100..900 with an optical-size axis (9..144),
       so one file covers the 70px hero and the ~16px card title and adjusts its
       contrast for each.

       The fallback stack is deliberately SERIF. If it listed sans-serif the page
       would change genre while the webfont loads, or permanently on a font
       failure. */
    --font-display: 'Fraunces', Georgia, 'Times New Roman', serif;
    --font-heading: var(--font-display);
    --font-body: 'Inter', system-ui, -apple-system, sans-serif;

    /* --- Radius (reconciles --border-radius-* and --radius) ------------------ */
    /* Six steps, because the measured distribution needed six and not four.
       styles.css declared 428 border-radius values across 42 distinct numbers;
       collapsing that onto sm/md/lg/pill alone would have forced 2px progress
       bars up to 12px and 4px badges up to 12px -- a 6x change on the smallest
       details in the product. 2xs and xs absorb that tail so the shift anywhere
       is at most 4px.

       These four existing values are load-bearing for all 19 pages, not just the
       dashboard: DO NOT change a number here to make some individual component
       land on its old value. Add a step or leave the component off the scale. */
    --radius-2xs: 4px;
    --radius-xs: 8px;
    --radius-sm: 12px;
    --radius-md: 16px;
    --radius-lg: 24px;
    --radius-pill: 999px;

    /* --- Touch targets ------------------------------------------------------- */
    --tap-min: 44px;

    /* --- Shared brand primitives (single source; both palettes point here) --- */
    --brand-accent: #a777e3;
    --brand-accent-2: #6e8efb;
    --brand-gradient: linear-gradient(135deg, #6e8efb, #a777e3);

    /* --- Action gradient -------------------------------------------------------
       The fill that means "this is the primary action". It is --brand-gradient's
       two stops in the OPPOSITE order, and that is deliberate rather than sloppy:
       --brand-gradient reads blue -> violet and is decorative (5 consumers via
       --accent-gradient: hero glows, section rules, progress tracks), while every
       primary button in the product reads violet -> blue. Flipping either to match
       the other would retint the decoration, so they stay as two tokens over one
       pair of primitives.

       FIVE rules used to declare this by hand, in five distinct gradients that all
       started at #a777e3 and then disagreed about where they ended -- differences
       nobody could name side by side, and all five had to be maintained:
         .glass-btn.primary   linear-gradient(135deg, #a777e3, #6e8efb)
         .hero-cta-primary    linear-gradient(135deg, #a777e3, #6e8efb)
         .shop-buy-btn        linear-gradient(135deg, #a777e3 0%, #7b44d4 50%, #6e3de7 100%)
         .stg-save-btn        linear-gradient(135deg, #a777e3 0%, #7d44cf 100%)
         .confirm-btn.primary linear-gradient(135deg, #a777e3, #6c5ce7)
       Plus a sixth site that no amount of CSS tokenising would have reached:
       dashboard.html carried three INLINE `background: linear-gradient(...)`
       attributes on primary buttons, which beat any rule at any specificity. Two of
       them were on `.confirm-btn.primary`, so the same modal Confirm button rendered
       #6e8efb in two dialogs and #6c5ce7 everywhere else. All three are gone; the
       class supplies the fill now.

       All five rules point here. .shop-buy-btn keeps its distinction where it was
       always doing the real work -- background-size: 200% 200% plus the
       btnGradientShift animation, which no other button has. That keyframe only
       animates background-position, so a 3-stop -> 2-stop swap does not touch it.
       Hover states are deliberately NOT tokenised: each is a lighten-on-hover of
       its own resting fill, which stays coherent, and they are a separate axis. */
    --action-gradient: linear-gradient(135deg, var(--brand-accent), var(--brand-accent-2));

    /* The label colour that goes ON the brand gradient. It is a token because the two
       are a PAIR: change the fill and every label has to be re-measured, and twenty-odd
       copies of a bare `color: #fff` do not say so.

       It is dark, and that was decided on pixels. White on this fill measures 3.03:1 on
       --brand-accent-2 and 3.29:1 on --brand-accent -- the ground under each label was
       read from a screenshot rather than modelled, because every ancestor-walking probe
       ignores background-image and had been scoring these against the near-black page
       instead of against the violet actually under the glyphs. 4.5:1 is required; none
       of these rules reaches 18.66px at weight 700, so 1.4.3's large-text relaxation to
       3:1 does not rescue any of them.

       Two ways out, both measured, and the cheaper one is not the obvious one:
         * darken the fill and keep white. Holding hue and saturation exactly, white
           needs #a777e3 -> #9358dd and #6e8efb -> #406afa, which is L* -9.0 and -11.4.
           At that distance a colour does not read as a darker shade of itself, it reads
           as a different colour -- so it changes the product's primary action colour,
           and only reaches 4.50:1 with no headroom.
         * keep the fill and darken the label. #1a1523 scores 5.44:1 on --brand-accent
           and 5.89:1 on --brand-accent-2, clearing the line with over a point in hand,
           and --brand-accent / --brand-accent-2 / --action-gradient stay untouched.
       The second is this.

       Scope is every rule that puts a LABEL on these two stops, not just the buttons:
       the user's own chat bubble, the diary's user bubble, the create buttons, the
       quest-modal buttons, the selected date cell, the facet count chips, the avatar
       initials. Half-applying it is worse than either extreme -- two primary buttons
       side by side with different label colours reads as a bug, which is why this is
       one token and not a per-rule judgement.

       Deliberately NOT on this token, and each for its own reason:
         * icon-only containers that already clear 1.4.11's 3:1 with white (#send-btn
           3.03, #scroll-to-bottom-btn 3.29). A glyph is not text; they are not
           violations, and recolouring them would be an unrequested aesthetic change.
         * .stg-save-btn:disabled, which replaces the fill with its own dark
           rgba(255,255,255,0.05). Washed-white is correct there, and an inactive
           component is exempt from 1.4.3 anyway.
         * the other accent families -- #b98cf0 (diary), #a78bfa (quests), #ff8a4b
           (ember cosmetic skin), #e74c3c (danger). They fail too, at 2.60 to 3.82, and
           #1a1523 would fix all of them (6.87 / 6.56 / 7.65 / 4.68). But white-on-red
           for danger and a warm-on-warm purchased cosmetic are deliberate design, not
           drift, so each is a separate call rather than a sweep.

       To go back to white text, this token is not enough on its own -- --brand-accent
       and --brand-accent-2 have to come down by that L* 9-11 at the same time, or every
       one of these surfaces fails 1.4.3. */
    --on-brand-gradient: #1a1523;

    /* --- Focus ring -----------------------------------------------------------
       One colour for every keyboard focus ring in the product. Before this token
       there were four focus rules using three different colours: var(--accent-color)
       (= --brand-accent, #a777e3), #c9a0f5 and #c9a6ff twice.
       #c9a6ff wins on two measured grounds, not on taste:
         * against the dark page ground it is 9.7:1, where --brand-accent is 6.0:1
           (WCAG 1.4.11 asks 3:1 for a focus indicator, so both pass -- but the ring
           has to read instantly, and this one does at a glance).
         * --brand-accent is unusable as a ring on .glass-btn.primary, whose own
           background IS linear-gradient(135deg, #a777e3, #6e8efb): a #a777e3 ring
           against #a777e3 is 1.0:1. #c9a6ff against that fill is 1.6:1, which is
           still poor -- which is why every ring in this file uses a POSITIVE
           outline-offset. The ring lands on the page ground, never on the button.
       Change this value and every focus ring in the product changes with it. */
    --focus-ring: #c9a6ff;

    /* --- Semantic status colours ---------------------------------------------
       #ff416c used to do two unrelated jobs: semantic danger (delete, unfollow,
       error toast, failed PIN) and decorative atmosphere (a hero orb, a shop
       mesh). Splitting them is what makes the decorative uses safe to recolour
       onto the brand hues without silently repainting every error state.
       The values here are the previous literals unchanged — adding this block
       on its own repaints nothing.
       -rgb holds bare channels because ~20 call sites need the same hue at a
       different alpha: rgba(var(--color-danger-rgb), 0.12).
       NOT semantic and deliberately left as literals: .card-tag.tag-tone (one
       hue in a six-category tag system) and .shop-card--premium (a warm stop
       inside a brand-violet shimmer). Retokenising those would make a future
       danger-red change leak into unrelated components. */
    --color-danger: #ff416c;
    --color-danger-rgb: 255, 65, 108;
    --color-danger-deep: #ff4b2b;
    --color-danger-border: rgba(var(--color-danger-rgb), 0.4);
    --color-ok: #34d399;
    --color-ok-rgb: 52, 211, 153;

    /* --- Layout anchors ------------------------------------------------------ */
    --sidebar-w: 260px;
    --settings-nav-w: 230px;

    /* --- Safe-area convenience (0 on non-notched devices) --------------------
       The ONLY place safe-area insets enter the stylesheets — nothing should call
       env(safe-area-inset-*) directly. That matters because the Android app
       overrides these four as inline styles on <html> (app-insets.js): Android
       WebView reports env() as 0 for the status and navigation bars, so a rule
       reading env() directly would silently stay unpadded in the app no matter
       how correct it looks. Routing everything through these tokens means the
       native override reaches every consumer at once, and the env() fallbacks
       below keep the web and iOS behaviour exactly as it was. */
    --safe-top: env(safe-area-inset-top, 0px);
    --safe-right: env(safe-area-inset-right, 0px);
    --safe-bottom: env(safe-area-inset-bottom, 0px);
    --safe-left: env(safe-area-inset-left, 0px);

    /* --- Mobile density layer -------------------------------------------------
       Surfaces consume these instead of hardcoding a phone padding each time,
       so retuning density is one edit here rather than an edit across ~60
       media queries. The values on :root are the DESKTOP defaults; the two
       blocks below redefine them at the canonical mobile breakpoints. */
    --pad-page: 32px;
    /* .page horizontal gutter */
    --pad-section: 24px;
    /* interior padding of a large section panel */
    --pad-card: 22px;
    /* interior padding of a card */
    --gap-card: 16px;
    /* gap between stacked cards / sections */
    --tab-bar-h: 0px;
    /* mobile bottom tab bar; 0 while it isn't rendered */
}

/* Only the three canonical mobile breakpoints (768 / 560 / 400) are used for new
   work — see the breakpoint policy note at the top of styles.css. */
@media (max-width: 768px) {
    :root {
        --pad-page: 16px;
        --pad-section: 16px;
        --pad-card: 14px;
        --gap-card: 12px;
        --tab-bar-h: 56px;
    }
}

@media (max-width: 400px) {
    :root {
        --pad-page: 12px;
        --pad-section: 14px;
        --pad-card: 12px;
        --gap-card: 10px;
    }
}
