/* ==========================================================================
   DESIGN TOKENS — visual identity lock (ibi-visual-identity-brief.md, step 1)
   ==========================================================================

   Definitions only. Nothing in this block is referenced by a rule yet, so
   adding it changes no rendering: the brand names below (--paper,
   --canal-blue, --wheat-gold, --ink) were already used throughout this file
   as var(--name, #HEX) with a hardcoded fallback and were never defined
   anywhere. Each value here is byte-identical to the fallback it now
   supersedes, so those call sites resolve to exactly what they resolved to
   before. Every other token here is a new name that nothing reads yet.

   Migration to these tokens is a later step. Do not delete the fallbacks in
   existing var() calls as part of adding this block.
   ========================================================================== */

:root {
  /* --- Locked brand palette (§1) -----------------------------------------
     The functional system, unchanged. Amber means "pending verification",
     red means "caution/risk", green carries tier state — those hues are
     load-bearing signal, not decoration, so none of them may be respent on
     branding. Signal Rust is deliberately absent from this block: the brief
     says keep whatever hex is already in styles.css, so it stays where it
     already lives rather than being restated (and possibly drifted) here. */
  --paper:        #F7F4EE;  /* page background */
  --canal-blue:   #1D4E6B;  /* primary / brand / interactive */
  --wheat-gold:   #E3A639;  /* secondary accent, non-status use only */
  --growth-green: #2F6D4E;  /* positive / confirmed */
  --ink:          #24211D;  /* body text */

  /* --- Indian flag identity (§2) ------------------------------------------
     Identity, not interface. This block is the one sanctioned place these
     names may appear outside the brand mark SVG and the single header
     hairline rule — the later "grep for stray flag tokens" audit should
     expect exactly this definition site and those two usages.

     --flag-saffron sits close enough to the pending-verification amber that
     any status-adjacent use of it destroys a distinction the reader needs.
     Never use these as text, fill, badge, border, hover, chart series,
     icon, background or focus ring. */
  --flag-saffron: #FF9933;
  --flag-white:   #FFFFFF;
  --flag-green:   #138808;
  --flag-navy:    #000080;

  /* --- Type scale (§3) ---------------------------------------------------
     Note these are sizes, and the pre-existing --text / --text-muted a few
     lines below are colours. Same prefix, different kind; read the suffix. */
  --text-xs:   0.75rem;   /* 12px — captions, source labels */
  --text-sm:   0.875rem;  /* 14px — secondary, table cells */
  --text-base: 1rem;      /* 16px — body, minimum for mobile */
  --text-lg:   1.125rem;  /* 18px — lead paragraphs */
  --text-xl:   1.5rem;    /* 24px — card titles */
  --text-2xl:  2rem;      /* 32px — section headings */
  --text-3xl:  2.75rem;   /* 44px — page titles */
  --text-4xl:  3.75rem;   /* 60px — hero only */

  --leading-tight:   1.15;  /* display */
  --leading-normal:  1.5;   /* UI */
  --leading-relaxed: 1.65;  /* body prose */

  /* Body-copy measure. Was NINE separate hardcoded copies of 68ch — the four
     .farming-leg-* rules and five older prose carriers — with a comment above
     one of them stating the rule in words and another explaining that the
     farming block used 68ch "so the two long-prose surfaces in this app
     agree". They agreed by coincidence, which is the thing a token is for.
     check-farming-leg.js fails if a literal reappears in a declaration.

     ⚠ TRACKING IS NOT TOKENISED AND THAT IS A DECISION, NOT AN OVERSIGHT.
     letter-spacing appears 48 times in this file across seven distinct live
     values (0, 0.1px, 0.2px, 0.3px, 0.4px, 0.5px, -0.5px, plus -1px on the
     success-meter emojis). Collapsing those into a scale means judging which
     of the seven share an intent, which is a design decision rather than a
     cleanup, and doing it by inspection would silently move type somebody set
     deliberately. Recorded so it stays findable. */
  --measure: 68ch;

  /* --- Spacing (§4) — 4px base, no value outside this scale --------------- */
  --space-1: 4px;   --space-2: 8px;   --space-3: 12px;  --space-4: 16px;
  --space-5: 24px;  --space-6: 32px;  --space-7: 48px;  --space-8: 64px;
  --space-9: 96px;

  /* --- Borders and focus rings (§4) ---------------------------------------

     ⚠ THE RING WIDTHS ARE SHARED, THE RING MECHANISM IS NOT UNIFIED. The three
     matcher rings are two-stop box-shadows; the global :focus-visible at the
     foot of this file is an outline with its own offset. A box-shadow follows
     border-radius and an outline does not, so converting one to the other is a
     behaviour change wearing a cleanup's clothes. Only the widths are shared.

     ⚠ AND THE FARMING POINTER'S OWN RING IS NOT THIS TOKEN EITHER. It is 2px /
     4px in --green, sized to sit inside a summary line rather than around a
     form control, and it is a deliberate different thing rather than a drifted
     copy of the same thing.

     --border-width is the hairline. 36 rules in this file still spell 1px out;
     the two farming-leg dividers below are repointed because they are in this
     commit's blast radius, and the rest are a sweep for another day rather
     than a rewrite smuggled into a contrast fix. .card-farming's 4px left rule
     is NOT this token — it is a category marker whose weight is the point. */
  --border-width: 1px;
  --ring-width: 3px;        /* inner stop / single-stop focus rings */
  --ring-width-outer: 6px;  /* outer stop where the ring is drawn on a fill */

  /* --- Radius (§4) -------------------------------------------------------- */
  --radius-sm:   2px;   /* badges, chips */
  --radius-md:   6px;   /* buttons, inputs */
  --radius-lg:  10px;   /* cards, panels */
  --radius-full: 999px;

  /* --- Elevation (§4) — three levels only, do not invent a fourth ---------
     Rest cards at 1, hover to 2, modals/dropdowns at 3. */
  --shadow-1: 0 1px 2px rgba(36,33,29,0.06), 0 1px 3px rgba(36,33,29,0.04);
  --shadow-2: 0 2px 6px rgba(36,33,29,0.07), 0 4px 12px rgba(36,33,29,0.05);
  --shadow-3: 0 8px 24px rgba(36,33,29,0.10), 0 2px 6px rgba(36,33,29,0.06);

  /* --- Motion (§5) -------------------------------------------------------
     Tokens only. The prefers-reduced-motion override the brief specifies is
     a live rule, not a token — it would change behaviour for reduced-motion
     readers today, which this step must not do. It lands with the motion
     step. */
  --ease:      cubic-bezier(0.2, 0, 0.1, 1);
  --dur-micro: 120ms;  /* hover, focus, colour */
  --dur-base:  200ms;  /* tab switch, card lift, disclosure */
  --dur-slow:  320ms;  /* modal, drawer */
}

:root {
  /* Header/brand anchor — constant across light/dark, a fixed brand identity rather than a theme-following surface */
  --hdr-bg: #1D4E6B;
  --hdr-text: #F7F4EE;
  /* ⚠ MOVED OFF #E3A639, WHICH IS --wheat-gold AND THE COIN'S MIDDLE STOP.
     In the dark colour scheme --risk-med-text is #e0a94a: rgb distance 18 from
     #E3A639, contrast 1.02:1 — the same colour to a reader. Section 1 spent the
     gold reservation on the argument that "nothing in the app signals status in
     gold", and the active nav pill put gold in the header on every screen,
     inches from where risk chips render. The amber was there first and section
     1 lists it under DO NOT TOUCH, so the pill moved instead.

     #F5D08A is measured, not picked: 10.89:1 against --ink (the pill's label),
     78 rgb from the nearest status colour of the thirteen, 103 from
     --flag-saffron, and 93 from --wheat-gold so it is visibly its own colour
     rather than a near-miss of the coin. #FBEBC4, the coin's own highlight, was
     the first choice and is wrong: it lands 27 from --risk-med-bg #fdf1de.

     This slot was chosen over a new token because --hdr-accent, -2 and -3 were
     all defined and referenced NOWHERE, so repainting one costs no palette
     growth and breaks nothing. --wheat-gold keeps #E3A639 and stays tied to the
     coin's gradient stop, which is what section 1's amendment describes. */
  --hdr-accent: #F5D08A;
  --hdr-accent-2: #2F6D4E;
  --hdr-accent-3: #3E7A9A;

  --bg: #f5f4f0;
  --panel: #ffffff;
  --border: #e0ddd4;
  --text: #262420;
  --text-muted: #6b665c;
  --saffron: #c9591a;
  --saffron-dark: #a3480f;

  /* ⚠ DECLARED ONCE AND DELIBERATELY NOT OVERRIDDEN IN THE DARK BLOCK BELOW.
     That puts it on check-contrast's "colour tokens with no dark value" list,
     which is exactly the list the original --growth-green defect came off — so
     this note is the entry's reason for being there. NOT FLIPPING IS THE POINT.
     Do not "fix" it by adding a dark value; that would reintroduce the failure
     this token exists to avoid.

     ⚠ A PER-SCHEME PAIRING WAS TRIED HERE FIRST AND IS UNSOUND. #fff on
     --saffron-dark in light, --ink on --saffron in dark: both pairings read
     correct from the stylesheet and each measures well clear of the floor in
     the scheme it renders in. check-contrast refuses it anyway, because it
     resolves every rule against BOTH token tables and so judges the
     cross-combinations, which are registered rather than asserted —
     the cross-combination in dark is 2.34:1
     and the cross-combination in light is 3.76:1
     That is not pedantry. --growth-green shipped correct in light and failed in
     dark on precisely the assumption that a combination which cannot render
     today never will. The guard judging it anyway is the whole reason this file
     has one, and the two figures above are what that judgement looks like when
     it is right.

     A token that resolves identically in both tables has no cross-combination
     to get wrong: #fff on #a3480f is 6.02:1, once, everywhere. Same shape as
     --ink, chosen over a raw hex so the non-flipping is recorded rather than
     left for the next reader to infer — and that ambiguity is what produced
     the original defect. Hover darkens rather than lightens, matching
     .btn-primary's direction.

     ⚠ BOTH FIGURES ARE STATED AS LIGHT/DARK PAIRS THOUGH EACH IS ONE NUMBER,
     and the repetition is the assertion. check-contrast resolves a pair claim
     against BOTH token tables, so writing the same value twice makes the guard
     re-derive the non-flipping on every commit rather than take this comment's
     word for it. A single-scheme claim would have checked one table and left
     the other free to drift — which is the failure mode being designed out.
     CTA base measured: 6.02:1 light  6.02:1 dark
     CTA hover measured: 7.69:1 light  7.69:1 dark */
  --cta-saffron: #a3480f;
  --cta-saffron-hover: #8a3c0c;

  /* ⚠ SAME FAMILY AS THE TWO ABOVE, AND THE SAME RULE APPLIES: declared once,
     never overridden in dark, and NOT to be "fixed" by giving it a dark value.
     What these three have in common is not the hue — it is that each is a FILL
     painted under a hardcoded #fff. A fill that lightens while its foreground
     stays put is the --saffron/.btn-primary defect exactly, so the fills that
     cannot move are the ones that carry a literal white.

     This one exists because body.report-theme's .btn-secondary:hover painted
     #fff on --rv-canal, which lightens to #5B9BC0 in dark and took the pair to
     3.05:1 — live, and invisible to check-contrast because a theme-scoped
     background does not resolve against the :root tables. Repointing at a
     :root-scoped frozen fill fixes the contrast AND hands the rule to the
     ordinary pair loop, which is the half that keeps it fixed.
     #1D4E6B is --rv-canal's own light value, so light rendering is unchanged.
     Secondary hover pair: 8.92:1 light  8.92:1 dark */
  --fill-canal: #1D4E6B;

  /* ⚠ SAME FAMILY, OPPOSITE TREATMENT, AND THE DIFFERENCE IS THE WHOLE RULE.
     The three tokens above are FROZEN because each is painted under a
     hardcoded #fff — a fill that moves under a foreground that cannot is the
     --saffron/.btn-primary defect. This one carries --text, which DOES flip,
     so freezing it would be the same mistake mirrored: a fixed gold under a
     foreground that goes near-white in dark is exactly the 1.53:1 this
     replaces. Where the foreground flips, the fill must flip with it.

     ⚠ IT REPLACES --rv-wheat ON .btn-secondary AND NOT ANYWHERE ELSE.
     --rv-wheat had three uses: a 20% radial backdrop glow, an 8% hover wash
     into --rv-card, and this button. The first two are atmosphere and never
     touch text; the button was its ONLY full-strength text-bearing use, which
     is why it was the only place it broke. --rv-wheat keeps its value and its
     two atmospheric jobs; the gold identity is unchanged in light.

     ⚠ DECLARED IN :root RATHER THAN ON body.report-theme, DELIBERATELY, AND
     THE REASON IS THE GUARD RATHER THAN THE PALETTE. A token declared in the
     theme scope does not resolve against the :root tables, so the rule using
     it sits in the "no resolvable background" count and is never measured —
     which is precisely how 1.53:1 survived. Scoping this tidily would have
     fixed the colour and preserved the blindness. If the pair loop is later
     taught to consult the theme tables, this can move back; until then, a
     token that is measured beats a token that is filed correctly.

     #6B4310 is rgb 166 from dark --risk-med-text and 161 from --wheat-gold, so
     it does not re-enter the collision check-gold-reservation pins.
     Secondary base pair: 7.21:1 light  7.29:1 dark */
  --fill-wheat: #E3A639;

  --green: #276b47;
  --green-bg: #eaf3ee;
  --blue: #2a5a8c;
  --blue-bg: #eaf1f9;
  --risk-low-bg: #e7f4ea;
  --risk-low-text: #1f7a3d;
  --risk-med-bg: #fdf1de;
  --risk-med-text: #a05a00;
  --risk-high-bg: #fbe9e7;
  --risk-high-text: #b3311c;
  --radius: 10px;
  --shadow: 0 1px 3px rgba(30, 25, 15, 0.08), 0 1px 2px rgba(30, 25, 15, 0.06);

  --sector-agri: #2e7d32;
  --sector-food: #b8860b;
  --sector-mfg: #546e7a;
  --sector-transport: #d84315;
  --sector-retail: #ad1457;
  --sector-tech: #1565c0;
  --sector-edu: #6a1b9a;
  --sector-health: #00796b;
  --sector-tourism: #b5900f;
  --sector-energy: #00838f;
  --sector-export: #3949ab;
  --sector-default: #6b665c;

  --future: #5b3fa6;
  --future-bg: #efeaf9;
  --future-text: #5b3fa6;
  --future-panel-start: #f7f4fd;
  --future-panel-end: #ffffff;

  --video: #c0392b;
  --video-bg: #fbebe9;

  --match: #1f7a5c;
  --match-bg: #e5f3ed;

  --trade: #92500a;
  --trade-bg: #fbeedc;
  --trade-panel-start: #fdf7ee;
  --trade-panel-end: #ffffff;

  --elite: #8a6d1f;
  --elite-bg: #faf3df;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #171613;
    --panel: #211f1b;
    --border: #38352d;
    --text: #eeece5;
    --text-muted: #a39d8e;
    --saffron: #e07a3a;
    --saffron-dark: #f0925a;
    --green: #4caf7d;
    --green-bg: #1b2a20;
    --blue: #6ba3d6;
    --blue-bg: #182533;
    --risk-low-bg: #17301f;
    --risk-low-text: #6fcf8f;
    --risk-med-bg: #332608;
    --risk-med-text: #e0a94a;
    --risk-high-bg: #331b16;
    --risk-high-text: #ea7b68;
    --shadow: 0 1px 3px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.3);

    --sector-agri: #66bb6a;
    --sector-food: #e0b13a;
    --sector-mfg: #90a4ae;
    --sector-transport: #ff8a65;
    --sector-retail: #f06292;
    --sector-tech: #64b5f6;
    --sector-edu: #ba68c8;
    --sector-health: #4db6ac;
    --sector-tourism: #e0c34a;
    --sector-energy: #4dd0e1;
    --sector-export: #7986cb;
    --sector-default: #a39d8e;

    --future: #b19ee8;
    --future-bg: #2a2340;
    --future-text: #c3b2ef;
    --future-panel-start: #211b33;
    --future-panel-end: #211f1b;

    --video: #e17a6d;
    --video-bg: #35201d;

    /* The dark half of the flipping fill. --fill-canal, --cta-saffron and
       --cta-saffron-hover are deliberately absent from this block; this one is
       deliberately present. See --fill-wheat's declaration for why the two
       treatments are opposites rather than an inconsistency. */
    --fill-wheat: #6B4310;

    --match: #5fcba0;
    --match-bg: #17322a;

    --trade: #e0a659;
    --trade-bg: #3a2a10;
    --trade-panel-start: #2b2013;
    --trade-panel-end: #211f1b;

    --elite: #e8c65a;
    --elite-bg: #332b12;
  }
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: 'Noto Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  background: var(--bg);
  color: var(--text);
  line-height: var(--leading-normal);
}

.display-font {
  font-family: 'Barlow Condensed', 'Arial Narrow', sans-serif;
}

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5) 20px;
  background: var(--hdr-bg);
  border-bottom: none;
  position: sticky;
  top: 0;
  z-index: 10;
}

/* The one tricolour rule the brief allows (section 2, allowed use 2): 3px,
   saffron / white / green in three equal segments, directly under the site
   header and nowhere else on the page.

   It replaces a 5px repeating diagonal stripe in wheat-gold, growth-green and
   --hdr-accent-3. Two decorative rules stacked under one header would be
   noise, and the brief caps this at one per page.

   "Not a gradient" is honoured: linear-gradient is the CSS function, but every
   stop is doubled at the same position, so there is no blend anywhere — three
   flat bands with hard edges at exactly 1/3 and 2/3. */
.topbar::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 3px;
  background: linear-gradient(
    to right,
    var(--flag-saffron) 0 33.333%,
    var(--flag-white) 33.333% 66.667%,
    var(--flag-green) 66.667% 100%
  );
}

.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-weight: 600;
  font-size: 17px;
}

/* Brand lockup styling lives in the wordmark block at the end of this file,
   and styles the output of renderWordmarkV2() in brand-mark-v2.js. */

/* Active tab indicator (section 6).
   ------------------------------------------------------------------
   ⚠ THIS PARAGRAPH SAID THE PILL WAS GONE, AND IT HAS BEEN WRONG SINCE
   535f999. There is a pill again: full height, radius-full, filled. It is
   corrected rather than deleted because the sentence that followed it was
   RIGHT, and being right early is worth keeping:

     "Section 1 reserves wheat-gold for non-status use, and 'which tab am I
      on' is status, so dropping that fill settles a small conflict."

   That conflict was not small and it was not settled — it was reintroduced
   when the pill came back in --wheat-gold, and it took a measurement to see
   it: gold and the dark-scheme --risk-med-text are 18 apart in rgb, contrast
   1.02:1. Whoever wrote that line reasoned it out from the brief without
   needing the numbers. The pill now fills with --hdr-accent #F5D08A, 78 clear
   of the nearest status colour, which is the same conclusion reached the
   expensive way.

   ⚠ ONE COLOUR DEVIATION, AND IT IS FORCED.
   Section 6 asks for a --canal-blue bar with --canal-blue active labels
   and --ink at 65% for inactive ones. This nav sits inside .topbar,
   whose background --hdr-bg IS #1D4E6B — the same colour as
   --canal-blue. Measured on that surface the bar and the active label
   are 1.00:1, which is not low contrast but the background colour
   exactly, and --ink at 65% is a near-black on a dark blue. Section 6's
   palette assumes the nav sits on paper.

   ⚠ AND THE HIERARCHY IS NO LONGER THE ONE SECTION 6 DESCRIBES. This said
   "one weight step, three opacity steps, a sliding bar — implemented exactly",
   and none of the three clauses is still true. The opacity steps are gone:
   inactive labels are solid --hdr-text, hover is an 8% tint of that colour
   behind the tab, and the active tab is --ink on an --hdr-accent pill. The bar
   still slides but it is a full-height pill rather than an underline. What
   survives of section 6 is the weight step, 500 to 600.

   Recorded rather than smoothed over, because the reason is worth keeping: an
   opacity step dims the icon, the label and any background as one, so it
   cannot be reasoned about per part, and it left no headroom for a hover
   signal — 0.65 to 0.85 to 1.00 spends the whole range on state. Colour and a
   background tint separate those concerns. Moving the nav onto a --paper strip
   would make section 6's literal colours correct and is still the real fix; it
   restructures the header, so it still waits for a ruling. */

.view-toggle {
  position: relative;              /* positioning context for the indicator */
  display: inline-flex;
  flex-wrap: nowrap;               /* section 6: no wrapping */
  gap: var(--space-1);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  /* proximity, not mandatory: a mandatory strip fights a reader who is
     dragging to see the last tab by yanking the nearest one to the edge. */
  scroll-snap-type: x proximity;
  /* Fade mask on the overflow edge, so a cut-off tab reads as "more this
     way" rather than as a tab that happens to end at the viewport.
   *
   * THIS IS THE DEFAULT RATHER THAN THE EXCEPTION ON PURPOSE. It is the
   * treatment that is safe at any width, so it is what the nav looks like
   * before positionNavIndicator() has run and if app.js never loads at all.
   * .is-fitted below is the narrower claim, and a claim needs a measurement. */
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent 100%);
          mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent 100%);
}

/* Set by positionNavIndicator() when the tabs measurably fit, which replaced a
   1264px media query — see the reasoning where it is set. Both declarations
   here are wrong whenever the strip DOES overflow, which is why neither is in
   the base rule: the fade would be an affordance for scrolling that cannot
   happen, and the centring would push the leading tabs off the left edge where
   scrollLeft cannot reach them.

   overflow-x stays `auto` in both states, and that is what makes the whole
   mechanism work rather than an oversight. `auto` is what lets the strip take
   a width from the flex line and report scrollWidth > clientWidth against it;
   flipped to `visible` — as the spec for this asked — the strip sizes to its
   own content instead, clientWidth equals scrollWidth at every viewport, and
   the overflow becomes both invisible to the measurement and unreachable by
   the reader. With content that fits, `auto` shows no scrollbar and differs
   from `visible` in nothing a reader can see. */
.view-toggle.is-fitted {
  justify-content: center;
  -webkit-mask-image: none;
          mask-image: none;
}

.view-toggle::-webkit-scrollbar { display: none; }

.toggle-btn {
  border: none;
  background: transparent;
  /* ⚠ THE opacity: 0.65 THAT WAS HERE IS DELETED, NOT OVERRIDDEN. It was the
     dull look, and it was section 6's inactive step: the colour was never the
     problem, --hdr-text is #F7F4EE with no alpha. Opacity is also the wrong
     tool for it — it dims the icon, the label and any background together and
     cannot be reasoned about per part. Anything reintroducing an inactive dim
     belongs in the colour, not on top of it. */
  color: var(--hdr-text);
  /* Was --space-2 --space-3 --space-3: the extra 4px underneath was clearance
     for the 3px underline, which no longer exists. On a full-height pill that
     asymmetry sits the label 2px above the fill's optical centre, so the
     padding is even now. Costs 4px of strip height. */
  padding: var(--space-2) var(--space-3);
  /* Matches .nav-indicator's radius so the hover tint below is the same shape
     as the active pill, rather than a square patch behind a rounded one. */
  border-radius: var(--radius-full);
  /* Lifts the label and icon above .nav-indicator's z-index 0. */
  position: relative;
  z-index: 1;
  font-family: 'Noto Sans', system-ui, sans-serif;
  font-size: var(--text-sm);
  font-weight: 500;                /* section 6: inactive */
  letter-spacing: 0;
  white-space: nowrap;
  cursor: pointer;
  /* Was opacity; hover is a background tint now, so this transitions what
     actually changes. Same --dur-micro the opacity fade used. */
  transition: background-color var(--dur-micro) var(--ease);
  /* inline-flex so the icon and the label sit on one baseline-centred row.
     6px is off the 4/8/12 spacing scale on purpose: --space-1 (4px) reads as
     the icon touching the word and --space-2 (8px) as two separate objects.
     The sibling .toggle-btn-paid already carries a 7px literal for the same
     reason, so a literal here is the house style rather than a break from it. */
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* Never let a tab shrink: the strip scrolls instead. Without this the last
     tabs compress toward their icons as the viewport narrows, which is the
     icon-only mobile nav we are explicitly not building. */
  flex: 0 0 auto;
  scroll-snap-align: start;
}

/* Hover needs its own signal now that the label is solid white at rest: there
   is no headroom left to brighten. A tint of the header's own text colour at
   8% reads as the tab lifting off the bar without introducing a new hue, and
   it is the shape of the active pill, so hovering pre-figures what selecting
   will do.

   :not(.active) because the active tab already carries the gold fill — a white
   wash over that would only dirty it, and the tab a reader is already on has
   nothing to invite. */
.toggle-btn:not(.active):hover {
  background-color: rgba(247, 244, 238, 0.08);
}

/* Sized in px, not em: these are 24-grid line drawings whose 1.5 stroke was
   drawn for one size, and letting them ride --text-sm would resample the
   stroke against a font scale they were never fitted to. */
.toggle-btn svg {
  width: 17px;
  height: 17px;
  flex: 0 0 auto;
}

/* Dark ink, not --hdr-text, because the active tab sits on the --hdr-accent
   pill rather than on the header blue. Measured on #F5D08A: --hdr-text #F7F4EE
   is 1.34:1, which fails AA by a mile, and --ink #24211D is 10.89:1, which
   clears AAA for normal text with room to spare. The icons need no rule of
   their own — they are stroked in currentColor, so they inherit this and go
   dark with the label. Verified in the browser rather than assumed.

   Both figures moved when the pill left #E3A639 (they were 1.96 and 7.47) and
   check-nav-contrast.js is what makes that a fact rather than a hope: it
   re-derives every ratio quoted here from the tokens the rules reference, so a
   repaint that leaves these sentences behind fails the suite. */
