/* ═══════════════════════════════════════════════════════════════
   FONTS — self-hosted, font-display: optional
═══════════════════════════════════════════════════════════════ */
/* CLS root cause (confirmed via chrome-devtools' CLSCulprits trace insight,
   2026-08-06): these three fonts were loaded from Google Fonts with
   font-display: swap -- text paints immediately in the fallback font, then
   swaps to the real one once it arrives, and that swap IS the layout shift
   (Fraunces/DM Sans have different glyph metrics than their fallbacks). This
   was independent of any other change: confirmed by reproducing on
   unmodified master (0.25-0.54 CLS locally under the same methodology).
   `swap` and `optional` are not two points on the same "how long do we wait"
   dial -- swap GUARANTEES the shift eventually happens (just later); optional
   is the only display mode that removes the shift as a possibility at all,
   because the browser commits to whichever font (real or fallback) is
   available within its ~100ms period and never swaps again for that page
   load, cached or not. `font-display: swap` remains on Google's URL in case
   a future edit reintroduces it there; self-hosting the three files below
   means this @font-face block is the one actually used, not Google's.
   These are the EXACT SAME bytes Google's CDN was serving for this site's
   CSS request (verified via fontTools against the fetched Google Fonts CSS
   before downloading) -- self-hosting is a zero-visual-diff origin change,
   not a different font. Weight ranges below match each file's real fvar
   axis (verified with fontTools, not assumed from the Google Fonts URL).
   Cached via service-worker.js's SHELL_FILES like the rest of the shell, so
   repeat visits (the primary usage pattern for an installed, habitually-
   checked PWA) never hit the network for these at all. */