.toggle-btn.active {
  background: transparent;         /* the pill provides the fill */
  color: var(--ink);
  /* opacity: 1 was here and is deleted with the 0.65 it existed to cancel.
     Restating the default is how a dim creeps back: the next reader sees a
     rule raising opacity and infers something is lowering it. */
  font-weight: 600;                /* section 6: active */
}

/* There is deliberately NO media query for the nav strip. Two were written and
   both were wrong: 900px, which turned overflow off across a 364px band where
   the content still overflowed, and 1264px, a correct measurement of a number
   that stops being correct as soon as anyone renames a tab. A breakpoint is a
   guess at a layout question; .view-toggle.is-fitted above is the answer to it,
   measured on the element itself. If you are about to add one back here,
   whatever you are keying on is probably also measurable. */

/* One shared element that slides, not a per-tab show/hide. Still the same
   element and the same is-ready gate below — a pill is a restyling of the
   indicator, not a second mechanism.

   ⚠ WIDTH IS ANIMATED HERE AND SECTION 5 OF THE BRIEF ALLOWS TRANSFORM AND
   OPACITY ONLY. This is a knowing departure, recorded rather than hidden. The
   old underline could take its width from scaleX off a fixed 100px base
   because a 3px bar has no shape to distort. A pill does: scaleX multiplies
   the horizontal axis of border-radius with it, so a 999px radius scaled to a
   259px tab flattens into an oval on the wide tabs and a lozenge on the narrow
   ones, and the corners visibly breathe as it slides. That is the rubbery
   read. Width is the only way to move the edge without deforming the corner,
   so transform carries the position and width carries the size.

   Both are on the same --ease and --dur-base in the is-ready rule, which is
   the other half of not reading as rubbery: two easings on one object arriving
   at different moments is what makes a slide feel elastic.

   MEASURED RATHER THAN ASSERTED, by pausing the transition through the Web
   Animations API and scrubbing it — 200ms cannot be sampled by timer in a
   background tab, where timers are clamped to a second. Moving from the 129px
   tab to the 261px one there are exactly two animations, transform and width,
   both 200ms, both cubic-bezier(0.2,0,0.1,1), both zero delay, and at every
   frame each is the SAME fraction through its own travel: 52.7% at t=50, 85%
   at t=100, 97% at t=150. That equality is the thing to re-check if this ever
   starts looking elastic again.

   The radius holds at 17.5px — half the 35px height, which is what 999px
   clamps to — at every frame. For contrast, the old scaleX approach at this
   width is a factor of 2.41, which would have left that corner an ellipse of
   42.2px horizontal against 17.5px vertical, changing shape continuously as
   the bar slid. Rendered side by side to confirm it rather than taking it on
   faith.

   z-index 0 against .toggle-btn's 1 puts the fill behind the label. The
   element is the strip's LAST child, so without this it would paint over the
   text it is meant to sit under. */
.nav-indicator {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;                       /* full height, not a 3px edge */
  width: 0;                        /* JS sets this; 0 keeps it unpainted */
  transform: translateX(0);
  /* --hdr-accent, not --wheat-gold: the token block above records why. Short
     version — gold and the dark-scheme risk amber are the same colour to a
     reader, and this pill is the most visible gold surface in the app. */
  background: var(--hdr-accent);
  border-radius: var(--radius-full);
  pointer-events: none;
  z-index: 0;
}

/* The slide is enabled only AFTER the bar has been painted once under the
   tab that is already active. Two reasons, and the second is a real bug
   rather than a preference:
     1. On load the bar should be under the current tab, not flying in from
        the left edge — the brief wants motion that confirms an interaction,
        and nothing has been interacted with yet.
     2. Creating the element and setting its transform in the same frame
        leaves a CSSTransition stuck at currentTime 0 with playState
        "running": the bar keeps the rule's width of 0 and never advances, so
        the indicator is invisible and no error is raised anywhere. */
.nav-indicator.is-ready {
  /* ONE token for both properties, deliberately. Position and size have to
     arrive together; a different duration or curve on either makes the pill
     stretch ahead of itself and snap back, which is the elastic read. */
  transition: transform var(--dur-base) var(--ease),
              width     var(--dur-base) var(--ease);
}

.container {
  max-width: 1080px;
  margin: 0 auto;
  padding: var(--space-5);
}

.hidden { display: none; }
.view.hidden { display: none; }

.panel {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 22px var(--space-5);
  margin-bottom: 20px;
}

.panel h2 {
  margin-top: 0;
  font-family: 'Barlow Condensed', 'Arial Narrow', sans-serif;
  font-weight: 800;
  font-size: 19px;
  letter-spacing: 0.2px;
  display: flex;
  align-items: center;
  gap: 10px;
}

.count-badge {
  background: var(--blue-bg);
  color: var(--blue);
  font-size: var(--text-xs);
  font-weight: 700;
  padding: 2px 9px;
  border-radius: var(--radius-full);
}

.panel-subtitle {
  color: var(--text-muted);
  font-size: 13.5px;
  margin: -6px 0 16px;
}

.subsection-title {
  font-size: var(--text-sm);
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 22px 0 14px;
  padding-top: 18px;
  border-top: 1px solid var(--border);
}

.filters-grid {
  display: grid;
  grid-template-columns: 1fr 1.3fr 1fr 1fr;
  gap: 14px;
}

.form-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 14px;
  margin-bottom: var(--space-1);
}

.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
/* ⚠ NEEDED BECAUSE .field WINS OTHERWISE. `.hidden { display: none }` is defined
   further up this file at equal specificity (0,1,0), so the later `.field`
   rule beats it and `class="field hidden"` stays VISIBLE — the opposite of what
   the markup says. Anywhere else in the app `.hidden` works; on a .field it
   needs this. */
.field.hidden { display: none; }
.field-wide { grid-column: 1 / -1; }

label {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-muted);
}

.optional { font-weight: 400; text-transform: none; }

input[type="text"], input[type="number"], select, textarea {
  font-family: inherit;
  font-size: var(--text-sm);
  padding: 9px 11px;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--bg);
  color: var(--text);
  width: 100%;
}

/* The border tint stays as the mouse-focus affordance. The outline: none that
   used to sit here is gone — see the focus ring block at the end of this file
   for what replaced it and why 63 elements were relying on a 1px border
   colour change as their only focus indicator. */
input:focus, select:focus, textarea:focus {
  border-color: var(--saffron);
}

textarea { resize: vertical; font-family: inherit; }

.form-actions {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-top: 6px;
}

.btn-primary {
  background: var(--saffron);
  color: #fff;
  border: none;
  padding: 10px 22px;
  border-radius: 7px;
  font-family: 'Barlow Condensed', 'Arial Narrow', sans-serif;
  font-size: var(--text-base);
  font-weight: 700;
  letter-spacing: 0.3px;
  cursor: pointer;
}

.btn-primary:hover { background: var(--saffron-dark); }

.save-confirmation {
  color: var(--green);
  font-weight: 600;
  font-size: var(--text-sm);
}

.save-confirmation.hidden { display: none; }

.empty-state {
  color: var(--text-muted);
  font-size: var(--text-sm);
  padding: 18px var(--space-1);
}

/* The "Interview list (admin)" block stood here: .interview-list,
   .interview-row and its four children. The Admin tab that rendered these rows
   went in e82d6d4 and the interview store behind it in de8448d, but these rules
   survived both — de8448d's message says it removed "five orphaned selectors",
   and seven class names were actually orphaned. Confirmed dead per selector
   across all tracked files, not in aggregate: each of the seven appeared only
   in its own definition here, with no consumer in any of the 164 .js or 8 .css
   files, and no runtime construction (no `card-${...}` or "interview-" + x) that
   could produce one of these names without a literal match. */

.btn-delete {
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-muted);
  border-radius: var(--radius-md);
  padding: 6px 10px;
  font-size: var(--text-xs);
  cursor: pointer;
  white-space: nowrap;
}

.btn-delete:hover { border-color: var(--risk-high-text); color: var(--risk-high-text); }

/* -------- Report view -------- */

.report-results { display: flex; flex-direction: column; gap: 18px; }

.section-label {
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: var(--text-muted);
  margin: var(--space-1) 0 10px;
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.section-label .pill {
  font-size: 11px;
  font-weight: 700;
  padding: 2px var(--space-2);
  border-radius: var(--radius-full);
  text-transform: none;
  letter-spacing: 0;
}

.pill-general { background: var(--blue-bg); color: var(--blue); }
.pill-future { background: var(--future-bg); color: var(--future-text); }
.pill-videos { background: var(--video-bg); color: var(--video); }
.pill-trade { background: var(--trade-bg); color: var(--trade); }

.card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 20px 22px;
  margin-bottom: 14px;
}

/* .card-interview stood here, green left border, applied to the interview card
   renderInterviewCard drew. Both went in de8448d. The green accent border the
   depth-comment below still names is .card-farming, which is live. */
.card-general { border-left: 4px solid var(--card-accent, var(--sector-default)); }

.sector-agri { --card-accent: var(--sector-agri); }
.sector-food { --card-accent: var(--sector-food); }
.sector-mfg { --card-accent: var(--sector-mfg); }
.sector-transport { --card-accent: var(--sector-transport); }
.sector-retail { --card-accent: var(--sector-retail); }
.sector-tech { --card-accent: var(--sector-tech); }
.sector-edu { --card-accent: var(--sector-edu); }
.sector-health { --card-accent: var(--sector-health); }
.sector-tourism { --card-accent: var(--sector-tourism); }
.sector-energy { --card-accent: var(--sector-energy); }
.sector-export { --card-accent: var(--sector-export); }

.card-title {
  font-family: 'Barlow Condensed', 'Arial Narrow', sans-serif;
  font-weight: 700;
  font-size: var(--text-lg);
  letter-spacing: 0.1px;
  margin-bottom: var(--space-3);
}

.card-title-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}

.card-title-row .card-title { margin-bottom: 0; }

/* The card's primary action, on a row of its own beneath the badges. --space-4
   above and --space-3 below: nearer what it acts on than what it follows, the
   same asymmetry that made the field labels read as headings rather than as
   furniture between two blocks. */
.card-action-row {
  display: flex;
  margin: var(--space-4) 0 var(--space-3);
}

.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--space-3);
  margin-bottom: 14px;
}

/* .stat / .stat-value / .stat-label are defined in provenance.css, appended
   at the end of this file — it owns the full tile treatment (big number,
   tiny label, provenance line, tier badge) for every figure on report cards. */

.text-block { margin-bottom: var(--space-3); }
.text-block:last-child { margin-bottom: 0; }

/* PROMOTED TO THE .challenge-head IDIOM, DELIBERATELY AND ACROSS EVERY LABEL IT
   SERVES. It was 12.5px/700/--text-muted with a 3px bottom margin and NO top
   margin at all, which is why it read as a caption on the text rather than a
   heading over it: smaller than the 14px body beneath it, lighter than it, and
   sitting in a gap identical on both sides so it belonged to neither block.

   ⚠ THE SIZE WENT DOWN, NOT UP, AND THAT WAS THE INSTRUCTION'S OWN FALLBACK.
   --text-sm (14px) was asked for first, on the reading that matching
   .challenge-head meant keeping the untokened 12.5px. It does not — that 12.5px
   is .thin-data-label's value. .challenge-head is var(--text-xs), 12px. So the
   two genuinely conflict, and the ruling was: match .challenge-head and say why.

   WHY: at 14px in Barlow Condensed 800 uppercase these labels would render
   LARGER than .challenge-head, putting "Equipment needed" above "Common
   challenges" in the ladder — inverting it rather than levelling it. 12px keeps
   them level, which is the one tier that was asked for. It also still satisfies
   the reason 14px was wanted: --text-xs is a token, so the untokened 12.5px is
   gone either way.

   THE RANK COMES FROM EVERYTHING EXCEPT SIZE, which is why losing half a pixel
   costs nothing: condensed face, 700 -> 800, uppercase, letter-spacing, and
   --text-muted -> --text. That last one is the whole visibility complaint and it
   is a contrast change as much as a rank one.
   Derived on this pair: 15.49:1 light  13.92:1 dark

   ⚠ AND THE PAIR IT REPLACED PASSED, WHICH IS THE POINT WORTH KEEPING.
   Measured before the repaint: 5.71:1 light  6.09:1 dark
   Both clear the 4.5:1 AA floor, so this
   was never a legibility defect and no accessibility argument was available for
   it — the labels read faint because they were SMALLER, LIGHTER and LESS SPACED
   than the body they introduced, which is a rank problem that contrast numbers
   cannot see. Recorded so nobody re-opens this expecting to find a failure.

   Both pairs are derived by check-contrast.js from the token declarations rather
   than asserted here. This rule sat in that guard's "scheme-responsive
   foreground, no resolvable background" bucket and could not be answered at all
   until a SURFACES entry was added for it, which is why one was.

   MARGINS ARE ASYMMETRIC ON PURPOSE — 16px above against 8px below, so the label
   is nearer the text it introduces than the block it follows, and reads as its
   heading rather than as furniture between two blocks. The top margin COLLAPSES
   THROUGH .text-block, which has no padding or border, so the gap between blocks
   becomes max(12, 16) = 16px rather than 28px. That is the intended result and
   not a happy accident: it is why the parent's margin-bottom was left alone.

   NOT GROUPED WITH .challenge-head despite matching it on five declarations,
   because the sixth differs — that rule has no top margin and this one is
   asymmetric by design. Grouping them would mean overriding the margin straight
   back out, which is two rules pretending to be one. */
.text-block-label {
  font-family: 'Barlow Condensed', 'Arial Narrow', sans-serif;
  font-size: var(--text-xs);
  font-weight: 800;
  letter-spacing: 0.3px;
  text-transform: uppercase;
  color: var(--text);
  margin: var(--space-4) 0 var(--space-2);
}

.text-block-body {
  font-size: var(--text-sm);
  white-space: pre-wrap;
}

.badge-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: 14px;
}

.badge {
  font-size: var(--text-xs);
  font-weight: 700;
  padding: var(--space-1) 10px;
  border-radius: var(--radius-full);
}

.badge-category { background: var(--blue-bg); color: var(--blue); }

.badge-sector {
  background: color-mix(in srgb, var(--card-accent, var(--sector-default)) 16%, var(--panel));
  color: var(--card-accent, var(--sector-default));
}

.badge-risk-low { background: var(--risk-low-bg); color: var(--risk-low-text); }
.badge-risk-medium { background: var(--risk-med-bg); color: var(--risk-med-text); }
.badge-risk-high { background: var(--risk-high-bg); color: var(--risk-high-text); }

.badge-subsidy-yes { background: var(--risk-low-bg); color: var(--risk-low-text); }
.badge-subsidy-no { background: var(--bg); color: var(--text-muted); border: 1px solid var(--border); }

/* Involvement type (Hands-on / Part-time / Passive) — business-model-inherent,
   shown on every card regardless of location coverage. */
.badge-involvement { background: var(--bg); color: var(--text-muted); border: 1px solid var(--border); }

.data-scope-note {
  background: var(--blue-bg);
  color: var(--blue);
  border-radius: 8px;
  padding: var(--space-3) var(--space-4);
  font-size: var(--text-sm);
  font-weight: 600;
  margin-bottom: var(--space-1);
}

/* -------- India expansion: per-state coverage + reference-data honesty -------- */

.state-coverage-note {
  font-size: 12.5px;
  color: var(--text-muted);
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}

/* Why the location picker exists. Same size and colour as the coverage note
   above, but no border-top: this sits directly under the picker as part of it,
   rather than as a separate footnote to the whole panel. Used on both the
   Report and Path pickers, with identical copy. */
.picker-why {
  font-size: 12.5px;
  color: var(--text-muted);
  margin-top: 10px;
  line-height: 1.45;
}

.varies-by-state-note {
  background: var(--risk-med-bg);
  color: var(--risk-med-text);
  border-radius: 8px;
  padding: 14px var(--space-4);
}

.varies-by-state-title {
  font-weight: 700;
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  margin-bottom: 6px;
}

.varies-by-state-note p {
  margin: 0;
  font-size: var(--text-sm);
  line-height: 1.55;
  color: var(--text);
}

.compare-not-verified {
  color: var(--text-muted);
  font-style: italic;
  font-size: 12.5px;
}

/* -------- Sector header row (report: general card) -------- */

.sector-header-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2) 10px;
  margin-bottom: var(--space-4);
}

.sector-template-caption {
  font-size: 12.5px;
  font-weight: 700;
  color: var(--card-accent, var(--text-muted));
  text-transform: uppercase;
  letter-spacing: 0.4px;
  margin-left: 2px;
}

/* -------- Success Meter (quick-scan risk indicator) -------- */

.success-meter {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 11.5px;
  font-weight: 600;
  padding: 2px 9px;
  border-radius: var(--radius-full);
  line-height: 1.4;
  cursor: default;
}

.success-meter-emojis { font-size: 11px; letter-spacing: -1px; }

.success-meter-label { white-space: nowrap; }

.success-meter-green { background: var(--risk-low-bg); color: var(--risk-low-text); }
.success-meter-yellow { background: var(--risk-med-bg); color: var(--risk-med-text); }
.success-meter-red { background: var(--risk-high-bg); color: var(--risk-high-text); }

/* -------- Path coverage meter --------
   Four segments: how many of Path to Success's four sourced steps name a
   source for this business. Geometry only here; the count and the denominator
   are argued in app.js at renderPathCoverageMeter.

   ⚠ THE FILLED SEGMENT IS CANAL BLUE, NOT GROWTH GREEN, AND THAT IS THE SAME
   ARGUMENT .ds-chip-verified LOSES A FEW HUNDRED LINES DOWN. Green is the
   confident state in this system, and a step that names a source is STILL
   pending verification — nothing in this catalogue is ever verified:true.
   Filling these segments green would tell the reader four sources had been
   confirmed when what has happened is that four sources have been NAMED. Canal
   Blue is the brand's structural colour with no status meaning assigned to it,
   which is exactly the claim being made: this step has something behind it.

   The empty segment is a hairline rather than a light fill, so a 0/4 meter
   reads as an outline someone has not filled in yet rather than as a dim
   version of a full one. */

.path-cov {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 11.5px;
  font-weight: 600;
  padding: 2px 9px;
  border-radius: var(--radius-sm);
  background: var(--bg);
  color: var(--text-muted);
  border: 1px solid var(--border);
  white-space: nowrap;
}

.path-cov-bar {
  display: inline-flex;
  gap: 2px;
}

.path-cov-seg {
  display: inline-block;
  width: 10px;
  height: 6px;
  border-radius: 1px;
  border: 1px solid var(--border);
  background: transparent;
}

.path-cov-seg-on {
  background: var(--canal-blue);
  border-color: var(--canal-blue);
}

.path-cov-label { white-space: nowrap; }

/* No sourced step at all — 23 of 99 businesses. The label carries the weight
   here because an all-empty bar alone is ambiguous between "none" and "not
   measured", and this one has been measured. */
.path-cov-none .path-cov-label { font-style: italic; }

/* -------- Seasonal / visitor month strip -------- */

.month-strip-block { margin-bottom: var(--space-4); }

.month-strip {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: var(--space-1);
}

.month-cell {
  text-align: center;
  font-size: 11px;
  font-weight: 700;
  padding: 7px 0;
  border-radius: 5px;
  background: var(--bg);
  color: var(--text-muted);
}

.month-cell.active {
  background: color-mix(in srgb, var(--card-accent, var(--blue)) 22%, var(--panel));
  color: var(--card-accent, var(--blue));
}

/* -------- Capital intensity meter -------- */

.intensity-block { margin-bottom: var(--space-4); }

.intensity-value { color: var(--text); font-weight: 700; text-transform: none; letter-spacing: 0; }

.intensity-meter {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-1);
  margin-top: var(--space-1);
}

.intensity-segment {
  height: 8px;
  border-radius: 4px;
  background: var(--bg);
  border: 1px solid var(--border);
}

.intensity-segment.filled {
  background: var(--card-accent, var(--blue));
  border-color: var(--card-accent, var(--blue));
}

/* -------- Field list (vertical label/value rows) -------- */

.field-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: var(--space-4);
  padding: var(--space-3) 14px;
  background: var(--bg);
  border-radius: 8px;
}

.field-list-item { display: flex; flex-direction: column; gap: 2px; }

.field-list-label {
  font-size: var(--text-xs);
  font-weight: 700;
  color: var(--card-accent, var(--text-muted));
}

.field-list-value { font-size: 13.5px; }

/* -------- Tag card row (demand-grid sectors) -------- */

.tag-card-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 10px;
  margin-bottom: 14px;
}

.tag-card {
  border: 1px solid var(--border);
  border-top: 3px solid var(--card-accent, var(--blue));
  border-radius: 8px;
  padding: 10px var(--space-3);
}

.tag-card-label {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  color: var(--card-accent, var(--text-muted));
  margin-bottom: var(--space-1);
}

.tag-card-value { font-size: 13.5px; }

/* -------- Trade risk block (export sector) -------- */

.trade-risk-block {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

.trade-risk-row {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 14px;
  border-radius: 8px;
  background: color-mix(in srgb, var(--card-accent, var(--blue)) 10%, var(--panel));
  border-left: 3px solid var(--card-accent, var(--blue));
}

.trade-risk-label {
  font-size: var(--text-xs);
  font-weight: 700;
  color: var(--card-accent, var(--blue));
}

.trade-risk-value { font-size: 13.5px; }

/* -------- Future Outlook: markup/CSS now lives in the future-outlook.css
   block appended at the end of this file (.future-outlook, .fo-*) -------- */

/* -------- Export market outlook (only shown for export-relevant businesses) -------- */

.card-export-outlook {
  --card-accent: var(--trade);
  background: linear-gradient(165deg, var(--trade-panel-start), var(--trade-panel-end));
  border: 1px solid var(--border);
  border-top: 3px solid var(--trade);
}

.export-outlook-disclaimer {
  background: var(--trade-bg);
  color: var(--trade);
  border: 1px solid color-mix(in srgb, var(--trade) 35%, transparent);
  border-radius: 8px;
  padding: 10px 14px;
  font-size: 12.5px;
  font-weight: 600;
  line-height: var(--leading-normal);
  margin-bottom: var(--space-4);
}

.export-outlook-header {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  margin-bottom: 14px;
}

.export-outlook-icon {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--trade) 18%, transparent);
  color: var(--trade);
  font-size: var(--text-base);
}

.export-outlook-title { font-weight: 700; font-size: 15px; color: var(--trade); }

.export-outlook-subtitle {
  font-size: 12.5px;
  color: var(--text-muted);
  font-style: italic;
  margin-top: 1px;
}

.export-outlook-fields { margin-bottom: 0; }

/* -------- Related videos (report) -------- */
/* Item-level classes (.video-grid, .video-card, etc.) now come from the
   appended videos.js stylesheet further down this file — this wrapper
   class is all that's still needed here. */

.card-videos { padding: 18px 20px; }

/* -------- Business matcher ("Find My Business") -------- */

/* ⚠ SCOPED TO #view-match ON PURPOSE. .panel-subtitle, .field, label and
   .btn-primary are shared with the Predictor, Farming and Investment forms —
   57 .field uses app-wide. Restyling them globally is a whole-app redesign, not
   this one. The pill rules further down are the deliberate exception. */

/* ⚠ THE FOUR-PARAGRAPH CAPTION IS GONE AND THE HEADING CARRIES THE PAGE.
   It explained the tool before the reader had used it, disclaimed a result they
   had not seen, and pushed the first question below the fold. The single line
   worth keeping — that the budget cannot be tested on every entry — moved into
   the results, and has since been removed from there too. The per-card
   "no cost figure" badge is now the only signal.

   The heading steps from 19px to --text-3xl in the display face already used
   for h2 everywhere, so this is a scale change rather than a new voice. */
#view-match .panel h2 {
  font-size: var(--text-3xl);
  line-height: var(--leading-tight);
  letter-spacing: -0.5px;
  color: var(--text);
  margin-bottom: var(--space-6);
}

#view-match .field { gap: var(--space-2); margin-bottom: var(--space-5); }
#view-match .field > label {
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--text);
}
#view-match .field > label .optional { font-size: var(--text-sm); color: var(--text-muted); font-weight: 400; }
#view-match input[type="number"] {
  font-size: var(--text-base);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
}
#view-match input[type="number"]:focus-visible {
  outline: none;
  border-color: var(--match);
  box-shadow: 0 0 0 var(--ring-width) var(--match-bg);
}
#view-match .form-actions { margin-top: var(--space-5); }
/* ⚠ THE BUTTON IS THE POINT OF THE PAGE AND WAS DRESSED AS A DEFAULT FORM
   CONTROL — 10px/22px, --text-base, the same treatment as a secondary action
   three tabs away. Scoped to #view-match rather than restyled globally, because
   .btn-primary is also the Predictor and Investment submit; making every button
   in the app shout is not the same request.
   ⚠ NO NEW COLOUR. --saffron is the existing UI orange (distinct from
   --flag-saffron, which check-flag-colours reserves), and the lift reuses the
   --shadow token rather than inventing one. */
#view-match .btn-primary {
  font-size: var(--text-lg);
  padding: var(--space-4) var(--space-7);
  border-radius: var(--radius-md);
  letter-spacing: 0.4px;
  box-shadow: var(--shadow);
  transition: background 120ms ease, box-shadow 120ms ease, transform 120ms ease;
}
#view-match .btn-primary .btn-arrow {
  display: inline-block;
  margin-left: var(--space-2);
  transition: transform 120ms ease;
}
#view-match .btn-primary:hover {
  transform: translateY(-1px);
  box-shadow: 0 4px 14px -6px rgba(36, 33, 29, .25);
}
#view-match .btn-primary:hover .btn-arrow { transform: translateX(3px); }
#view-match .btn-primary:active { transform: translateY(0); box-shadow: var(--shadow); }
/* keyboard focus on the primary action was invisible */
#view-match .btn-primary:focus-visible {
  outline: none;
  box-shadow: 0 0 0 var(--ring-width) var(--bg), 0 0 0 var(--ring-width-outer) var(--saffron);
}


/* ⚠ 176px, NOT 150px, AND nowrap ON THE PILL. At 150 the longest sector name
   ("Renewable Energy") wrapped to two lines inside its pill while every other
   sector sat on one, so one option looked like a different control. The track
   is sized so the longest label fits rather than the label being made to fit. */
.checkbox-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(176px, 1fr));
  gap: var(--space-2);
}

/* ⚠ THESE TWO ARE ONE COMPONENT AND ARE RESTYLED GLOBALLY, unlike everything
   above. They are used by the Predictor (7) and Investment (2) as well as the
   matcher (8); scoping the new treatment to #view-match would leave two
   different pill styles in one app, which is worse than upgrading all three.
   Nothing here changes hue — only size, spacing, and the hover and focus states
   that were missing. */
.checkbox-pill, .radio-pill {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  font-weight: 600;
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  padding: var(--space-2) var(--space-4);
  cursor: pointer;
  background: var(--bg);
  user-select: none;
  white-space: nowrap;
  transition: border-color 120ms ease, background 120ms ease, color 120ms ease;
}
.checkbox-pill:hover, .radio-pill:hover { border-color: var(--match); }
/* keyboard focus was invisible; the ring reuses --match-bg rather than adding a hue */
.checkbox-pill:has(input:focus-visible), .radio-pill:has(input:focus-visible) {
  box-shadow: 0 0 0 var(--ring-width) var(--match-bg);
  border-color: var(--match);
}

.checkbox-pill:has(input:checked), .radio-pill:has(input:checked) {
  border-color: var(--match);
  background: var(--match-bg);
  color: var(--match);
  font-weight: 700;
}

.checkbox-pill input, .radio-pill input { accent-color: var(--match); margin: 0; }

.radio-row { display: flex; flex-wrap: wrap; gap: var(--space-2); }

.match-results { display: flex; flex-direction: column; gap: var(--space-1); }

.match-card-list { display: flex; flex-direction: column; gap: var(--space-3); }

.match-card {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  width: 100%;
  text-align: left;
  background: var(--panel);
  border: 1px solid var(--border);
  border-left: 4px solid var(--match);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: var(--space-4) 18px;
  font-family: inherit;
  cursor: pointer;
  transition: transform var(--dur-base) var(--ease), box-shadow var(--dur-base) var(--ease);
}

/* Section 5 puts cards at 2px; this one lifted 1px, which is the button
   distance. Its hover shadow stays hardcoded — see the elevation note at the
   end of this file for why the --shadow-* scale is not applied yet. */
.match-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 10px rgba(30, 25, 15, 0.12);
}

.match-card-rank {
  flex-shrink: 0;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--match-bg);
  color: var(--match);
  font-weight: 700;
  font-size: var(--text-sm);
  display: flex;
  align-items: center;
  justify-content: center;
}

.match-card-main { flex: 1; min-width: 0; }

.match-card-title-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}

.match-card-title { font-weight: 700; font-size: var(--text-base); }

.match-card-score-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
}

/* ⚠ THE FILL STAYS --match AND MUST NOT DRIFT TOWARD --wheat-gold. This is a
   status bar, and check-gold-reservation.js protects the premise that nothing
   in this app signals status in gold — a premise already broken once in dark
   mode by --risk-med-text. A second collision would retire it entirely. */
.match-score-bar {
  flex: 1;
  max-width: 220px;
  height: 8px;
  border-radius: var(--radius-full);
  background: var(--bg);
  overflow: hidden;
}

.match-score-fill { height: 100%; background: var(--match); border-radius: var(--radius-full); }

.match-score-value { font-size: var(--text-sm); font-weight: 700; color: var(--match); white-space: nowrap; }

.match-card-explanation { margin: 0; font-size: var(--text-sm); color: var(--text-muted); line-height: var(--leading-normal); }

/* A data gap, not a risk grade — deliberately neutral rather than amber, so
   it does not read as a warning about the business itself. */
.badge-nocost {
  background: var(--bg);
  color: var(--text-muted);
  border: 1px solid var(--border);
  font-weight: 600;
}

/* ⚠ .match-uncosted-note WAS HERE. Its only consumer, renderMatchUncostedNote
   in app.js, has been removed; the per-card "no cost figure" badge is now the
   only signal that a business was ranked without a budget check. The rule is
   deleted rather than left dormant so a future reader does not style a class
   nothing emits. */

.match-card-cta {
  flex-shrink: 0;
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--match);
  white-space: nowrap;
}

/* -------- Generic secondary button -------- */

.btn-secondary {
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--border);
  padding: var(--space-2) var(--space-4);
  border-radius: 7px;
  font-family: 'Barlow Condensed', 'Arial Narrow', sans-serif;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.2px;
  cursor: pointer;
  white-space: nowrap;
}

.btn-secondary:hover:not(:disabled) { border-color: var(--blue); color: var(--blue); }
.btn-secondary:disabled { opacity: 0.5; cursor: not-allowed; }

/* -------- Report actions bar (title + Download Report) -------- */

.report-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: var(--space-1);
}

.report-actions-title { font-size: 17px; font-weight: 700; }
.report-actions-district { color: var(--text-muted); font-weight: 600; }

/* -------- Add-to-comparison button (on the general card) -------- */

/* ⚠ THIS WAS A 12px PILL IN --blue, AND THE COLOUR WAS THE REAL PROBLEM.
   --blue on this page means PROVENANCE: .pill-general is the "seeded dataset"
   marker, and .badge-category and the section-label pill use it too. The card's
   one call to action was wearing the vocabulary of a data-quality label, at a
   smaller size than everything around it. It now takes the .btn-primary idiom —
   Barlow Condensed, --text-base, 700, 0.3px — which is what this page already
   uses to mean "this is the action", and --blue goes back to meaning provenance.

   ⚠ IT DOES NOT REUSE .btn-primary, AND THE REASON IS IN check-contrast's
   KNOWN_FAILURES. That rule is #fff on --saffron and is pinned as failing in
   BOTH schemes — the figures live in that guard's own list rather than being
   repeated here, with its note that repainting it "is a visible product change
   and belongs in its own commit". Reusing the class would have shipped a new
   and deliberately prominent control at the dark figure. So this is the same
   IDIOM with a pairing that clears AA, in a token of its own — the figures and
   the argument for a non-flipping token live on --cta-saffron's declaration.

   ⚠ THAT MAKES THIS BUTTON AND .btn-primary VISIBLY DIFFER IN DARK MODE, and
   the difference is the point rather than an oversight: this one is correct and
   .btn-primary is the pinned failure. Recorded so the repaint commit that
   check-contrast anticipates knows the target pairing already exists here and
   works in both schemes — --cta-saffron is a proven pairing to copy.

   ⚠ NO @media BLOCK, AND ITS ABSENCE IS THE DESIGN. --cta-saffron resolves to
   the same value in both token tables, so there is no second pairing to state
   and no cross-combination for the guard to catch. The reasoning, and the
   per-scheme attempt it replaced, are recorded on the token itself. */
.btn-compare-add {
  align-self: flex-start;
  background: var(--cta-saffron);
  color: #fff;
  border: 1px solid transparent;
  padding: 10px 22px;
  border-radius: 7px;
  font-family: 'Barlow Condensed', 'Arial Narrow', sans-serif;
  font-size: var(--text-base);
  font-weight: 700;
  letter-spacing: 0.3px;
  cursor: pointer;
  white-space: nowrap;
}

.btn-compare-add:hover:not(:disabled) { background: var(--cta-saffron-hover); }

/* Confirmation, not action — --match already means "selected" in the pickers,
   and a shortlisted card is reporting state rather than offering one. */
.btn-compare-add.in-comparison { background: var(--match-bg); color: var(--match); }

/* ⚠ opacity: 0.55 STOOD HERE AND WAS THE MUTED-ON-MUTED FAILURE. Fading the
   whole element composites text and background toward the panel together, so
   the pair collapses: 2.14:1 in light and 2.27:1 in dark, both far under the
   floor, on a control whose entire job is to explain why it cannot be used.
   An explicit pairing instead, and it reads as inactive because it drops the
   saffron rather than because it is faint.
   Disabled pair: 5.18:1 light  6.70:1 dark */
.btn-compare-add:disabled {
  background: var(--bg);
  color: var(--text-muted);
  border-color: var(--border);
  cursor: not-allowed;
}

/* -------- Comparison bar + table -------- */

.comparison-bar-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: var(--space-3);
}

.comparison-bar-title { font-weight: 700; font-size: 15px; display: flex; align-items: center; gap: var(--space-2); }

.comparison-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }

.empty-hint { color: var(--text-muted); font-size: 13.5px; }

.comparison-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: var(--blue-bg);
  color: var(--blue);
  border-radius: var(--radius-full);
  padding: 5px var(--space-2) 5px var(--space-3);
  font-size: 13px;
  font-weight: 600;
}

.comparison-chip-district { font-weight: 400; opacity: 0.85; }

.comparison-chip-remove {
  border: none;
  background: transparent;
  color: inherit;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  padding: 2px var(--space-1);
  border-radius: 50%;
}

.comparison-chip-remove:hover { background: rgba(0, 0, 0, 0.08); }

.comparison-table-wrap { margin-top: var(--space-4); overflow-x: auto; }

.compare-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13.5px;
}

.compare-table th, .compare-table td {
  border: 1px solid var(--border);
  padding: 10px var(--space-3);
  text-align: left;
  vertical-align: top;
}

.compare-table thead th { background: var(--bg); }

.compare-row-label { font-weight: 700; color: var(--text-muted); white-space: nowrap; background: var(--bg); }

.compare-col-title { font-weight: 700; font-size: var(--text-sm); }
.compare-col-district { color: var(--text-muted); font-size: 12.5px; margin: 2px 0 var(--space-2); }

.compare-disclaimer { color: var(--text-muted); font-size: 12.5px; margin: 10px 0 0; }

/* .panel-subtitle-inline outlived the block it was declared in. The Success
   stories carousel styles that surrounded it are gone with the carousel; this
   rule stays because investment-desk.js still renders the class. */

.panel-subtitle-inline { font-size: 12.5px; font-weight: 400; color: var(--text-muted); }

/* The Interview coverage dashboard styles stood here. The dashboard was
   removed with the Admin tab; nothing renders .coverage-* any more. */

/* -------- Print-to-PDF stylesheet -------- */

@media print {
  .topbar, .filters-panel, .comparison-bar, .view-toggle {
    display: none !important;
  }
  .container { max-width: 100%; padding: 0; }
  #report-results .card-videos,
  #report-results .card-export-outlook,
  #report-results .section-label:has(+ .card-videos),
  #report-results .section-label:has(+ .card-export-outlook),
  .btn-compare-add,
  #report-download-btn {
    display: none !important;
  }
  body { background: #fff; color: #000; }
  .card, .data-scope-note {
    box-shadow: none !important;
    border: 1px solid #ccc !important;
    break-inside: avoid;
  }
}

/* -------- Success Predictor -------- */

.predictor-disclaimer {
  background: var(--risk-med-bg);
  color: var(--risk-med-text);
  border: 1px solid color-mix(in srgb, var(--risk-med-text) 30%, transparent);
  border-radius: 8px;
  padding: 10px 14px;
  font-size: 13px;
  font-weight: 600;
  margin-bottom: 18px;
}

.predictor-results { display: flex; flex-direction: column; gap: 18px; }

.card-predictor-result {
  --zone-color: var(--risk-high-text);
  --zone-bg: var(--risk-high-bg);
  border-left: 4px solid var(--zone-color);
  background: var(--zone-bg);
}

.predictor-zone-red { --zone-color: var(--risk-high-text); --zone-bg: var(--risk-high-bg); }
.predictor-zone-growth { --zone-color: var(--risk-med-text); --zone-bg: var(--risk-med-bg); }
.predictor-zone-momentum { --zone-color: var(--risk-low-text); --zone-bg: var(--risk-low-bg); }
.predictor-zone-elite { --zone-color: var(--elite); --zone-bg: var(--elite-bg); }
/* Deliberately neutral. An unrated result is an absence of evidence, and
   giving it the red zone’s colour would reinstate by styling the verdict the
   copy just withheld. */
.predictor-zone-unrated { --zone-color: var(--text-muted); --zone-bg: var(--bg); }
.predictor-score-unrated { opacity: 0.55; }

.predictor-score-row {
  display: flex;
  align-items: center;
  gap: 18px;
  margin-bottom: 10px;
}

.predictor-score-number {
  font-size: 48px;
  font-weight: 800;
  color: var(--zone-color);
  line-height: 1;
}

.predictor-zone-label { font-size: var(--text-lg); font-weight: 700; color: var(--zone-color); }
.predictor-zone-sub { font-size: 13.5px; color: var(--text-muted); margin-top: 2px; }

.predictor-source-note {
  font-size: 12.5px;
  color: var(--text-muted);
  padding-top: 10px;
  border-top: 1px solid var(--border);
}

.reality-check-list { display: flex; flex-direction: column; gap: 14px; }

.reality-row {
  display: grid;
  grid-template-columns: 160px 1fr;
  gap: 14px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--border);
}

.reality-row:last-child { border-bottom: none; padding-bottom: 0; }

.reality-row-label { font-weight: 700; font-size: 13.5px; color: var(--text-muted); }

.reality-row-line { font-size: 13.5px; margin-bottom: 3px; }

.reality-row-source { color: var(--text-muted); font-size: var(--text-xs); }

.reality-row-gap { font-size: 13.5px; font-weight: 600; margin-top: var(--space-1); }

.recommendations-list { margin: 0; padding-left: 20px; display: flex; flex-direction: column; gap: var(--space-2); }
.recommendations-list li { font-size: var(--text-sm); line-height: var(--leading-normal); }

/* -------- Predictor expense/revenue calculators -------- */

.calc-block {
  background: var(--bg);
  border-radius: 8px;
  padding: 14px var(--space-4);
  margin-bottom: 14px;
}

.calc-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 10px;
}

.calc-header label { font-size: 13px; font-weight: 600; color: var(--text-muted); }

.btn-suggest { padding: 6px var(--space-3); font-size: 12.5px; }

.calc-suggest-source {
  background: var(--match-bg);
  color: var(--match);
  border-radius: 7px;
  padding: var(--space-2) var(--space-3);
  font-size: 12.5px;
  line-height: var(--leading-normal);
  margin-bottom: var(--space-3);
}

.calc-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: 10px;
  margin-bottom: var(--space-3);
}

.calc-grid-3 { grid-template-columns: repeat(3, 1fr); }

.calc-field { display: flex; flex-direction: column; gap: var(--space-1); }
.calc-field label { font-size: 11.5px; color: var(--text-muted); }
.calc-field input { padding: 7px 9px; font-size: 13px; }

.calc-total-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-top: 10px;
  border-top: 1px solid var(--border);
  font-size: 13.5px;
  font-weight: 600;
}

.calc-total-value { font-size: var(--text-base); font-weight: 800; color: var(--saffron); }

/* -------- Path to Success --------
   Replaced the Credential Pathway block, whose selectors (.credential-results,
   .card-credential-body, .credential-cta-row) had no markup left to style once
   that view was restructured. Nothing else in the file referenced them. */

.path-results { display: flex; flex-direction: column; gap: var(--space-4); }

.path-header { border-left: 4px solid var(--saffron); }
.path-header-sub { font-size: 13px; color: var(--text-muted); margin: var(--space-1) 0 0; }

/* Each step is a card carrying a number, a title, and an at-a-glance badge —
   the badge is where a gap announces itself ("not sourced", "none found") so a
   reader scanning the column can see which steps have real content before
   reading any of it. */
.path-step { border-left: 4px solid var(--blue); }
.path-step-gap { border-left-color: var(--text-muted); background: var(--panel); }

.path-step-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 10px;
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--border);
}

.path-step-num {
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--blue);
}

.path-step-title { font-size: var(--text-base); font-weight: 700; flex: 1 1 auto; }

.path-step-badge {
  font-size: 11.5px;
  font-weight: 700;
  padding: 2px 9px;
  border-radius: var(--radius-full);
  background: var(--border);
  color: var(--text-muted);
  white-space: nowrap;
}
.path-step-gap .path-step-num { color: var(--text-muted); }

.path-step-body { font-size: var(--text-sm); line-height: 1.6; }
.path-step-body p { margin: 0 0 10px; }
.path-step-body p:last-child { margin-bottom: 0; }

.path-lead { font-weight: 600; }
.path-muted { font-size: 12.5px; color: var(--text-muted); }

/* The standing note above Step 1, filled by initGrowthNote() at startup.
 *
 * ⚠ DISTINCTIVE THROUGH SIZE AND AIR, NOT WEIGHT, CASE OR COLOUR — and each of
 * those three is a refusal with a reason.
 *
 *   COLOUR: none declared, so it inherits. Everything coloured on this site
 *   carries provenance meaning — amber is the pending chip state, red is
 *   documented risk, green is refused outright one comment over — and a
 *   decorative block at the top of the page would spend that vocabulary on
 *   furniture. It also leaves this rule NOT colour-bearing, so check-contrast
 *   has no background to resolve and no SURFACES entry arises.
 *
 *   WEIGHT AND CASE: 500 on the lead and 400 on the body, sentence case, no
 *   letter-spacing. STEP 1 · EXPLORE sits directly below this at 700 and
 *   uppercase; matching it would put two competing eyebrows in the first 100
 *   pixels of the page, and the quieter one would still be the louder claim.
 *
 *   SIZE AND SPACE: --text-lg against the eyebrow's --text-xs, and --space-6
 *   above and below. The separation is the margin. A border or a tint would
 *   read as a callout — a thing being sold — and this is a standing fact about
 *   the catalogue, not an announcement. */
/* ⚠ ZERO MARGIN, BECAUSE IT LIVES IN A GRID CELL NOW. This carried
   `margin: var(--space-6) 0` while it sat above the panel as a block of its own,
   where the air WAS the separation. Inside .filters-grid that same margin adds
   64px to the picker row and drops the text below the labels beside it — the
   grid's own 18px gap is the separation here, and the row is what it has to
   align to. Measured: with margins zeroed the note is 89px against the picker
   fields' 75px, so the card grows 14px and nothing else moves.

   grid-column spans tracks 3 and 4, which are empty: the grid declares four
   columns, holds two visible fields, and the retired state/city pair is
   display:none and consumes no track. Spanning is what makes the line wrap at
   444px rather than 213px — at 213px the body runs to four lines and reads as a
   narrow column of leftovers.

   max-width stays and is inert at this width (--measure is 68ch ≈ 622px against
   a 444px cell). It is kept for the day the grid changes rather than removed as
   dead: a note that escapes into a wide cell should still stop at the measure. */
/* ⚠ IT WAS 18px BESIDE 13px LABELS AND READ LOUDER THAN THE CONTROLS. --text-lg
   was right when this was a standalone block above the panel with air around it;
   in the picker row it made an aside the largest type in the card, with nothing
   holding it to the row.
 *
 * THE SIZES ARE THE CLOSEST THE SCALE ALLOWS, AND THAT IS WORTH STATING: the
 * tokens are --text-xs 12px and --text-sm 14px, the picker labels are 13px, and
 * THERE IS NO TOKEN BETWEEN THEM. A heading that is larger than the body but no
 * larger than the labels therefore has exactly one value available — 13px, which
 * is not a new size but the one the base `label` rule already declares for this
 * role, at the same 600 weight. The note's heading reads as a peer of "Business
 * type" and "Where you are", which is what an aside in this row should be.
 *
 * ⚠ 1px, NOT 3px, AND THE DIFFERENCE IS VOCABULARY RATHER THAN TASTE. A 3px
 * left accent is .path-disclaimer-strong's idiom — reused by the Ask panel's
 * trust badge — and it means "this qualifies or warrants what is around it".
 * This note warrants nothing; it states the catalogue's size. A hairline in
 * --border gives it structure without borrowing that meaning, and --border is a
 * line colour already used for hairlines throughout, not a tint.
 *
 * Still no colour declared on the text: it inherits. */
.growth-note {
  grid-column: 3 / -1;
  margin: 0;
  max-width: var(--measure);
  border-left: 1px solid var(--border);
  padding-left: var(--space-3);
}

.growth-note p {
  font-size: var(--text-xs);
  line-height: 1.5;
  margin: 0;
}

/* Specificity: `.growth-note p` is (0,1,1), so a bare class would lose the size
   it needs to override. Same reason .growth-note p + p is written as a pair. */
.growth-note p.growth-note-lead {
  font-size: 13px;
  font-weight: 600;
}

/* p + p rather than .growth-note-body, and not !important: `.growth-note p`
   zeroes the margin at specificity (0,1,1), which a single class would lose to.
   Same shape as .thin-data-note p + p — the gap belongs to the pair, not to the
   second element. */
.growth-note p + p {
  font-weight: 400;
  margin-top: var(--space-2);
}

/* A gap is stated in plain language, not hidden — same weight as real content
   so it cannot be mistaken for a rendering failure. */
.path-gap-text { font-weight: 600; color: var(--text-muted); }

/* The secondary line under a gap step. Quieter than .path-gap-text, which
   states the gap itself at full weight — this only says why the gap is empty,
   and a reader who already understood the gap should be able to skip it. */
.path-step-gap-note {
  font-size: var(--text-xs);
  line-height: 1.55;
  color: var(--text-muted);
  font-style: italic;
  margin-top: 10px;
  padding-top: var(--space-2);
  border-top: 1px solid var(--border);
}

.path-note {
  font-size: 12.5px;
  line-height: 1.55;
  background: rgba(0, 0, 0, 0.035);
  border-left: 3px solid var(--border);
  padding: var(--space-2) 11px;
  margin: 10px 0 0;
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
}
.path-note-inline { font-size: var(--text-xs); color: var(--text-muted); margin-top: 3px; }

/* Step 2 — licences */
.path-licence-list { list-style: none; margin: 0; padding: 0; }
.path-licence-list > li {
  padding: 10px 0;
  border-bottom: 1px solid var(--border);
}
.path-licence-list > li:last-child { border-bottom: 0; }
.path-licence-name { font-weight: 700; margin-bottom: 3px; }
.path-licence-meta { font-size: 12.5px; line-height: var(--leading-normal); }
.path-licence-src { font-size: 11.5px; color: var(--text-muted); font-style: italic; margin-top: 3px; }
/* The reason a timeline is missing, kept under the "Not published" answer rather
   than inside it — so the labelled slot stays short enough to skim. */
.path-licence-quiet { color: var(--text-muted); }

/* Step 6 — money help. The one number a reader came for, pulled out of the
   sentence so it survives a skim. Deliberately NOT a status colour: --wheat-gold
   is reserved for branding and the risk palette is reserved for risk, so this
   leans on weight and size instead of hue and stays legible to a reader who is
   translating as they go. */
.path-subsidy-amount {
  font-weight: 800;
  font-size: 16px;
  margin: 2px 0 4px;
  line-height: var(--leading-tight, 1.25);
}
/* The no-percentage form is a whole sentence, not a figure — same prominence
   would shout a paragraph at the reader, so it steps back to body size. */
.path-subsidy-amount-text { font-size: 13.5px; font-weight: 600; }

/* Quoted funding sentences. Set apart with a rule rather than a tint, because
   every status colour in this app already means something and a quote is not a
   status. It reads as an inset quotation, which is what it is. */
.path-funding {
  margin: 8px 0 6px;
  padding-left: 10px;
  border-left: 2px solid var(--border);
}
.path-funding-head {
  font-size: 12.5px;
  font-weight: 700;
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  margin-bottom: 3px;
}
.path-funding-line { font-size: 13px; margin: 2px 0; line-height: var(--leading-normal); }
.path-funding-note { font-size: 11.5px; color: var(--text-muted); font-style: italic; margin-top: 3px; }

/* Step 3 — institutes */
.path-inst-list { list-style: none; margin: 6px 0 0; padding: 0; }
.path-inst-list > li {
  padding: 10px 0;
  border-bottom: 1px solid var(--border);
}
.path-inst-list > li:last-child { border-bottom: 0; }
.path-inst-name { font-weight: 700; }
.path-inst-course { font-size: 13px; margin-top: 2px; }
.path-inst-where { font-size: var(--text-xs); color: var(--text-muted); margin-top: 3px; }
.path-inst-pointer { margin-top: 6px; }
.path-inst-pointer summary {
  cursor: pointer;
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--blue);
}
.path-inst-pointer p {
  font-size: 12.5px;
  line-height: 1.55;
  margin: 6px 0 0;
  color: var(--text-muted);
}

/* Step 4 — durations */
.path-duration-list { margin: 0 0 10px; padding-left: 20px; }
.path-duration-list li { margin-bottom: 7px; line-height: var(--leading-normal); }

/* Step 5 — equipment costs */
.path-equip-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
  margin: var(--space-2) 0 var(--space-1);
}
.path-equip-table th,
.path-equip-table td {
  text-align: left;
  padding: 7px var(--space-2);
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}
.path-equip-table th {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
}
.path-equip-table .path-num { text-align: right; white-space: nowrap; }
.path-aspub { font-size: 10.5px; color: var(--text-muted); font-weight: 400; }
.path-equip-sub td { font-weight: 600; }
.path-equip-total td {
  font-weight: 800;
  border-top: 2px solid var(--border);
  border-bottom: 0;
}

.path-src-block {
  font-size: var(--text-xs);
  line-height: 1.55;
  margin-top: 10px;
  padding: 9px 11px;
  background: rgba(0, 0, 0, 0.03);
  border-radius: var(--radius-md);
  word-break: break-word;
}
.path-src-block a { color: var(--blue); }