@font-face {
  font-family: "Fraunces";
  src: url("fonts/fraunces-variable-latin.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: optional;
}
@font-face {
  font-family: "DM Sans";
  src: url("fonts/dmsans-variable-latin.woff2") format("woff2");
  font-weight: 100 1000;
  font-style: normal;
  font-display: optional;
}
@font-face {
  font-family: "Syne";
  src: url("fonts/syne-variable-latin.woff2") format("woff2");
  font-weight: 400 800;
  font-style: normal;
  font-display: optional;
}

/* Rupee sign (₹, U+20B9) — a 1.1KB subset covering exactly one glyph, needed
   by EVERY visitor regardless of language: none of the three Latin faces
   above (verified via fontTools cmap check, 2026-08-10) include U+20B9, so
   every price display on this page (hero price, comparison cards, karat
   strip, methodology stats) was already falling all the way through to the
   full Noto Sans Devanagari face just to paint one currency symbol -- a
   121KB fetch, unconditionally, for every English visitor, contradicting
   this file's own long-standing "costs nothing for English visitors" claim
   about the Devanagari fonts below. Adding the Serif Devanagari face
   (2026-08-10) would have doubled that unconditional cost to ~248KB before
   this fix. This subset (built from Noto Sans Devanagari's own ₹ glyph via
   `fonttools subset --unicodes=U+20B9`, keeps its wght 100-900 variable axis
   so it interpolates to match surrounding text weight same as any other
   glyph) is inserted into all three stacks below, before either Devanagari
   fallback, and both Devanagari faces' unicode-range is trimmed to exclude
   U+20B9 so there's no overlap/ambiguity about which face serves the glyph.
   Precached in SHELL_FILES and preloaded unconditionally in index.html, like
   the three Latin faces above -- not gated behind html[lang="hi"] the way the
   Devanagari faces are, since ₹ appears on every page load either way. */
@font-face {
  font-family: "Rupee Sign";
  src: url("fonts/rupee-sign.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: optional;
  unicode-range: U+20B9;
}

/* Devanagari (Hindi) — Noto Sans Devanagari, devanagari-subset only (not the
   latin/latin-ext subsets Google also serves under this family — English text
   on this page already has Fraunces/DM Sans/Syne, so only the Devanagari glyph
   range is needed here). Same self-hosting rationale as the three faces above:
   the exact bytes Google's CDN serves for this subset (verified via fontTools:
   full U+0900-097F coverage, wght 100-900 variable, confirmed 2026-08-10), same
   font-display:optional CLS discipline. unicode-range excludes U+20B9 (₹) --
   see the Rupee Sign face above, which owns that codepoint exclusively so this
   121KB face is never fetched just to paint a currency symbol.
   Declaring this rule costs nothing for English visitors on its own — a browser
   only fetches an @font-face's src when something on the page actually renders
   using that font-family. The family is appended, unscoped, to every stack in
   the TOKENS block below (see that comment for why) rather than gated by an
   html[lang="hi"] selector — the unicode-range above is what keeps the fetch
   conditional instead, and this file's own TOKENS comment names the one
   real exception (the language toggle glyph) to the "never on an English
   page" claim. */
@font-face {
  font-family: "Noto Sans Devanagari";
  src: url("fonts/notosans-devanagari-variable.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: optional;
  unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF;
}

/* Devanagari display face — pairs with Fraunces in --display so Hindi headings/
   verdict text get the same serif hierarchy the English page has, instead of
   falling through to the same Noto Sans face body text uses (the pre-2026-08-10
   behavior: every Hindi weight/role looked identical because --display,
   --heading, and --sans all resolved to one flat sans face for Devanagari,
   English's serif/sans distinction. Same self-hosting rationale and verification
   as the Sans face above: exact bytes Google's CDN serves for this subset
   (verified via fontTools: full U+0900-097F coverage, wght 100-900 variable,
   confirmed 2026-08-10). --heading/--sans deliberately keep Noto Sans Devanagari
   unchanged -- only --display's Devanagari member changes. unicode-range
   excludes U+20B9 (₹) for the same reason as the Sans face above. */
@font-face {
  font-family: "Noto Serif Devanagari";
  src: url("fonts/notoserif-devanagari-variable.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: optional;
  unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09;
}

/* ═══════════════════════════════════════════════════════════════
   TOKENS
═══════════════════════════════════════════════════════════════ */
:root {
  /* Dark-mode palette (default) */
  --ink:          #1A1612;   /* page background */
  --surface:      #241E16;   /* card background */
  --surface-2:    #2C2218;   /* elevated surface */
  --surface-alt:  #1C1710;   /* alternate surface for table row tinting */
  --line:         #3A3028;   /* border */
  --line-strong:  #4A3E32;

  --cream:        #F5EDE0;   /* primary text */
  --cream-dim:    #C8BFAE;   /* secondary text */
  --cream-mute:   #9A9282;   /* muted text — WCAG AA 4.86:1 on --surface-2 */

  /* Gold palette — new umber spectrum */
  --gold-deep:    #633806;   /* light mode price */
  --gold-rich:    #854F0B;
  --gold-warm:    #BA7517;   /* sparkline accent */
  --gold-soft:    #FAEEDA;   /* sparkline fill, soft accent */
  --gold:         #E09B2E;   /* dark mode price, links, highlights */
  --gold-glow:    rgba(212, 147, 42, 0.18);

  /* Hero price material — a gold-metal gradient (warm > primary > rich),
     tuned per color-scheme below since --gold itself already means a
     different literal color in each mode. */
  --hero-gradient: linear-gradient(135deg, var(--gold-warm) 0%, var(--gold) 45%, var(--gold-rich) 100%);

  /* Semantic up/down */
  --up:           #c66a4b;   /* terracotta — price rose (bad for buyer) */
  --down:         #6a9a72;   /* sage — price fell (good for buyer) */

  /* Verdict banner token colours (dark mode) */
  --verdict-down-bg:     rgba(106, 154, 114, 0.14);
  --verdict-down-border: rgba(106, 154, 114, 0.35);
  --verdict-down-text:   #B8E6C4;

  --verdict-up-bg:       rgba(198, 106, 75, 0.14);
  --verdict-up-border:   rgba(198, 106, 75, 0.35);
  --verdict-up-text:     #F0A898;

  --verdict-flat-bg:     rgba(212, 147, 42, 0.10);
  --verdict-flat-border: rgba(212, 147, 42, 0.28);
  --verdict-flat-text:   #E8C87A;

  --verdict-unknown-bg:     var(--surface-2);
  --verdict-unknown-border: var(--line);
  --verdict-unknown-text:   var(--cream-mute);

  /* Comparison card sentiments */
  --cmp-good-bg:     rgba(106, 154, 114, 0.22);
  --cmp-good-border: rgba(106, 154, 114, 0.30);
  --cmp-good-text:   #9ED8AA;

  --cmp-caution-bg:     rgba(212, 147, 42, 0.20);
  --cmp-caution-border: rgba(212, 147, 42, 0.28);
  --cmp-caution-text:   #E8C87A;

  /* System/freshness status colors — a deliberately distinct hue family from
     --up/--down (price direction) and --cmp-good/--cmp-caution (comparison
     sentiment). Previously freshness--ok reused var(--down) and
     freshness--stale reused var(--up) directly, so "data is fresh" and
     "price fell" shared a color channel, as did "not updating" and "price
     rose" -- a red header tint could be misread as a price alarm rather
     than a connectivity/staleness one. This is the only cool-toned family
     on an otherwise warm palette, on purpose: reserved exclusively for
     system/connectivity state so its meaning never overlaps with content. */
  --status-live:          #7FADC4;  /* fresh/live data -- muted steel-blue */
  --status-live-bg:       rgba(127, 173, 196, 0.14);
  --status-live-border:   rgba(127, 173, 196, 0.38);

  --status-carryover:        #C9A15A;  /* estimate / weekend carryover / mild staleness -- muted bronze, distinct from --gold's brighter warm tone */
  --status-carryover-bg:     rgba(201, 161, 90, 0.14);
  --status-carryover-border: rgba(201, 161, 90, 0.38);

  --status-stale:        #E0637A;  /* not updating / offline -- rose-crimson, distinct from --up's orange-terracotta */
  --status-stale-bg:     rgba(224, 99, 122, 0.16);
  --status-stale-border: rgba(224, 99, 122, 0.42);

  /* Typography */
  /* Rupee Sign + both Devanagari faces appended to their stacks, not scoped by
     html[lang] -- each is the ONLY font in its stack covering its glyph(s)
     (Fraunces/Cormorant/Syne/DM Sans cover neither Devanagari nor ₹, verified
     via fontTools, 2026-08-10), so per-character font fallback already routes
     each character to the right face automatically, on English pages AND
     Hindi pages alike, with no selector needed.
     Rupee Sign is NOT the "costs English visitors nothing" case the two
     Devanagari faces are -- ₹ appears in every price display regardless of
     language, so this ~1KB face is fetched unconditionally by every visitor,
     same tier as Fraunces/DM Sans/Syne (see its own @font-face comment for
     the full story of why this face exists at all).
     The two Devanagari faces genuinely do cost English visitors nothing on
     their own: each face's unicode-range (U+0900-097F et al., now excluding
     U+20B9 -- see their own comments) means the browser only ever fetches one
     when a Devanagari character actually needs painting -- true everywhere on
     an English page except one: #lang-toggle's own label reads "हिं" while
     English is active (app.js's applyLanguage, "shows the OTHER language"),
     so that one 2-glyph string (rendered via --sans, so only Noto Sans
     Devanagari) is a real, accepted exception to "never on an English page".
     Not preloaded for it (the preload above stays conditional on Hindi being
     the resolved starting language) -- worst case those two glyphs paint in
     the fallback stack's own Devanagari coverage (present on every mainstream
     OS) until/unless the font arrives, never a blank/tofu render, and the
     cost of always preloading a whole variable font for two button glyphs
     would outweigh that. Also means numerals/Latin mixed into Hindi text
     (prices, "22K", the EN toggle label) keep rendering via DM Sans's
     tabular-nums, not either Devanagari face's digit shapes -- exactly the
     mixed-script behavior we want.
     --display uses the Serif face (not Sans) deliberately -- see that
     @font-face's own comment for why: it's what gives Hindi headings/verdict
     text the same serif hierarchy Fraunces gives English, instead of every
     Hindi role collapsing onto one flat sans face. --heading/--sans keep Noto
     Sans Devanagari, matching Syne/DM Sans's own sans character. */
  --display: "Fraunces", "Rupee Sign", "Cormorant Garamond", "Noto Serif Devanagari", Georgia, serif;
  --heading: "Syne", "Rupee Sign", "Noto Sans Devanagari", system-ui, sans-serif;
  --sans:    "DM Sans", "Rupee Sign", "Noto Sans Devanagari", system-ui, -apple-system, sans-serif;

  /* Spacing (8px base) */
  --space-1:  4px;  --space-2:  8px;  --space-3: 12px;
  --space-4: 16px;  --space-5: 20px;  --space-6: 24px;
  --space-7: 32px;  --space-8: 48px;  --space-9: 64px;

  /* Type scale */
  --ts-xs:   11px;  --ts-sm:  13px;  --ts-base: 16px;
  --ts-md:   20px;  --ts-lg:  24px;  --ts-xl:   32px;
  --ts-2xl:  40px;  --ts-3xl: 56px;

  /* Radii */
  --radius-sm: 10px;
  --radius-md: 12px;
  --radius-lg: 16px;

  /* Elevation — dark-tuned: a soft dark drop shadow reads as depth against
     --ink, paired with a faint top-edge highlight (a hairline of the
     surface catching light from above) since a plain drop-shadow alone
     reads flat on a dark background. Light mode below overrides both. */
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.30), inset 0 1px 0 rgba(255, 255, 255, 0.03);
  --shadow-md: 0 4px 14px rgba(0, 0, 0, 0.34), inset 0 1px 0 rgba(255, 255, 255, 0.04);
  --shadow-lg: 0 10px 28px rgba(0, 0, 0, 0.38), inset 0 1px 0 rgba(255, 255, 255, 0.05);
}

/* Light mode overrides — colours only, layout unchanged */
@media (prefers-color-scheme: light) {
  :root {
    --ink:          #FBF7EE;
    --surface:      #FFFFFF;
    --surface-2:    #F5EDE0;
    --line:         #E2D5C0;
    --line-strong:  #C8B89A;

    --cream:        #1A1208;
    --cream-dim:    #412C14;
    --cream-mute:   #6B5540;

    --gold:         #633806;   /* --gold-deep becomes primary in light mode */
    --gold-glow:    rgba(99, 56, 6, 0.12);

    --verdict-down-text:   #173404;
    --verdict-up-text:     #501313;
    --verdict-flat-text:   #412402;

    --cmp-good-text:    #173404;
    --cmp-caution-text: #412402;

    /* Status family — darkened/richened for contrast on a light surface,
       same pattern as the verdict/comparison text overrides above. */
    --status-live:          #2E5A72;
    --status-live-bg:       rgba(46, 90, 114, 0.10);
    --status-live-border:   rgba(46, 90, 114, 0.30);

    --status-carryover:        #7A5A1E;
    --status-carryover-bg:     rgba(122, 90, 30, 0.10);
    --status-carryover-border: rgba(122, 90, 30, 0.30);

    --status-stale:        #A32B44;
    --status-stale-bg:     rgba(163, 43, 68, 0.10);
    --status-stale-border: rgba(163, 43, 68, 0.28);

    /* Elevation — light mode: a conventional soft shadow off a light
       surface; no top-highlight needed (nothing to catch light against). */
    --shadow-sm: 0 1px 3px rgba(26, 18, 8, 0.07);
    --shadow-md: 0 4px 14px rgba(26, 18, 8, 0.09);
    --shadow-lg: 0 10px 28px rgba(26, 18, 8, 0.12);

    /* Light mode needs darker stops than dark mode for contrast on white. */
    --hero-gradient: linear-gradient(135deg, var(--gold-rich) 0%, var(--gold-deep) 55%, var(--gold-deep) 100%);
  }
}

/* ═══════════════════════════════════════════════════════════════
   DEVANAGARI TEXT SAFETY (html[lang="hi"])
═══════════════════════════════════════════════════════════════ */
/* Bug (reported 2026-08-10): "आज का सार" (todaysReadEyebrow) rendered as
   "सा र" -- a visible gap splitting सार apart. Root cause confirmed by
   tracing the exact element: .commentary-eyebrow carries letter-spacing:
   0.18em (for the uppercase-tracking look on Latin/English text), and
   Devanagari conjuncts + matras are positioned by the shaping engine
   assuming zero inter-glyph space -- any nonzero letter-spacing forces
   the browser to insert a gap between glyphs that must sit flush (base
   consonant + matra, or two conjunct-forming consonants), breaking the
   shaped cluster apart mid-word. This is universal to the script, not a
   quirk of this one string: EVERY element below that can carry Hindi
   text and also declares its own letter-spacing (which wins over
   inheritance -- letter-spacing is inherited, but an element's own
   explicit value always overrides an inherited one, so setting this only
   on <html> would not reach any of them) needs the override restated here
   with higher specificity (the html[lang="hi"] prefix beats the bare
   selector everywhere in this file, since every one of theirs is a single
   class/type selector with no attribute prefix of its own).
   Deliberately excludes elements that only ever render digits/₹ amounts
   (.hero-price via h1, .cmp-value, .karat-price, .karat-label, .meth-stat-value,
   Chart.js canvas labels) -- their letter-spacing is real, intentional
   numeral tracking, unrelated to this bug, and safe to keep since no
   Devanagari character is ever painted through them.
   New translatable element? If it can render Hindi text (any data-i18n*
   target, or any element app.js fills via t()) AND the class you give it
   sets its own letter-spacing, add that class to this list -- otherwise
   it inherits this exact bug the next time someone reaches for
   letter-spacing on an uppercase/tracked label. */
html[lang="hi"] h2,
html[lang="hi"] h3,
html[lang="hi"] .app-title,
html[lang="hi"] .freshness-pill,
html[lang="hi"] .hero-location,
html[lang="hi"] .lang-toggle,
html[lang="hi"] .eyebrow,
html[lang="hi"] .hero-change-label,
html[lang="hi"] .verdict-headline,
html[lang="hi"] .sparkline-label-left,
html[lang="hi"] .sparkline-label-right,
html[lang="hi"] .cmp-label,
html[lang="hi"] .cmp-sub,
html[lang="hi"] .karat-sub,
html[lang="hi"] .commentary-eyebrow,
html[lang="hi"] .commentary-meta,
html[lang="hi"] .section-karat-note,
html[lang="hi"] .history-table th,
html[lang="hi"] .methodology-summary,
html[lang="hi"] .meth-stat-label,
html[lang="hi"] .stale-banner,
html[lang="hi"] .offline-banner,
html[lang="hi"] .bottom-nav-item,
html[lang="hi"] .hcard-time {
  letter-spacing: normal;
}

/* ═══════════════════════════════════════════════════════════════
   RESET & BASE
═══════════════════════════════════════════════════════════════ */
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }

/* ─── FOCUS STATE — global, keyboard-only ────────────────────────────────────
   No explicit focus styling existed anywhere before this -- every
   interactive element relied on the browser's own default outline, which
   is inconsistent across custom-appearance controls (pills, circular icon
   buttons, cards). :focus-visible (not :focus) so this never shows on a
   mouse/touch click, only on real keyboard navigation. */
:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

/* ─── TAP FEEDBACK — Ψ3C.2 (CSS-only, global) ────────────────────────────── */
/* All buttons: transparent highlight + scale-down on press */
button {
  -webkit-tap-highlight-color: transparent;
  transition: transform 100ms ease-out;
}
button:active { transform: scale(0.95); }

body {
  background: var(--ink);
  color: var(--cream);
  font-family: var(--sans);
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  min-height: 100vh;
  min-height: 100dvh;
  /* Safe-area top is absorbed by .app-header (Ψ3C.1b) — body only handles sides */
  padding-left:  env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
  background-image:
    radial-gradient(1100px 600px at 90% -10%, rgba(212,147,42,0.07), transparent 60%),
    radial-gradient(900px 500px at -10% 110%, rgba(212,147,42,0.04), transparent 70%);
}

a { color: var(--gold); text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 3px; }

h1 { margin: 0; font-family: var(--display); font-weight: 400; letter-spacing: -0.02em; }
h2 { margin: 0; font-family: var(--heading); font-weight: 700; letter-spacing: -0.01em; }
h3 { margin: 0; font-family: var(--display); font-weight: 500; letter-spacing: -0.01em; }

/* ─── Grain overlay ─── */
.grain {
  position: fixed; inset: 0; pointer-events: none; z-index: 1;
  opacity: 0.05; mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.7'/></svg>");
}

main {
  position: relative; z-index: 2;
  max-width: 980px;
  margin: 0 auto;
  /* Bottom padding clears the fixed bottom-nav (56px) + safe area + breathing room */
  padding: clamp(20px, 4vw, 48px) clamp(20px, 5vw, 56px)
           calc(80px + env(safe-area-inset-bottom, 0px));
}

/* ═══════════════════════════════════════════════════════════════
   DESKTOP TWO-ZONE GRID (≥1024px)
   Below 1024px both wrappers are `display: contents` -- they vanish from
   the box tree entirely (children become direct flow children of `main`,
   identical DOM order, zero selectors elsewhere reference `main >` so
   nothing else depends on this), which is how this stays a true no-op on
   mobile/tablet rather than an extra nested block needing its own spacing
   reset. Grid kicks in only at the breakpoint below.
═══════════════════════════════════════════════════════════════ */
.layout-left, .layout-right { display: contents; }

@media (min-width: 1024px) {
  main { max-width: 1240px; }

  .layout-grid {
    display: grid;
    grid-template-columns: minmax(320px, 400px) 1fr;
    gap: clamp(var(--space-7), 4vw, var(--space-9));
    /* Deliberately NOT align-items:start. Sticky positioning's constraint
       rectangle is the STICKY ELEMENT'S OWN containing block (.layout-left,
       its direct block-container ancestor) -- not the grid area/track. With
       align-items:start, .layout-left's own box shrinks to hero-card's
       content height, so the sticky constraint rectangle is only ~700px
       tall and releases almost immediately on scroll (confirmed empirically:
       hero-card vanished after ~1200px of scroll, nowhere near the page's
       real length). Default align-items (stretch) makes .layout-left fill
       the full row height (row auto-sizes to the taller right column
       regardless), giving hero-card's sticky containing block real height
       to move within for the whole page. hero-card itself is NOT stretched
       by this -- position:sticky elements size to their own content
       regardless of an ancestor's stretched height, so the visible card
       stays hero-card-sized either way; only the invisible containing box
       around it needs to be tall. */
  }
  .layout-left, .layout-right { display: block; }

  /* Hero stays in view while the right column scrolls past it. Releases
     cleanly once .layout-left's (now full-height, see above) containing
     block ends -- i.e. once the right column's content ends -- no manual
     height/overflow math needed, and no risk of trapping scroll since
     nothing here sets overflow on any ancestor between this and the
     viewport. On short viewports where the hero card's own height plus the
     sticky offset exceeds the visible area, it simply isn't fully visible
     while stuck (same as any sticky element) -- it never clips or scrolls
     internally, so no content becomes unreachable, and normal page scroll
     still applies once the containing block's height is exhausted. Verified
     at 1024px and 1280px, including short (~700px) viewport heights, and
     confirmed the fix by scrolling well past 1200px and seeing hero-card
     still pinned, via live screenshots. */
  .layout-left .hero-card {
    position: sticky;
    top: calc(52px + env(safe-area-inset-top, 0px) + var(--space-6));
  }

  /* .hero-price's mobile sizing is `clamp(56px, 16vw, 160px)` -- 16vw tracks
     the viewport because on mobile the hero IS the viewport width (up to
     main's 980px cap). In the two-zone grid the left column is a FIXED
     minmax(320px, 400px) regardless of viewport, so the same vw-based clamp
     hits its 160px ceiling while the column is only ~400px wide, overflowing
     the price off the edge of its own card. Override with a column-width-
     appropriate ceiling instead. */
  .layout-left .hero-price {
    font-size: clamp(60px, 7vw, 84px);
  }
}

/* Prose line-length cap -- on mobile every card is already narrower than
   65ch so this is a no-op there; it only matters once the right column
   (or any single-column wide layout) gives a paragraph more width than is
   comfortable to read. Targets the actual text-bearing elements, not their
   containing cards, so card backgrounds/padding still span full width. */
.commentary-text,
.verdict-reason,
.meth-text,
.meth-list,
.meth-note,
.good-price-proof,
.driver-headline,
.driver-state {
  max-width: 65ch;
}

/* Scroll offset so sections don't hide under the sticky header when anchor-scrolled */
#section-home,
#section-trend,
#section-history,
#section-info {
  scroll-margin-top: calc(52px + env(safe-area-inset-top, 0px) + 8px);
}

/* ═══════════════════════════════════════════════════════════════
   1. APP HEADER — ambient at rest, elevation on scroll (Ψ3C.1b)
   Absorbs safe-area-inset-top so status-bar zone + nav bar are one
   continuous surface. Fully opaque — no bleed-through on scroll.
   Negative margins removed; header is a <body> child at natural width.
═══════════════════════════════════════════════════════════════ */
.app-header {
  position: sticky;
  top: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  /* padding-top absorbs status-bar zone; min-height includes it so the
     background covers the full area from screen top through nav bar. */
  min-height: calc(52px + env(safe-area-inset-top, 0px));
  padding: env(safe-area-inset-top, 0px) clamp(20px, 5vw, 56px) 0;
  background: var(--ink);
  /* At rest: no border, no shadow — merges with page. JS adds .scrolled
     when scrollY > 0, which applies elevation (border + shadow). */
  transition: background 0.3s ease-out, border-color 0.3s ease-out;
}

.app-header.scrolled {
  border-bottom: 1px solid var(--line);
  box-shadow: 0 1px 8px rgba(0, 0, 0, 0.28);
}

/* Freshness alert states always show a tinted border (alert indicator).
   Uses --status-* (not --up/--gold) -- see the token block's comment for
   why freshness state needs its own hue family. The base rule's
   transition (above) softens the swap between states. */
.app-header:has(.freshness--stale) {
  background: linear-gradient(var(--status-stale-bg), var(--status-stale-bg)), var(--ink);
  border-bottom: 1px solid var(--status-stale-border);
}
.app-header:has(.freshness--warn) {
  background: linear-gradient(var(--status-carryover-bg), var(--status-carryover-bg)), var(--ink);
  border-bottom: 1px solid var(--status-carryover-border);
}

/* ─── APP HEADER LAYOUT ───────────────────────────────────────────────────── */
.app-header-left {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}

.app-title {
  font-family: var(--display);
  font-size: 20px;
  font-weight: 500;
  letter-spacing: -0.01em;
  color: var(--gold);
  line-height: 1;
}

.app-header-right {
  display: flex;
  align-items: center;
  gap: 6px;
}

.freshness-pill {
  font-size: var(--ts-xs);
  font-family: var(--sans);
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--cream-mute);
  font-variant-numeric: tabular-nums;
}
.freshness-pill { transition: color 0.3s ease-out; }
.freshness-pill.freshness--ok    { color: var(--status-live); }
.freshness-pill.freshness--warn  { color: var(--status-carryover); }
.freshness-pill.freshness--stale { color: var(--status-stale); font-weight: 700; }

/* ─── HERO LOCATION — relocated from header to hero area (Ψ3C.1) ─────────── */
.hero-location {
  margin: -4px 0 var(--space-3);
  font-size: var(--ts-xs);
  color: var(--cream-mute);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.refresh-btn,
.share-btn,
.pwa-help-btn {
  background: none;
  border: none;
  color: var(--cream-mute);
  font-size: 15px;
  line-height: 1;
  padding: 4px;
  cursor: pointer;
  /* 44px hit area (was 28px, below the 44px touch-target minimum) -- the
     visible glyph stays 15px, only the tappable circle grows. */
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  flex-shrink: 0;
}
.refresh-btn:active,
.share-btn:active,
.pwa-help-btn:active { color: var(--gold); }

/* Language toggle — a compact pill (not a circular icon button like
   refresh/pwa-help above) since it shows 2-character text ("हिं"/"EN"), not a
   single glyph. Placed first in .app-header-right (left of the freshness
   pill) so it reads as a settings control grouped apart from status text.
   44px min-height keeps the touch target on par with the icon buttons despite
   the visually shorter pill shape. */
.lang-toggle {
  background: var(--surface-2);
  border: 1px solid var(--line);
  color: var(--cream-mute);
  font-family: var(--sans);
  font-size: var(--ts-xs);
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 0 10px;
  min-height: 36px;
  min-width: 40px;
  border-radius: 999px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  flex-shrink: 0;
  transition: color 0.15s ease-out, border-color 0.15s ease-out, transform 100ms ease-out;
}
.lang-toggle:active { color: var(--gold); border-color: var(--line-strong); transform: scale(0.95); }
@media (hover: hover) {
  .lang-toggle:hover { color: var(--gold); border-color: var(--line-strong); }
}
/* Author-side display overrides the UA [hidden]{display:none}; restore all. */
.pwa-help-btn[hidden]     { display: none !important; }
.pwa-help-panel[hidden]   { display: none !important; }
.first-visit-panel[hidden] { display: none !important; }
/* Ψ3C.1 latent bug fixes (norm #14): inline-flex/flex defeats UA [hidden] rule */
.hero-change[hidden]    { display: none !important; }
.sparkline-wrap[hidden] { display: none !important; }
/* Same class of bug (norm #14), reintroduced by the install-prompt-panel's own
   `display: flex` -- dismissing it set the hidden attribute correctly (app.js's
   click handler was never broken) but had zero visual effect, since the panel's
   own flex rule tied with and beat the UA's [hidden]{display:none} rule. */
.install-prompt-panel[hidden] { display: none !important; }

@keyframes refresh-spin {
  to { transform: rotate(360deg); }
}
.refresh-btn--spinning {
  animation: refresh-spin 0.8s linear infinite;
  pointer-events: none;
  color: var(--gold);
}

/* ─── PWA HELP PANEL (D5 — iOS standalone only) ─────────────────────────── */
.pwa-help-panel {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 10px clamp(20px, 5vw, 56px);
  background: rgba(212, 147, 42, 0.10);
  border-bottom: 1px solid rgba(212, 147, 42, 0.22);
}
.pwa-help-panel p {
  margin: 0;
  flex: 1;
  font-size: var(--ts-xs);
  color: var(--cream-dim);
  line-height: 1.55;
}
.pwa-help-close {
  background: none;
  border: none;
  color: var(--cream-mute);
  font-size: 11px;
  cursor: pointer;
  padding: 2px 4px;
  flex-shrink: 0;
  margin-top: 1px;
  -webkit-tap-highlight-color: transparent;
}

/* ─── INSTALL PROMPT PANEL (iOS Safari, not-yet-installed only) ─────────── */
.install-prompt-panel {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 10px clamp(20px, 5vw, 56px);
  background: rgba(212, 147, 42, 0.10);
  border-bottom: 1px solid rgba(212, 147, 42, 0.22);
}
.install-prompt-panel p {
  margin: 0;
  flex: 1;
  font-size: var(--ts-xs);
  color: var(--cream-dim);
  line-height: 1.55;
}
.install-prompt-close {
  background: none;
  border: none;
  color: var(--cream-mute);
  font-size: 11px;
  cursor: pointer;
  padding: 2px 4px;
  flex-shrink: 0;
  margin-top: 1px;
  -webkit-tap-highlight-color: transparent;
}

/* ─── FIRST-VISIT ORIENTATION STRIP — same visual register as the install
   prompt above (both are dismissible one-time nudges), distinct tint so it
   doesn't read as the same message twice if a visitor happens to see both. ─── */
.first-visit-panel {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 10px clamp(20px, 5vw, 56px);
  background: var(--surface-2);
  border-bottom: 1px solid var(--line);
}
.first-visit-panel p {
  margin: 0;
  flex: 1;
  font-size: var(--ts-xs);
  color: var(--cream-dim);
  line-height: 1.55;
}
.first-visit-close {
  background: none;
  border: none;
  color: var(--cream-mute);
  font-size: 11px;
  cursor: pointer;
  padding: 2px 4px;
  flex-shrink: 0;
  margin-top: 1px;
  -webkit-tap-highlight-color: transparent;
}

/* ─── SHARE TOAST — clipboard-fallback confirmation only (Web Share API needs
   no visual feedback of its own; the native share sheet IS the feedback). ─── */
.toast {
  position: fixed;
  bottom: calc(72px + env(safe-area-inset-bottom, 0px));
  left: 50%;
  transform: translateX(-50%) translateY(8px);
  background: var(--surface-2);
  border: 1px solid var(--line-strong);
  color: var(--cream);
  font-size: var(--ts-sm);
  padding: 10px 18px;
  border-radius: 999px;
  box-shadow: var(--shadow-md);
  z-index: 20;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease-out, transform 0.2s ease-out;
  max-width: calc(100vw - 40px);
  text-align: center;
}
.toast.toast--visible { opacity: 1; transform: translateX(-50%) translateY(0); }
.toast[hidden] { display: none; }

/* ═══════════════════════════════════════════════════════════════
   FORECAST STALENESS BANNER
═══════════════════════════════════════════════════════════════ */
.stale-banner {
  width: 100%;
  padding: 10px 20px;
  background: var(--status-carryover-bg);
  border-bottom: 1px solid var(--status-carryover-border);
  color: var(--status-carryover);
  font-size: var(--ts-xs);
  font-family: var(--sans);
  text-align: center;
  letter-spacing: 0.03em;
}

/* ═══════════════════════════════════════════════════════════════
   OFFLINE BANNER (Φ16)
   Shown when navigator.onLine === false. Same --status-stale register as
   freshness--stale (a harder failure than mere carryover). Suppresses
   #stale-banner (precedence).
═══════════════════════════════════════════════════════════════ */
.offline-banner {
  width: 100%;
  padding: 10px 20px;
  background: var(--status-stale-bg);
  border-bottom: 1px solid var(--status-stale-border);
  color: var(--status-stale);
  font-size: var(--ts-xs);
  font-family: var(--sans);
  text-align: center;
  letter-spacing: 0.03em;
  font-weight: 500;
}

/* ═══════════════════════════════════════════════════════════════
   2. HERO CARD
═══════════════════════════════════════════════════════════════ */
.hero-card {
  position: relative;
  padding-top: clamp(32px, 6vw, 64px);
  margin-bottom: clamp(28px, 6vw, 56px);
  /* CLS fix (2026-08-06): hero-skeleton was in normal flow, a sibling of the real
     eyebrow/price/location/change/verdict-banner/sparkline content it represents --
     hiding it while they un-hide changed hero-card's height (confirmed empirically:
     force-hiding hero-skeleton entirely made CLS far WORSE, ~1.15, proving it does
     reserve real, load-bearing space today, just not exactly the right amount).
     min-height mirrors hero-skeleton's own natural stacked height (eyebrow 12px +
     price ~52-96px + change 28px + verdict 72px + spark 56px + gaps/margin), same
     imprecision class as history-wrap/commentary-body's estimates -- not pixel-
     exact against the real content's variable height (verdict-reason text length,
     conditional estimate-range/last-confirmed lines), but eliminates the skeleton-
     to-content swap as a shift source, which is the actual mechanism being fixed. */
  min-height: 340px;
}

/* Hero ambient glow — replaces the removed CSS-3D coin's "signature element"
   role with material (light + gradient) instead of an object. `::before`,
   position:absolute + negative z-index inside .hero-card's own stacking
   context (established by .hero-card's position:relative above), so it
   always paints behind every hero text element regardless of viewport
   width -- same stacking guarantee the coin used to rely on. Static (no
   animation): the coin's failure mode was decoration for its own sake
   spinning/floating with no tie to meaning; a soft fixed wash of light
   reads as material warmth without competing for attention or drawing a
   perf/CLS cost. --gold-glow already carries a light-mode override (a
   darker, lower-alpha value tuned for a white surface), which is exactly
   why this reads as gold in both schemes where the coin's flat sphere
   fill did not -- confirmed via light-mode screenshot during the audit
   pass, where the coin rendered as a plain brown ball. */
.hero-card::before {
  content: "";
  position: absolute;
  z-index: -1;
  top: -40px;
  right: -80px;
  left: 20%;
  height: clamp(260px, 42vw, 460px);
  background: radial-gradient(closest-side, var(--gold-glow) 0%, transparent 75%);
  pointer-events: none;
}
@media (max-width: 420px) {
  .hero-card::before { opacity: 0.75; }
}

.eyebrow {
  margin: 0 0 12px;
  font-size: var(--ts-xs);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--cream-mute);
}