/* Step 6 — time to operational.
   The disclaimer sits ABOVE the numbers deliberately: a reader who only sees
   the figures must have already passed the sentence saying they are not a
   prediction. */
.path-disclaimer-strong {
  font-size: 13px;
  line-height: 1.55;
  font-weight: 600;
  background: rgba(220, 120, 0, 0.09);
  border-left: 3px solid var(--saffron);
  padding: 9px 11px;
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  margin: 0 0 var(--space-3);
}

.path-timeline-figures {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}
.path-figure {
  flex: 1 1 200px;
  padding: 11px 13px;
  border: 1px solid var(--border);
  border-radius: 8px;
}
.path-figure-val { font-size: 20px; font-weight: 800; color: var(--blue); }
.path-figure-lab { font-size: 12.5px; font-weight: 700; margin-top: 2px; }
.path-figure-sub { font-size: 11.5px; color: var(--text-muted); margin-top: 3px; line-height: 1.45; }

.path-timeline-list { margin: 0; padding-left: 20px; font-size: 13px; }
.path-timeline-list li { margin-bottom: var(--space-1); }

/* ==========================================================================
   VISUAL IDENTITY PROTOTYPE — Report view only (v2: depth over outlines)
   Scoped entirely under body.report-theme (toggled by showView() whenever
   the Report tab is active) so Find My Business / Success Predictor /
   Path to Success / Admin stay pixel-identical, for a clean side-by-side
   comparison. Per design-brief.md: kill hard borders/block shadows in favor
   of soft elevation, spacing, and one large atmospheric background wash;
   make the report's own numbers (startup cost, revenue, etc.) the visual
   hook; stagger sections in on load; add hover-lift/press-compress micro-
   interactions; and push typographic drama into headings only. Nothing here
   touches the sector, risk, predictor-zone, or future/export/video/match
   color-coding, and no disclaimer / data-provenance line's text or plain
   neutral treatment is touched — only chrome (background, shadow, radius,
   spacing, motion, heading and stat type scale).
   ========================================================================== */

body.report-theme {
  --rv-paper: #F7F4EE;
  --rv-card: #FFFFFF;
  --rv-ink: #24211D;
  --rv-canal: #1D4E6B;
  --rv-wheat: #E3A639;
  --rv-growth: #2F6D4E;
  --rv-shadow: 0 1px 2px rgba(36, 33, 29, .06), 0 10px 28px rgba(36, 33, 29, .09);
  --rv-shadow-hover: 0 2px 6px rgba(36, 33, 29, .09), 0 22px 46px rgba(36, 33, 29, .16);
}

@media (prefers-color-scheme: dark) {
  body.report-theme {
    --rv-paper: #201D17;
    --rv-card: #2B2620;
    --rv-ink: #F2EFE7;
    --rv-canal: #5B9BC0;
    --rv-wheat: #F0BB5E;
    --rv-growth: #55A67F;
    --rv-shadow: 0 1px 2px rgba(0, 0, 0, .35), 0 10px 30px rgba(0, 0, 0, .45);
    --rv-shadow-hover: 0 4px 10px rgba(0, 0, 0, .4), 0 24px 52px rgba(0, 0, 0, .55);
  }
}

body.report-theme { background: var(--rv-paper); }

body.report-theme #view-report { color: var(--rv-ink); }

/* One atmospheric presence, quiet and large-scale, behind everything — a
   soft field-horizon glow (mustard field at golden hour) rather than a
   motif repeated per card. Fixed to the viewport so it reads as a constant
   backdrop rather than scrolling content; low opacity and fully behind
   opaque card surfaces, so it never touches text contrast. */
body.report-theme::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(
    120% 55% at 50% 100%,
    color-mix(in srgb, var(--rv-wheat) 20%, transparent) 0%,
    transparent 70%
  );
}

/* Depth over outlines: soft layered elevation instead of a hard offset
   shadow, generous radius instead of a boxed corner. Deliberately doesn't
   touch `border` — existing sector/interview/outlook accent borders (green,
   sector-accent left borders, future's dashed violet top, export's amber
   top) are untouched and still render exactly as before. */
body.report-theme #view-report .panel,
body.report-theme #view-report .card {
  background: var(--rv-card);
  border-radius: 14px;
  box-shadow: var(--rv-shadow);
  /* Section 5: cards lift over --dur-base. The shadow and background ride the
     same timing so the lift reads as one movement rather than three. */
  transition: transform var(--dur-base) var(--ease),
              box-shadow var(--dur-base) var(--ease),
              background-color var(--dur-base) var(--ease);
}

/* Micro-response: cards lift slightly and warm on hover — nothing static
   that could respond stays static. Section 5 fixes the lift at 2px; it was
   3px here. */
body.report-theme #view-report .panel:hover,
body.report-theme #view-report .card:hover {
  transform: translateY(-2px);
  box-shadow: var(--rv-shadow-hover);
  background: color-mix(in srgb, var(--rv-card) 92%, var(--rv-wheat) 8%);
}

body.report-theme #view-report .panel { padding: 28px 30px; }

body.report-theme #view-report .report-results { gap: 30px; }

body.report-theme #view-report .filters-grid { gap: 18px; }

/* Section 5's button behaviour: lift 1px on hover, return to 0 on press.
   Replaces a scale(.96) compress-on-press, which is a different gesture —
   the brief asks for a button that rises to meet the cursor and is pushed
   back down, not one that shrinks. */
body.report-theme #view-report button {
  transition: transform var(--dur-micro) var(--ease),
              box-shadow var(--dur-micro) var(--ease),
              background-color var(--dur-micro) var(--ease);
}

body.report-theme #view-report button:hover {
  transform: translateY(-1px);
}

body.report-theme #view-report button:active {
  transform: translateY(0);
}

/* Journey eyebrow — a small taste of the cross-view progress framing to come.
   Deliberately tiny and quiet: it's the "label" half of the big-number/
   tiny-label contrast this brief asks for elsewhere. */
.journey-eyebrow {
  font-family: 'Barlow Condensed', 'Arial Narrow', sans-serif;
  font-size: var(--text-xs);
  font-weight: 800;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--rv-canal, var(--blue));
  margin-bottom: var(--space-1);
}

/* Typographic drama in headings only — body text and all data stay calm
   (untouched, still Noto Sans at normal scale). */
body.report-theme #view-report .panel h2 {
  font-size: 25px;
  font-weight: 900;
  letter-spacing: -.4px;
}

body.report-theme #view-report .card-title {
  font-size: 21px;
  font-weight: 900;
  letter-spacing: -.3px;
}

/* Make the numbers the spectacle: startup cost, revenue, etc. get an almost
   uncomfortably large treatment; the label shrinks to a quiet caption below
   it. The big-number/tiny-label/.stat/.stat-value/.stat-label treatment
   itself now lives in provenance.css (appended below), which owns the same
   idea plus the provenance line and tier badge underneath each figure. */
body.report-theme #view-report .stat-grid {
  gap: 22px 30px;
}

/* The staggered reveal that used to live here is gone (step 6, section 5:
   "No scroll-triggered reveals, no entrance animations, no parallax").
 *
 * It was a rv-reveal keyframe drifting each section up 14px over 480ms, with
 * nth-child delays fanning out to 560ms, and it re-ran on every business and
 * district pick rather than only on first load. So the last card of a report
 * finished arriving roughly a second after the first, every time the reader
 * changed their selection.
 *
 * The brief's reason for banning this is the product's own: this is a
 * reference tool people read while making financial decisions, and motion
 * should confirm an interaction rather than perform. A reader comparing two
 * businesses pays that second on every comparison.
 *
 * The reduced-motion override that accompanied it is gone too — it is
 * subsumed by the global one at the end of this file. */

/* CTAs keep the opportunity/momentum palette, now with soft elevation +
   micro-interaction instead of a hard offset shadow and 2px border. */
/* ⚠ WAS --rv-ink ON --rv-wheat, near-white #F2EFE7 on light gold #F0BB5E.
   The gold it was painting, dark: 1.53:1
   Unreadable rather than merely low, and worse than anything in
   KNOWN_FAILURES. It was never pinned there and should not be: that list is
   for defects deferred with a reason, and an unreadable control is not a
   deferral candidate. The figure stays registered as a scope:'theme' claim
   because both tokens are still live, so it is re-derived rather than recalled.

   Both halves moved, and both moves are about being MEASURABLE as much as
   being legible. --rv-ink and --rv-wheat are declared on body.report-theme, so
   neither resolved against the :root tables and the rule was never measured at
   all — it sat in the "no resolvable background" count while failing. --text
   is --rv-ink's :root twin (rgb 5 apart in both schemes, imperceptible here)
   and --fill-wheat carries the gold. The pair is now computed on every run.
   Keeping --rv-ink over the new fill would have read:
   The --rv-ink pairing declined: 7.46:1 light  7.50:1 dark
   — the same button to a reader, and a quarter of a point better. What it
   would not have been is MEASURED, which is what the swap actually buys. */
body.report-theme #view-report .btn-secondary {
  background: var(--fill-wheat);
  color: var(--text);
  border: none;
  box-shadow: var(--rv-shadow);
}

body.report-theme #view-report .btn-secondary:hover:not(:disabled) {
  background: var(--fill-canal);   /* frozen: see --fill-canal, was 3.05:1 in dark */
  color: #fff;
  /* 1px, not 2: section 5 gives 2px to cards and 1px to buttons, and this
     is a button. It lifted 2px, so it moved like the panel it sits on. */
  transform: translateY(-1px);
  box-shadow: var(--rv-shadow-hover);
}

body.report-theme #view-report .btn-secondary:disabled {
  background: var(--rv-card);
  color: var(--text-muted);
  box-shadow: none;
}

/* ⚠ THIS IS THE RULE THAT ACTUALLY PAINTS THE BUTTON, AND IT USED TO WIN
   SILENTLY. body.report-theme is on the body from init — Report is the default
   tab — and renderCompareButton() has exactly one call site, inside
   #view-report. At 0,2,2 this beat the 0,1,0 base rule everywhere the control
   renders, so the base rule's colour was styling a scope nothing reaches. Every
   earlier argument about which saffron to paint was about a rule that never
   appeared on screen; the browser reported rgb(47,109,78) throughout.

   ⚠ AND check-contrast COULD NOT SEE IT, WHICH IS WHY IT SURVIVED. The pair
   loop resolves every rule against the :root tables. --rv-growth is declared on
   body.report-theme, so the background did not resolve, the rule fell into the
   "no resolvable background" bucket, and because the foreground is a literal
   #fff rather than a light-only TOKEN the guard's light-only abort did not fire
   either.
   The green it was painting: 6.14:1 light  2.94:1 dark
   That dark figure is WORSE than the one KNOWN_FAILURES pins .btn-primary at —
   the failure this button declined to reuse — and unlike that one it was
   unpinned, because nothing had ever measured it. The comparison is left to the
   guard's own list rather than restated here, for the same reason the base rule
   gives: a figure copied into a second place is the copy that goes stale.
   It stays stated, and registered as a scope:'theme' claim, because --rv-growth
   is still live elsewhere — so the number is re-derived on every commit rather
   than being a sentence about a colour nobody checks any more.

   Repointed at --cta-saffron, which is :root-scoped. That is not only a repaint:
   it takes the rule OUT of the unresolvable bucket, so the ordinary pair loop
   now measures this selector in both schemes like any other. The claim records
   the figure; the pair loop is what would catch a future regression. */
body.report-theme #view-report .btn-compare-add {
  background: var(--cta-saffron);
  color: #fff;
}

body.report-theme #view-report .btn-compare-add:hover:not(:disabled) {
  transform: translateY(-1px);   /* button distance, was the card's 2px */
  box-shadow: var(--rv-shadow-hover);
}

/* ⚠ SAME BLINDNESS, SAME FIX. This repainted the background to --rv-canal and
   inherited #fff from the rule above, so the base rule's --match on --match-bg
   never rendered either — "✓ Shortlisted" was a canal-blue button, and it
   failed dark for the same unmeasured reason.
   The canal blue it was painting: 8.92:1 light  3.05:1 dark
   --match and --match-bg both carry values in BOTH schemes, so the pair moves
   together the way --rv-canal against a fixed #fff did not.
   Shortlisted pair: 4.60:1 light  6.90:1 dark */
body.report-theme #view-report .btn-compare-add.in-comparison {
  background: var(--match-bg);
  color: var(--match);
}

body.report-theme #view-report .btn-compare-add:disabled {
  background: var(--bg);
  color: var(--text-muted);
}

@media (max-width: 720px) {
  .filters-grid, .form-grid { grid-template-columns: 1fr; }
  /* ⚠ THE SPAN HAS TO BE RELEASED HERE OR IT BREAKS THE COLLAPSE ABOVE IT.
     .growth-note carries `grid-column: 3 / -1` to sit in the two spare tracks on
     a wide screen. In a one-column grid there is no column 3, so the span forces
     IMPLICIT columns back into existence and the rule above stops meaning what
     it says: measured in a forced 380px simulation, the two pickers came back
     side by side at 56px and 244px instead of stacking, and only the note
     dropped to its own row. `auto` puts it back in the single column, last, in
     source order — which is where a line about catalogue size belongs once a
     reader is looking at the two controls they came for. */
  .growth-note { grid-column: auto; }
  /* .interview-row's mobile stacking stood here, orphaned with the rule it overrode. */
  .month-strip { grid-template-columns: repeat(6, 1fr); }
  .match-card { flex-wrap: wrap; }
  .match-card-cta { margin-left: 50px; }
  .coverage-columns { grid-template-columns: 1fr; }
  .reality-row { grid-template-columns: 1fr; }
  .calc-grid-3 { grid-template-columns: 1fr; }
}

/* ==========================================================================
   Appended from videos.js's companion stylesheet (videos.css).
   Hardcoded hex colors adapted to the report-theme tokens (Canal Blue,
   Growth Green, Wheat Gold, Ink, Paper) with sensible fallbacks so this
   still renders sanely if used outside the report-theme scope. The dark
   translucent thumbnail scrim (.video-remove) and pure-white text on it are
   left as literal black/white on purpose — that's an image-overlay pattern,
   not a page surface, and would lose contrast if it followed light/dark
   theme tokens instead. Structural properties (grid, spacing, radius, type
   scale) are unchanged from the original file.
   ========================================================================== */

.video-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: 14px;
  margin-top: 10px;
}

.video-card {
  position: relative;
  border: 1px solid color-mix(in srgb, var(--rv-ink, var(--text)) 14%, transparent);
  border-radius: 8px;
  overflow: hidden;
  background: var(--rv-card, var(--panel));
}

.video-card img {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  display: block;
}

.video-meta {
  padding: 9px 10px 11px;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.video-title {
  font-size: .86rem;
  font-weight: 600;
  line-height: 1.3;
  color: inherit;
  text-decoration: none;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.video-title:hover { text-decoration: underline; }

.video-channel {
  font-size: .75rem;
  opacity: .7;
}

.video-badge {
  align-self: flex-start;
  font-size: .64rem;
  letter-spacing: .05em;
  text-transform: uppercase;
  padding: 2px 7px;
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--rv-growth, var(--green)) 18%, transparent);
  color: var(--rv-growth, var(--green));
  margin-top: 2px;
}

.video-remove {
  position: absolute;
  top: 6px;
  right: 6px;
  font-size: .7rem;
  padding: 3px var(--space-2);
  border: none;
  border-radius: 4px;
  background: rgba(0, 0, 0, .72);
  color: #fff;
  cursor: pointer;
}

.video-remove:hover { background: var(--risk-high-text); }

/* admin controls */

.video-admin {
  margin-top: 14px;
  padding-top: var(--space-3);
  border-top: 1px dashed color-mix(in srgb, var(--rv-ink, var(--text)) 18%, transparent);
}

.video-add-row {
  display: flex;
  gap: var(--space-2);
  margin-top: 10px;
  flex-wrap: wrap;
}

.video-input {
  flex: 1 1 220px;
  padding: var(--space-2) 10px;
  font-size: .85rem;
  border: 1px solid color-mix(in srgb, var(--rv-ink, var(--text)) 22%, transparent);
  border-radius: var(--radius-md);
}

.video-btn {
  display: inline-block;
  padding: var(--space-2) 14px;
  font-size: .82rem;
  font-weight: 600;
  border: none;
  border-radius: var(--radius-md);
  background: var(--rv-canal, var(--blue));
  color: #fff;
  text-decoration: none;
  cursor: pointer;
}

.video-btn:hover { filter: brightness(1.1); }
.video-btn--add { background: var(--rv-growth, var(--green)); }
.video-btn:disabled { opacity: .6; cursor: default; }

.video-status,
.video-hint {
  font-size: .8rem;
  opacity: .75;
  margin-top: var(--space-2);
}

/* ============================================================
   Appended from provenance.css — provenance + big-number stats
   Hardcoded hex values adapted to this app's theme tokens.
   ============================================================ */

/* Replaced with stat-fix.css — sized for 7-digit rupee figures, which the
   earlier clamp(2.4rem, 7vw, 4.2rem) was too large for and caused overlap. */

/* The grid must be allowed to reflow, not squeeze */
.stat-grid,
.report-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(165px, 1fr));
  gap: 18px 22px;
  align-items: start;
}

.stat {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0; /* lets grid items actually shrink */
}

/* Sized for 7-digit rupee figures, not for "20%" */
/* Ruled: 24px. That is --text-xl, and it is what lets this figure carry the
   mono face section 3 asks of every rupee amount.
 *
 * It was Barlow Condensed 800 at clamp(1.45rem, 3.4vw, 2.35rem), sized under
 * a comment saying it was fitted to 7-digit rupee figures. Condensed to
 * monospace is the widest face swap available, and at the old 37.6px ceiling
 * the catalogue's widest figure, ₹10,00,000, needed 226px inside a 173px grid
 * cell — and because this element is white-space: nowrap with overflow
 * visible, the excess painted over the neighbouring figure rather than
 * clipping. At 24px that same figure needs 144px, which fits with room.
 *
 * Weight drops 800 -> 700 because 700 is the heaviest Noto Sans Mono weight
 * the font link requests (400;500;700, per section 3). Asking for 800 would
 * have had the browser synthesise a bolder face by smearing the 700 — a
 * fake bold on the one element in the app whose whole job is legible digits.
 *
 * letter-spacing goes too: it was -.01em to tighten a condensed face, and
 * negative tracking on a monospace face fights the fixed advance width that
 * is the entire reason for using one. */
.stat-value {
  font-weight: 700;
  font-size: var(--text-xl);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--rv-ink, #24211D);
  white-space: nowrap;
  overflow-wrap: normal;
}

/* Ranges like "₹3,00,000 – ₹8,00,000" need to break, not overflow. Same size
   as the base now: this variant wraps, so length is not a constraint on it,
   and two sizes for the same figure was the ad-hoc sizing the scale exists to
   remove. Dormant — the renderers emit percent ranges into plain .stat-value. */
.stat-value--range {
  white-space: normal;
  font-size: var(--text-xl);
  line-height: 1.08;
}

.stat-label {
  font-size: .7rem;
  letter-spacing: .07em;
  text-transform: uppercase;
  opacity: .6;
  line-height: 1.3;
}

/* Provenance caption for the risk badge. riskLevel drives the badge, the
   success meter, the comparison row and the recommendation engine, and had no
   source of any kind — while every financial figure beside it is labelled an
   unsourced estimate and a caution card suppresses its financial block outright
   for being unsourced. This says so on the same terms. */
.risk-prov {
  font-size: .68rem;
  opacity: .62;
  font-style: italic;
  align-self: center;
  margin-left: -2px;
}

/* STATE 4 — not sourced. Plain text, no border, no background, as section 7
   asks. 270 of these render across the 98 entries ("General estimate — not
   survey-sourced", "Not available for Punjab"), making it the second most
   common provenance surface in the app.
 *
 * RULED: THE OPACITY STAYS AT 72%. Section 7's state row asks for --ink at
 * 40% and that half is not applied. This is settled — do not re-derive it.
 *
 * The section contradicts itself and the overriding half won. The state row
 * says 40%; the section closes with a rule: "The honesty is the feature; do
 * not let the design hide it." This element was already at 72%, so 40% would
 * have made the not-sourced disclosure LESS visible than it was — the design
 * hiding the honesty, in the one place the product exists to be honest about.
 *
 * The two halves of that spec are coupled and only one was available. The
 * brief's chip is quiet in colour and loud in words: it reads "NOT SOURCED"
 * in caps. Ours reads "General estimate — not survey-sourced" in sentence
 * case, and section 9 forbids content changes, so the loud half could not be
 * built. Applying the quiet half alone loses on both axes.
 *
 * Measured: --ink at 40% is 2.43:1 on a white card and 2.38:1 on --paper,
 * against the 4.5:1 WCAG AA needs for body text. At 72% it clears it. */
.stat-prov {
  font-size: .72rem;
  opacity: .72;
  margin-top: var(--space-1);
  line-height: 1.45;
  border: none;
  background: none;
}

.prov-unavailable {
  border: none;
  background: none;
}

.stat--unavailable .stat-value { opacity: .3; font-weight: 500; }

@media (max-width: 560px) {
  .stat-grid,
  .report-stats { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: var(--space-4); }
  /* The two font-size overrides that sat here are gone. They were
     clamp(1.3rem, 6.5vw, 1.75rem) and clamp(1.05rem, 5vw, 1.4rem), written
     against a desktop base that scaled up to 37.6px. With the base now a flat
     24px they would have made the figure LARGER on a phone than on a desktop
     at any viewport past 369px — the override would have inverted. 24px fits
     the mobile grid: the single column measures 248px at a 360px viewport and
     the widest figure needs 144px. */
}

/* Tier badges — differentiated by weight, not loudness */
.prov-badge {
  display: inline-block;
  font-size: .62rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  padding: 2px 6px;
  border-radius: 3px;
  margin-right: 5px;
  white-space: nowrap;
}