/* ─── Hero price ─── */
/* Gold-material treatment: a gradient across the gold spectrum (not a flat
   color) clipped to the digits themselves, plus a light-sweep that plays
   ONCE when the price is first revealed (element goes from [hidden] to
   visible -- CSS animations on a newly-rendered element play automatically,
   no JS trigger needed). Deliberately not looping -- a looping shine on the
   actual price would compete with the skeleton shimmer pattern used
   elsewhere and read as busy/gimmicky rather than a one-time reveal. */
.hero-price {
  position: relative;
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(56px, 16vw, 160px);
  line-height: 0.92;
  letter-spacing: -0.04em;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1, "lnum" 1;
  margin: 0 0 var(--space-4);
  background-image:
    linear-gradient(115deg, transparent 35%, rgba(255, 255, 255, 0.55) 48%, transparent 60%),
    var(--hero-gradient);
  background-size: 250% 100%, 100% 100%;
  background-position: 150% 0, 0 0;
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
  animation: hero-sweep 1s ease-out 1 both;
}
@keyframes hero-sweep {
  from { background-position: 150% 0, 0 0; }
  to   { background-position: -50% 0, 0 0; }
}
.hero-price .rupee {
  font-style: italic;
  font-weight: 400;
  margin-right: 0.05em;
  opacity: 0.7;
  /* Opt out of the parent's gradient/transparent-text treatment -- `background`
     isn't inherited, so without this the symbol would inherit color:transparent
     with no background of its own and vanish. A flat --gold accent on the small
     currency mark against the big gradient digits is also an intentional
     hierarchy, not just a workaround. */
  background: none;
  -webkit-background-clip: initial;
  background-clip: initial;
  color: var(--gold);
}

/* Landscape phones with a short viewport (a rotated 375-440px-wide phone lands
   in roughly this height range): the base clamp(56px,16vw,160px) above sizes
   purely off width, so on a wide-but-short landscape phone the price alone can
   consume nearly the full viewport height, pushing the change pill and verdict
   banner off-screen without scrolling. Constrain by viewport height instead
   once height, not width, is the binding dimension. Scoped to
   orientation:landscape + max-height so portrait phones (where height is
   plentiful) keep the original width-based sizing untouched, and so this can
   never affect the portrait viewport Lighthouse CI measures CLS against. */
@media (orientation: landscape) and (max-height: 500px) {
  .hero-price { font-size: clamp(40px, 13vh, 88px); margin-bottom: 8px; }
  .hero-card {
    padding-top: clamp(8px, 2vh, 16px);
    margin-bottom: clamp(8px, 2vh, 16px);
    min-height: 0;
  }
  .eyebrow { margin-bottom: 4px; }
  .hero-change { margin-bottom: 8px; padding: 4px 12px; }
  .verdict-banner { margin-bottom: 8px; padding: 10px 14px; }
  /* Least essential element on a viewport this short — the verdict banner
     above it is what answers "should I care today?"; the sparkline is
     supplementary and already pushed below the fold at these heights, so
     hiding it (rather than leaving it half-cut) reclaims real room instead
     of spending it on a chart nobody can see anyway. */
  .sparkline-wrap { display: none !important; }
}
.hero-estimate-range {
  margin: -8px 0 var(--space-3);
  font-size: var(--ts-sm);
  color: var(--cream-mute);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1, "lnum" 1;
}
.hero-last-confirmed {
  margin: 0 0 var(--space-3);
  font-size: var(--ts-sm);
  color: var(--cream-mute);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1, "lnum" 1;
}

/* ─── Today's change ─── */
.hero-change {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  padding: 7px 14px;
  border-radius: 999px;
  font-size: var(--ts-sm);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  margin-bottom: var(--space-5);
}
.hero-change[data-direction="up"]   { background: rgba(198,106,75,0.12);  color: var(--up);   }
.hero-change[data-direction="down"] { background: rgba(106,154,114,0.12); color: var(--down); }
.hero-change[data-direction="flat"] { background: var(--surface-2);       color: var(--cream-mute); }
.hero-change-arrow { font-weight: 700; font-size: 15px; }
.hero-change-label { color: var(--cream-mute); font-size: var(--ts-xs); letter-spacing: 0.04em; font-weight: 400; }

/* ─── Verdict banner ─── */
.verdict-banner {
  border-radius: var(--radius-lg);
  padding: var(--space-4) var(--space-5);
  margin-bottom: var(--space-5);
  border: 1px solid var(--verdict-unknown-border);
  background: var(--verdict-unknown-bg);
  color: var(--verdict-unknown-text);
  transition: background 0.25s, border-color 0.25s, color 0.25s;
}
.verdict-banner[data-type="down"] {
  background: var(--verdict-down-bg);
  border-color: var(--verdict-down-border);
  color: var(--verdict-down-text);
}
.verdict-banner[data-type="up"] {
  background: var(--verdict-up-bg);
  border-color: var(--verdict-up-border);
  color: var(--verdict-up-text);
}
.verdict-banner[data-type="flat"] {
  background: var(--verdict-flat-bg);
  border-color: var(--verdict-flat-border);
  color: var(--verdict-flat-text);
}

.verdict-header {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: 6px;
}
.verdict-icon {
  font-size: 16px;
  line-height: 1;
  flex-shrink: 0;
}
.verdict-headline {
  font-family: var(--display);
  font-size: clamp(15px, 2.5vw, 20px);
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: -0.01em;
}
.verdict-reason {
  margin: 0;
  font-size: var(--ts-sm);
  line-height: 1.55;
  opacity: 0.85;
}

/* ─── 7-day sparkline ─── */
.sparkline-wrap {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-3);
}
.sparkline {
  flex: 1;
  height: 56px;
  min-width: 0;
  overflow: visible;
}
.sparkline-label-left,
.sparkline-label-right {
  font-size: var(--ts-xs);
  color: var(--cream-mute);
  letter-spacing: 0.04em;
  white-space: nowrap;
  flex-shrink: 0;
}
.sparkline-label-right { text-align: right; font-variant-numeric: tabular-nums; }

/* ═══════════════════════════════════════════════════════════════
   3. COMPARISON CARDS
═══════════════════════════════════════════════════════════════ */
/* Tight gap into karat-strip below -- both are "today's numbers" and should
   read as one cluster; the bigger gap belongs after karat-strip, where the
   topic actually changes to commentary. */
.comparison-section { margin-bottom: var(--space-4); }

.comparison-cards {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
}

.comparison-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  padding: var(--space-4) var(--space-4) var(--space-3);
  box-shadow: var(--shadow-sm);
  transition: background 0.2s, border-color 0.2s, transform 100ms ease-out, box-shadow 0.2s;
}
.comparison-card:active { transform: scale(0.98); }
/* `hover: hover` excludes touch devices -- otherwise :hover can stick after
   a tap until the next tap elsewhere, reading as a stuck/broken state. */
@media (hover: hover) {
  .comparison-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); border-color: var(--line-strong); }
}
.comparison-card[data-sentiment="good"] {
  background: var(--cmp-good-bg);
  border-color: var(--cmp-good-border);
}
.comparison-card[data-sentiment="caution"] {
  background: var(--cmp-caution-bg);
  border-color: var(--cmp-caution-border);
}
.cmp-label {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--cream-mute);
  margin-bottom: 6px;
}
.cmp-value {
  font-family: var(--display);
  font-size: clamp(18px, 3.5vw, 26px);
  font-weight: 500;
  color: var(--cream);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1, "lnum" 1;
  line-height: 1.1;
}
.comparison-card[data-sentiment="good"]    .cmp-value { color: var(--cmp-good-text); }
.comparison-card[data-sentiment="caution"] .cmp-value { color: var(--cmp-caution-text); }
.cmp-sub {
  /* Tier 1 CLS fix (2026-08-10): starts empty in HTML (populated by renderComparisons()),
     unlike .cmp-value which always has a "—" placeholder -- without a reserved height, an
     empty-to-one-line-of-text swap shifts the card below it. min-height matches its own
     measured single-line rendered height (16.5px, mobile viewport, live production). */
  min-height: 16.5px;
  margin-top: 4px;
  font-size: var(--ts-xs);
  color: var(--cream-mute);
  letter-spacing: 0.02em;
}

/* ═══════════════════════════════════════════════════════════════
   4. KARAT STRIP — compact 24K / 18K
═══════════════════════════════════════════════════════════════ */
.karat-strip {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  /* Last of the "today's numbers" cluster -- larger gap here (vs. the
     tightened comparison-section above) is the deliberate topic break
     before commentary starts. */
  margin-bottom: clamp(48px, 9vw, 64px);
}
.karat-card {
  display: flex;
  flex-direction: column;
  gap: 4px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  padding: var(--space-4) var(--space-5);
  box-shadow: var(--shadow-sm);
  transition: transform 100ms ease-out, box-shadow 0.2s, border-color 0.2s;
}
.karat-card:active { transform: scale(0.98); }
@media (hover: hover) {
  .karat-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); border-color: var(--line-strong); }
}
.karat-label {
  font-size: var(--ts-xs);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--gold);
  font-weight: 500;
}
.karat-price {
  font-family: var(--display);
  font-size: clamp(20px, 4vw, 30px);
  font-weight: 500;
  color: var(--cream);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1, "lnum" 1;
  line-height: 1.1;
}
.karat-price .rupee { color: var(--gold); font-style: italic; margin-right: 0.04em; opacity: 0.7; }
.karat-sub {
  font-size: var(--ts-xs);
  color: var(--cream-mute);
  letter-spacing: 0.02em;
}

/* ═══════════════════════════════════════════════════════════════
   4b. PURCHASE CALCULATOR
═══════════════════════════════════════════════════════════════ */
.calculator-section { margin-bottom: clamp(40px, 9vw, 56px); }
.calculator-section h2 {
  font-size: clamp(22px, 3.5vw, 32px);
  margin-bottom: var(--space-5);
}
.calculator-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: clamp(16px, 4vw, 24px);
  box-shadow: var(--shadow-sm);
}
.calc-inputs {
  display: flex;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin-bottom: 4px;
}
.calc-input-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
  flex: 1;
  min-width: 130px;
}
.calc-input-group label {
  font-size: var(--ts-xs);
  color: var(--cream-mute);
}
/* 44px min-height touch target; inputmode="decimal" on the input itself (see
   index.html) is what actually triggers the numeric keypad on mobile -- this
   just makes the tappable control itself comfortably sized once it's up. */
.calc-input {
  font-family: var(--sans);
  font-size: var(--ts-base);
  font-variant-numeric: tabular-nums;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 0 var(--space-4);
  min-height: 44px;
  width: 100%;
  color: var(--cream);
}
.calc-input:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}
.calc-making-hint {
  font-size: var(--ts-xs);
  color: var(--cream-mute);
  margin: 4px 0 var(--space-5);
}

/* CLS fix, same absolute-overlay pattern as commentary-body/model-signal-wrap
   above -- calc-skeleton is hidden without ever changing calc-wrap's height,
   since calc-results starts empty and only gets content once renderCalculator()
   has real readings/forecast to compute against. min-height is the measured
   near-maximal case, not the zero-making-charge default: the making-charge
   row and the estimated-price note are both conditionally rendered, so the
   card is taller once a visitor enters a making % and/or the headline price
   is in ibja_calibrated/fusion_consensus mode. Measured scrollHeight at
   375px (tallest of 375/390/428, both languages, 2026-08-11) with both of
   those present: 240px. 250px leaves a small margin without padding far
   beyond the measured case. */
.calc-wrap {
  position: relative;
  min-height: 250px;
}
.calc-skeleton {
  position: absolute;
  inset: 0;
  z-index: 1;
}
.calc-skeleton[hidden] { display: none; }

.calc-result-card {
  background: var(--surface-2);
  border-radius: var(--radius-md);
  padding: var(--space-4) var(--space-5);
}
.calc-result-karat {
  font-size: var(--ts-xs);
  letter-spacing: 0.06em;
  color: var(--gold);
  font-weight: 500;
  margin-bottom: var(--space-3);
}
.calc-result-row {
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
  font-size: var(--ts-sm);
  color: var(--cream-dim);
  padding: 3px 0;
  font-variant-numeric: tabular-nums;
}
.calc-result-row--total {
  border-top: 1px solid var(--line);
  margin-top: 6px;
  padding-top: 10px;
  font-family: var(--display);
  font-size: clamp(20px, 4vw, 26px);
  font-weight: 500;
  color: var(--cream);
}
.calc-estimated-note {
  margin: var(--space-2) 0 0;
  font-size: var(--ts-xs);
  color: var(--status-carryover);
  font-style: italic;
}
.calc-other-karats {
  margin: var(--space-3) 0 0;
  font-size: var(--ts-xs);
  color: var(--cream-mute);
  font-variant-numeric: tabular-nums;
}
.calc-empty {
  color: var(--cream-mute);
  font-size: var(--ts-sm);
  margin: 0;
}