.prov-badge--field    { background: color-mix(in srgb, var(--rv-growth, #2f6d4e) 16%, transparent); color: var(--rv-growth, #1d6b34); font-weight: 700; }
.prov-badge--dataset  { background: color-mix(in srgb, var(--rv-canal, #1d4e6b) 14%, transparent); color: var(--rv-canal, #1d4e6b); font-weight: 600; }
.prov-badge--modelled { background: var(--bg, #f0efec); color: var(--text-muted, #5f5a52); font-weight: 500; }

/* Mixed-tier warning above a comparison */
.prov-warning {
  font-size: .8rem;
  padding: var(--space-2) var(--space-3);
  margin: 0 0 var(--space-3);
  border-left: 3px solid var(--risk-high-text, #b5482d);
  background: color-mix(in srgb, var(--risk-high-text, #b5482d) 6%, transparent);
  color: var(--rv-ink, var(--text));
}

.prov-unavailable { font-size: .82rem; opacity: .7; }

/* Outlook drivers */
.driver-list { list-style: none; margin: var(--space-2) 0 0; padding: 0; }

.driver {
  display: flex;
  gap: 10px;
  align-items: baseline;
  padding: 9px 0;
  border-top: 1px solid color-mix(in srgb, var(--rv-ink, #000) 7%, transparent);
}

.driver:first-child { border-top: none; }

.driver-mark {
  font-size: 1.05rem;
  line-height: 1;
  width: 1.1em;
  flex: none;
}

.driver--rising    .driver-mark { color: var(--rv-growth, #2f6d4e); }
.driver--flat      .driver-mark { color: var(--text-muted, #5f5a52); }
.driver--declining .driver-mark { color: var(--risk-high-text, #b5482d); }

.driver-body { display: flex; flex-direction: column; gap: 2px; }
.driver-factor { font-weight: 600; font-size: .9rem; }

.driver-dir {
  font-size: .72rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  opacity: .6;
}

.driver-source { font-size: .74rem; opacity: .68; }

/* mobile .stat-value sizing now lives in the stat-fix.css block above
   (@media max-width: 560px) — this duplicate 480px override was removed
   since it conflicted with (and would have won over) the fixed sizing. */

/* ============================================================
   Appended from cost-model.css — partial cost figures
   Hardcoded hex values adapted to this app's theme tokens.
   ============================================================ */

/* A partial total reads as provisional, not broken */
.stat--partial .stat-value { opacity: .88; }

.cost-partial {
  display: inline-block;
  font-size: .7rem;
  padding: 2px 7px;
  border-radius: 3px;
  background: var(--bg);
  color: var(--text-muted);
  border: var(--border-width) solid var(--border);
  font-weight: 600;
  margin-right: var(--space-1);
}

/* Breakdown: closed by default, never hidden */
.cost-detail { display: inline; }

.cost-detail summary {
  display: inline;
  cursor: pointer;
  font-size: .74rem;
  text-decoration: underline;
  text-decoration-style: dotted;
  text-underline-offset: 2px;
  opacity: .8;
  list-style: none;
}

.cost-detail summary::-webkit-details-marker { display: none; }
.cost-detail summary::marker { content: ''; }
.cost-detail summary:hover { opacity: 1; }

.cost-detail[open] summary { font-weight: 600; }

.cost-detail[open] > span {
  display: block;
  margin-top: 6px;
  padding: var(--space-2) 10px;
  font-size: .74rem;
  line-height: 1.55;
  background: color-mix(in srgb, var(--rv-ink, #000) 3.5%, transparent);
  border-radius: 5px;
  border-left: 2px solid color-mix(in srgb, var(--rv-canal, #1d4e6b) 35%, transparent);
}

@media (max-width: 480px) {
  .cost-detail[open] > span { font-size: .72rem; }
}

/* ============================================================
   Appended from future-outlook.css (verbatim — no token mapping
   was requested for this one; it is a self-contained editorial
   palette distinct from the rest of the theme).
   ============================================================ */

/* Container Card */
.future-outlook {
  background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
  border: 1px solid #e2e8f0;
  border-left: 5px solid #2563eb; /* Bold primary color accent line */
  border-radius: 16px;
  padding: 2rem;
  box-shadow: 0 10px 25px -5px rgba(15, 23, 42, 0.05), 0 8px 10px -6px rgba(15, 23, 42, 0.01);
  margin: 1.5rem 0;
  font-family: 'Plus Jakarta Sans', 'Inter', -apple-system, sans-serif;
}

/* Header Wrapper */
.fo-header {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  margin-bottom: 0.5rem;
}

/* Icon Badge */
.fo-icon {
  width: 36px;
  height: 36px;
  background: rgba(37, 99, 235, 0.1);
  color: #2563eb;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.1rem;
  font-weight: bold;
}

/* Main Title */
.fo-header h3 {
  font-family: 'Space Grotesk', sans-serif;
  font-size: 1.35rem;
  font-weight: 700;
  color: #0f172a;
  margin: 0;
  letter-spacing: -0.02em;
}

/* Meta Pill Badge */
.fo-meta {
  display: inline-block;
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: #475569;
  background: #f1f5f9;
  padding: 0.35rem 0.75rem;
  border-radius: 9999px;
  margin-bottom: 1.25rem;
}

/* Horizontal Rule Divider */
.fo-rule {
  height: 1px;
  background: linear-gradient(90deg, #e2e8f0 0%, rgba(226, 232, 240, 0) 100%);
  margin-bottom: 1.25rem;
}

/* Body Paragraphs */
.fo-body {
  font-size: 0.975rem;
  line-height: 1.7;
  color: #334155;
  margin-bottom: 1rem;
}

.fo-body strong {
  color: #0f172a;
  font-weight: 600;
}

/* Key Risk / Highlight Callout Box at Bottom */
.fo-risk {
  margin-top: 1.25rem;
  padding: 0.85rem 1.15rem;
  background: #fffbe0;
  border: 1px solid #fef08a;
  border-radius: 10px;
  font-size: 0.875rem;
  color: #854d0e;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* ============================================================
   Sector artwork removed — the block that stood here styled it.

   .sector-art, .sector-art--icon, .sector-plate-svg and
   .sector-badge-icon are all gone with sector-icons.js. The plate sat at the
   top of every report and, sitting directly under the shortlist bar, had
   started reading as the empty-state illustration for a shortlist with no
   items — which it never was. It rendered on every card unconditionally.

   ⚠ .sector-badge-icon WAS ALREADY DEAD BEFORE THIS. sectorBadgeIcon() was
   defined and never called by anything, so the class was never emitted; it is
   noted rather than mourned. Its twin file sector-icons.css was likewise never
   loaded by index.html — only styles.css is — so the duplicate rules in it had
   no effect at all. Both are deleted.

   What replaces the space is .headline-fact below, or nothing.
   ============================================================ */

/* One sourced fact, or an absent line. Deliberately quiet: this is a figure
   carried up from the scheme row below, not a banner, and it must not compete
   with the business name it sits above. The chip is the same
   `pending verification` chip every other figure in this app carries — a bare
   percentage here would be the only unqualified number on the page. */
.headline-fact {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin: 0 0 14px;
  font-size: var(--text-sm);
}

.headline-fact-label {
  color: var(--text-muted);
  font-weight: 600;
}

.headline-fact-value {
  color: var(--text);
  font-weight: 700;
  font-size: var(--text-base);
}

/* ============================================================
   IBI wordmark lockup — styles renderWordmarkV2() in brand-mark-v2.js

   ⚠ THIS IS THE ONLY COPY. It was appended here from a brand-mark-v2.css
   that no longer exists, and for a while both existed: index.html loads
   styles.css and nothing else, so the standalone file was never served and
   was free to drift from these rules while looking authoritative. It was
   deleted along with brand-mark.js and brand-mark.css, the v1 pair, which
   nothing had loaded either. Edit the lockup here.
   ============================================================ */

.ibi-wordmark {
  display: inline-flex;
  align-items: center;
  gap: 14px;
}

.ibi-wordmark-badge {
  display: inline-flex;
  border-radius: 12px;
  filter: drop-shadow(0 3px 8px rgba(0,0,0,.28));
  flex: none;
}

.ibi-wordmark-badge svg { display: block; }

.ibi-wordmark-text {
  font-family: 'Barlow Condensed', system-ui, sans-serif;
  font-weight: 800;
  font-size: 1.5rem;
  letter-spacing: .01em;
  color: var(--paper, #F7F4EE);
  text-transform: uppercase;
  white-space: nowrap;
}

@media (max-width: 480px) {
  .ibi-wordmark { gap: 10px; }
  .ibi-wordmark-text { font-size: 1.15rem; }
}

/* ============================================================
   Appended from resources.css - Resources tab
   ============================================================ */

.resources-view {
  max-width: 780px;
  margin: 0 auto;
  padding: var(--space-2) 0 var(--space-6);
}

/* ⚠ --text, NOT --ink. --ink is #24211D and is declared once, in the light
   :root, with no dark value — it is the light-only twin of --text, which is
   #262420 light and #eeece5 dark. On --bg this heading rendered at 1.13:1 in the
   dark scheme: not "low contrast", unreadable. Nothing in the stylesheet derives
   a token from --ink, so moving off it is local rather than structural. Same
   move as .invest-disclaimer, one commit earlier. --text on --bg is 14.08:1
   light  15.31:1 dark. */
.resources-intro h2 {
  font-family: 'Barlow Condensed', system-ui, sans-serif;
  font-weight: 800;
  font-size: 1.9rem;
  margin: 0 0 6px;
  color: var(--text);
}

.resources-intro p {
  font-size: .92rem;
  line-height: 1.6;
  opacity: .75;
  max-width: 60ch;
  margin: 0 0 30px;
}

.resource-group {
  margin-bottom: 28px;
}

.resource-group h3 {
  font-family: 'Barlow Condensed', system-ui, sans-serif;
  font-weight: 700;
  font-size: 1.15rem;
  color: var(--canal-blue, #1D4E6B);
  margin: 0 0 var(--space-1);
}

.resource-blurb {
  font-size: .84rem;
  opacity: .68;
  margin: 0 0 var(--space-3);
}

.resource-links {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 10px;
}

.resource-link {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: var(--space-3) 14px;
  border-radius: 8px;
  background: var(--paper, #F7F4EE);
  border: 1px solid rgba(36,33,29,.08);
  text-decoration: none;
  color: inherit;
  transition: transform var(--dur-micro) var(--ease), box-shadow var(--dur-micro) var(--ease);
}

.resource-link:hover,
.resource-link:focus-visible {
  transform: translateY(-1px);
  box-shadow: 0 4px 14px -6px rgba(36,33,29,.25);
  border-color: rgba(29,78,107,.25);
}

.resource-link-name {
  font-weight: 700;
  font-size: .92rem;
  color: var(--canal-blue, #1D4E6B);
}

.resource-link-note {
  font-size: .78rem;
  opacity: .68;
  line-height: 1.4;
}

.resources-disclaimer {
  font-size: .74rem;
  opacity: .55;
  margin-top: 30px;
  padding-top: var(--space-4);
  border-top: 1px solid rgba(36,33,29,.08);
}

@media (max-width: 480px) {
  .resource-links { grid-template-columns: 1fr; }
}

/* ============================================================
   Appended from premium.css - locked features + unlock prompt
   ============================================================ */

.locked-panel {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2);
  padding: 26px 26px var(--space-5);
  border-radius: var(--radius-lg);
  background:
    linear-gradient(180deg, rgba(29,78,107,.05), rgba(29,78,107,0) 55%),
    var(--paper, #F7F4EE);
  border: 1px solid rgba(29,78,107,.16);
  overflow: hidden;
}

.locked-panel::after {
  content: "";
  position: absolute;
  top: -60px;
  right: -60px;
  width: 160px;
  height: 160px;
  border-radius: 50%;
  background: var(--wheat-gold, #E3A639);
  opacity: .07;
  pointer-events: none;
}

.locked-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--canal-blue, #1D4E6B);
  color: var(--wheat-gold, #E3A639);
  flex: none;
  box-shadow: 0 3px 10px -3px rgba(29,78,107,.5);
}

.locked-title {
  font-family: 'Barlow Condensed', system-ui, sans-serif;
  font-weight: 800;
  font-size: 1.28rem;
  margin: var(--space-1) 0 0;
  color: var(--ink, #24211D);
}

.locked-blurb {
  font-size: .9rem;
  line-height: 1.55;
  opacity: .76;
  margin: 0;
  max-width: 46ch;
  position: relative;
  z-index: 1;
}

.locked-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-top: var(--space-2);
}

.locked-cta {
  font-family: 'Barlow Condensed', system-ui, sans-serif;
  font-weight: 700;
  font-size: 1rem;
  letter-spacing: .02em;
  padding: 10px 22px;
  border: none;
  border-radius: 7px;
  cursor: pointer;
  color: #12384D;
  background: linear-gradient(180deg, #F3C15E, var(--wheat-gold, #E3A639));
  box-shadow: 0 2px 8px -2px rgba(227,166,57,.7);
  /* .1s is not on the motion scale (--dur-micro is 120ms), so only the
     box-shadow half could be tokenised. Left explicit rather than rounded. */
  transition: transform .1s ease, box-shadow var(--dur-micro) var(--ease);
}

.locked-cta:hover  { transform: translateY(-1px); box-shadow: 0 4px 14px -3px rgba(227,166,57,.8); }
.locked-cta:active { transform: translateY(0); }

.locked-price-note {
  font-size: .76rem;
  opacity: .62;
}

/* ---------- unlock prompt ---------- */

.unlock-prompt {
  padding: 22px var(--space-5);
  border-radius: var(--radius-lg);
  background: var(--paper, #F7F4EE);
  border: 1px solid rgba(29,78,107,.2);
  max-width: 460px;
}

.unlock-prompt h4 {
  font-family: 'Barlow Condensed', system-ui, sans-serif;
  font-weight: 800;
  font-size: 1.25rem;
  margin: 0 0 var(--space-1);
  color: var(--ink, #24211D);
}

.unlock-prompt p {
  font-size: .86rem;
  opacity: .74;
  margin: 0 0 14px;
}

.unlock-row {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.unlock-input {
  flex: 1 1 180px;
  padding: 9px var(--space-3);
  font-size: .9rem;
  border: 1px solid rgba(36,33,29,.22);
  border-radius: var(--radius-md);
  background: #fff;
}

/* The per-element focus ring that sat here is gone; the global :focus-visible
   rule at the end of this file covers it. Not just tidying — at (0,2,0) this
   selector outranked the global one, so it would have kept --canal-blue in
   dark mode and been the one input in the app whose focus ring stayed at
   1.84:1. Its offset was 1px against the specified 2px as well. */

.unlock-btn {
  padding: 9px 20px;
  font-weight: 700;
  font-size: .88rem;
  border: none;
  border-radius: var(--radius-md);
  cursor: pointer;
  color: var(--paper, #F7F4EE);
  background: var(--canal-blue, #1D4E6B);
}

.unlock-btn:hover { filter: brightness(1.12); }

.unlock-status {
  font-size: .8rem;
  margin: 10px 0 0;
  min-height: 1.2em;
}

.unlock-status--ok  { color: #1d6b34; }
.unlock-status--bad { color: #b5482d; }

@media (max-width: 480px) {
  .locked-panel { padding: 20px 18px; }
  .locked-cta { width: 100%; text-align: center; }
}

/* ============================================================
   Farming Options browse view
   ============================================================ */

.farming-results {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.farming-result-count {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-muted);
  margin-bottom: 2px;
}

.card-farming { border-left: 4px solid var(--green); }

.badge-farming-category { background: var(--blue-bg); color: var(--blue); }

.farming-meta-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 18px;
  font-size: 13px;
  color: var(--text-muted);
  margin-bottom: 10px;
}

.farming-meta-item b { color: var(--text); font-weight: 600; }

/* ---------- the livestock financial row ------------------------------------

   ⚠ THE STRUCTURE AND THE CLASSES ARE THE BUSINESS CARD'S, NOT NEW ONES. The
   group renders .stat-grid > .stat > (.stat-value, .stat-label, .stat-prov),
   which stat-fix.css already sizes, reflows and breaks for seven-digit rupee
   ranges. Only two things are added here, and both exist because this surface
   holds a rule the business report does not.

   ⚠ ONE: THE SIZES ARE SCOPED AND TOKENISED. stat-fix.css sets .stat-value with
   a clamp() topping out near 2.35rem and applies it to compiled estimates as
   readily as to sourced figures — renderGeneralEstimateStat uses the plain
   class. Here an estimate may not exceed body text, so .farming-fin overrides
   both: --text-2xl for a sourced figure, --text-base for an estimate. Tokens
   rather than the clamp because the ceiling is asserted against the --text-*
   ladder and a clamp() is not on it.

   ⚠ TWO: .stat-value--estimate IS A NEW MODIFIER. Nothing on a business card
   distinguishes a compiled figure from a quoted one by size, so there was no
   existing class to reuse. It is written unscoped as well as scoped so the
   report surface can adopt the same cap later without a second name.

   ⚠ AND .farming-fin-detail IS THE SLOT THE OLD DESIGN LACKED. The qualifying
   sentence — yield bands, shed dimensions, what the unit actually is — used to
   be inside the figure element, which is why a paragraph rendered at headline
   size. It sits between the label and the provenance line, at body size or
   below, where a caption belongs. */
.farming-fin { margin: var(--space-3) 0 var(--space-4); }

.farming-fin .stat-value {
  font-family: inherit;
  font-size: var(--text-2xl);
  line-height: var(--leading-tight);
}

/* The absolute ceiling. An estimate, a range or an editorial judgement may not
   exceed body text — see check-farming-leg.js for why this is not expressed as
   one step under the sourced size. */
.stat-value--estimate,
.farming-fin .stat-value--estimate {
  font-size: var(--text-base);
  line-height: var(--leading-normal);
}

/* A range still needs to wrap rather than overflow, exactly as on a business
   card; it just does so from the scoped size rather than the clamp. */
.farming-fin .stat-value--range { white-space: normal; }

.farming-fin-detail {
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  color: var(--text-muted);
  margin-top: var(--space-1);
  max-width: var(--measure);
}

/* ⚠ THE INVERSION THIS FIXES WAS LIVE ON 112 CARDS. .farming-notes carried no
   colour at all, so it inherited full --text — while .farming-meta-row above it
   was already --text-muted and .farming-leg-source below it is too. The notes
   are unsourced editorial prose written for this catalogue; the leg rows are
   sourced findings with a citation under each. The loudest text on the card was
   the one thing with nothing behind it.

   It drops to --text-muted, which puts it level with a researched negative and
   below a populated finding — at or below the negative band, never above it.
   check-farming-leg.js ranks the four bands rather than comparing them for
   equality, so dropping the finding to muted as well would fail rather than
   quietly satisfy the rule.

   ⚠ line-height 1.55 IS PRE-EXISTING AND LEFT ALONE, as is .farming-meta-row's
   13px above — both outside the --leading-* and --text-* scales. Recorded in
   check-farming-leg.js's header rather than changed: nudging body prose while
   fixing a contrast bug hides one change inside another. */
.farming-notes {
  font-size: var(--text-sm);
  line-height: 1.55;
  color: var(--text-muted);
  margin: 0;
}

/* -------- the agronomy leg (renderFarmingAgronomyBlock) -------- */

/* ⚠ PROMINENCE TRACKS SOURCE STRENGTH, AND WHAT THIS ENFORCES IS THE CEILING.
   A figure quoted from a named document may be set large; a figure that is an
   estimate, a range or an editorial judgement may not exceed body text. ONE
   CLASS CARRIES BOTH: .farming-leg-item is the finding slot for all six
   rendered legs, and an msp row quoted from a gazette notification is
   indistinguishable in the markup from a future soil or harvest row that may
   be a range. There is no modifier to select on.

   So the ceiling is applied universally — findings are set at --text-base and
   never larger. That deliberately declines the permission to enlarge a quoted
   figure, because taking it would also enlarge every weak-sourced row that
   lands later, and the rule would be broken by the next commit rather than by
   this one. Setting a quoted figure larger needs a class the renderer does not
   emit; adding one is a renderer decision, not a stylesheet one. */

.farming-leg {
  margin-top: var(--space-4);
  padding-top: var(--space-3);
  border-top: var(--border-width) solid var(--border);
}

.farming-leg + .farming-leg {
  margin-top: var(--space-3);
}

/* A LABEL, NOT A TITLE. Small and tracked so the finding beneath it stays the
   content of the block; uppercase and bold so it still reads as the leg's
   name. Set in --text-muted because the leg name is navigation — the reader
   already knows they are on a farming card. */
.farming-leg-head {
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: var(--space-2);
}

.farming-leg-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Rows within one leg are separated rather than run together: two findings
   from two documents are two claims, not one paragraph. */
.farming-leg-list li + li {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: var(--border-width) solid var(--border);
}

/* The claim itself. Body size, never larger — see the ceiling note above.
   ⚠ THE MEASURE IS THE REASON THIS HAS A max-width. The longest claim in the
   data is 312 characters and the longest pointer 649; across a full-width card
   those set a line long enough to lose your place between lines. The measure is
   --measure, which the five older prose carriers now share — they agreed on
   68ch by coincidence before, in nine separate declarations. */
.farming-leg-item {
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
  color: var(--text);
  max-width: var(--measure);
}

/* Provenance recedes. It is what makes the claim checkable, not what the
   reader is being told — and it is never the largest thing in the block. */
.farming-leg-source {
  font-size: var(--text-xs);
  line-height: var(--leading-normal);
  color: var(--text-muted);
  margin-top: var(--space-1);
  max-width: var(--measure);
}

.farming-leg-pointer {
  margin-top: var(--space-2);
}

.farming-leg-pointer > summary {
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
  cursor: pointer;
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) 0;
  border-radius: var(--radius-sm);
}

.farming-leg-pointer > summary::-webkit-details-marker { display: none; }

.farming-leg-pointer > summary::before {
  content: "+";
  font-weight: 700;
  line-height: 1;
}

.farming-leg-pointer[open] > summary::before { content: "\2212"; }

.farming-leg-pointer > summary:hover { color: var(--text); }

/* Keyboard focus was invisible on every other <details> in this app; it is not
   here. The ring reuses --green, the colour the farming card is already
   bordered with, rather than adding a hue. */
.farming-leg-pointer > summary:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--panel), 0 0 0 4px var(--green);
}

.farming-leg-pointer > p {
  font-size: var(--text-xs);
  line-height: var(--leading-relaxed);
  color: var(--text-muted);
  max-width: var(--measure);
  margin: var(--space-2) 0 0;
  padding-left: var(--space-3);
  border-left: 2px solid var(--border);
}

/* ⚠ A RESEARCHED-NEGATIVE IS SET QUIETER THAN A POPULATED LEG AND NEVER
   LOUDER, WHICH IS HARDER THAN IT SOUNDS HERE — the negatives are the LONGEST
   rows in the data. desi-cotton runs 312 characters, paddy-basmati and
   sugarcane 269 each, against a longest finding of 172. So "quieter" is
   contrast only: the claim drops to --text-muted while size, measure and
   leading are untouched. Shrinking the longest and most-read text on the page
   would be punishing a reader for the catalogue's honesty.

   Selected by sibling rather than by a modifier class because the renderer
   emits the lead paragraph immediately before the negative list and nothing
   before the positive one — so a leg carrying both keeps its findings at full
   contrast and drops only the negatives beneath the lead. */
.farming-leg-found-lead {
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  color: var(--text-muted);
  margin: 0 0 var(--space-2);
  max-width: var(--measure);
}

.farming-leg-found-lead ~ .farming-leg-list .farming-leg-item {
  color: var(--text-muted);
}

/* ============================================================
   Caution-tier card variant (business-data-tiers.js)
   Short card, no financial section — the warning is the content.
   ============================================================ */

.card-caution {
  border-left: 4px solid var(--risk-high-text);
}

.caution-warning {
  background: var(--risk-high-bg);
  border-radius: 8px;
  padding: var(--space-4) 18px;
  margin-top: var(--space-1);
}

.caution-warning-label {
  font-family: 'Barlow Condensed', 'Arial Narrow', sans-serif;
  font-weight: 800;
  font-size: 13px;
  letter-spacing: 0.3px;
  text-transform: uppercase;
  color: var(--risk-high-text);
  margin-bottom: var(--space-2);
}

/* The disclosure line on a caution card that is withholding figures it holds.
   Inside .caution-warning rather than below it, because it qualifies the label
   in that box and not the card as a whole. Smaller and muted against the
   warning prose above it: it is a note about what is absent, and it must not
   compete with the warning it sits under. Tokens only — no bare sizes. */
.caution-withheld {
  margin: var(--space-3) 0 0;
  padding-top: var(--space-3);
  border-top: var(--border-width) solid var(--border);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  color: var(--text-muted);
  max-width: var(--measure);
}

.caution-warning p {
  margin: 0;
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--text);
}

/* ============================================================
   Flagged-tier risk banner (business-data-tiers.js)
   Sits above the financial stats on a card that DOES have data.
   Deliberately amber + outlined, against the caution card's solid
   red fill, so "full data with a caution" and "no data, caution
   only" are not mistaken for each other at a glance.
   ============================================================ */

.risk-banner {
  background: var(--risk-med-bg);
  border: 1px solid color-mix(in srgb, var(--risk-med-text) 32%, transparent);
  border-left: 4px solid var(--risk-med-text);
  border-radius: 8px;
  padding: var(--space-3) var(--space-4);
  margin: var(--space-1) 0 14px;
}

.risk-banner-label {
  display: flex;
  align-items: center;
  gap: 7px;
  font-family: 'Barlow Condensed', 'Arial Narrow', sans-serif;
  font-weight: 800;
  font-size: 12.5px;
  letter-spacing: 0.3px;
  text-transform: uppercase;
  color: var(--risk-med-text);
  margin-bottom: 6px;
}

.risk-banner-icon {
  font-size: 13px;
  line-height: 1;
}

.risk-banner p {
  margin: 0;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--text);
}

/* National outlook shares the .future-outlook shell but is visually separated
   from it — it is directional and unverified, where the Punjab outlook is
   long-standing written analysis. Dashed edge matches .data-sources, which
   already means "unconfirmed" in this palette. */
.national-outlook {
  border-style: dashed;
}

.national-outlook .fo-header {
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* ============================================================
   Partial-tier card treatment
   Signals "less complete", NOT "risky". Deliberately introduces
   no new colour: the accent bar stays this card's own sector
   colour, just dashed and part-transparent, and the figures drop
   to the existing muted token. Amber (.risk-banner) and red
   (.caution-warning) keep their hazard meanings untouched.
   ============================================================ */

.card-partial {
  border-left-style: dashed;
  border-left-color: color-mix(in srgb, var(--card-accent, var(--sector-default)) 45%, transparent);
}

/* Figures on a partial card are provisional by definition — every one of them
   is either "Not available" or a legacy estimate pending re-sourcing. Muting
   the value makes the grid read as unsettled without hiding anything. */
.card-partial .stat-value {
  color: var(--text-muted);
}

/* ============================================================
   Partial-tier "what's missing" note
   Informational, NOT a warning. A partial business is thinly
   documented, not risky — amber/red are reserved for
   .risk-banner and .caution-warning, so this uses the neutral
   muted treatment with a solid left rule to distinguish it from
   the dashed .data-sources block that follows it.
   ============================================================ */

.thin-data-note {
  border-left: 3px solid var(--text-muted);
  background: color-mix(in srgb, var(--text-muted) 6%, transparent);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  padding: 11px 14px;
  margin: 14px 0 var(--space-1);
}

/* Common challenges — its own frame, deliberately NOT a promotion of the shared
   .text-block-label. Four other headings use that class (Seasonal notes,
   Equipment needed, Skills required, and the commercial trio) and restyling it
   would raise all five to the same rank, which is the opposite of the point.

   ⚠ THAT PROMOTION HAS SINCE BEEN MADE, ON 5 AUGUST 2026, AND THE PARAGRAPH
   ABOVE IS LEFT STANDING BECAUSE IT STILL EXPLAINS WHY THIS FRAME EXISTS. What
   changed is not the argument but what it was weighed against: those labels were
   reported as reading faint and failing as headings, which the paragraph above
   never denied — it only said the fix should not come out of this frame's rank.

   ⚠ AND THE COUNT IN IT WAS SHORT, WHICH IS PART OF WHY THE PROMOTION WAS TAKEN
   DELIBERATELY RATHER THAN AS A TWEAK. It says four other headings. The class is
   written from THREE emitters in app.js, not one, and serves EIGHT labels: the
   five text-block ones, plus the commercial trio's third member, plus
   renderMonthStrip's "Peak season" / "Peak visitor season" caption and
   renderIntensityMeter's "Capital intensity". Restyling it was therefore a
   bigger change than this comment implied, and the ruling was explicit that all
   of them move together — "a third visual tier would be worse than either
   extreme".

   WHAT PROTECTS THIS FRAME NOW IS SIZE PARITY, NOT SIZE GAP. .text-block-label
   matches this rule's face, weight, case, spacing and colour, and matches its
   var(--text-xs) rather than exceeding it. So the two read as the same KIND of
   heading, and this frame keeps its rank through what it still has and the
   labels do not: a left rule, a tinted ground, and a name of its own. */

   Everything here is an existing token. No new colour is introduced, --wheat-gold
   and --growth-green are not referenced, and the class names contain none of the
   status words check-gold-reservation.js scopes its rule to. The structural
   pattern — left rule, faint tint, Barlow Condensed uppercase label — is lifted
   from .thin-data-note directly below, so this reads as an existing part of the
   system at a higher rank rather than as a new component. The one difference is
   deliberate: the label is var(--text), not var(--text-muted). */
/* ⚠ ONE FRAME, TWO BLOCKS, AND THE RANK LADDER BELOW IT WAS DELIBERATELY
   REMOVED. Until now .why-block sat one full rank under .challenge-block — 2px
   rule against 3px, --text-muted against --text, no tint against a 4% wash —
   and the reasoning was recorded at length: `challenges` is protective,
   `why` is persuasive, `why` renders last on the card, and fix-banners.js was
   widened to cover `why` precisely because it is the field with the recorded
   overclaim history. That reasoning is preserved in full at .why-block below.
   Flattening the two was a product decision taken knowingly, not a tidy-up.

   Grouped rather than duplicated, because after flattening the two rules were
   character-identical apart from margin. Two copies of an identical frame is
   two places for it to stop matching, and the whole point of this change is
   that they match. */
.challenge-block,
.why-block {
  border-left: 3px solid var(--text);
  background: color-mix(in srgb, var(--text) 4%, transparent);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  padding: 11px 14px;
}
.challenge-block { margin: var(--space-3) 0; }
.why-block       { margin: var(--space-3) 0 0; }

.challenge-head,
.why-head {
  font-family: 'Barlow Condensed', 'Arial Narrow', sans-serif;
  font-weight: 800;
  font-size: var(--text-xs);
  letter-spacing: 0.3px;
  text-transform: uppercase;
  color: var(--text);
  margin-bottom: var(--space-2);
}

/* Grouped with .challenge-list for the same reason .why-block is grouped with
   .challenge-block above: after the `why` field gained the list path the two
   rules would have been character-identical, and two copies of an identical
   frame is two places for it to stop matching. */
.challenge-list,
.why-list {
  margin: 0;
  padding-left: 18px;
  max-width: var(--measure);
}

.challenge-list li,
.why-list li {
  font-size: var(--text-sm);
  line-height: 1.55;
  margin-bottom: 5px;
}

.challenge-list li:last-child,
.why-list li:last-child { margin-bottom: 0; }

/* A lead-in is bold and nothing else — no colour, no size change, no letter-
   spacing. It is a scanning aid, not a new rank: the bullet it opens is still a
   peer of the bullets around it, and giving it a colour would spend the
   provenance vocabulary (amber warns, red refuses) on typography. `strong` is
   already bold by default, so this rule exists to pin the WEIGHT against a
   future base-style change rather than to create the effect. */
.lead-in { font-weight: 700; }

/* The seven prose entries share the frame and the heading; only the body element
   differs, so a prose card cannot read as a half-styled list card. */
.challenge-prose {
  margin: 0;
  font-size: var(--text-sm);
  line-height: 1.55;
  max-width: var(--measure);
}

/* "Why it's promising in Punjab" — promoted, and deliberately ONE FULL RANK
   BELOW .challenge-block above it, on the ladder this file already uses:

     rule weight   4px card/warning (.card-general, .caution-warning, .risk-banner)
                   3px block        (.thin-data-note, .challenge-block)
                   2px quiet        <- this
     rule colour   var(--text) (.challenge-block)  >  var(--text-muted)  <- this
     tint          4% (.challenge-block)  >  none  <- this

   So it gains the frame and the Barlow Condensed uppercase label, and stays
   muted and untinted. The rank order is the point: `challenges` is protective
   and `why` is persuasive, `why` already renders LAST on the card, and
   fix-banners.js exists in its present form because `why` is the field with the
   recorded overclaim history. Equal weight would make the persuasive block the
   closing argument.

   No new colour. --wheat-gold and --growth-green are not referenced, and
   --card-accent / --sector-* are deliberately untouched: those carry tier at a
   glance on .card-general and .card-partial, and repeating them inside the card
   would dilute that. The class names contain none of the status words
   check-gold-reservation.js scopes its rule to.

   .text-block-label IS NOT MODIFIED. Four headings still use it — Seasonal
   notes, Equipment needed, Skills required and the commercial trio — and this
   heading has simply stopped being one of them.

   max-width sits on .why-body rather than being added to the --measure group
   above,
   because `why` leaving .text-block-body must not silently lose the measure
   cap it had. Same treatment as .challenge-prose. */
/* ⚠ THE RANK LADDER ABOVE THIS COMMENT NO LONGER APPLIES, AND THE COMMENT IS
   KEPT BECAUSE THE REASONING IS STILL THE BEST RECORD OF WHAT WAS TRADED AWAY.

   .why-block used to declare 2px/--text-muted/no-tint here, one full rank under
   .challenge-block, and everything above explains why. As of this change both
   blocks share one frame at the top of this section: 3px, --text, 4% wash, and
   a --text heading. `why` now closes the card at the same visual weight as
   `challenges`.

   ⚠ WHAT THAT COSTS, STATED PLAINLY SO IT IS NOT REDISCOVERED. fix-banners.js
   covers `why` because `why` is where the overclaim history lives — Solar Pump
   called the business largely subsidy-driven while its own body said the scheme
   is closed to new applicants. The rank difference was one of the things
   holding the persuasive block below the protective one. It is now a guard's
   job alone. If fix-banners.js is ever narrowed, this is the paragraph that
   says what used to share the load.

   ⚠ AND IT IS A CONTRAST IMPROVEMENT, WHICH IS NOT WHY IT WAS DONE. The
   heading moved from --text-muted to --text and gained the wash beneath it;
   both pairs cleared AA before and the new one clears it by a wide margin.
   NO RATIO IS QUOTED HERE ON PURPOSE. Both heads are registered in
   check-contrast's SURFACES as of this change, so the numbers are derived on
   every run instead of written down once and left to rot. A figure in a comment
   is a claim somebody has to keep true; a figure in SURFACES is measured. The
   guard made this point itself — the first draft of this comment quoted the old
   --text-muted ratio and was refused, because no live rule produces it any
   more. */

.why-body {
  margin: 0;
  font-size: var(--text-sm);
  line-height: 1.55;
  max-width: var(--measure);
}

.thin-data-label {
  font-family: 'Barlow Condensed', 'Arial Narrow', sans-serif;
  font-weight: 800;
  font-size: 12.5px;
  letter-spacing: 0.3px;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 5px;
}

.thin-data-note p {
  margin: 0;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--text);
}

/* A note carrying ⚠/✓ markers now renders one paragraph per marker — see
   splitNoteAtMarkers. The rule above zeroes the margin, so without this the
   paragraphs butt together and the split is invisible: the markup would change
   and the card would not. This adds the gap and nothing else.
 *
 * ⚠ DELIBERATELY NO COLOUR, TINT OR RULE ON A MARKER PARAGRAPH. Those
 * paragraphs are the qualifications and corrections, and making them look like
 * it is the obvious next thought — but it would turn this into a colour-bearing
 * selector, which check-contrast requires to resolve its background, and the
 * wash here is a color-mix over the card rather than a token. That needs a
 * SURFACES entry and the guard work that goes with it. It is worth doing on its
 * own terms, in its own commit, and not smuggled into a layout change. */
.thin-data-note p + p {
  margin-top: var(--space-2);
}

/* When both blocks render, the sources block follows immediately — tighten the
   gap so they read as one "here's the gap, here's how to close it" unit. */
.thin-data-note + .data-sources {
  margin-top: var(--space-2);
}

/* One-line definition of the business, ahead of the stats. Muted and slightly
   larger than body text so it reads as a subtitle to the card title rather
   than as another content block competing with them. Present only on the 48
   entries that carry whatItIs. */
.what-it-is {
  font-size: 14.5px;
  line-height: 1.55;
  color: var(--text-muted);
  margin: 2px 0 14px;
  max-width: var(--measure);
}

/* On a caution card it sits between the title and the warning, so it must not
   pull attention away from the label directly beneath it. */
.card-caution .what-it-is {
  margin-bottom: 10px;
}

/* ============================================================
   Licences & registrations
   Practical checklist, NOT a provenance caveat — so it deliberately
   does not reuse .data-sources (dashed border = "go and verify this").
   Solid left rule in the card's own sector accent, so it reads as
   part of the business profile rather than as a warning. No amber or
   red: those are spoken for by .risk-banner and .caution-warning.
   ============================================================ */

/* The PPCB category caveat is the one thing here a reader could act on
   wrongly, so it gets weight without borrowing the risk palette. */

@media (max-width: 560px) {
}

/* ============================================================
   Data-sources block (full-tier businesses)
   Pointers to documents, NOT figures. Styled neutral//informational
   on purpose — it must not read as a risk warning (amber/red are
   already spoken for by .risk-banner and .caution-warning) and must
   not read as a confirmed stat tile either.
   ============================================================ */

.data-sources {
  border: 1px dashed var(--border);
  border-radius: 8px;
  padding: 14px var(--space-4);
  margin: 14px 0 var(--space-1);
  background: color-mix(in srgb, var(--text-muted) 4%, transparent);
}

.ds-header-title {
  font-family: 'Barlow Condensed', 'Arial Narrow', sans-serif;
  font-weight: 800;
  font-size: 13px;
  letter-spacing: 0.3px;
  text-transform: uppercase;
  color: var(--text-muted);
}

.ds-note {
  margin: 6px 0 var(--space-3);
  font-size: 12.5px;
  line-height: var(--leading-normal);
  color: var(--text-muted);
}

.ds-section { margin-top: var(--space-3); }
.ds-section:first-of-type { margin-top: 0; }

.ds-section-title {
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.2px;
  text-transform: uppercase;
  color: var(--text);
  margin-bottom: 6px;
}

/* Legacy-figure disclosure sits directly under the stat grid it qualifies,
   separated by a rule so it reads as a caveat on those numbers rather than as
   another source pointer. */
.ds-section-legacy {
  border-bottom: 1px solid var(--border);
  padding-bottom: var(--space-3);
}

/* Market references are visually separated from the subsidy rows above by a
   rule — they are market data, not policy figures, and must not read as a
   continuation of the scheme list. */
.ds-section-market {
  border-top: 1px solid var(--border);
  padding-top: var(--space-3);
}

/* Training sits below the market references and is separated the same way, for
   the same reason: it is the only leg naming a PLACE TO GO rather than a
   document to open, and it must not read as a continuation of the price
   references above it. */
.ds-section-training {
  border-top: 1px solid var(--border);
  padding-top: var(--space-3);
}

/* Each scheme is its own bounded row so two schemes can never look like one
   combined entitlement. */
.ds-scheme {
  border-left: 2px solid var(--border);
  padding-left: 10px;
  margin-bottom: var(--space-2);
}
.ds-scheme:last-child { margin-bottom: 0; }

.ds-row { margin-bottom: var(--space-1); }

.ds-row-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.ds-row-label {
  font-size: 13.5px;
  font-weight: 600;
  color: var(--text);
}

/* -------- Staggered entrance for provenance rows and coverage meters --------

   ⚠ GATED ON no-preference RATHER THAN LEFT TO THE GLOBAL REDUCED-MOTION RULE,
   and the difference is not stylistic. That rule (near the end of this file)
   clamps animation-duration and animation-iteration-count — it does not touch
   animation-DELAY. An 8th row here carries a 280ms delay, so under that rule a
   reduced-motion reader would get a 0.01ms animation that still does not start
   for 280ms: content sitting at opacity 0 for a third of a second and then
   snapping in. The accessibility setting would have produced the worst version
   of the effect rather than none of it.

   Declaring the animation only inside no-preference means a reduced-motion
   reader has no animation, no delay and no opacity to recover from — the rows
   are simply there.

   The cap at 8 is deliberate: past that the last row on a long card would wait
   most of a second for no benefit, so every row from the 9th on shares the 8th's
   delay and the whole block still settles inside ~500ms. */
@media (prefers-reduced-motion: no-preference) {
  /* ⚠ TRANSFORM ONLY. NO OPACITY. THE FIRST VERSION OF THIS ANIMATED FROM
     opacity:0 AND IT HID EVERY PROVENANCE ROW ON THE PAGE.

     Measured, not feared: with `both` fill and an opacity:0 start, four rows on
     a real report card sat at computed opacity 0 indefinitely. getAnimations()
     showed why — playState "running", currentTime 0, startTime 0. A document
     that is not being painted does not advance its animation timeline, so the
     animation never leaves its first frame and the fill mode holds it there.
     Any condition that stops the clock — a background tab, which is what
     "open in new tab" produces — turns a decorative entrance into content that
     is not on the page.

     Animating transform alone removes the failure mode rather than papering
     over it. If the clock never starts, a row sits 4px low and fully legible;
     if it does, it rises. There is no state of this animation in which the
     provenance argument is invisible, which is the only property worth having
     here. Opacity is not worth one.

     ⚠ AND THE DELAY COMES FROM THE MARKUP, NOT FROM nth-child. The first
     version staggered on :nth-child and did not stagger at all: .ds-row is not
     a flat sibling list — some rows are child 1 of a .ds-scheme, others child 2
     of a .ds-section — so the selector produced 0ms, 40ms, 0ms, 40ms on a real
     card. dsRowIndex in app.js emits a per-block sequence into --ds-i, which is
     position in the BLOCK rather than position in a parent, and is what the
     stagger actually meant.

     Capped at 7 so a long block still settles inside ~480ms. */
  @keyframes ibi-row-rise {
    from { transform: translateY(4px); }
    to   { transform: none; }
  }

  .ds-row {
    animation: ibi-row-rise var(--dur-base) var(--ease) both;
    animation-delay: calc(min(var(--ds-i, 0), 7) * 40ms);
  }

  .path-cov {
    animation: ibi-row-rise var(--dur-base) var(--ease) both;
  }
}

/* NO COUNT-UP ON THE FIGURES, AND IT IS NOT AN OMISSION. A count-up needs a
   number to count to, and this catalogue's money figures are not numbers — they
   are strings holding RANGES: "₹1,00,000 – ₹3,00,000", "25% – 40%", "1 – 3".
   Animating one means parsing it and picking a single value to land on, which
   would put a midpoint on screen that no source states, in the largest type on
   the card, as the final frame of an animation that draws the eye to it. The
   stat grid's whole argument is that the range is the honest object. */

/* ⚠ THE .methodology RULES WERE HERE — six of them, giving the Resources
   methodology block the body measure the rest of the app's long text takes.
   Their only consumer was renderMethodology() in resources.js, which has been
   removed along with the section it rendered. Deleted rather than left dormant
   so a future reader does not style a class nothing emits. */

/* Tier in the comparison table. Deliberately unpainted — tier is a statement
   about this catalogue's coverage, not about how good or dangerous a business
   is, and a colour here would be read as the second thing. The risk badge one
   row down is the coloured element, and it is the one that means it. */
.compare-tier {
  font-weight: 600;
  border-bottom: 1px dotted var(--border);
  cursor: help;
}

/* The source note, collapsed. The toggle names what is behind it and measures
   it; it deliberately shows no slice of the prose — see dsPointer in app.js for
   why a truncated pointer is a defect this repo has already shipped once. */
.ds-pointer {
  margin-top: var(--space-1);
}

.ds-pointer-summary {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
  cursor: pointer;
  font-size: var(--text-xs);
  color: var(--canal-blue);
  padding: 2px 0;
  list-style: none;          /* Firefox */
  user-select: none;
}

/* WebKit/Blink draw their own triangle through a pseudo-element that
   list-style alone does not remove. */
.ds-pointer-summary::-webkit-details-marker { display: none; }

/* Own caret, so its rotation is ours to control and it sits on the text
   baseline rather than wherever the UA parks the default one. */
.ds-pointer-summary::before {
  content: "";
  display: inline-block;
  width: 0;
  height: 0;
  border-left: 4px solid currentColor;
  border-top: 3.5px solid transparent;
  border-bottom: 3.5px solid transparent;
  margin-right: var(--space-1);
  /* --dur-base is the token whose own comment names "disclosure" as its case,
     and --ease is the curve the nav indicator already turns on. A local
     `120ms ease` here would have been a second motion vocabulary in a file
     that has one. */
  transition: transform var(--dur-base) var(--ease);
  transform-origin: 25% 50%;
}

.ds-pointer[open] > .ds-pointer-summary::before { transform: rotate(90deg); }

.ds-pointer-cue {
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.3px;
}

.ds-pointer-summary:hover .ds-pointer-cue { text-decoration: underline; }

/* The length is a fact about the note, not a claim about it. Muted so it reads
   as a measurement rather than as a second label. */
.ds-pointer-len {
  color: var(--text-muted);
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
}

/* NO :focus-visible RULE HERE, DELIBERATELY. A <summary> is focusable and does
   need a ring — the global :focus-visible at the end of this file already draws
   it, including the dark-scheme colour override. A rule here would be (0,2,0)
   and would outrank that override, which is precisely the defect recorded above
   .unlock-input: a per-element ring that "would have kept --canal-blue in dark
   mode and been the one input in the app whose focus ring stayed at 1.84:1".
   Adding one back for this control would recreate it for this control. */

.ds-row-pointer {
  font-size: 13px;
  line-height: var(--leading-normal);
  color: var(--text-muted);
  margin-top: 2px;
}

.ds-applies {
  font-size: var(--text-xs);
  color: var(--text-muted);
  font-style: italic;
  margin-top: 2px;
}

/* ==========================================================================
   THE PROVENANCE CHIP (section 7) — one component, four states
   ==========================================================================

   Shared geometry lives here once and every state below only sets colour and
   border, which is what "build one component, use it everywhere" buys: a new
   state cannot drift in size, weight or radius.

   Two of the four states are dormant. Swept across all 98 entries and all
   eight tabs, the only provenance elements the live app renders are
   .ds-chip-pending and .stat-prov. .prov-badge never appears anywhere —
   provenance.js defines and exports it, nothing calls it — and neither does
   .cost-detail. They are styled here so they are correct the moment anything
   renders them, not because they render now.
   ========================================================================== */

.ds-chip,
.prov-badge {
  display: inline-block;
  font-family: 'Noto Sans', system-ui, sans-serif;
  font-size: var(--text-xs);        /* section 7: source name at --text-xs */
  font-weight: 700;
  letter-spacing: 0.3px;
  text-transform: uppercase;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);  /* section 4 assigns --radius-sm to chips */
  border: 1px solid transparent;    /* reserved so no state changes the box */
  background: transparent;
  white-space: nowrap;
  vertical-align: baseline;
}

/* STATE 2 — pending verification. The amber token is untouched, as section 7
   and section 11 both require; only the geometry above is shared. This is the
   state the catalogue actually uses: 323 of them across the 98 entries. */
.ds-chip-pending {
  background: var(--risk-med-bg);
  color: var(--risk-med-text);
}

/* ⚠ STATE 1 — "named source" is NOT applied to .ds-chip-verified, and this is
   the one place the brief's model and the product's disagree.
 *
 * Section 7 gives a named source a --growth-green border, i.e. a confident
 * state distinct from pending. This catalogue does not have that state. A
 * figure with a named source is STILL pending here — the chip beside a
 * .ds-row-label naming its source reads "pending verification" — and nothing
 * in the data is ever verified: true, which check-tier-drift asserts on every
 * commit. Painting named sources green would assert a verification the
 * catalogue deliberately withholds, which is the opposite of the honesty
 * section 7 exists to serve.
 *
 * So .ds-chip-verified keeps its existing tier colours and stays dormant by
 * design. The green treatment goes only to provenance.js's own tiers below,
 * which do claim a source rather than a verification. */
.ds-chip-verified {
  background: var(--risk-low-bg, #e3f4e6);
  color: var(--risk-low-text, #1f7a3d);
}

/* STATE 3 — no source recorded. ⚠ THIS IS NOT THE GREEN NAMED-SOURCE STATE THE
 * COMMENT DIRECTLY ABOVE REFUSES, AND THE DISTINCTION IS THE WHOLE REASON IT IS
 * ALLOWED TO EXIST. That state would mark the PRESENCE of a source this
 * catalogue has not confirmed, which is a verification claim it declines to
 * make. This marks the ABSENCE of a source altogether: 22 rows whose pointer is
 * a topic — "Gur mandi prices" is the whole of one — carrying no figure, no
 * date, no document and no admission that they carry none. Refusing to assert
 * confidence and refusing to disclose emptiness are opposite failures, and the
 * comment above forbids only the first.
 *
 * ⚠ AND IT RETIRES. The state is driven by pointer-roster.js, a debt list, not
 * by a property of the data. Reword a row and its text leaves the set and the
 * chip reverts to pending the same commit. When the list empties this rule is
 * unreachable and goes with the file — it is not a permanent third member of
 * the provenance vocabulary.
 *
 * QUIET NEUTRAL, NOT A THIRD ALARM. Amber is pending, red is documented risk,
 * green is refused above; an absence is none of those. --border and --text both
 * carry values in the light :root AND the dark override, which is the whole
 * lesson of the --growth-green repointing recorded above: a token with no dark
 * value renders 2.68:1 the moment the scheme flips.
 * Measured on this pair: 11.41:1 light  10.36:1 dark
 * — #262420 on #e0ddd4 and #eeece5 on #38352d, against the 4.5:1 AA needs.
 * Both declared here, so the rule resolves its own background and
 * check-contrast needs no SURFACES entry for it.
 *
 * ⚠ A NEARER-LOOKING PAIR WAS REJECTED ON MEASUREMENT, not on taste.
 * --text-muted on --border is 4.20:1 light  4.53:1 dark
 * — it fails AA in exactly one scheme, which is the failure a single-scheme
 * eyeball misses, and it is registered in this guard's CLAIMS table as a
 * rejected pair so the figures stay accounted for rather than asserted. */
.ds-chip-nosource {
  background: var(--border);
  color: var(--text);
}

/* STATE 1 — named source, on provenance.js's Field-verified / Published data
   tiers. Dormant: costs-data.js is a generated Object.assign(COST_DATA, {}),
   so composeFigure() resolves nothing and neither badge path is reached.

   ⚠ REPOINTED off --growth-green 2026-08-03. --growth-green is declared once,
   in the light :root, with no dark value, so this was #2F6D4E on #211f1b —
   2.68:1 — whenever the data does land. --risk-low-text carries a value in both
   schemes, and .ds-chip-verified directly above already uses it, so this block
   now has one green-text answer rather than two. The figure this comment used
   to carry, 6.14:1, was --growth-green on white and is superseded.

   ⚠ AND THE BACKGROUND IS SET HERE ON PURPOSE. A first pass repointed only the
   text and quoted a ratio against bare --panel. This element does not sit on
   bare --panel: a single-selector rule ~1400 lines above paints it a 16% wash of
   var(--rv-growth, #2f6d4e), and --rv-growth is declared on body.report-theme
   rather than :root, so outside the report theme that fallback literal is what
   renders. The true pair was 4.27:1 in light — still under the floor, and quoted
   as 5.37:1. Declaring an opaque background here overrides the wash and settles
   both schemes at once. Measured, named-source badge: 4.74:1 light  7.44:1 dark. */
.prov-badge--field,
.prov-badge--dataset {
  border-color: var(--risk-low-text);
  background: var(--risk-low-bg);
  color: var(--risk-low-text);
}

/* STATE 3 — modelled estimate. Dashed, because a dashed edge reads as
   "constructed" rather than "collected". 8.92:1 on a white card. Dormant.
   Its "shows arithmetic" affordance already exists: cost-model.js wraps
   partsBreakdown() in <details class="cost-detail">. That is a click-to-open
   on the ARITHMETIC, not on the chip, so it does not breach the rule below. */
.prov-badge--modelled {
  border-style: dashed;
  border-color: var(--canal-blue);
  color: var(--canal-blue);
}

/* Subsidy rate, per scheme row. No :empty rule and no min-height anywhere in
   here: a scheme with no curated rate emits no .ds-rate element at all, so
   there is nothing to collapse. An empty track would read as "no subsidy". */
.ds-rate {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin: 5px 0 var(--space-1);
}

.ds-rate-bar {
  flex: 1;
  max-width: 160px;
  min-width: 90px;
  height: 7px;
  border-radius: var(--radius-full);
  background: var(--bg);
  overflow: hidden;
}

/* Deliberately NOT --match green. A subsidy rate nobody has confirmed should
   not borrow the colour this dashboard uses for a computed, trusted score. */
.ds-rate-fill {
  height: 100%;
  border-radius: var(--radius-full);
  background: var(--risk-med-text, #9a6b00);
}

.ds-rate-value {
  font-size: 12.5px;
  font-weight: 700;
  color: var(--risk-med-text, #9a6b00);
  white-space: nowrap;
}

.ds-rate-applies {
  font-size: 11.5px;
  color: var(--muted);
  flex-basis: 100%;
}

/* Why this row has no rate. Deliberately NOT bar-shaped and not chip-shaped:
   no track, no pill, no percentage colour. It must not read as a rate of zero
   or as a weaker version of a rate — it is a different kind of statement. */
.ds-norate {
  margin: 5px 0 var(--space-1);
  padding-left: var(--space-2);
  border-left: 2px solid var(--border, #d9d9d9);
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--muted);
}

/* When the claim was last established. Only the seven sentences that make a
   temporal claim carry one, so its ABSENCE is meaningful too — it marks a
   statement that cannot go stale rather than one nobody dated. */
.ds-norate-asof {
  display: block;
  margin-top: 3px;
  font-size: 10.5px;
  font-style: italic;
  opacity: 0.8;
}

/* ============================================================
   Cross-references between overlapping entries
   (add-cross-references.js / renderRelatedEntries)

   Navigational, not a warning — so it deliberately avoids the
   amber/red of .risk-banner and .caution-warning and the muted
   grey of .thin-data-note. A dotted left rule in the link accent
   reads as "there is somewhere else to go" rather than "something
   is wrong here". Sits last in the card, after the figures and
   their provenance.
   ============================================================ */

/* ⚠ --green, NOT --growth-green. --growth-green has no dark value, which made
   the label below 2.60:1 and the dotted rule invisible on a dark panel. --green
   is #276b47 light / #4caf7d dark — at a 4% tint the light rendering is
   indistinguishable from what this was. */
.related-entries {
  border-left: 3px dotted var(--green);
  background: color-mix(in srgb, var(--green) 4%, transparent);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  padding: 11px 14px;
  margin: 14px 0 var(--space-1);
}

.related-entries-label {
  font-family: 'Barlow Condensed', 'Arial Narrow', sans-serif;
  font-weight: 800;
  font-size: 12.5px;
  letter-spacing: 0.3px;
  text-transform: uppercase;
  color: var(--green);
  margin-bottom: 7px;
}

.related-entry + .related-entry {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid color-mix(in srgb, var(--text-muted) 18%, transparent);
}

.related-entry-name {
  font-weight: 700;
  font-size: 13.5px;
  color: var(--text);
  margin-bottom: 3px;
}

.related-entry-tag {
  display: inline-block;
  font-family: 'Barlow Condensed', 'Arial Narrow', sans-serif;
  font-weight: 700;
  font-size: 11px;
  letter-spacing: 0.3px;
  text-transform: uppercase;
  padding: 1px 7px;
  border-radius: var(--radius-lg);
  margin-left: 6px;
  vertical-align: 1px;
}

/* "same business" is the stronger claim, so it gets the stronger chip.
   ⚠ OPAQUE ON PURPOSE. Keeping the translucent 16% wash and only repointing the
   token measures 4.81:1 light / 4.40:1 dark — the dark side still fails. A wash
   of a colour over a wash of the same colour cannot separate far enough. The
   --green-bg / --green pair is 5.66:1 light / 5.53:1 dark. */
.related-entry-tag-duplicate {
  background: var(--green-bg);
  color: var(--green);
}

.related-entry-tag-related {
  background: color-mix(in srgb, var(--text-muted) 14%, transparent);
  color: var(--text-muted);
}

.related-entries p {
  margin: 0;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--text);
}

/* ============================================================
   PATH TO SUCCESS TEASER — closes the Report and the Predictor
   ------------------------------------------------------------
   Reads as a next step, not an interstitial. It reuses the
   existing --blue family rather than introducing a promotional
   colour: the amber risk-banner and the red caution card carry
   specific hazard meanings in this app, and a sales block must
   not be able to borrow either. Deliberately no gradient,
   no pulse, no badge — the block earns attention from what it
   lists, and it sits at the very bottom of a finished page.
   ============================================================ */

.card-path-teaser {
  border-left: 4px solid var(--blue);
  background: var(--blue-bg);
}

.path-teaser-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: 10px;
}

.path-teaser-title {
  font-family: 'Barlow Condensed', 'Arial Narrow', sans-serif;
  font-size: 19px;
  font-weight: 800;
  letter-spacing: 0.3px;
  text-transform: uppercase;
  color: var(--blue);
}

/* A plain fact in a plain box. No animation and no contrast
   spike — a price that shouts reads as a price that is trying
   to get past you. */
.path-teaser-price {
  background: var(--panel);
  border: 1px solid var(--blue);
  color: var(--blue);
  border-radius: var(--radius-full);
  padding: 3px var(--space-3);
  font-size: 12.5px;
  font-weight: 700;
  white-space: nowrap;
}

/* Teaser count band. The counts are the proof someone did the work, so they lead
   at display size instead of sitting mid-sentence at 13.5px.

   ⚠ GOLD IS A FILL HERE AND NEVER TEXT, WHICH IS THE WHOLE DARK-SCHEME ANSWER.
   Measured: --wheat-gold #E3A639 against dark --risk-med-text #e0a94a is rgb
   distance 18, contrast 1.02:1 — the same colour to a reader — and 56 of the 99
   businesses carry Medium risk, so that badge and this band share a screen on the
   majority of cards. The obvious "safer" variant, a gold EDGE with a gold
   numeral, was mocked in both schemes and is WORSE: it puts gold into a text
   role, which is exactly where the risk palette already lives. Keeping gold as a
   solid fill and the numeral in --ink keeps the two in different roles even at
   identical hue. Ink on gold measures 7.46:1, so one treatment serves both
   schemes and no dark override is needed.

   The class names avoid risk/tier/status/pending/verified on purpose:
   check-gold-reservation.js fails any rule whose SELECTOR matches those words and
   references gold, because section 1 spent the gold reservation on the argument
   that nothing in this app signals status in gold. */
/* ⚠ FLEX WITH A max-width, NOT `repeat(auto-fit, 1fr)`, AND A SCREENSHOT IS WHY.
   auto-fit made a single cell span the full card — Dairy Products has 11 costed
   items and a timeline count of 0, so it filtered to ONE cell and rendered as a
   1000px gold bar. Capping each cell means one cell reads as one cell and four
   still share the row. 44 of the 76 banded businesses show fewer than four. */
.path-band {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0 0 10px;
}
/* ⚠ EACH CELL CARRIES ITS OWN BORDER; the band has no background of its own.
   A shared background with 1px gaps looked right at four cells and left a ~740px
   grey slab beside one — and 24 of the 76 banded businesses show exactly one.
   Self-contained cells read correctly at every count from 1 to 4. */
.path-band-cell {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 3px;
  padding: 12px 14px 11px;
  flex: 1 1 130px;
  max-width: 260px;
}
.path-band-cell-paid { background: var(--wheat-gold); }
.path-band-n {
  font-family: 'Barlow Condensed', 'Arial Narrow', sans-serif;
  font-size: 44px;
  font-weight: 800;
  line-height: 0.9;
  letter-spacing: -0.5px;
  color: var(--blue);
}
.path-band-cell-paid .path-band-n { color: var(--ink); }
.path-band-u {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-top: 7px;
}
.path-band-cell-paid .path-band-u { color: var(--ink); opacity: 0.75; }
.path-band-work { margin: 0 0 10px; font-size: 14.5px; line-height: 1.55; }

.path-teaser-lead {
  margin: 0 0 10px;
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--text);
}

.path-teaser-sub {
  margin: 0 0 var(--space-2);
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--text);
}

.path-teaser-sub-gap {
  margin-top: 14px;
  color: var(--text-muted);
}

.path-teaser-list {
  margin: 0 0 var(--space-1);
  padding-left: 20px;
}

.path-teaser-list li {
  font-size: 13.5px;
  line-height: 1.6;
  margin-bottom: 7px;
}

.path-teaser-has { color: var(--text); }

/* Gaps are muted, never hidden. They are listed in the same
   block and the same type size as what is being sold — a
   reader comparing the two lists is the point. */
.path-teaser-gap { color: var(--text-muted); }

.path-teaser-cta { margin-top: var(--space-3); }

.path-teaser-foot {
  margin: 10px 0 0;
  font-size: 12.5px;
  line-height: var(--leading-normal);
  color: var(--text-muted);
}

/* Nothing to sell for this business. Drops the accent colour
   entirely so it cannot be mistaken for an offer. */
.card-path-teaser-empty {
  background: var(--panel);
  border-left: 4px solid var(--border);
}

.card-path-teaser-empty .path-teaser-title {
  color: var(--text-muted);
  text-transform: none;
  font-size: 17px;
  display: block;
  margin-bottom: var(--space-2);
}

/* ---- the nav's price chip ---- */

.toggle-btn-paid { display: inline-flex; align-items: center; gap: 7px; }

/* Section 6: the locked tab keeps its lock glyph inline at --text-sm and 50%,
   and explicitly not in gold or a warning colour — it is a state, not an
   alert. The chip that carried it is gone: a filled, bordered pill is alert
   styling, which is the thing the brief rules out.

   The word stays smaller than the glyph. The brief models a locked tab as
   carrying a lock alone; these carry "locked" beside it, and setting that at
   --text-sm would make the paid tabs the loudest labels in the nav, which
   inverts the emphasis the section is asking for.

   ⚠ THIS SAID "budgets $1.99" AND WAS TWICE STALE — the price had moved to
   $2.99, and the chip no longer carries a price at all. A figure in the nav
   reads as a price tag on a menu item, and it also understated the gate: it
   named the equipment budgets when the whole tab is behind the unlock. Two
   tabs use this class now, Path and the Investment & NRI Desk. */

.nav-price {
  font-family: 'Noto Sans', system-ui, sans-serif;
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  padding: 0;
  border: none;
  background: none;
  opacity: 0.5;
  white-space: nowrap;
}

.nav-price [aria-hidden="true"] { font-size: var(--text-sm); }

/* The teaser once the reader has paid. No price, no lock — the
   block stays as a table of contents rather than an offer. */
.path-teaser-unlocked {
  background: var(--green-bg);
  border: 1px solid var(--green);
  color: var(--green);
  border-radius: var(--radius-full);
  padding: 3px var(--space-3);
  font-size: 12.5px;
  font-weight: 700;
  letter-spacing: 0.2px;
  white-space: nowrap;
}

/* Sits above the lock panel on a gated Path tab and says what
   THIS business would give you — the same computed list the
   teaser uses, so it cannot promise a step that is a gap. */
.card-path-locked-context {
  border-left: 4px solid var(--blue);
  background: var(--blue-bg);
}

/* Step 5 when the costed items have not been released to this
   browser. Deliberately not styled as an ad: the citation
   block is the same one an unlocked card shows, because the
   document is public and only the extraction is not. */
.path-equip-locked {
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  padding: var(--space-4) 18px;
  background: var(--bg);
}

.path-equip-locked .path-lead { margin-top: 0; }
.path-equip-locked .btn-primary { margin-top: var(--space-3); }

/* Pay button sits above the code row in the unlock panel; the
   code path is not a downgrade, it is how comps and testing
   work, so neither is styled as secondary to the other. */
.unlock-pay { margin-bottom: 10px; }

.unlock-pay-note {
  margin: 6px 0 0;
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--risk-med-text);
  background: var(--risk-med-bg);
  border-radius: var(--radius-md);
  padding: var(--space-1) 9px;
  display: inline-block;
}

/* Recovery sits collapsed under the unlock panel. Someone here
   to buy should not read past a support form to do it; someone
   locked out is looking for exactly this. */
.unlock-recover {
  margin-top: 14px;
  border-top: 1px solid var(--border);
  padding-top: var(--space-3);
}

.unlock-recover summary {
  cursor: pointer;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-muted);
}

.unlock-recover summary:hover { color: var(--text); }

.unlock-recover-note {
  margin: 10px 0;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--text-muted);
}

.unlock-recover-note code {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0 var(--space-1);
  font-size: var(--text-xs);
}

/* ---------- Investment & NRI Desk ---------- */

.invest-results { display: flex; flex-direction: column; gap: 1.25rem; }

.invest-disclaimer {
  margin: 0;
  padding: 0.75rem 1rem;
  border: 1px solid var(--risk-med-text);
  background: var(--risk-med-bg);
  color: var(--text);
  border-radius: 8px;
  font-weight: 600;
  font-size: 0.95rem;
}
.invest-disclaimer-foot { margin-top: 0.25rem; }

.invest-matrix-wrap { overflow-x: auto; }
.invest-matrix { width: 100%; border-collapse: collapse; }
.invest-matrix th[scope="row"] {
  text-align: left;
  vertical-align: top;
  width: 12rem;
  min-width: 10rem;
  padding: 1rem 1rem 1rem 0;
  border-top: 1px solid var(--border, #e2e2e2);
}
.invest-row-label { display: block; font-weight: 700; }
.invest-row-sub { display: block; font-size: 0.85rem; opacity: 0.75; font-weight: 400; }
.invest-cell {
  vertical-align: top;
  padding: 1rem 0;
  border-top: 1px solid var(--border, #e2e2e2);
}
.invest-cell-unsourced p { margin: 0.4rem 0 0; opacity: 0.8; font-size: 0.92rem; }
.invest-unsourced-tag,
.invest-status-tag {
  display: inline-block;
  padding: 0.15rem 0.5rem;
  border-radius: var(--radius-full);
  background: var(--bg);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}
.invest-flag {
  margin: 0 0 0.6rem;
  padding: 0.6rem 0.75rem;
  border-left: 3px solid var(--risk-med-text);
  background: var(--risk-med-bg);
  font-size: 0.92rem;
}
.invest-note { margin: 0 0 0.6rem; font-size: 0.92rem; opacity: 0.85; }
.invest-matrix-foot { margin: 1rem 0 0; font-size: 0.85rem; opacity: 0.75; }

.fema-clause { margin: 0 0 0.85rem; }
.fema-clause blockquote {
  margin: 0;
  padding: 0.6rem 0.85rem;
  border-left: 3px solid var(--growth-green);
  background: var(--panel);
  font-size: 0.93rem;
  line-height: var(--leading-normal);
}
.fema-clause figcaption {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.35rem;
  font-size: 0.78rem;
  opacity: 0.8;
}
.fema-para { font-weight: 700; }
.fema-updated { font-style: italic; }

/* ---- question layer ------------------------------------------------------
   Verdict badges are siblings of .invest-status-tag: same pill, same size, same
   uppercase. Positive state gets colour; the absent state gets grey plus a
   hairline, which is what .badge-nocost, .badge-subsidy-no and
   .badge-involvement already do for absence elsewhere in this app.

   ⚠ NO NEW COLOUR TOKEN. Confirmed is the --risk-low pair, conditional the
   --risk-med pair, and the third state is --bg / --text-muted / --border.
   Acceptance criterion 1 holds. Two steps of one scale for the two coloured
   states, which is why --risk-low was taken over the also-eligible
   --green/--green-bg (5.66 light, 5.53 dark — more headroom, but it would put
   one badge on the risk ramp and its neighbour on a generic positive, a mixed
   metaphor in a two-badge row).

   ⚠ CORRECTED 2026-08-03. This comment previously read "5.05 and 5.71
   confirmed" and confirmed was --growth-green over a color-mix of itself and
   --panel. Both halves were wrong. --growth-green is one of five brand tokens
   declared ONCE, in the light :root, and never overridden for dark — unlike
   --risk-low/-med/-high, which carry a value in each scheme. So the dark
   rendering was #2F6D4E on #232a22: 2.40:1, under the 4.5:1 AA floor for this
   12px bold uppercase text and under even the 3:1 non-text floor. The 5.71
   figure was not a measurement of anything. It was found by recomputing all six
   ratios from the token values during the verification pass, rather than by
   reading the rendered page or by a guard: check-nav-contrast.js scopes to the
   header, check-flag-colours.js to the flag palette, and nothing else in the
   suite reads these rules at all. That is worth recording, because recomputing
   from the tokens is exactly what a guard could do on every commit instead of
   once — and a token with no dark value is a defect a recomputation finds and
   an eye reading the light scheme never will.
   A comment whose only job is to carry a figure is worse than absent when the
   figure is out by a factor of 2.4.

   Figures below are per scheme, measured against the tokens each scheme
   actually declares, not stated bare:
     confirmed    --risk-low-text on --risk-low-bg    4.74:1 light   7.44:1 dark
     conditional  --risk-med-text on --risk-med-bg    4.75:1 light   7.00:1 dark
     none         --text-muted on --bg                5.18:1 light   6.70:1 dark
     not allowed  --risk-high-text on --risk-high-bg  5.31:1 light   5.75:1 dark
   All eight clear 4.5:1. Light confirmed at 4.74 is the thinnest margin here —
   0.24 over the floor — and it is 12px bold uppercase at 0.03em, so it passes
   as normal text without relying on any large-text exemption. The hairline on
   the third state is --border, 1.23:1 light and 1.48:1 dark against --bg: it
   is a boundary, not a carrier of meaning, and the word inside the pill is
   what states the absence.

   ⚠ THE FOURTH STATE, ADDED AFTER THE ANSWERS WERE REWRITTEN IN PLAIN WORDS.
   "Conditional" had been carrying two different things: a genuine "yes, with
   limits" (the repatriation cap, the loan carve-outs) and a flat NO with one
   exception (can an NRI buy farmland). For a reader who knows no legal English
   that is not merely hard, it is wrong — HAS LIMITS over a prohibition reads as
   a qualified yes. The farmland answer is the rule that decides an NRI’s whole
   plan for Punjab, so it is the worst possible place to blur.

   --risk-high was chosen over a new token for three reasons, and the third is
   stated carefully here because an earlier draft of this paragraph put it
   loosely enough to be misread. It is already declared in BOTH schemes. It
   completes a ramp a reader learns in one glance: low = clear rule, med = has
   limits, high = not allowed. And on the FLOOR — the worse of a pair’s two
   schemes — it is the safest of the four badges: read the table above down its
   worse-scheme column and this pair sits at the top of it.

   ⚠ THE FLOOR IS THE ONLY COMPARISON BEING MADE, and it is deliberately the
   one that matters. A pill has to pass in whichever scheme the reader happens
   to be in, so what a badge actually guarantees is its worse scheme, never its
   better one. THIS PAIR IS NOT "THE HIGHEST CONTRAST OF THE FOUR" — in the
   dark scheme it is the LOWEST of the four, with confirmed, conditional and
   none all above it. Best worst case is a narrower claim than best contrast,
   and only the narrower one is made. Nothing should be repainted on a reading
   of "risk-high has the best contrast", because that is not what this says.

   No figure is restated in this paragraph, on purpose. The table above is the
   single place the eight numbers live and the one check-contrast.js re-derives
   through CLAIMS. A second copy down here would be a figure nothing checks —
   which is the exact defect that caused this comment block to be written.

   The badge word and the answer text are the same change — a reader meets them
   in the same glance — so they ship together rather than as two commits.

   ⚠ NO CLASS HERE CONTAINS "status". check-gold-reservation.js treats any
   selector matching risk|tier|status|prov|warn|alert as status-semantic, and
   these carry the risk-ramp pairs deliberately. Putting the word "status" in
   one of these names would have pulled it under the gold rule for no reason.
   (This paragraph previously said the badges carry --growth-green; that stopped
   being true when the confirmed badge moved to --risk-low, and it is corrected
   here rather than left standing in a block being amended.) */
/* The sector briefing — first panel on the tab, above the question stack.
   Deliberately quiet: no left rule, no tint, no accent. It introduces the
   panels below it and must not outrank them, and the tab's colour vocabulary
   is already spent on verdict badges and provenance chips. Everything here is
   an existing token. */
/* The orientation line above the bullets. Muted and set at body size rather
   than as a lead paragraph: it frames the panel and must not compete with the
   bullets, which are the content. It is the one line in this panel with no
   citation under it — see renderSectorBriefing for why. */
.invest-brief-intro {
  margin: 0 0 var(--space-4);
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--text-muted);
  max-width: var(--measure);
}

.invest-brief-group + .invest-brief-group { margin-top: var(--space-4); }

.invest-brief-group h4 {
  font-family: 'Barlow Condensed', 'Arial Narrow', sans-serif;
  font-size: var(--text-xs);
  font-weight: 800;
  letter-spacing: 0.3px;
  text-transform: uppercase;
  color: var(--text);
  margin: 0 0 var(--space-2);
}

.invest-brief-list { margin: 0; padding-left: 18px; max-width: var(--measure); }

.invest-brief-list li {
  font-size: var(--text-sm);
  line-height: 1.6;
  margin-bottom: var(--space-2);
}
.invest-brief-list li:last-child { margin-bottom: 0; }

/* The citation and the gap marker both sit on their own line under the bullet
   they qualify, small and muted, so a bullet reads as one sentence and its
   provenance does not compete with it. The gap marker is NOT amber: on this tab
   amber means a verdict has limits, and spending that on "we did not find a
   document" would make an absence look like a warning about the business. */
.invest-brief-cite,
.invest-brief-gap {
  display: block;
  margin-top: 3px;
  font-size: var(--text-xs);
  color: var(--text-muted);
}
.invest-brief-cite { font-variant-numeric: tabular-nums; }
.invest-brief-gap::before { content: 'Not sourced — '; font-weight: 700; }

.invest-questions { display: flex; flex-direction: column; gap: var(--space-2); }

/* ---- the card, laid out ---------------------------------------------------
   ⚠ THE LEFT BAR IS THE FEATURE, NOT THE DECORATION. A reader scrolling this
   list should be able to see the answer state without reading the answer, and
   the bar is the only element on a collapsed card that can carry that. Keyed to
   the same four verdicts as the badge, so the two can never disagree: both read
   the ONE class the renderer puts on the badge.

   :has() rather than a class on the <details>, because the verdict already
   exists in the markup and adding a second copy of it to the card element would
   be two things to keep in step. Four other rules in this file use :has().

   ⚠ THE INSET BOX HAS NO HAIRLINE, AND THAT IS DELIBERATE. --bg plus a hairline
   plus a small radius is exactly .law-source-gap and .nri-services-gap — the
   marker that means UNVERIFIED. The answer is the most verified thing on the
   card, and giving it the disclosure's frame would teach a reader the opposite
   of what those boxes mean. Fill only: the tint does the containing, and the
   one bordered grey box in these cards keeps meaning one thing. */

.invest-question {
  border-left-width: 5px;
  border-left-style: solid;
}
/* The four states. --border restates the shorthand's own colour rather than
   relying on it, so the table can be read as four rows instead of three plus an
   absence. */
.invest-question:has(.invest-verdict-confirmed)   { border-left-color: var(--risk-low-text); }
.invest-question:has(.invest-verdict-conditional) { border-left-color: var(--risk-med-text); }
.invest-question:has(.invest-verdict-notallowed)  { border-left-color: var(--risk-high-text); }
.invest-question:has(.invest-verdict-none)        { border-left-color: var(--border); }

.invest-question {
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-md);
  background: var(--panel);
}
.invest-question summary {
  display: grid;
  /* ⚠ A THIRD COLUMN, FOR A FOURTH GRID ITEM. summary::after is a grid item
     like the three spans, and left to auto-placement it would have landed in
     column 1 of a new row — under the answer box rather than at the end of the
     question row. Every item below is placed explicitly for the same reason:
     with one definite-column item already in the flow, auto-placement order is
     something to reason about, and there is no reason to have to. */
  grid-template-columns: auto 1fr auto;
  gap: var(--space-1) var(--space-3);
  /* ⚠ THE MARKER SUPPRESSION WAS ONE RULE AND NEEDED THREE. Only
     ::-webkit-details-marker was here. No triangle shows today because
     `display: grid` takes the summary out of list-item layout and the marker
     box is never generated — but that is an accident of the layout, not a
     decision, and it would come back the moment display changed. With our own
     chevron on the row, the failure is now two arrows rather than one. Matched
     to what .cost-detail and .ds-pointer-summary already do in this file. */
  list-style: none;
  /* centred, not baseline: the badge is a pill with its own padding, and on a
     baseline the text inside it sits low against the question beside it. */
  align-items: center;
  padding: var(--space-3) var(--space-4);
  cursor: pointer;
}
.invest-question summary::-webkit-details-marker { display: none; }
.invest-question summary::marker { content: ''; }
.invest-question[open] { box-shadow: var(--shadow-1); }
.invest-question[open] summary { border-bottom: var(--border-width) solid var(--border); }

.invest-question summary > .invest-verdict { grid-column: 1; grid-row: 1; }
.invest-q-text { grid-column: 2; grid-row: 1; font-weight: 700; font-size: var(--text-base); }
/* The one-line answer, in a tinted inset so it reads as the answer rather than
   as more card text. Spans both columns: indenting it under the question would
   make it look like a continuation of that sentence, which is the thing the box
   exists to stop.

   ⚠ --text, NOT --text-muted, WHICH IS WHAT IT WAS. A muted answer beside a
   full-strength question inverts the hierarchy of an answer-first page — and
   this line is the answer. The badge and the tint already do the separating,
   so dimming the type as well spent a third signal saying what the first two
   had said. */
.invest-q-headline {
  grid-column: 1 / -1;
  grid-row: 2;
  margin-top: var(--space-1);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  background: var(--bg);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  color: var(--text);
}
/* ---- the disclosure affordance -------------------------------------------
   ⚠ THIS IS NOT A FOURTH GLYPH. ✓ ⚠ ✕ say WHAT THE ANSWER IS; this says THERE
   IS MORE, and the two vocabularies must not blur. It is kept apart on every
   axis available: the far right rather than beside the badge, --text-muted
   rather than any verdict colour, and drawn rather than typed — no character
   of any kind, so it cannot be mistaken for one member of a set of three.

   ⚠ AND DRAWN IS ALSO THE ACCESSIBILITY ANSWER. Pseudo-element `content` is
   exposed to the accessibility tree in several browsers, so a chevron typed as
   a character can be announced as one, in the middle of a control whose state
   <details>/<summary> already announces. content is '' and the shape is two
   borders on an empty box: there is no text to read out. The affordance is
   purely visual, which is what it should be — the expanded state was never
   missing from the accessible name, only from the picture.

   --dur-base carries the rotation. Its own comment in the token block names
   this case: "tab switch, card lift, disclosure". The global
   prefers-reduced-motion block already reaches *::after and collapses the
   transition to 0.01ms, so nothing extra is needed for it here.

   ⚠ IT DECLARES A COLOUR AND A BACKGROUND ON PURPOSE. Borders drawn from
   currentColor would have let this rule carry a colour that check-contrast
   never sees, because that guard pairs `color` against a background and skips
   what it cannot resolve. Stating both puts the chevron inside the measured
   set. It is a graphic, so WCAG's floor for it is the non-text one rather than
   4.5 — but NON_TEXT is a pinned registry in the guard and widening it is an
   abort, so this is held to the strict text threshold instead and clears it.
   Held to the wrong-but-stricter rule beats not being held at all. */
.invest-question summary::after {
  content: '';
  grid-column: 3;
  grid-row: 1;
  width: 0.45rem;
  height: 0.45rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  color: var(--text-muted);
  background: var(--panel);
  transform: translateY(-0.15em) rotate(45deg);
  transition: transform var(--dur-base) var(--ease);
}
/* Down when closed, up when open. -135 rather than 225 so the shape turns back
   the way it came instead of continuing round. */
.invest-question[open] summary::after {
  transform: translateY(0.1em) rotate(-135deg);
}

.invest-q-body { padding: var(--space-3) var(--space-4) var(--space-4); }
.invest-q-explain,
.invest-q-unsourced {
  margin: 0 0 var(--space-3);
  font-size: 0.92rem;
  line-height: var(--leading-normal);
  max-width: var(--measure);
}
.invest-q-unsourced { color: var(--text-muted); }

.invest-verdict {
  display: inline-block;
  padding: 0.15rem 0.5rem;
  border-radius: var(--radius-full);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  white-space: nowrap;
}
.invest-verdict-confirmed {
  background: var(--risk-low-bg);
  color: var(--risk-low-text);
}
.invest-verdict-conditional {
  background: var(--risk-med-bg);
  color: var(--risk-med-text);
}
.invest-verdict-notallowed {
  background: var(--risk-high-bg);
  color: var(--risk-high-text);
}
.invest-verdict-none {
  background: var(--bg);
  color: var(--text-muted);
  border: var(--border-width) solid var(--border);
}

/* ⚠ THE NARROW BREAKPOINT IS GONE, NOT FORGOTTEN. It collapsed the summary to
   one column, which put the badge ABOVE the question — the stacked layout this
   pass exists to replace, still shipping below 560px. The badge is nowrap and
   about six characters wide; two columns hold down to the narrowest phone, and
   the question wraps in the second one as it already did. */

.invest-land-warning { border: 2px solid var(--risk-med-text); }
.invest-land-tail { margin: 0.5rem 0 0; font-size: 0.92rem; }

.invest-primer-block { margin-bottom: 1rem; }
.invest-primer-block h4 { margin: 0 0 0.5rem; }

.invest-probe-list { margin: 0.5rem 0; padding-left: 1.1rem; font-size: 0.9rem; }
.invest-probe-list li { margin-bottom: 0.35rem; }
.invest-probe-list code { font-size: 0.85rem; }
.invest-punjab-lead { margin: 0.25rem 0 0; }
.invest-controls-note,
.invest-withheld { font-size: 0.9rem; opacity: 0.85; }

.invest-probe-ip {
  margin-left: 0.4rem;
  font-size: 0.78rem;
  opacity: 0.65;
  font-variant-numeric: tabular-nums;
}
.invest-probe-layer,
.invest-probe-block { font-size: 0.9rem; opacity: 0.85; }

/* ---- a judgment, and a directory -----------------------------------------
   Two proof kinds arrived with the fraud and who-helps cards, and neither is a
   Master Direction. What they have in common is that each renders a paragraph
   RECORDING AN ABSENCE: the judgment has no citation on file, and not one
   figure in the services directory has been read from a government page.

   ⚠ THOSE TWO PARAGRAPHS ARE WHY THIS BLOCK EXISTS. Unstyled they rendered as
   ordinary body text, which is the .stat-prov failure exactly — the design
   hiding the honesty, in the one place the product exists to be honest about.
   A card that discloses its gap in type indistinguishable from its answer has
   not disclosed anything.

   THE TREATMENT IS ABSENCE, NOT RISK. The obvious reach is the --risk-med pair
   and it is wrong: amber says "this is dangerous"; these say "we have not
   checked this". That is what .invest-verdict-none and .badge-nocost already
   say elsewhere in this file — muted text, a hairline, a --bg fill — so these
   say it the same way, and the rule a reader can learn is that IN THESE CARDS
   A BORDERED GREY BOX MEANS UNVERIFIED. Nothing else here gets one, which is
   why .nri-services takes a left rule instead: a box inside a box would spend
   the one signal on a container that is not making a disclosure.

   ⚠ NO RATIO IS QUOTED HERE, ON PURPOSE. Every pair below is a token declared
   in both schemes and check-contrast.js measures all of them. Writing the
   numbers into this comment would put a second copy beside something the guard
   already holds — the drift the SURFACES anti-drift rule exists to stop — and
   a figure stated in a comment with no CLAIMS entry is a claim nothing checks,
   which is the shape of the defect that guard was written for.

   ⚠ AND THE PHRASE BELOW IS DELIBERATELY NOT "X on Y". Naming the pairs in that
   form collided with a live CLAIMS anchor and took the guard to CANNOT
   ESTABLISH: those anchors are matched as plain substrings anywhere in the
   file, so a comment can break one by describing the same two tokens. The
   pairs here are --text-muted over --panel, and --text-muted over --bg. */

/* A judgment is not a Master Direction, and the left rule is the only thing
   that says so at a glance. --blue is declared in both schemes and signals
   nothing else in this app; the gold reservation is untouched. */
.law-clause blockquote { border-left-color: var(--blue); }

/* ⚠ OPACITY IS INVISIBLE TO check-contrast.js. It compares colour pairs, so a
   caption faded to 0.8 measures as though it were not faded at all. The one
   caption in this file that can carry a MISSING citation therefore states its
   colour rather than implying it, so the guard sees what a reader sees. */
.law-clause figcaption {
  opacity: 1;
  color: var(--text-muted);
  background: var(--panel);
}
/* The court is the identity of a judgment the way a paragraph number is the
   identity of a Direction, so it takes the bold and the date gives it up. */
.law-court { font-weight: 700; }
.law-clause .fema-para { font-weight: 400; }
/* Italic marks it as a stand-in rather than a reference. Colour is inherited
   from the caption above, where it is measured. */
.law-gap { font-style: italic; }

.law-context {
  margin: 0.4rem 0 0;
  font-size: 0.85rem;
  line-height: var(--leading-normal);
  max-width: var(--measure);
  color: var(--text-muted);
  background: var(--panel);
}

/* The directory: held facts, set apart from the sentences around them without
   claiming the unverified box. */
.nri-services {
  margin: 0 0 var(--space-3);
  padding-left: var(--space-3);
  border-left: var(--border-width) solid var(--border);
}
/* No colour declared. These lines want --text, which is what they inherit, and
   a colour nobody needs is a pair that can go wrong later for nothing. */
.nri-services-line {
  margin: 0 0 var(--space-2);
  font-size: 0.92rem;
  line-height: var(--leading-normal);
  max-width: var(--measure);
}
.nri-services-line:last-child { margin-bottom: 0; }

/* The two disclosures, styled identically on purpose: one records a missing
   citation and the other an unverified directory, and a reader who learns to
   recognise one should recognise the other without being taught twice. */
.law-source-gap,
.nri-services-gap {
  margin: var(--space-2) 0 0;
  padding: var(--space-2) var(--space-3);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg);
  color: var(--text-muted);
  font-size: 0.85rem;
  line-height: var(--leading-normal);
  max-width: var(--measure);
}

/* ==========================================================================
   TYPOGRAPHY PASS (ibi-visual-identity-brief.md section 3, step 3)
   ==========================================================================

   Figures in Noto Sans Mono with tabular numerals. Nothing in this file used
   a monospace face before this block, which is the condition the brief
   describes: this is a numbers product whose numbers were set in proportional
   type and did not line up in a column.

   The class list below is what the renderers were observed to emit, not what
   their names suggest. Every candidate was sampled across all 98 entries
   before being included or dropped:

     .tag-card-value   dropped — prose ("Very high; multiple visits per day")
     .path-figure-sub  dropped — prose
     .intensity-value  dropped — a word, not a number
     .invest-cell      dropped — wraps <p> notes and clauses, not figures
     .path-step-num    dropped — reads "Step 1"; an ordinal label, not a figure
     .path-step-badge  dropped — mixed; sometimes a rupee total, sometimes
                       "not sourced", so mono would set prose in mono
     th.path-num       dropped — the header labels "Qty" / "Cost" / "Unit cost"
     td.path-num       kept   — the same class on the body rows carries
                       formatRupees(), so the element is selected rather than
                       the class

   .stat-value IS NOW IN THIS LIST, ruled to 24px. It was held out while it
   sat at Barlow Condensed clamp(1.45rem, 3.4vw, 2.35rem), because condensed
   to monospace is the widest face swap available and at that 37.6px ceiling
   the catalogue's widest figure, ₹10,00,000, needed 226px inside a 173px
   grid cell — and since the element is white-space: nowrap with overflow
   visible, the excess painted over the neighbouring figure instead of
   clipping. At --text-xl it needs 144px and fits. See the rule itself for the
   weight and letter-spacing consequences of the face change.

   The rest of the classes below sit at 16-20px, where mono costs a few
   pixels and nothing overflows. .stat-value is the exception at 24px, and
   the one that had to be measured rather than assumed — verified in the
   rendered page across all 98 entries, not just the card in front of me. */

.stat-value,
.stat-value--range,
.ds-rate-value,
.path-figure-val,
.predictor-score-number,
.match-score-value,
.headline-fact-value,
.calc-total-value,
.path-equip-total,
td.path-num {
  font-family: 'Noto Sans Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-variant-numeric: tabular-nums;
}

/* Body copy measure. The cap is --measure (68ch, section 3); this block was
   one of nine sites that each spelled the number out.

   These four are the prose carriers, again by measurement rather than by
   name: across 40 entries they average 124, 1044, 298 and 187 characters.
   .ds-row-pointer is the one that matters — it runs to 16,946 characters on
   its longest entry (8,575 when this line was written; the catalogue grew), and it is the provenance argument the product is built
   on, so it is the text most worth making readable. */

.text-block-body,
.ds-row-pointer,
.ds-norate,
.ds-rate-applies {
  max-width: var(--measure);
}

/* ==========================================================================
   ELEVATION — WHY THE --shadow-* SCALE IS NOT APPLIED (step 6, section 4)
   ==========================================================================

   Section 4 puts resting cards on --shadow-1, hover on --shadow-2 and
   modals/dropdowns on --shadow-3. None of that is wired up, and the blocker
   is dark mode rather than taste.

   Every shadow token this would replace has a prefers-color-scheme variant:
   --shadow flips to rgba(0,0,0,.4)/.3, and --rv-shadow / --rv-shadow-hover
   flip to rgba(0,0,0,.45) and rgba(0,0,0,.55). The brief's --shadow-1/2/3 are
   single fixed values in rgba(36,33,29,...) with no dark counterpart, because
   the brief does not mention dark mode at all. Migrating would leave every
   card in dark mode carrying a warm near-black shadow tuned for a paper
   background, which on a #171613 canvas is close to invisible. That is a
   functional regression, not a restyle, so it waits for the dark-mode ruling.

   Worth recording that the flattening IS deliberate on the brief's side, and
   the numbers say so. --shadow-1's first layer, 0 1px 2px rgba(36,33,29,0.06),
   is byte-identical to --rv-shadow's first layer, and --shadow-2's first layer
   differs from --rv-shadow-hover's only in alpha (0.07 against .09). The
   second layers are where they part: --rv-shadow lifts a card 28px of blur at
   .09, --shadow-1 sits it down at 3px and .04; --rv-shadow-hover throws 46px
   at .16, --shadow-2 offers 12px at .05. So the brief was written looking at
   these exact values and chose to flatten them hard. Applying it is a real
   decision about how much the report cards should float, not a mechanical
   swap — and it is the same decision as the dark-mode one, because the answer
   has to cover both schemes.

   Seven box-shadows remain hardcoded. Four of them are coloured glows rather
   than neutral elevation — rgba(29,78,107,.5) under the canal-blue CTA and
   rgba(227,166,57,.7)/.8 under the gold ones — and the --shadow-* scale is
   neutral, so mapping them would silently drop the colour. One belongs to the
   .future-outlook block, which is still exempt.
   ========================================================================== */

/* ==========================================================================
   REDUCED MOTION (section 5)
   ==========================================================================

   Deferred from step 1 on purpose: this is a live rule, not a token, and
   adding it then would have changed behaviour in a step whose whole claim was
   that nothing rendered differently. Step 6 is where motion lands, so it
   lands here.

   It replaces a narrower override that covered only the report-view reveal
   animation, which no longer exists. Everything is in scope now: the tab
   indicator slide, card lifts, button presses and any transition added later,
   including ones nobody remembers to opt in.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ==========================================================================
   KEYBOARD FOCUS RINGS (step 8, section 8)
   ==========================================================================

   Section 8 asks for a visible ring on every interactive element:
   --canal-blue, 2px, 2px offset.

   What was here before: `input:focus, select:focus, textarea:focus { outline:
   none }` with a 1px border tint as the only replacement. That covered the 49
   inputs, 11 selects and 3 textareas in index.html — 63 elements whose entire
   focus indicator was one pixel of border colour, on :focus rather than
   :focus-visible. The 16 buttons had no focus styling at all and fell back to
   whatever the browser drew. Two elements, .resource-link and .unlock-input,
   had real focus-visible treatments, and those two were the exception.

   :focus-visible rather than :focus, so the ring answers the keyboard and does
   not fire on every mouse click. The border tint above still answers the mouse.

   ⚠ THE DARK-SCHEME OVERRIDE BELOW PRE-EMPTS THE OPEN DARK-MODE DECISION, and
   deliberately. --canal-blue measures 8.92:1 on a white panel and 8.10:1 on
   the light page, and 1.84:1 on a dark panel and 2.03:1 on the dark page —
   under the 3:1 WCAG 2.2 requires of a focus indicator. Everywhere else a
   missing dark variant is a cosmetic debt that can wait for a ruling; here it
   is a keyboard user who cannot see where they are. The override uses #5B9BC0,
   which is not a new colour: it is this file's own dark-scheme --rv-canal, and
   it measures 5.40:1 on a dark panel. Delete the override to go back to the
   brief's literal single value.
   ========================================================================== */

:focus-visible {
  outline: 2px solid var(--canal-blue);
  outline-offset: 2px;
}

@media (prefers-color-scheme: dark) {
  :focus-visible {
    outline-color: #5B9BC0;
  }
}

/* ⚠ AND THE SAME COLLISION AS THE TAB INDICATOR, caught by tabbing rather
   than by reading. The header background --hdr-bg IS #1D4E6B, the same colour
   as --canal-blue, so the specified ring drew at 1.00:1 on the nav and a
   keyboard user tabbing through all eight primary tabs saw nothing at all.
   The ring was there and correct — 2px solid rgb(29,78,107), offset 2px — and
   completely invisible.

   Only the colour changes; the 2px and the 2px offset are section 8's. Same
   reasoning and same surface as the tab indicator's deviation, and it goes
   away with the same fix — moving the nav onto a --paper strip. --hdr-text on
   --hdr-bg is 8.13:1. */
.topbar :focus-visible {
  outline-color: var(--hdr-text);
}

/* ── Step 2 document legs: exemption, study requirement, loan documents ─────
   A new leg gets its own class names even though the visual language matches
   Step 3's institute rows. Borrowing .path-inst-* would have worked on the day
   and made the two impossible to restyle apart afterwards — the institute list
   answers "where can I train", these answer "what paper do I already need",
   and they will not stay identical.

   ⚠ THE THREE STATES ARE STYLED, NOT JUST THE TWO. A researched negative is a
   FINDING and reads at full weight, like a positive row. Only an absent leg —
   the unasked question — drops to the muted .path-doc-gap treatment. Styling a
   sourced negative as if it were a gap is the same collapse the data shape was
   built to prevent, one layer up. */
.path-doc-leg { margin: 0 0 4px; }
.path-doc-leg + .path-doc-leg { margin-top: 14px; }

.path-doc-head {
  font-weight: 700;
  font-size: 13px;
  letter-spacing: 0.01em;
  margin: 14px 0 2px;
}
.path-doc-leg:first-child > .path-doc-head { margin-top: 0; }

.path-doc-list { list-style: none; margin: 6px 0 0; padding: 0; }
.path-doc-list > li { padding: 10px 0; border-bottom: 1px solid var(--border); }
.path-doc-list > li:last-child { border-bottom: 0; }

/* A finding — positive row or researched negative — both at full weight. */
.path-doc-item { font-weight: 700; }
.path-doc-who { font-size: var(--text-xs); color: var(--text-muted); margin-top: 3px; }

/* The line that says a negative is a finding, not a gap. Same register as
   .path-muted; it introduces evidence rather than being the evidence. */
.path-doc-found-lead { font-size: 12.5px; color: var(--text-muted); margin: 4px 0 0; }

.path-doc-pointer { margin-top: 6px; }
.path-doc-pointer summary {
  cursor: pointer;
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--blue);
}
.path-doc-pointer p {
  font-size: 12.5px;
  line-height: 1.55;
  margin: 6px 0 0;
  color: var(--text-muted);
}

/* The gap message — one or more legs nobody has looked at yet. Muted, matching
   .path-gap-text, because this is the only one of the three states that is an
   absence rather than a result. */
.path-doc-gap {
  font-weight: 600;
  color: var(--text-muted);
  line-height: 1.6;
  margin: 12px 0 0;
}
.path-doc-leg + .path-doc-gap { margin-top: 16px; }

/* ==========================================================================
   THE INFORMATION BOT — the ask panel
   ==========================================================================

   ⚠ THESE RULES ARE INERT AS THEY SHIP AND THAT IS DELIBERATE, NOT DEAD CODE.
   The markup they style does not exist yet: index.html does not load bot.js or
   bot-index.js, and the panel is wired in a later commit. The guard that proves
   the bot cannot reach paid content had to land BEFORE the page could load the
   index — otherwise there is a window where 241 KB derived from the catalogue
   is reachable and nothing checks what is in it, which is exactly the window
   the LICENSE_LIBRARY leak lived in. The CSS rides along with the guard because
   it has no such hazard. DO NOT DELETE THESE AS UNUSED SELECTORS.

   ⚠ THE CITATION IS THE PRODUCT, NOT A FOOTNOTE, AND THE LAYOUT SAYS SO. The
   document name and date sit directly beneath the quote, in flow, never behind
   a disclosure and never below a fold — visible before the reader has finished
   reading the quote itself. An answer whose source you have to go looking for
   is indistinguishable at a glance from an answer with no source, and that
   glance is the entire difference this feature sells.

   ⚠ AND THE NO-SOURCE STATE IS VISIBLY A DIFFERENT THING. It reuses the Path
   gap vocabulary exactly — --text-muted on a --panel ground with a quiet left
   edge — because a reader who has seen a gap step on Path should recognise the
   shape without being taught it twice. Two vocabularies for one meaning is how
   an app stops being trusted at a glance.

   Every colour is an existing token declared in both schemes; check-contrast
   measures rather than skips them. No class name here contains status, warn,
   alert, risk, prov, tier or caution. */

.ask-panel {
  background: var(--panel);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  margin: var(--space-5) 0;
}

/* The promise, stated before the box a reader types into. Full weight and full
   contrast: it is the sentence the feature is sold on, not a disclaimer. */
.ask-trust {
  color: var(--text);
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
  max-width: var(--measure);
  margin: 0 0 var(--space-3);
}
.ask-trust p { margin: 0 0 var(--space-1); }
.ask-trust p:last-child { margin-bottom: 0; }

/* ---------- the trust statement, as a credibility badge --------------------

   ⚠ THIS REUSES .path-disclaimer-strong's VISUAL LANGUAGE RATHER THAN INVENTING
   ONE. That pattern already exists for a short block that has to carry more
   weight than the prose around it: weight 600, a subtle tint, a border-left
   accent, and radius on the right corners only so the accent edge stays square.
   Copying it means the Ask panel reads as part of this app rather than as a
   feature someone bolted on with its own idea of emphasis.

   ⚠ 600, NOT 700. Full bold across three complete sentences reads as shouting,
   and a promise about honesty that shouts is self-defeating. 600 is the same
   weight .path-disclaimer-strong and .invest-disclaimer already use.

   ⚠ THE TINT IS MIXED WITH --panel, NOT WITH transparent, AND THAT IS FOR THE
   GUARD. check-contrast derives a surface by resolving the chain to a solid
   base; a mix against `transparent` leaves an alpha the guard would have to
   composite, and mixing against --panel — the Ask panel's own background —
   produces an opaque colour it can read directly in both schemes. Same reason
   the colour is --text rather than anything softer: this is the sentence the
   feature is sold on, and it takes the highest-contrast token available.

   ⚠ 1.08em RESOLVES AGAINST THE PANEL, NOT AGAINST .ask-trust. This rule is
   declared AFTER .ask-trust and overrides its var(--text-sm), so the em base is
   .ask-panel's inherited 16px and the rendered size is ~17.3px. That is the
   "slightly larger than body" the badge wants. It is deliberately NOT past
   18.66px, which is where WCAG would start treating bold text as large and drop
   the threshold from 4.5:1 to 3:1 — this block should be held to the stricter
   number, not excused from it. */
.bot-trust-statement {
  /* ⚠ THE BODY STACK, AND THE DISPLAY FACE WAS TRIED FIRST AND REJECTED.
     This carried 'Barlow Condensed' — the .display-font stack used by .panel h2,
     .card-title and .btn-primary — on the reasoning that a credibility badge
     should not look like body text. Reverted, because of who reads it: bot.js's
     own note on these three sentences says they are written short and one-fact
     because the reader's third language is English. A condensed display face is
     drawn for headlines and costs readability across three full sentences of
     running prose. Weight, size, tint and the accent rule carry the emphasis
     instead, and they do it without narrowing the letterforms.

     `inherit`, not a restatement of the stack. body declares
     'Noto Sans', -apple-system, … and nothing between it and this block sets a
     face, so inheriting IS the body stack by construction — there is no second
     copy to drift when that declaration changes. The stylesheet already uses
     font-family: inherit in four places for the same reason. */
  font-family: inherit;
  font-weight: 600;
  font-size: 1.08em;
  line-height: 1.7;
  color: var(--text);
  background: color-mix(in srgb, var(--saffron) 8%, var(--panel));
  border-left: 3px solid var(--saffron);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  padding: var(--space-3) var(--space-4);
  margin: 0 0 var(--space-4);
  max-width: var(--measure);
}
.bot-trust-statement p { margin: 0 0 var(--space-2); }
.bot-trust-statement p:last-child { margin-bottom: 0; }

/* ⚠ THE INSTITUTION LIST IS RENDERED FROM BOT_INSTITUTIONS AND IS NOT DECORATION.
   The trust line names no institution on purpose — an earlier draft named five
   and two of them appear as a publisher nowhere in this catalogue. These ten are
   derived, so the list cannot claim a source the corpus does not have. */
.ask-sources {
  list-style: none;
  padding: 0;
  margin: 0 0 var(--space-4);
  font-size: var(--text-xs);
  line-height: var(--leading-normal);
  color: var(--text-muted);
}
.ask-sources li { margin: 0 0 2px; }

.ask-form { display: flex; gap: var(--space-2); flex-wrap: wrap; }
.ask-input {
  flex: 1 1 18rem;
  min-width: 0;
  font: inherit;
  color: var(--text);
  background: var(--bg);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-3);
}
.ask-input::placeholder { color: var(--text-muted); }

.ask-answer { margin-top: var(--space-4); }
.ask-lead {
  font-weight: 600;
  color: var(--text);
  margin: 0 0 var(--space-3);
  max-width: var(--measure);
}

/* One quoted unit. The left edge carries the kind — full-strength for a sourced
   answer, quiet for a recorded gap — the same signal .path-step-gap uses one tab
   over, so the two read as one system. */
.ask-unit {
  border-left: 5px solid var(--card-accent);
  padding: var(--space-2) 0 var(--space-2) var(--space-3);
  margin: 0 0 var(--space-4);
}
.ask-unit:last-child { margin-bottom: 0; }
.ask-unit-biz {
  font-size: var(--text-xs);
  color: var(--text-muted);
  margin-bottom: 2px;
}

/* ⚠ QUOTED WORD FOR WORD, AND IT LOOKS QUOTED. Full contrast, generous
   leading, held to the measure — this is the government's sentence, not ours,
   and the one thing a reader is being asked to trust. */
.ask-quote {
  margin: 0 0 var(--space-2);
  padding: 0;
  border: 0;
  color: var(--text);
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
  max-width: var(--measure);
}

.ask-cite {
  font-size: var(--text-xs);
  line-height: var(--leading-normal);
  color: var(--text-muted);
}
.ask-cite-doc { font-weight: 600; }
.ask-cite-pub::before,
.ask-cite-date::before { content: " \00b7 "; }

/* The catalogue's own doubt, carried through unchanged. 594 of 723 units are
   marked not independently verified, and Path already tells a reader "We have
   not checked it with the scheme office." Quoting them without this line would
   make the bot more confident than the catalogue it is quoting. */
.ask-unchecked {
  margin: var(--space-2) 0 0;
  font-size: var(--text-xs);
  line-height: var(--leading-normal);
  color: var(--text-muted);
  max-width: var(--measure);
}

/* The recorded gap. Same shape as the Path gap steps — quiet edge, panel
   ground — because it is the same statement: we looked, and here is what came
   back. It is a headline answer, so the quote inside keeps full contrast. */
.ask-gap { border-left-color: var(--text-muted); }
.ask-gap-answer .ask-lead { color: var(--text-muted); }

/* The boundary. No price, no button, nothing to click — a deliberate edge
   rather than a failed search, and it should not look like an error. */
.ask-boundary p { max-width: var(--measure); line-height: var(--leading-relaxed); }
.ask-boundary .ask-lead { color: var(--text); }
.ask-foot {
  margin: var(--space-3) 0 0;
  font-size: var(--text-xs);
  line-height: var(--leading-normal);
  color: var(--text-muted);
}
.ask-dic { color: var(--text); }

.ask-counts { color: var(--text); margin: 0 0 var(--space-3); }
.ask-locked { border-left: 5px solid var(--card-accent); padding-left: var(--space-3); }

.ask-options { list-style: none; padding: 0; margin: 0 0 var(--space-3); }
.ask-options li { margin: 0 0 var(--space-1); }
.ask-option {
  font: inherit;
  color: var(--text);
  background: var(--bg);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--space-1) var(--space-2);
  cursor: pointer;
  transition: border-color var(--dur-base) var(--ease);
}
.ask-option:hover { border-color: var(--card-accent); }

.ask-more { margin-top: var(--space-2); font-size: var(--text-xs); }
.ask-more summary { color: var(--text-muted); cursor: pointer; }
.ask-more p {
  margin: var(--space-2) 0 0;
  color: var(--text-muted);
  line-height: var(--leading-normal);
  max-width: var(--measure);
}