/* ═══════════════════════════════════════════════════════════════
   5. COMMENTARY — always visible
═══════════════════════════════════════════════════════════════ */
.commentary-section { margin-bottom: clamp(40px, 9vw, 56px); }
.commentary-card {
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--space-5) var(--space-6);
}
.commentary-eyebrow {
  font-size: var(--ts-xs);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--gold);
  margin: 0 0 10px;
}
.commentary-text {
  font-style: italic;
  font-size: 15px;
  line-height: 1.65;
  color: var(--cream-dim);
  margin: 0 0 8px;
}
.commentary-meta {
  font-size: var(--ts-xs);
  color: var(--cream-mute);
  letter-spacing: 0.04em;
  margin: 0;
}
.commentary-meta--stale { color: var(--status-carryover); }

/* ═══════════════════════════════════════════════════════════════
   6. TREND CHART
═══════════════════════════════════════════════════════════════ */
.model-signal-section { margin-bottom: clamp(40px, 9vw, 56px); }
.model-signal-section h2 { font-size: clamp(22px, 3.5vw, 32px); margin-bottom: var(--space-5); }
.chart-section { margin-bottom: clamp(40px, 9vw, 56px); }
.chart-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: var(--space-6);
}
.chart-section h2 {
  font-size: clamp(22px, 3.5vw, 32px);
  letter-spacing: -0.01em;
}
.range-toggle {
  display: inline-flex;
  position: relative;       /* anchor for the sliding pill ::after */
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 4px;
}
.range-toggle button {
  flex: 1;                  /* equal-width so pill math is exact */
  position: relative; z-index: 1; /* text layers above sliding pill */
  background: transparent; border: 0; cursor: pointer;
  color: var(--cream-mute);
  font-family: var(--sans); font-size: var(--ts-sm); font-weight: 500;
  padding: 6px 14px; border-radius: 999px;
  transition: color 0.2s, transform 100ms ease-out;
  /* 44px touch target at every width, not just ≤640px -- touchscreen
     laptops/tablets exist above that breakpoint too. */
  min-height: 44px;
  min-width: 44px;
}
.range-toggle button:hover { color: var(--cream); }
/* Active: text turns dark (readable on gold pill); background handled by ::after */
.range-toggle button.active { background: transparent; color: var(--ink); }

/* Sliding pill — Ψ3C.2: moves with translateX, no JS for the slide */
.range-toggle::after {
  content: '';
  position: absolute;
  top: 4px; bottom: 4px;
  left: 4px;
  width: calc((100% - 8px) / 3);   /* 1/3 of inner width (8px = 4px padding × 2) */
  background: var(--gold);
  border-radius: 999px;
  pointer-events: none;
  transition: transform 200ms ease-out;
  transform: translateX(0);         /* default: first button (7d) */
}
/* :has() — CSS-only pill positioning, no JS needed for the slide */
.range-toggle:has(button:nth-child(2).active)::after { transform: translateX(100%); }
.range-toggle:has(button:nth-child(3).active)::after { transform: translateX(200%); }
.chart-wrap {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--space-3) var(--space-5);
  aspect-ratio: 16 / 5;
  box-shadow: var(--shadow-sm);
}
/* Touch-scrub: pan-y (not the default "auto") lets a horizontal/diagonal
   drag on the canvas reach Chart.js's own touchmove handling instead of the
   browser immediately treating it as a page-scroll gesture, so dragging a
   finger across the chart continuously updates the tooltip like a mouse
   hover would. Vertical scrolling still passes through untouched. */
#chart { touch-action: pan-y; }

/* ═══════════════════════════════════════════════════════════════
   7. HISTORY — table (desktop) + card list (mobile)
═══════════════════════════════════════════════════════════════ */
.history-section { margin-bottom: clamp(40px, 9vw, 56px); }
.history-section h2 {
  font-size: clamp(22px, 3.5vw, 32px);
  margin-bottom: var(--space-6);
}
.section-karat-note {
  font-size: 12px;
  color: var(--cream-mute);
  margin: -8px 0 12px;
  letter-spacing: 0.03em;
}

.history-wrap {
  position: relative;
  min-height: 180px; /* room for the skeleton overlay before any real rows exist */
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  overflow: hidden;
}

/* Desktop: table visible, card list hidden */
.history-table { display: table; }
.history-cards { display: none; }

.history-table {
  width: 100%; border-collapse: collapse;
  font-size: 14px;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1, "lnum" 1;
}
.history-table th,
.history-table td {
  padding: var(--space-4) var(--space-5);
  text-align: left;
  border-bottom: 1px solid var(--line);
}
.history-table th:first-child, .history-table td:first-child { width: 35%; }
.history-table th.num, .history-table td.num { width: 15%; text-align: right; }
.history-table tbody tr:last-child td { border-bottom: 0; }
.history-table tbody tr:nth-child(even) td { background: var(--surface-alt); }
.history-table th {
  font-size: var(--ts-xs); letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--cream-mute); font-weight: 500;
  background: var(--surface-2);
}
.history-table .rupee { color: var(--gold); font-style: italic; }
.delta-up   { color: var(--up);        }
.delta-down { color: var(--down);      }
.delta-flat { color: var(--cream-mute); opacity: 0.45; }
.empty { text-align: center; color: var(--cream-mute); padding: 30px !important; }

/* Show-all button -- visibility driven purely by the [hidden] attribute
   (app.js toggles it) so it works at every viewport, not just the mobile
   card-list breakpoint. Previously force-hidden here outside ≤640px, which
   is why the desktop table never truncated at all. */
.history-show-all {
  width: 100%;
  min-height: 44px;
  padding: var(--space-3) 0;
  margin-top: var(--space-3);
  background: none; border: none;
  font-family: var(--sans); font-size: var(--ts-sm);
  color: var(--gold); cursor: pointer; text-align: center;
  transition: color 0.15s ease-out, transform 100ms ease-out;
}
.history-show-all:hover  { color: var(--cream); }
.history-show-all:active { transform: scale(0.98); }

/* ═══════════════════════════════════════════════════════════════
   8. METHODOLOGY ACCORDION
═══════════════════════════════════════════════════════════════ */
.methodology {
  margin-bottom: clamp(var(--space-7), 8vw, var(--space-8));
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.methodology-summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-5) var(--space-6);
  font-size: var(--ts-sm);
  font-weight: 500;
  letter-spacing: 0.03em;
  color: var(--cream-mute);
  user-select: none;
  min-height: 52px;
}
.methodology-summary::-webkit-details-marker { display: none; }
.methodology-summary::after {
  content: "›";
  font-size: 20px; line-height: 1;
  transform: rotate(90deg);
  transition: transform 0.2s;
  display: inline-block; flex-shrink: 0;
}
.methodology[open] .methodology-summary::after { transform: rotate(270deg); }
.methodology[open] .methodology-summary {
  border-bottom: 1px solid var(--line);
  color: var(--cream-dim);
}
.methodology-summary:hover { color: var(--cream); }

.methodology-body {
  padding: var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-7);
}
.meth-loading {
  font-size: var(--ts-sm);
  color: var(--cream-mute);
  margin: 0;
}

/* Methodology inner sections */
.meth-section { /* grouping, no style */ }
.meth-heading {
  font-family: var(--display);
  font-size: clamp(16px, 2.5vw, 20px);
  font-weight: 500;
  color: var(--cream);
  margin: 0 0 var(--space-4);
}
.meth-text {
  font-size: var(--ts-sm);
  color: var(--cream-dim);
  line-height: 1.6;
  margin: 0 0 var(--space-3);
}
.meth-list {
  font-size: var(--ts-sm);
  color: var(--cream-dim);
  line-height: 1.7;
  margin: 0;
  padding-left: var(--space-5);
}
.meth-list li { margin-bottom: 4px; }
.meth-note {
  font-size: var(--ts-xs);
  color: var(--cream-mute);
  line-height: 1.6;
  margin: var(--space-4) 0 0;
  padding: var(--space-3) var(--space-4);
  background: var(--surface-2);
  border-radius: var(--radius-sm);
}

/* Banners inside methodology */
.meth-banner {
  font-size: var(--ts-sm);
  line-height: 1.55;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
}
.meth-banner--warn {
  background: rgba(212, 147, 42, 0.10);
  border-color: rgba(212, 147, 42, 0.28);
  color: var(--verdict-flat-text);
}
.meth-banner--info {
  background: var(--surface-2);
  border-color: var(--line);
  color: var(--cream-mute);
}

/* Stats grid inside methodology */
.meth-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 10px;
}
.meth-stat {
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  padding: var(--space-4) var(--space-4) var(--space-3);
}
.meth-stat-label {
  font-size: var(--ts-xs);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--cream-mute);
  margin-bottom: 6px;
}
.meth-stat-value {
  font-family: var(--display);
  font-size: 26px;
  font-weight: 500;
  color: var(--cream);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}
.meth-stat-sub {
  margin-top: 4px;
  font-size: var(--ts-xs);
  color: var(--cream-mute);
}

/* ═══════════════════════════════════════════════════════════════
   9. FOOTER
═══════════════════════════════════════════════════════════════ */
.site-footer {
  border-top: 1px solid var(--line);
  padding-top: var(--space-6);
  padding-bottom: calc(var(--space-5) + env(safe-area-inset-bottom, 0px));
  font-size: var(--ts-sm);
  color: var(--cream-dim);
}
.site-footer p { margin: 0 0 6px; }
.site-footer .muted { color: var(--cream-mute); font-size: var(--ts-xs); }

/* ═══════════════════════════════════════════════════════════════
   SKELETON SHIMMER — shown while fetching
═══════════════════════════════════════════════════════════════ */
/* CLS fix (2026-08-06): the previous single-layer version animated
   `background-position` directly on .skel, which Chrome's own CLSCulprits
   trace insight flags as a non-composited animation (background-position
   isn't a compositor-only property, so the browser repaints every frame
   instead of just re-compositing a layer) -- confirmed as the actual CLS
   source on both pre- and post-redesign code via a live trace, unrelated to
   any of that redesign's changes. Same visual result (a light band sweeping
   left-to-right over a solid base, 1.6s loop, default ease timing), same
   duration, restructured onto a `::after` pseudo-element animated via
   `transform: translateX()` instead -- transform IS compositor-only, so
   this runs on the GPU compositor thread with no repaint per frame.
   `overflow: hidden` + `position: relative` on .skel clips the sweep to the
   element's own rounded corners (border-radius, set here or via an inline
   override) and gives ::after's `inset: 0` something to position against. */
.skel {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: var(--surface);
}
.skel::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg,
    transparent 0%,
    var(--surface-2) 50%,
    transparent 100%);
  transform: translateX(-100%);
  animation: shimmer-sweep 1.6s infinite;
}
@keyframes shimmer-sweep {
  100% { transform: translateX(100%); }
}
.hero-skeleton {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.skel-eyebrow { height: 12px; width: 140px; }
.skel-price   { height: clamp(52px, 12vw, 96px); width: clamp(200px, 50vw, 380px); border-radius: 8px; }
.skel-change  { height: 28px; width: 120px; border-radius: 999px; }
.skel-verdict { height: 72px; width: 100%; border-radius: var(--radius-lg); }
.skel-spark   { height: 56px; width: 100%; }
.hero-skeleton[hidden] { display: none; }

/* Methodology panel skeleton — shown until renderMethodology() replaces
   #methodology-body's innerHTML entirely (see app.js). */
.meth-skeleton { display: flex; flex-direction: column; gap: var(--space-4); }
.skel-meth-heading { height: 16px; width: 160px; }
.skel-meth-line     { height: 12px; width: 100%; }
.skel-meth-line--short { width: 60%; }

/* "Live" pulse dot on the freshness pill — subtle, currentColor so it tracks
   the pill's existing --ok/--warn/--stale color classes automatically. */
@keyframes freshness-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.35; }
}
.freshness-pill.freshness--ok::before {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-right: 5px;
  border-radius: 50%;
  background: currentColor;
  vertical-align: middle;
  animation: freshness-pulse 2s ease-in-out infinite;
}

/* ═══════════════════════════════════════════════════════════════
   RESPONSIVE — mobile card history ≤640px
═══════════════════════════════════════════════════════════════ */
@media (max-width: 640px) {
  /* Switch to card list for history */
  .history-table { display: none; }
  .history-cards {
    display: block;
    list-style: none;
    margin: 0; padding: 0;
  }

  .hdate-divider {
    font-family: var(--heading);
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--cream-mute);
    padding: var(--space-3) var(--space-5) var(--space-2);
    border-bottom: 1px solid var(--line);
    background: var(--surface-2);
    list-style: none;
  }
  .history-card {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-5);
    border-bottom: 1px solid var(--line);
    list-style: none;
  }
  .history-card:last-child { border-bottom: 0; }
  .history-card:active { background: var(--surface-2); }
  .hcard-time {
    font-size: var(--ts-xs);
    color: var(--cream-mute);
    letter-spacing: 0.02em;
    flex-shrink: 0;
    min-width: 56px;
  }
  .hcard-price {
    font-family: var(--display);
    font-size: 22px;
    font-weight: 500;
    color: var(--cream);
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum" 1, "lnum" 1;
    line-height: 1.1;
    flex: 1;
  }
  .hcard-price .rupee { color: var(--gold); font-style: italic; }
  .hcard-delta {
    font-size: var(--ts-xs);
    font-variant-numeric: tabular-nums;
    font-weight: 500;
    text-align: right;
    flex-shrink: 0;
    min-width: 60px;
  }
  .hcard-delta--up   { color: var(--up);   }
  .hcard-delta--down { color: var(--down); }
  .hcard-delta--flat { color: var(--cream-mute); opacity: 0.45; }
  .hcard-empty {
    padding: 30px;
    text-align: center;
    color: var(--cream-mute);
    font-size: var(--ts-sm);
    list-style: none;
  }

  /* Comparison cards: 2+1 on narrow screens */
  .comparison-cards { grid-template-columns: 1fr 1fr; }
  .comparison-cards .comparison-card:last-child { grid-column: 1 / -1; }

  /* Chart */
  .chart-wrap { aspect-ratio: 3 / 2; padding: var(--space-2) var(--space-4); }
  .range-toggle button { padding: 10px 16px; }

  /* Karat strip stays 2-col — no change needed */

  /* Methodology body padding */
  .methodology-body { padding: var(--space-4); gap: var(--space-6); }
  .meth-stats { grid-template-columns: 1fr 1fr; }
}

/* ═══════════════════════════════════════════════════════════════
   RESPONSIVE — extra-small ≤375px
═══════════════════════════════════════════════════════════════ */
@media (max-width: 375px) {
  .comparison-cards { grid-template-columns: 1fr; }
  .comparison-cards .comparison-card:last-child { grid-column: auto; }
  .cmp-value { font-size: 18px; }
  .karat-price { font-size: 18px; }
  .meth-stat-value { font-size: 20px; }
  .hcard-price { font-size: 22px; }
}

/* ═══════════════════════════════════════════════════════════════
   RESPONSIVE — tablets ≥641px, constrained comparison cards
   min-height excludes landscape phones that land in this same width band
   (e.g. a 375px-wide phone rotated is 667px wide × 375px tall) — those are
   short, not tall, so they fall through to the base (fluid, mobile-tuned)
   rule below instead of this tablet-specific override. See the landscape
   hero-price fix above for the same width-only-breakpoint root cause.
═══════════════════════════════════════════════════════════════ */
@media (min-width: 641px) and (max-width: 860px) and (min-height: 700px) {
  .comparison-cards { grid-template-columns: repeat(3, 1fr); gap: 10px; }
  .cmp-value { font-size: 20px; }
}

/* ═══════════════════════════════════════════════════════════════
   MODEL SIGNAL CARD — Ψ2B dedicated signal component
═══════════════════════════════════════════════════════════════ */
.signal-card {
  border: 1px solid var(--line);
  border-left: 4px solid var(--cream-mute);
  border-radius: var(--radius-lg);
  background: var(--surface);
  padding: var(--space-4) var(--space-5);
  transition: border-left-color 0.25s;
}
.signal-card--up   { border-left-color: var(--up);   }
.signal-card--down { border-left-color: var(--down); }

.signal-direction-row {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
}
.signal-arrow {
  font-family: var(--display);
  font-size: 28px;
  font-weight: 400;
  line-height: 1;
  flex-shrink: 0;
}
.signal-label {
  font-family: var(--heading);
  font-size: 28px;
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1;
}
.signal-card--up   .signal-arrow,
.signal-card--up   .signal-label { color: var(--up); }
.signal-card--down .signal-arrow,
.signal-card--down .signal-label { color: var(--down); }

.signal-strength {
  font-size: var(--ts-sm);
  color: var(--cream-mute);
  margin-left: auto;
  align-self: center;
  font-variant-numeric: tabular-nums;
}
.signal-stats-row {
  font-size: var(--ts-xs);
  color: var(--cream-mute);
  display: flex;
  flex-wrap: wrap;
  gap: 4px 6px;
  align-items: center;
  line-height: 1.5;
}
.signal-dot {
  color: var(--line-strong);
  font-size: 9px;
}
.signal-note {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--line);
  font-size: var(--ts-xs);
  color: var(--cream-mute);
  line-height: 1.55;
}

/* ═══════════════════════════════════════════════════════════════
   BOTTOM NAV — scroll-anchor style (Ψ3C.1)
═══════════════════════════════════════════════════════════════ */
.bottom-nav {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 10;
  display: flex;
  align-items: stretch;
  background: rgba(26, 22, 18, 0.94);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-top: 1px solid var(--line);
  /* Safe area: extends background under home indicator */
  padding-bottom: env(safe-area-inset-bottom, 0px);
  box-shadow: 0 -1px 12px rgba(0, 0, 0, 0.30);
}

@media (prefers-color-scheme: light) {
  .bottom-nav { background: rgba(251, 247, 238, 0.94); }
}

.bottom-nav-item {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  padding: 10px 4px 8px;
  min-height: 56px;
  text-decoration: none;
  color: var(--cream-mute);
  font-family: var(--sans);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  /* Active indicator: thin top-border in gold */
  border-top: 2px solid transparent;
  transition: color 0.18s;
}

.bottom-nav-item:hover { color: var(--cream); text-decoration: none; }

.bottom-nav-item.active {
  color: var(--gold);
  border-top-color: var(--gold);
}

.bottom-nav-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  flex-shrink: 0;
}

/* Tablet: slightly larger nav items */
@media (min-width: 641px) and (max-width: 899px) {
  .bottom-nav-item { font-size: 11px; padding: 12px 8px 10px; }
}

/* Desktop ≥900px: hide bottom nav — desktop users have scroll/keyboard;
   a full-width fixed bar reads as broken-mobile at this width.
   `hover:hover` + `pointer:fine` (the same real-desktop signal already used
   for .comparison-card/.karat-card hover effects above) — width alone isn't
   enough: a landscape "Plus"-size phone (e.g. 926x428) is ≥900px wide too,
   and a width-only query wrongly hid the nav for exactly those touch users,
   who still need it. */
@media (min-width: 900px) and (hover: hover) and (pointer: fine) {
  .bottom-nav { display: none; }
  /* Restore original main bottom padding (no nav to clear) */
  main { padding-bottom: calc(var(--space-7) + env(safe-area-inset-bottom, 0px)); }
}

/* ═══════════════════════════════════════════════════════════════
   MOTION — Ψ3C.2: card stagger + chart fade + pull indicator
═══════════════════════════════════════════════════════════════ */

/* Card stagger — fade-in + slide-up on data refresh */
@keyframes card-fade-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}
.card-enter {
  animation: card-fade-in 200ms ease-out both;
}

/* Chart fade on timeframe switch */
@keyframes chart-out { from { opacity: 1; } to { opacity: 0; } }
@keyframes chart-in  { from { opacity: 0; } to { opacity: 1; } }
.chart-wrap.chart-fade-out { animation: chart-out 150ms ease-out forwards; }
.chart-wrap.chart-fade-in  { animation: chart-in  200ms ease-out forwards; }

/* Pull-to-refresh indicator — Ψ3C.2 */
.pull-indicator {
  position: fixed;
  /* sits just below the app-header; header height is 52px + safe-area */
  top: calc(52px + env(safe-area-inset-top, 0px));
  left: 0;
  right: 0;
  height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 5;               /* below app-header (10) */
  pointer-events: none;
  opacity: 0;
  color: var(--gold);
  font-size: 22px;
  transition: opacity 150ms;
}
.pull-indicator--ready .pull-icon {
  display: inline-block;
  animation: refresh-spin 0.6s linear infinite;
}
.pull-indicator--loading .pull-icon {
  display: inline-block;
  animation: refresh-spin 0.8s linear infinite;
}

/* During active pull: prevent native overscroll on html (iOS 16+) */
html.ptr-pulling {
  overscroll-behavior-y: none;
}

/* ── Outlook card (Φ8C') ────────────────────────────────────────────────────── */
.outlook-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: clamp(16px, 4vw, 24px);
}
.outlook-range-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 4px;
}
.outlook-range-label {
  font-size: 12px;
  color: var(--cream-mute);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: nowrap;
}
.outlook-range-value {
  font-family: var(--display);
  font-size: clamp(18px, 3vw, 22px);
  font-weight: 600;
  color: var(--cream);
}
.outlook-range-note {
  font-size: 13px;
  color: var(--cream-mute);
  margin: 0 0 12px;
}
.outlook-trend {
  font-size: 15px;
  color: var(--cream);
  margin: 0 0 12px;
  font-weight: 500;
}
.outlook-current-note {
  font-size: 13px;
  color: var(--cream-mute);
  margin: 12px 0 0;
  padding-top: 12px;
  border-top: 1px solid var(--line);
}

/* ── Good-price verdict (Φ11-2) ─────────────────────────────────────────────── */
.good-price-verdict {
  font-size: clamp(15px, 2.5vw, 18px);
  font-weight: 600;
  color: var(--cream);
  margin: 0 0 10px;
  line-height: 1.3;
}
.good-price-verdict--cheap     { color: #6a9a72; }
.good-price-verdict--below-mid { color: #8aad91; }
.good-price-verdict--high      { color: #c66a4b; }
.good-price-supporting {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.good-price-supporting li {
  font-size: 14px;
  color: var(--cream-mute);
  max-width: 65ch;
}
.good-price-divergence { font-style: italic; font-size: 12px; }
.good-price-proof {
  font-size: 13px;
  color: var(--cream-mute);
  margin: -4px 0 10px;
}
.good-price-data-note {
  font-size: 12px;
  color: #c66a4b;
  margin: 0 0 8px;
  font-style: italic;
}
/* The single most actionable number the forecast produces (what to expect by the
   next trading day) gets its own visually distinct treatment -- previously this
   only existed inside the collapsed methodology accordion as "80% range: X-Y". */
.good-price-tomorrow {
  font-size: 14px;
  color: var(--cream);
  margin: 10px 0 0;
  padding: 10px 14px;
  background: var(--gold-glow);
  border: 1px solid rgba(212, 147, 42, 0.28);
  border-radius: var(--radius-sm);
}
.good-price-tomorrow strong { color: var(--gold); font-weight: 600; }

.outlook-reliability { margin-top: 8px; }
.outlook-reliability-note {
  font-size: 13px;
  color: var(--cream-dim);
  margin: 0;
}

.outlook-volatility {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--line);
}
.outlook-volatility-note {
  font-size: 13px;
  color: var(--cream-mute);
  margin: 0 0 8px;
}
.outlook-weekly-movement-note {
  font-size: 13px;
  color: var(--cream-mute);
  margin: 0;
}

/* ── Driver context (Φ14-2) ─────────────────────────────────────────────────── */
.driver-context-section {
  margin-bottom: clamp(32px, 7vw, 48px);
}
/* Tier 2 CLS fix (2026-08-10): same absolute-overlay pattern as
   .model-signal-wrap/.commentary-body/.history-wrap. min-height is measured
   from live production content (73.625px, mobile viewport, 2026-08-10,
   driver-state text only -- no attribution headline present that day) --
   same imprecision tolerance as elsewhere: won't be pixel-exact on a day
   the headline is also present, but eliminates the shift for the observed
   case. */
.driver-context-wrap {
  position: relative;
  min-height: 74px;
}
.driver-context-skeleton {
  position: absolute;
  inset: 0;
  z-index: 1;
}
.driver-context-skeleton[hidden] { display: none; }
.driver-context-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: clamp(16px, 4vw, 24px);
}
.driver-headline {
  font-size: clamp(14px, 2.2vw, 16px);
  color: var(--cream);
  margin: 0 0 10px;
  line-height: 1.45;
}
.driver-state {
  font-size: 13px;
  color: var(--cream-mute);
  margin: 0;
  line-height: 1.55;
}

/* ── Track record section (Φ8C') ────────────────────────────────────────────── */
.track-record-section {
  margin-bottom: clamp(40px, 9vw, 56px);
  scroll-margin-top: 64px;
}
.track-record-section h2 {
  font-size: clamp(22px, 3.5vw, 32px);
  margin-bottom: var(--space-3);
}
.track-record-caption {
  font-size: 13px;
  color: var(--cream-mute);
  margin: 0 0 var(--space-4);
}
.track-record-chart-wrap {
  position: relative;
  height: clamp(200px, 40vw, 280px);
}

/* ── Commentary skeleton ────────────────────────────────────────────────────── */
/* CLS fix (2026-08-06): commentary-skeleton was a plain sibling of commentary-text/
   -meta -- hiding it while they un-hide changed commentary-body's document height
   (skeleton's ~58px of bars vs. real commentary's variable 1-3 sentence height),
   a real, reproducible layout shift confirmed via Lighthouse's layout-shifts
   diagnostic and isolated from font-loading and the scroll-reveal transform (both
   independently ruled out by disabling each and re-measuring). Same pattern as
   history-wrap/history-skeleton: skeleton becomes an absolute overlay inside a
   position:relative, min-height'd wrapper, so hiding it never changes document
   height. min-height is a typical-content estimate (2-3 sentences + meta line),
   same imprecision class as history-wrap's own min-height estimate -- not
   pixel-exact for unusually long commentary, but eliminates the shift for the
   normal case, which is what the pre-fix version never had at all. */
.commentary-body {
  position: relative;
  min-height: 90px;
}
.commentary-skeleton {
  position: absolute;
  inset: 0;
  z-index: 1;
  padding: 4px 0;
}
.commentary-skeleton[hidden] { display: none; }

/* CLS fix (2026-08-10): model-signal-skeleton was a plain sibling of
   model-signal-body inside model-signal-section -- hiding it while body's
   content un-hid changed the section's document height, a real,
   reproducible layout shift confirmed via CI Lighthouse's layout-shifts
   diagnostic (run 31355234247: 2 shifts, ~0.97 each, one attributed to
   <main>). Same pattern as commentary-body/history-wrap: skeleton becomes
   an absolute overlay inside a position:relative wrapper, so hiding it
   never changes document height. min-height is measured from live
   production content -- same imprecision class as commentary-body's own
   estimate: model-signal-body's real height varies with how many optional
   supporting lines apply that day (divergence/trend/band/support/
   reliability/weekly-movement notes), so this won't be pixel-exact on a
   lighter-content day, but eliminates the shift for the near-maximal case.
   Remeasured 2026-08-11 (was 550px/552px measured) after adding the
   reliability note (item 2) and weekly-movement note (item 3) to this same
   card -- both add real height, and the old value was already reused
   without reverifying it, exactly the kind of stale-CLS-budget regression
   this comment's own philosophy exists to prevent. Measured scrollHeight
   across both languages x 375/390/428px (2026-08-11, live data): Hindi
   658/596/561px, English 698/680/623px -- English 375px was tallest at
   698px. 720px leaves ~22px headroom above that without padding as
   generously as it'd take to survive a much longer future addition, which
   isn't the goal here (this value should be remeasured again the next time
   this card's content changes, not treated as permanent headroom). */
.model-signal-wrap {
  position: relative;
  min-height: 720px;
}
.model-signal-skeleton {
  position: absolute;
  inset: 0;
  z-index: 1;
  padding: 4px 0;
}
.model-signal-skeleton[hidden] { display: none; }

/* ── History skeleton ───────────────────────────────────────────────────────── */
.history-skeleton {
  position: absolute;
  inset: 0;
  z-index: 1;
  padding: 12px;
  background: var(--surface);
}
.history-skeleton[hidden] { display: none; }

/* ── How-good panel ─────────────────────────────────────────────────────────── */
.meth-how-good { background: rgba(224,155,46,0.04); border-radius: var(--radius-lg); padding: clamp(12px, 3vw, 20px); }
.meth-how-good .meth-text { margin-bottom: 16px; }
.meth-how-good .meth-text:last-child { margin-bottom: 0; }

/* ═══════════════════════════════════════════════════════════════
   SCROLL REVEALS (feel-alive pass)
═══════════════════════════════════════════════════════════════ */
/* Progressive enhancement: `.reveal-on-scroll` alone (no JS) has ZERO visual
   effect -- app.js's initScrollReveals() adds `.reveal-armed` only after
   confirming IntersectionObserver support and successful setup, so a JS
   error or unsupported browser never leaves a section stuck invisible. The
   hero card is deliberately never part of this system -- price must render
   instantly, never gated behind a scroll trigger. Reduced-motion needs no
   separate handling here: the global transition-duration kill-switch below
   already collapses this 0.5s transition to near-zero, so content still pops
   in (just without the fade/slide motion) rather than staying hidden. */
.reveal-on-scroll.reveal-armed {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 0.5s ease-out, transform 0.5s ease-out;
}
.reveal-on-scroll.reveal-armed.reveal-visible {
  opacity: 1;
  transform: translateY(0);
}

/* ═══════════════════════════════════════════════════════════════
   REDUCED MOTION
═══════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
