@font-face {
  font-family: "Newsreader";
  src: url("/lcoh/price/fonts/newsreader-opsz-vf.woff2") format("woff2"),
       url("/lcoh/price/fonts/newsreader-vf.woff2") format("woff2");
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Satoshi";
  src: url("/lcoh/price/fonts/satoshi-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Satoshi";
  src: url("/lcoh/price/fonts/satoshi-500.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Satoshi";
  src: url("/lcoh/price/fonts/satoshi-700.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
/* 3-2 step 1 (harness/tasks/H2PRISM-LCOH-PRODUCT-20260922/stage6/3-2/TOKENS.md) -- single :root
   token block. Every visual value the calculator uses lives here; app.css and advanced.css read
   these variables instead of repeating literals. */
:root {
  --font-display: "Newsreader", Georgia, serif;
  --font-ui: "Satoshi", system-ui, -apple-system, "Segoe UI", sans-serif;

  
  
  
  
  
  
  
  
  

  
  
  
  
  
  
  
  

  
  
  
  /* 3-2 step2 (COMPONENTS.md 1): slider rail colour, one step darker than --surface-2 so a bare
     rail reads as a track rather than disappearing into a card surface. */
  
  
  
  /* S-06: darkened from TOKENS.md's original #857c70 (3.46-4.04:1 on --paper/--surface/--surface-2
     at the 11-12px sizes this token is used at, all below the 4.5:1 small-text floor -- V-1-4/I-1-7).
     #6b6358 clears 4.5:1 on all three surfaces. TOKENS.md v1.1. */
  
  
  

  
  

  /* cost category colours, TOKENS.md OKLCH P2 set */
  --c-capex: oklch(0.36 0.06 250);
  --c-opex: oklch(0.54 0.06 175); /* 3-2 step1: darkened from TOKENS.md 0.58 -- at 0.58, both white and --ink text on this fill measured below the 4.5:1 requirement (4.15/4.24); 0.54 clears it (4.90) while keeping hue/chroma and category-order intact. Flagged for parent review. */
  --c-power: oklch(0.74 0.15 70);
  --c-water: oklch(0.84 0.045 230);
  --c-stack: oklch(0.52 0.13 25);
  --c-feed: oklch(0.46 0.02 60);
  --c-aux: oklch(0.80 0.10 85);
  --c-carbon: oklch(0.30 0.03 40);
  /* L-17 (3-2 batch 2, TOKENS.md v1.2): SOEC heat gets its own token, distinct from --c-power by
     hue and lightness (deuteranopia-checked, see TOKENS.md v1.2 for the CIEDE2000/deltaL margins). */
  --c-heat: oklch(0.62 0.13 50);

  /* Direction colours (3-2 batch 2, TOKENS.md v1.2): sensitivity lower/higher, B-A difference,
     above/below -- never a cost-category colour and never --positive/--negative (those stay
     reserved for cash-flow sign). Indigo/rose pair, checked against all 8 --c-* tokens and each
     other for CIEDE2000 >= 15 or deltaL* >= 8 in both normal and simulated-deuteranopia vision. */
  --dir-down: oklch(0.50 0.10 265); /* C3-10 v1.4 quieter indigo */
  --dir-up: oklch(0.62 0.10 12); /* C3-10 v1.4 quieter rose */
  /* R3-01 (TOKENS v1.5): cash-flow sign. Not --dir-* (price direction) and not a --c-* category.
     Muted slate / warm grey. CVD-checked against the category set, the direction pair, and each other. */
--cash-neg: oklch(0.38 0.09 60);
--cash-pos: oklch(0.70 0.09 158);

  
  
  

  
  
  
  
  /* 3-2 batch 3 (TOKENS.md v1.3): in-page boxes use a line, not a shadow. Overlay only. */
  --shadow-1: none;
  
  

  --ease-panel: cubic-bezier(0.23, 1, 0.32, 1);
  --dur-panel: 200ms;
  --dur-press: 120ms;

  /* R6 Move 1 direction colours (LCOH better/worse, "matches"/"copied" confirmations). TOKENS.md
     explicitly defers this pair to bundle ③ ("방향색은 범주색과 섮지 않는다. 묶음 ③에서 정한다."); kept at
     their original R6 hex values here so behaviour stays identical this step. */
  --positive: var(--s-op);
  --positive-ink: var(--s-op);
  --negative: var(--s-cancel);
  --trace: var(--faint);

  color: var(--ink);
  background: var(--paper);
  font-family: var(--font-ui);
  font-size: var(--fs-input);
  line-height: 1.45;
}
@media (max-width: 900px) {
  :root {  }
}
* { box-sizing: border-box; }
/* R7: "html, body { overflow-x: hidden }" (R6 and earlier) paired one non-visible axis with an
   unset y-axis on BOTH elements. Per the CSS Overflow spec that silently promotes the other axis
   to overflow-y:auto on each, so body became a second, inert scroll container nested inside html's
   real one; every position:sticky descendant then computed its stuck offset against body (which
   never actually receives scroll deltas) instead of the real viewport scroller, so the sticky input
   rail silently never stuck at all (confirmed by scripted scroll+measure: top tracked plain -scrollY
   at any scroll offset). Horizontal clipping now lives on html only, which is the real scroller in
   standards mode and was already becoming the scroll container either way. */
html { max-width: 100%; overflow-x: hidden; }
body { max-width: 100%; margin: 0; }
button, input, select { font: inherit; color: inherit; }
sub, .unit-sub { font-size: var(--fs-meta); line-height: 0; }
button { cursor: pointer; }
/* 3-2 step2 (COMPONENTS.md 8): every pressable control gets the same 120ms press feedback. Excluded:
   .hero-bar-seg (a stacked-bar segment clipped by its track's own overflow:hidden -- scaling it
   would visibly clip against its neighbours) and disabled buttons (nothing to confirm pressing). */
button:not(.hero-bar-seg) { transition: transform var(--dur-press) ease-out; }
button:not(.hero-bar-seg):not(:disabled):active { transform: scale(0.97); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.skip {
  position: absolute; left: 12px; top: -40px; background: var(--ink); color: var(--on-accent); padding: var(--sp-2) var(--sp-3);
}
.skip:focus { top: 8px; min-height: 44px; box-sizing: border-box; }
.top { padding: var(--sp-4) var(--sp-6) 0; max-width: 1280px; margin: 0 auto; }
.headline-tools, article > header, .sell-row {
  display: flex; justify-content: space-between; gap: var(--sp-4); align-items: baseline;
}
.crumb { margin: 0; color: var(--ink-2); font-size: var(--fs-meta); letter-spacing: 0.01em; white-space: nowrap; }
.crumb span + span { margin-left: 8px; }
.status-line { display: flex; gap: var(--sp-3); color: var(--ink-2); font-size: var(--fs-meta); font-variant-numeric: tabular-nums; white-space: nowrap; }
.masthead-row { display: flex; align-items: center; gap: var(--sp-4); min-height: 56px; flex-wrap: nowrap; border-bottom: 1px solid var(--line); }
.masthead-left { display: flex; flex-direction: column; align-items: flex-start; justify-content: center; gap: var(--sp-1); flex: 1 1 auto; min-width: 0; padding: var(--sp-2) 0; }
.masthead-title { font-family: var(--font-display); font-weight: 500; font-size: var(--fs-title); line-height: 1; margin: 0; letter-spacing: -0.01em; white-space: nowrap; }
.masthead-actions { display: flex; gap: var(--sp-2); align-items: center; flex: none; }
.masthead-row .status-line { margin-left: auto; align-items: center; gap: var(--sp-2); color: var(--ink-3); }
.meta-chip { display: inline-flex; align-items: center; min-height: 24px; padding: 0 var(--sp-2); border: 1px solid var(--line); border-radius: var(--r-pill); background: transparent; color: var(--ink-3); font-size: var(--fs-meta); line-height: 1; }
.text-link {
  border: 0; background: transparent; padding: 0 var(--sp-2); min-height: 44px; color: var(--ink-2);
  text-decoration: underline; text-underline-offset: 3px; font-size: var(--fs-sm);
  display: inline-flex; align-items: center;
}
.text-link:hover { color: var(--ink); }
#share[data-state="copied"] { color: var(--positive-ink); text-decoration-color: var(--positive); }
.masthead-sub { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
.masthead-sub.dismissed { display: none; }
.stages button, .tech button, .stmt-tabs button, .ladder-toggle, article header button, .tech-prompt button, .keep-note button {
  min-height: 44px; border: 1px solid var(--line-2); background: var(--surface); border-radius: var(--r-sm);
}
.stages { display: flex; gap: 0; }
.stages button {
  border: 0; border-bottom: 2px solid transparent; background: transparent; border-radius: 0;
  padding: 0 var(--sp-4); color: var(--ink-2); margin-bottom: 0; min-height: 44px;
}
.stages button[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--ink); }
.hidden-note { margin: 0; color: var(--warn); font-size: var(--fs-body); }
main {
  max-width: 1280px; margin: 0 auto; padding: var(--sp-2) var(--sp-6) var(--sp-8);
  display: grid; grid-template-columns: 340px minmax(0, 1fr); gap: var(--sp-7); align-items: start;
}
/* R6 Move 2: the rail is sticky within the page and no longer clips its own content in an inner
   scroll box. A tall rail on Standard/Pro simply scrolls with the page once its height exceeds the viewport. */
/* R7: this sticky declaration was already correct; the real bug was the html/body overflow-x
   pairing above silently making body an inert scroll container that swallowed every sticky
   descendant's stuck offset (see that rule's comment). With the fix above, the grid area's own
   height (set by the tall .result column, since main's default row sizing uses max-content across
   the row regardless of align-self) already gives this short rail plenty of room to travel, so it
   now stays visible for the whole page instead of scrolling away and leaving an empty column. */
.inputs { position: sticky; top: 16px; align-self: start; padding-bottom: var(--sp-2); }
/* H-09 (3-3, Hayeon: "I have to scroll all the way down; there should be separate scrolling and
   maybe toggles"): at >=1024px the rail becomes its own scroll region (height fixed to the
   viewport minus its own sticky top offset and a matching bottom gutter) instead of growing with
   the page. overscroll-behavior: contain keeps a drag that reaches the rail's own end from also
   scrolling the page underneath it. The two fade bars are sticky *children* of this same scroll
   container (not the page) -- the standard CSS trick for a cue that tracks the scrollport, not
   the viewport -- and only turn visible (has-scroll-top/-bottom, set in app.js) once there is
   actually more content past that edge. */
@media (min-width: 1024px) {
  /* F8 (3-3 final): a fixed "height: calc(100vh - 32px)" assumed the rail was always exactly
     16px from the viewport top -- true once position:sticky has actually caught, but not at
     initial load (the rail's un-stuck flow position sits below the masthead, well past 16px,
     so this fixed height ran the box 40-50px past the bottom of the viewport) and not in the
     rail's own last ~30-50px of scroll room (the sticky containing block -- main's own grid
     row -- ends at almost exactly the rail's own height, so native position:sticky releases the
     rail early and its top header scrolls a few dozen px above the viewport). max-height (not
     height) here is only ever the CEILING JS's updateInputsRailHeight computes from the rail's
     own *live* position every scroll/resize tick -- see that function's comment in app.js -- so
     the box is always exactly as tall as the room actually available, at every scroll position,
     and the group header is never cut off.
  */
  .inputs { max-height: calc(100vh - 32px); overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }
  /* H-09 fade cue, take 2: a separate sticky overlay div (the standard "scroll shadow" markup)
     visually sits on top of the rail's own field text at the exact edge it needs to fade near --
     app.css's own G5 gate (run_gates.py) correctly flags any non-opaque sticky element overlapping
     real text as a collision, and an opaque bar would hide the fade effect entirely, so there is
     no version of that approach that is both a real gradient and gate-clean. A mask-image on the
     scroll container itself achieves the identical visual result (the container's own rendered
     edge -- content, not a separate overlay -- fades to transparent) without adding any new
     position:sticky/fixed element for that check to ever consider; has-scroll-top/-bottom
     (app.js updateInputsRailFade) switch which edge(s) actually fade, so a short, non-scrollable
     rail renders with no mask at all. */
  .inputs { -webkit-mask-image: none; mask-image: none; }
  .inputs.has-scroll-top { -webkit-mask-image: linear-gradient(to bottom, transparent, black 20px); mask-image: linear-gradient(to bottom, transparent, black 20px); }
  .inputs.has-scroll-bottom { -webkit-mask-image: linear-gradient(to top, transparent, black 20px); mask-image: linear-gradient(to top, transparent, black 20px); }
  .inputs.has-scroll-top.has-scroll-bottom {
    -webkit-mask-image: linear-gradient(to bottom, transparent, black 20px, black calc(100% - 20px), transparent);
    mask-image: linear-gradient(to bottom, transparent, black 20px, black calc(100% - 20px), transparent);
  }
}
/* H-09: the rail's four collapsible sections (Technology & plant / Costs / Finance / More
   assumptions). Headers are static markup (index.html) with a 44px target; bodies hold whichever
   of renderForm's per-group containers actually has fields for the current stage/pathway -- an
   empty group hides its whole section (app.js syncInputSectionVisibility). */
/* Q1-1 (3-3 r1c): .input-sections is a grid item of form's single implicit column. Grid items
   default to min-width: auto (= max-content of their content), so the tech grid's own intrinsic
   sizing could push this box past the rail's fixed 340px track and visibly overrun the rail edge.
   min-width: 0 lets the implicit column -- and everything stacked inside it -- shrink to the
   340px the rail actually has. */
.input-sections { display: flex; flex-direction: column; min-width: 0; }
.input-section { border-bottom: 1px solid var(--line); }
.input-section:last-child { border-bottom: 0; }
.input-section[hidden] { display: none; }
.input-section-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); width: 100%;
  min-height: 44px; padding: var(--sp-2) 0; border: 0; background: transparent; color: var(--ink-3);
  text-align: left; font: inherit;
}
.input-section-head[aria-expanded="true"] { color: var(--ink); }
.input-section-title { font-size: var(--fs-meta); letter-spacing: 0.08em; text-transform: uppercase; font-weight: 600; }
.input-section-chev { width: 16px; height: 16px; flex: none; position: relative; }
.input-section-chev::before {
  content: ""; position: absolute; inset: 0; margin: auto; width: 7px; height: 7px;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg); transition: transform var(--dur-panel) var(--ease-panel);
}
.input-section-head[aria-expanded="true"] .input-section-chev::before { transform: rotate(-135deg); margin-top: 3px; }
/* Collapse is a max-height clip, not [hidden]/display:none: a fully hidden ancestor would also
   remove Standard/Pro's Finance controls (discount rate, debt share, ...) from the accessibility
   tree, which several machine gates (run_gates.py's visible_role, matching real assistive-tech
   behaviour for role/label queries) rely on reaching directly without first expanding the UI --
   see app.js's setSectionFocusability for the matching keyboard-tab-order fix (a collapsed
   section's own controls are pulled out of Tab order, even though this max-height technique
   otherwise leaves them technically "visible"). */
.input-section-body { padding: 0 0 var(--sp-4); display: flex; flex-direction: column; gap: var(--sp-2); overflow: hidden; max-height: 4000px; transition: max-height var(--dur-panel) var(--ease-panel); }
.input-section-body.is-collapsed { max-height: 0; padding-top: 0; padding-bottom: 0; }
.input-section-fields { display: grid; gap: var(--sp-2); }
.input-section-fields:empty { display: none; }
/* S-08: the rail's control shell is now a fixed 40px box everywhere (TOKENS.md v1.1), so this no
   longer needs its own compact override -- the R6 Move 2 note above described a 38px/20px-vs-16px
   difference from the (now gone) generic .control that neither value exists as anymore. */
.kicker, .metric-name, figcaption, .lede, article h2 { margin: 0; }
.kicker { font-size: var(--fs-meta); letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-3); }
/* R6 Move 2: technology picker compacted into two segmented rows, family label to the left. */
.tech { display: grid; gap: var(--sp-1); margin-top: 6px; }
/* Q1-1: .tech-family is a grid item of .tech (a grid container); grid items default to
   min-width: auto, which let the family-row's own intrinsic button sizing push this row (and the
   buttons in it) past .tech's now-fixed 340px box instead of sharing it. min-width: 0 lets it
   shrink to the space .tech actually has. */
.tech-family { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
/* R7: "Electrolysis" uppercased at 10.5px overran the old fixed 62px column and visibly collided
   with the adjacent PEM button; widen the column to fit the longer family name. */
.tech-family-label { flex: none; width: 84px; font-size: var(--fs-cap); color: var(--ink-3); letter-spacing: 0.03em; text-transform: uppercase; line-height: 1.2; white-space: nowrap; }
/* L-14: a fixed 2x2 grid, not flex-wrap -- with exactly 4 pathways per family (electrolysis:
   PEM/Alkaline/AEM/SOEC, thermal: SMR/SMR+CCS/ATR+CCS/Coal) flex-wrap let 3 short labels fit one
   row and wrapped the 4th alone, where flex: 1 1 0 then stretched it to the full row width (SOEC
   248px beside three 80px siblings, V-1-9). Two equal columns give every chip the same width at
   any viewport and a repeating 2-row rhythm across both families, and comfortably fits the longest
   label ("SMR+CCS (blue)") at the rail's own ~250px content width, where 4 equal columns would not. */
.tech-family-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); flex: 1; gap: var(--sp-1) var(--sp-2); min-width: 0; }
/* 3-2 step2 (COMPONENTS.md 3): technology chips. Selection reads as a border weight + fill change
   (border 1px to 1.5px --ink, transparent to --surface), never a solid black fill, so a chip and a
   segmented control never compete as two different "selected" languages on the same rail. */
.tech button {
  padding: 0 var(--sp-3); font-size: var(--fs-sm); font-weight: 500; min-height: 30px; width: 100%; white-space: nowrap;
  border: 1px solid var(--line); background: transparent; color: var(--ink-2); border-radius: var(--r-ctrl);
  transition: background var(--dur-panel) ease-out, border-color var(--dur-panel) ease-out, transform var(--dur-press) ease-out;
}
.tech button[aria-pressed="true"] { border: 1.5px solid var(--ink); background: var(--surface); color: var(--ink); font-weight: 600; }
@media (hover: hover) and (pointer: fine) {
  .tech button:hover { background: var(--surface-2); }
}
.tech button:active { transform: scale(0.97); }
.tech-prompt { margin-top: 10px; padding: var(--sp-3); border: 1px solid var(--line-2); background: var(--surface); font-size: var(--fs-body); }
.tech-prompt p { margin: 0 0 10px; }
.tech-prompt-row { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
.tech-prompt-row button { padding: 0 var(--sp-3); font-size: var(--fs-sm); }
.tech-prompt-row button:first-child { background: var(--ink); color: var(--surface); border-color: var(--ink); }
form { margin-top: 10px; display: grid; gap: var(--sp-2); }
.field { display: grid; gap: var(--sp-1); }
.field-label-row { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-2); }
.field label { font-size: var(--fs-body); }
.field .meta { color: var(--ink-3); font-size: var(--fs-meta); }
/* S-03/L-13: the Reset button and the origin chip share the label row's right-hand cluster. */
.field-label-actions { display: flex; align-items: center; gap: var(--sp-2); }
.field-reset {
  border: 0; background: none; padding: 2px 0; font-size: var(--fs-meta); color: var(--ink-2);
  text-decoration: none; line-height: 1.3;
}
@media (hover: hover) and (pointer: fine) {
  .field-reset:hover { text-decoration: underline; }
}
.field-reset:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* S-02: inline validation message, 12px --error, sits under the control it describes. */
.field-error { margin: 2px 0 0; font-size: var(--fs-meta); color: var(--error); }
.field-error[hidden] { display: none; }
/* R6 Move 2: provenance moves onto the label line as a small-caps chip instead of a line below the field. */
/* Kept as the ".origin" class (not renamed) so it stays a stable hook for provenance state everywhere
   it appears (field label rows, the sell-row sales origin, etc). */
/* S-08 (TOKENS.md v1.1): the underline shell is gone -- a quiet full box instead (height 40,
   --surface, --r-md, an inset 1px low-contrast ring per the reference input measured in TOKENS.md
   v1.1's own note). V-1-6/I-1-13 both read the old bottom-line-only treatment as "the number looks
   like output text, not an editable field"; a real box on every side reads as editable at rest,
   not just once focused. */
.control {
  display: flex; align-items: center; gap: var(--sp-2); height: 40px; padding: 0 var(--sp-3);
  background: var(--surface); border-radius: var(--r-ctrl); border: 0;
  box-shadow: inset 0 0 0 1px var(--shadow-ink);
  transition: box-shadow var(--dur-panel) ease-out;
}
.control input, .control select, .sell-row input, .selector select {
  border: 0; background: transparent; width: 100%; min-width: 0; padding: 0;
  font-variant-numeric: tabular-nums;
}
.sell-row input, .selector select { min-height: 44px; }
.control input, .control select { height: 100%; font-size: var(--fs-input); }
/* S-08: number fields are type=text/inputmode=decimal now (S-15's thousands-separator formatting
   needs that), so the spin-button rules only still matter for the few plain type=number inputs
   left outside .control (sales price, target price). */
input[type="number"]::-webkit-outer-spin-button, input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
input[type="number"] { -moz-appearance: textfield; appearance: textfield; }
@media (hover: hover) and (pointer: fine) {
  .control:hover { box-shadow: inset 0 0 0 1px var(--shadow-ink); }
}
/* S-08: focus ring reads as belonging to the input itself (2px --accent, drawn on the shell so it
   never nudges layout), on focus-visible *and* plain focus -- a number field is the primary way to
   enter an exact price, so a mouse-click focus should look just as editable as a keyboard one. */
.control:has(input:focus), .control:has(select:focus) { box-shadow: inset 0 0 0 2px var(--accent); }
.control input:focus, .control select:focus, .control input:focus-visible, .control select:focus-visible { outline: none; }
/* S-02: an invalid entry keeps its own colour on the shell (even while focused, so it never looks
   momentarily "fixed" just by staying in the field), independent of the accent focus ring above. */
.control:has(input[aria-invalid="true"]) { box-shadow: inset 0 0 0 1.5px var(--error); }
.unit-tag { color: var(--ink-3); font-size: var(--fs-meta); white-space: nowrap; }
/* R6 Move 2: the converted-unit mirror sits as a grey suffix on the unit-tag, not a separate line. */
.unit-tag .mirror { margin-left: 4px; }
/* 3-2 step2 (COMPONENTS.md 5): status tags. Default carries no fill or border at all (still the
   overwhelmingly common case, R8 5); "your number" and "unsourced default" both read as a small
   filled chip in their own colour instead of an outlined pill, so a glance across the rail tells
   "plain text = default" from "tinted chip = worth a second look" without reading every label. */
.origin {
  font-size: var(--fs-cap); text-transform: uppercase; letter-spacing: 0.05em; color: var(--ink-3);
  border: 0; border-radius: var(--r-sm); padding: 0; white-space: nowrap;
}
.origin.user { color: var(--accent); background: var(--accent-soft); border-radius: 0; padding: 2px 6px; }
.origin.illustrative { color: var(--ink-3); }
.origin.unsourced { color: var(--warn); background: var(--warn-soft); border-radius: 0; padding: 2px 6px; }
.keep-note { color: var(--ink-2); font-size: var(--fs-body); }
.keep-note button { margin-right: 8px; padding: 0 var(--sp-3); }
.metric-name { color: var(--ink-2); font-size: var(--fs-body); }
.number-row { display: flex; align-items: baseline; gap: var(--sp-3); margin: 4px 0 0; min-height: 92px; }
.number { font-family: var(--font-display); font-size: var(--fs-display); line-height: 0.9; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; }
.number.pending { font-family: var(--font-ui); font-size: var(--fs-h2); letter-spacing: 0; line-height: 1.2; max-width: 16ch; }
.trace { font-family: var(--font-display); font-size: var(--fs-h2); color: var(--trace); font-variant-numeric: tabular-nums; text-decoration: line-through; text-decoration-thickness: 1px; }
.unit { color: var(--ink-2); font-size: var(--fs-input); }
.condition, .reason, .sell-note { margin: 8px 0 0; color: var(--ink-2); font-size: var(--fs-body); max-width: 62ch; }
.condition-unsourced { border: 0; padding: 0; margin: 0; background: transparent; color: var(--ink-2); font: inherit; text-decoration: underline; text-underline-offset: 2px; cursor: pointer; }
/* R8 4: keep the headline condition line from leaving a single orphan word on its own line when it
   wraps (seen at 1440px on Quick with certain condition-text lengths). */
.condition { text-wrap: balance; }
.reason { color: var(--warn); }
.empty-note { margin: 0; color: var(--ink-2); font-size: var(--fs-body); max-width: 52ch; }
.selector { font-size: var(--fs-sm); color: var(--ink-2); display: flex; align-items: center; gap: var(--sp-2); }
.composition { margin: 28px 0 0; }
/* R10 Move 5: hidden by default (desktop and Standard/Pro); the mobile media query below shows it
   again when not [hidden] and JS has populated it (Quick stage only, see renderMobileQuickSliders). */
.mobile-quick-sliders { display: none; }
/* R10 Move 2: the price as the hero figure -- one full-width stacked bar in the five component
   colours, on the same axis domain as the per-component legend rows below it, with the headline
   value at its right end. The previous per-row rows become a quieter legend underneath. */
.hero-bar-wrap { margin: 10px 0 4px; }
.hero-bar-row { display: flex; align-items: center; gap: var(--sp-4); }
/* 3-2 step2 (COMPONENTS.md 7): the hero stacked bar. 44px height (documented literal, not on the
   --r-* scale: TOKENS.md exception rule for figure geometry) with a 6px outer radius -- one step
   softer than --r-sm(4) since this is the page's single largest filled shape, not a form control.
   A 2px flex gap between segments (and --surface-2 for any unfilled remainder past the last
   segment, when the row total sits below the shared axis max) replaces the old edge-to-edge fill. */
.hero-bar-track {
  position: relative; flex: 1 1 auto; height: 44px; border-radius: 0; background: var(--surface-2);
  display: flex; gap: 2px; overflow: hidden;
}
  .hero-bar-seg {
  position: relative; height: 100%; display: flex; align-items: center; justify-content: center;
  padding: 0; margin: 0; border: 0; font: inherit; cursor: pointer; min-width: 4px; overflow: hidden;
  transition: opacity var(--dur-press) ease-out;
}
/* C3-03: 2 px --accent ring, offset 2 px, on the segment itself. The old inset white ring sat
   inside a 2 px water slice and never showed. */
.hero-bar-seg:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; z-index: 4; }
/* S-14 (3-2 batch 2): hovering one segment used to dim every other segment to 0.55 opacity, which
   fights the exact side-by-side comparison a stacked cost bar exists for. The hovered segment now
   gets a 2px --ink top notch (matching the .selected bottom underline's own weight/colour) and
   every other segment stays at full opacity. */
@media (hover: hover) and (pointer: fine) {
  .hero-bar-seg:hover::before {
    content: ""; position: absolute; left: 0; right: 0; top: 0; height: 2px; background: var(--ink); z-index: 1;
  }
}
/* Selected = the segment whose detail is currently open; an underline (not a border/outline, which
   would fight the segment's own edges) keeps the identifying mark clear of the value label above it. */
.hero-bar-seg.selected::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2px; background: var(--ink);
}
.hero-bar-seg-value {
  color: var(--on-accent); font-size: var(--fs-sm); font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap;
  text-shadow: 0 1px 1px var(--shadow-ink);
}
/* R8 1b pattern, reused at the whole-bar scale: a falling total keeps a grey ghost block wider than
   (and behind) the new combined fill; a rising total draws the previous total as a thin outline
   tick on top instead (the same-styled ghost would otherwise sit entirely under the wider fill). */
.hero-bar-ghost-block { position: absolute; left: 0; top: 0; bottom: 0; background: var(--trace); opacity: 0.4; z-index: 0; }
/* R10c: previously a full track-height line (top:-4px to bottom:-4px), which could cross straight
   through a segment's own centered value label (the marker's x position follows the previous
   *total*, not this segment's box, so it can land mid-label in whichever segment that total falls
   inside). A 6px notch on the top edge keeps the marker visible without ever entering the vertical
   band the centered label text occupies. */
.hero-bar-ghost-tick { position: absolute; top: 0; height: 6px; width: 2px; border-left: 2px solid var(--ink); opacity: 0.85; z-index: 3; transform: translateX(-1px); }
.hero-bar-headline { flex: none; display: flex; align-items: baseline; gap: var(--sp-1); }
.hero-bar-headline .hbh-value { font-family: var(--font-display); font-size: var(--fs-h2); line-height: 1; font-variant-numeric: tabular-nums; }
.hero-bar-headline .hbh-pending { font-family: var(--font-ui); font-size: var(--fs-body); }
.hero-bar-headline .hbh-unit { font-size: var(--fs-sm); color: var(--ink-2); }
/* R10b: hero ticks share the stacked-bar track width, not the legend label gutter. */
.hero-bar-axis {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: var(--sp-4); align-items: end; margin: 2px 0 0;
}
.hero-bar-axis .bar-axis-track { height: 18px; }
.hero-bar-axis-slot { width: var(--hero-axis-slot, 0px); height: 1px; }
/* COMPONENTS.md 7: hero axis ticks read one step smaller/quieter (--fs-cap, 11px) than the shared
   .bar-axis-tick default (--fs-meta, 12px) used by the smaller legend/tornado/ladder axes. */
.hero-bar-axis .bar-axis-tick { font-size: var(--fs-cap); }
/* H-08 (3-3, Hayeon: "a waterfall's point is that the bars connect; that is not visible"):
   position:relative so the JS-measured .waterfall-connector spans (app.js attachWaterfallConnectors)
   can be plain absolutely-positioned children of this same element. */
.legend-rows { display: grid; gap: var(--sp-1); margin-top: 10px; position: relative; }
.waterfall-connector { position: absolute; width: 0; border-left: 1px dashed var(--line-2); pointer-events: none; z-index: 1; }
/* H-07 (3-3, Hayeon: "the list under the cost composition should be separated ... in English"):
   the waterfall list gets its own titled, lightly bordered box, distinct from the hero bar
   summary above it (which stays outside .legend-panel, unchanged). */
.legend-panel { margin-top: 16px; padding: var(--sp-3); border: 1px solid var(--line); border-radius: 0; background: var(--surface); }
.legend-panel-title { margin: 0 0 var(--sp-2); font-size: var(--fs-meta); text-transform: uppercase; letter-spacing: 0.07em; color: var(--ink-3); }
.legend-panel .legend-rows { margin-top: 0; }
.legend-rows > .bar-axis-row { margin-bottom: 0; }
/* R6 Move 1: shared "figure" frame -- small-caps title, unit at the right end of the same line. */
figcaption { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 6px; }
figcaption span:first-child { font-size: var(--fs-meta); text-transform: uppercase; letter-spacing: 0.07em; color: var(--ink-2); }
figcaption .fig-unit { font-size: var(--fs-meta); color: var(--ink-3); font-variant-numeric: tabular-nums; }
.bar-axis-row { display: grid; grid-template-columns: 148px minmax(0, 1fr) 64px; gap: var(--sp-2); margin-bottom: 4px; }
.bar-axis-track { position: relative; height: 16px; border-bottom: 1px solid var(--line); }
/* Sourcing/advanced grammar (3-2 batch 2): quiet axis text is 11px --ink-3 everywhere, not just
   the hero bar's own override -- so the power-sourcing figure (the only other buildAxisRow caller)
   reads the same weight as every other axis on the page. */
.bar-axis-tick { position: absolute; bottom: 3px; transform: translateX(-50%); font-size: var(--fs-cap); color: var(--ink-3); font-variant-numeric: tabular-nums; white-space: nowrap; }
.bar-axis-tick::before { content: ""; position: absolute; left: 50%; bottom: -4px; width: 1px; height: 4px; background: var(--line-2); }
/* R10: same edge-clipping fix as the ladder axis (B(e)) -- a centered 0 tick shifts half its own
   width past the track's left edge, where it can be clipped by the page's overflow-x: hidden. */
.bar-axis-tick.axis-start { transform: translateX(0); }
.bar-axis-tick.axis-start::before { left: 0; }
.bar-axis-tick.axis-end { transform: translateX(-100%); }
.bar-axis-tick.axis-end::before { left: 100%; }
/* 3-2 step2 (COMPONENTS.md 7): the composition legend rows under the hero bar. The old fixed
   64px value gutter kept every value flush against the row's right edge regardless of how short
   its own bar was -- the exact "value detached from its bar" defect COMPONENTS.md 7 calls out.
   The value column is dropped; .bar-value is instead an absolutely positioned child of .bar-track,
   placed 8px past the fill's own rendered end (renderBar clamps it back on-track for long bars). */
.bar-row { display: grid; grid-template-columns: 148px minmax(0, 1fr); gap: var(--sp-2); align-items: center; min-height: 44px; font-size: var(--fs-sm); }
.bar-label { font-size: var(--fs-body); color: var(--ink-2); }
.bar-track { position: relative; height: 10px; background: transparent; overflow: visible; }
.bar-fill { position: absolute; left: 0; top: 0; height: 10px; border-radius: 0; background: var(--ink); }
.bar-fill { min-width: 4px; }
@media (pointer: coarse) {
  .legend-rows .bar-row.bar-row-clickable { min-height: 44px; }
  .caseb-field .control,
  .caseb-field .control input { min-height: 44px; height: 44px; }
}
.bar-fill { min-width: 4px; min-height: 10px; }
.legend-swatch, .swatch { min-width: 10px; min-height: 10px; }
/* 3-3 (Hayeon, waterfall): closing total row -- a quiet rule above it separates it from the
   stacked pieces, and the bar itself uses --ink (not a category colour) since it represents the
   whole discounted price, not one component. */
/* Q1-2 (3-3 r1c): the Total row's fill spans (near) the full track, so the cost rows' shared
   "float 8px past the fill's own end" placement (and its off-track fallback) has nowhere safe to
   land and instead pushed the value left, on top of the label. Keep this row on the exact same
   148/176/108px label + track grid as every cost row above it (so the waterfall's own axis/track
   width -- and therefore where every bar's right edge lands -- stays identical between the Total
   row and the cost rows; giving Total its own extra grid column narrowed the track and threw the
   whole chart's axis scale off, check_waterfall.py's own regression). Instead pin the value to
   the track's own right edge with plain CSS, independent of the fill's width. The label may wrap
   here (unlike the cost rows' forced nowrap) since this column also carries the longest label on
   the page ("Total (discounted LCOH)"). */
.bar-row-total { margin-top: 6px; padding-top: 10px; border-top: 1px solid var(--line-2); }
.bar-row-total .bar-label { font-weight: 600; color: var(--ink); white-space: normal; }
.bar-row-total .bar-value-static {
  position: absolute; top: 50%; left: auto; right: 0; transform: translateY(-50%);
  text-align: right; white-space: nowrap;
}
.bar-fill-total { background: var(--ink); }
@media (min-width: 901px) {
  .hero-bar-seg { overflow: visible; }
}
.bar-fill.trace { background: transparent; box-shadow: inset 0 -10px 0 var(--trace); opacity: 0.55; }
/* R8 1b: rising-value marker -- a thin outline tick at the previous position, drawn after (so on
   top of) the current fill, since a rising value's ghost block would otherwise sit entirely under
   the now-wider opaque fill and never be visible. */
.bar-fill.trace-tick {
  width: 2px; height: 24px; top: -3px; background: transparent; border-left: 2px solid var(--trace);
  box-shadow: none; opacity: 0.9; transform: translateX(-1px);
}
/* R7: value and share now stack on two independently no-wrap lines (was one inline run, "2.25 \u00b7
   33%", that broke mid-token on a narrow mobile value column). Stacking also frees horizontal room
   for the plot track itself. */
/* renderBar sets --bar-value-left (percent past the fill's end, clamped) so this stays a single
   position rule regardless of the row's own value. */
.bar-value {
  position: absolute; top: 50%; left: var(--bar-value-left, 0); transform: translateY(-50%);
  display: flex; align-items: baseline; gap: var(--sp-1); line-height: 1.2; white-space: nowrap;
}
.bar-value.align-right { left: auto; right: 0; }
.bar-value-num { font-size: var(--fs-body); font-weight: 500; color: var(--ink); white-space: nowrap; font-variant-numeric: tabular-nums; }
.bar-value .bar-share { color: var(--ink-3); font-size: var(--fs-meta); white-space: nowrap; font-variant-numeric: tabular-nums; }
.bar-value.below-track { top: 100%; left: 0 !important; right: auto !important; transform: none; margin-top: 3px; }
.bar-row.bar-row-value-below { padding-bottom: 18px; }
/* 3-2 ④ (orchestrator): an implicit grid track sizes to its widest min-content child; the 1286px
   statements table then widened every Pro section (cash flow drew at 1286px, page overflowed
   346px at 1440). One explicit minmax(0,1fr) track keeps wide tables inside their own scroller. */
.std, .pro { margin-top: 36px; display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-5); max-width: 100%; }
.std[hidden], .pro[hidden], [hidden] { display: none !important; }
article h2 { font-family: var(--font-display); font-weight: 500; font-size: var(--fs-h2); }
.lede { color: var(--ink-2); font-size: var(--fs-body); margin-top: 4px; max-width: 62ch; }
/* R10 Move 4: prose out, frame in -- a small round "i" toggle sits next to each article title;
   the explanatory paragraph (previously always visible between the title and the figure) is
   revealed on demand instead of taking up permanent space. */
.header-title-group { display: flex; align-items: baseline; gap: var(--sp-2); min-width: 0; }
.info-toggle {
  position: relative; flex: none; width: 44px; height: 44px; margin: -11px -11px -11px -4px;
  min-height: 0; min-width: 0; border: 0; border-radius: var(--r-round); background: transparent; color: var(--ink-2);
  font-family: var(--font-display); font-style: italic; font-size: var(--fs-sm); line-height: 1; padding: 0;
  display: inline-flex; align-items: center; justify-content: center; align-self: center;
}
.info-toggle::before {
  content: "i"; position: absolute; width: 22px; height: 22px; border-radius: var(--r-round); border: 1px solid var(--line);
  background: var(--surface); display: flex; align-items: center; justify-content: center; pointer-events: none;
}
.info-toggle[aria-expanded="true"]::before { background: var(--ink); color: var(--surface); border-color: var(--ink); }
.info-toggle:focus-visible,
.info-toggle[aria-expanded="true"]:focus-visible,
.info-toggle[aria-expanded="true"] { outline: 2px solid var(--accent); outline-offset: 2px; }
/* H-05 (3-3): compact "i" toggle for an inline source citation (detail panel SOURCE line, ladder
   trace row, sources drawer row) -- same wireInfoToggle contract and .info-popover as the header
   .info-toggle above, sized for a short inline label instead of a 44px-tall header row. Same
   invisible-padding touch-target technique as .field-reset below: padding + a matching negative
   margin cancel back to a small 16px visual dot while keeping a taller pointer hit area. */
.source-info-toggle {
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; padding: 14px 6px; margin: -14px 0; min-height: 0; min-width: 0;
  border: 0; background: transparent; color: var(--ink-2); vertical-align: middle; flex: none;
}
.source-info-toggle::before {
  content: "i"; position: absolute; width: 16px; height: 16px; border-radius: var(--r-round); border: 1px solid var(--line);
  background: var(--surface); display: flex; align-items: center; justify-content: center; pointer-events: none;
  font-family: var(--font-display); font-style: italic; font-size: var(--fs-meta); line-height: 1; color: var(--ink-2);
}
.source-info-toggle[aria-expanded="true"]::before { background: var(--ink); color: var(--surface); border-color: var(--ink); }
.source-info-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.lede.info-body { display: none !important; }
.info-popover {
  position: fixed; z-index: 40; max-width: 280px; margin: 0; padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--line); border-radius: var(--r-float); background: var(--surface);
  box-shadow: var(--shadow-pop); color: var(--ink-2); font-size: var(--fs-sm); line-height: 1.45;
  opacity: 0; transform: translateY(4px); pointer-events: none;
  transition: opacity var(--dur-panel) var(--ease-panel), transform var(--dur-panel) var(--ease-panel);
}
/* H-A4 (3-3 R1D): a source citation's full text is now up to three lines (source + as-published
   figure, converted figure in words, exact factors) joined by "\n" -- pre-line renders those
   explicit breaks while still wrapping each line normally; the header lede's own plain prose
   (no "\n") is unaffected since pre-line only changes how *explicit* breaks/collapsible spaces
   are handled, not normal wrapping. */
.info-popover p { white-space: pre-line; }
.info-popover.open { opacity: 1; transform: translateY(0); pointer-events: auto; }
.info-popover[data-instant="true"] { transition: none; }
.info-popover-arrow {
  position: absolute; width: 8px; height: 8px; background: var(--surface);
  border-left: 1px solid var(--line); border-top: 1px solid var(--line); transform: rotate(45deg);
}
.inline-detail-body > :first-child { margin-top: var(--sp-4); }
.detail-value-grid, .detail-subhead, .detail-gap, .detail-formula, .detail-desc, .detail-source { margin-left: 0; }
@media (prefers-reduced-motion: reduce) {
  .info-popover { transition: opacity var(--dur-panel) ease; transform: none; }
}
article header button { padding: 0 var(--sp-3); font-size: var(--fs-body); }
.table-scroll { overflow-x: auto; max-width: 100%; }
.table-scroll { position: relative; }
.stmt-wrap {
  max-height: 70vh;
  overflow-x: auto;
  overflow-y: auto;
}
/* F-04 (3-2 final, orchestrator): the full-width statements experiment collided with the sticky inputs rail (G5 "Assumptions" overlaps) and was reverted. Statements keep the in-box horizontal scroll with sticky year column and scroll cue; revisit layout in 3-3. */
@media (max-width: 700px) {
  .stmt-wrap { max-height: 60vh; }
}
/* T4-03: the statement header stays put while the wrap scrolls. The year corner sits above both. */
.stmt thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--paper);
}
.compare-table, .stmt {
  width: 100%;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-table-body, var(--fs-body));
  margin-top: 12px;
}
.compare-table th, .compare-table td, .stmt th, .stmt td {
  text-align: right;
  padding: 8px 12px;
  height: 38px;
  border-bottom: 1px solid var(--line);
  border-left: 0;
  border-right: 0;
  white-space: nowrap;
  font-weight: 400;
  vertical-align: middle;
}
.stmt { width: max-content; min-width: 100%; }
.compare-table { width: 100%; }
.compare-table thead th, .stmt thead th {
  font-size: var(--fs-meta);
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3);
  border-bottom: 1px solid var(--line-2);
  background: var(--paper);
  height: 36px;
}
.compare-table th:first-child, .compare-table td:first-child, .stmt th:first-child, .stmt td:first-child {
  text-align: left;
  color: var(--ink-2);
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--paper);
  padding-left: 0;
}
/* R2E-A6 (3-3 R2, Sonnet): line 664's shared sticky-left-column rule set every th:first-child/
   td:first-child to z-index 1, then this rule (later in the cascade, same specificity as the
   old ".stmt thead th:first-child { z-index: 3 }" three lines up) silently re-lowered the
   statements corner cell back to z-index 2 -- the exact same value as every other sticky header
   cell it needs to sit above, so paint order (a later sibling th paints over an earlier one at
   equal z-index) let a header cell slide over the corner during horizontal scroll and show a
   clipped text fragment beside it. Split so the corner keeps a definitively higher z-index than
   the header row (2) and the sticky first column body (1): corner (3) > header (2) > column (1) > body. */
.compare-table thead th:first-child { z-index: 2; color: var(--ink-3); }
.stmt thead th:first-child { z-index: 3; color: var(--ink-3); background: var(--paper); }
/* F4 (3-3 final): the sticky Year column had no border of its own -- only a box-shadow that
   appears once scrolled -- so a header cell sliding underneath it while scrolling read as
   "OST USD K" bleeding in right beside the corner with no visible seam at all between the two.
   A quiet always-on 1px border gives the column a real edge at every scroll position; the
   existing box-shadow (only while is-scrolled, see below) still adds the deeper cue once there
   is anything to hide behind it. */
.stmt th:first-child, .stmt td:first-child { border-right: 1px solid var(--line); }
.table-scroll.is-scrolled .compare-table th:first-child,
.table-scroll.is-scrolled .compare-table td:first-child,
.table-scroll.is-scrolled .stmt th:first-child,
.table-scroll.is-scrolled .stmt td:first-child {
  box-shadow: 8px 0 8px -8px var(--shadow-ink);
}
/* F4 (3-3 final): a right-edge fade cue (same mask-image technique as the Assumptions rail's own
   has-scroll-top/-bottom, so this never adds a new sticky/fixed overlay for G5 to flag) shows
   only while the statements table can still scroll further right; markTableScroll toggles
   is-scrollable-right the same way it already toggles is-scrolled/is-v-scrolled. Column
   scroll-snap keeps a horizontal drag from stopping mid-column right beside the sticky Year
   column -- every non-Year column snaps its own left edge to the viewport, so scrolling always
   settles on a whole column instead of leaving half of one peeking out next to Year. */
.stmt-wrap.is-scrollable-right {
  -webkit-mask-image: linear-gradient(to right, black calc(100% - 24px), transparent);
  mask-image: linear-gradient(to right, black calc(100% - 24px), transparent);
}
.stmt-wrap { scroll-snap-type: x proximity; }
.stmt th:not(:first-child), .stmt td:not(:first-child) { scroll-snap-align: start; }
/* H-A6 (3-3 R1D): the sticky header row's own quiet edge shadow, shown only once the table has
   actually scrolled vertically (is-v-scrolled, toggled in markTableScroll) -- same treatment the
   sticky first column already gets for horizontal scroll, just on the opposite axis. */
.table-scroll.is-v-scrolled .stmt thead th {
  box-shadow: 0 8px 8px -8px var(--shadow-ink);
}
@media (min-width: 1440px) {
  /* H-A6 (3-3 R1D, Hayeon: "at >=1440 let the statements section use the full content width, not
     the narrow column"): main's own 1280px reading-width cap (line ~189) leaves an unused margin
     beside it at wide viewports; this recovers that margin plus main's own right padding for just
     this one table, entirely to the RIGHT of the existing gap. Grid track 1 (the 340px
     Assumptions rail) is a fixed-width grid column and is completely untouched by this -- unlike
     the reverted F-04 attempt (LEDGER.md: "화면 밖 폭 늘리기 방식", a beyond-viewport bleed that
     ignored the grid and caused 5 G5 text collisions with the rail), this never crosses the
     gap into the rail's own column, so it cannot re-create that overlap. */
  #sec-statements .stmt-wrap {
    width: calc(100% + (100vw - 1280px) / 2 + var(--sp-6));
    max-width: none;
  }
}
.stmt tbody tr.stmt-total td {
  border-top: 1px solid var(--ink);
  border-bottom: 0;
  font-weight: 600;
  color: var(--ink);
}
.stmt tbody tr.stmt-total td:first-child { font-weight: 600; color: var(--ink); }
.stmt tbody tr:nth-child(n+13):not(.stmt-total) td {
  background-color: color-mix(in srgb, var(--surface-2) 8%, transparent);
}
.stmt tbody tr:nth-child(n+13):not(.stmt-total) td:first-child {
  background-image: linear-gradient(color-mix(in srgb, var(--surface-2) 8%, transparent), color-mix(in srgb, var(--surface-2) 8%, transparent)), linear-gradient(var(--paper), var(--paper));
  background-color: transparent;
}
.compare-table td.dir-down { color: var(--dir-down); }
.compare-table td.dir-up { color: var(--dir-up); }
.compare-table input { width: 88px; border: 0; border-bottom: 1px solid var(--line-2); background: transparent; text-align: right; min-height: 36px; font-variant-numeric: tabular-nums; }
.compare-table input:focus { border-color: var(--accent); outline: 2px solid var(--accent); outline-offset: 1px; }
.scroll-cue {
  position: absolute;
  top: 8px;
  right: 0;
  margin: 0;
  font-size: var(--fs-meta);
  letter-spacing: 0.04em;
  color: var(--ink-3);
  pointer-events: none;
}
.scroll-cue[hidden] { display: none; }
.stmt-note { color: var(--ink-3); font-size: var(--fs-meta); margin: 8px 0 0; }
.tornado-svg text { font-family: var(--font-ui); }
.sell-row { align-items: end; margin-top: 12px; }
.sell-row label { display: grid; gap: var(--sp-1); font-size: var(--fs-body); }
.sell-row input { font-size: var(--fs-h2); font-family: var(--font-display); width: 140px; border-bottom: 1px solid var(--ink); }
/* R9 4b: flex sized each stat by its own content, so "Payback" and "Project IRR" ended up much
   narrower than "After-tax required price" and their captions wrapped across 6-8 lines. An explicit
   grid gives every metric the same column width. */
/* R10 B(c): 5 equal columns at ~164px each clamped every caption's basis text with an ellipsis
   mid-word ("After-tax, unlevered, real\u2026"). Two columns per row instead of five gives each
   stat roughly 2.4x the width, and the caption below raises from a 2-line clamp to 3, so the full
   condition text (now shown in full for the two required-price stats, R10 B(c)) fits without
   truncation at 1440. */
.sell-row dl { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: var(--sp-4) var(--sp-5); margin: 0; }
.sell-row dt { color: var(--ink-3); font-size: var(--fs-meta); }
/* R7: signed amounts (e.g. Project NPV's minus sign) used to wrap onto their own line inside the
   flex dl; keep each stat on one line. */
/* R9 4b: 22px was sized for the old unequal flex columns, where a wide value (e.g. "7.31 USD/kg")
   could claim as much width as it needed. In the new equal grid columns, 22px overflowed a ~91px
   column into the next one (measured: the "After-tax required price" and "Levered required price"
   values visually touching). 16px keeps every value -- including the widest, "7.31 USD/kg" -- clear
   inside its own column with room to spare. */
.sell-row dd { margin: 0; font-variant-numeric: tabular-nums lining-nums; font-size: var(--fs-input); font-family: var(--font-display); white-space: nowrap; text-align: left; }
/* R9 4b: the 20ch cap forced a wrap independent of the column's real (now equal) width; match the
   column instead, and cap at two lines so any caption longer than that must move to the title
   tooltip (the metric's inline detail) rather than grow the row. */
/* R10 B(c): raised from a 2-line clamp (which ellipsis-truncated the basis text mid-phrase) to 3
   lines, now that the wider two-per-row grid above gives each caption roughly 2.4x the column
   width to work with. */
.sell-stat-condition { margin: 4px 0 0; color: var(--ink-2); font-size: var(--fs-meta); max-width: 100%; line-height: 1.35; white-space: normal; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
/* R10 B(c): the two required-price captions carry the full finance condition (debt/equity/tax/
   depreciation), which can exceed even a 3-line clamp at this column width; let those wrap freely
   instead of silently re-clipping the "full condition text" B(c) asks for. */
.sell-stat-condition-full { display: block; -webkit-line-clamp: unset; overflow: visible; }
.ladder-toggle { width: 100%; display: flex; justify-content: space-between; align-items: center; padding: 0 var(--sp-1); background: transparent; border: 0; border-bottom: 1px solid var(--line); font-family: var(--font-display); font-size: var(--fs-h2); }
.chev { width: 8px; height: 8px; border-right: 1px solid var(--ink); border-bottom: 1px solid var(--ink); transform: rotate(45deg); margin-right: 8px; }
.ladder-toggle[aria-expanded="true"] .chev { transform: rotate(-135deg); margin-top: 6px; }
.ladder-list { list-style: none; margin: 8px 0 0; padding: 0; }
.ladder-list li { display: grid; grid-template-columns: 1fr auto; gap: var(--sp-2) var(--sp-4); padding: var(--sp-2) 0; border-bottom: 1px solid var(--line); }
.ladder-list .delta { grid-column: 1 / -1; color: var(--ink-3); font-size: var(--fs-sm); }
.ladder-list strong { font-variant-numeric: tabular-nums; font-weight: 500; }
.chart { width: 100%; height: auto; display: block; max-width: 100%; }
.chart text { font-family: var(--font-ui); fill: var(--ink-2); font-size: var(--fs-meta); }
.stmt-tabs { display: flex; gap: var(--sp-2); margin-top: 12px; }
.stmt-tabs button { padding: 0 var(--sp-3); }
.stmt-tabs button[aria-selected="true"] { background: var(--ink); color: var(--surface); border-color: var(--ink); }
.balance { margin: 0; font-size: var(--fs-body); }
.balance.ok { color: var(--positive-ink); }
.balance.bad, .balance.wait { color: var(--warn); }
.entries ul, #entries { list-style: none; margin: 12px 0 0; padding: 0; }
#entries li { border-bottom: 1px solid var(--line); }
#entries a { display: flex; justify-content: space-between; gap: var(--sp-3); min-height: 44px; align-items: center; color: inherit; text-decoration: none; }
#entries a span { color: var(--ink-3); font-size: var(--fs-sm); }
.toast { position: fixed; bottom: 16px; left: 16px; background: var(--ink); color: var(--on-accent); padding: var(--sp-2) var(--sp-3); }

.title-actions { display: flex; gap: var(--sp-2); align-items: center; }
.version-notice {
  max-width: 1280px; margin: 12px auto 0; padding: var(--sp-4) var(--sp-5); background: var(--surface); border: 1px solid var(--line); border-radius: 0;
  display: flex; justify-content: space-between; align-items: center; gap: var(--sp-4); flex-wrap: wrap;
}
.version-notice p { margin: 0; color: var(--warn); font-size: var(--fs-body); max-width: 62ch; }
.version-notice button { min-height: 44px; padding: 0 var(--sp-4); border: 1px solid var(--line); background: var(--surface); border-radius: var(--r-ctrl); color: var(--ink); }
.was-line { margin: 4px 0 0; color: var(--ink-2); font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }
.field-note { margin: 2px 0 0; color: var(--ink-3); font-size: var(--fs-meta); grid-column: 1 / -1; }
.quick-preview { margin: 22px 0 0; padding: var(--sp-4); border: 1px solid var(--line); border-radius: 0; background: var(--surface); }
.quick-preview-heading { margin: 0 0 8px; font-size: var(--fs-sm); color: var(--ink-2); text-transform: uppercase; letter-spacing: 0.04em; }
.quick-preview-row { display: flex; justify-content: space-between; gap: var(--sp-3); padding: var(--sp-1) 0; border-bottom: 1px solid var(--line); font-variant-numeric: tabular-nums; font-size: var(--fs-body); }
.quick-preview-hint { margin: 10px 0 0; color: var(--ink-3); font-size: var(--fs-sm); }
.power-chart-wrap { margin-top: 4px; }
.power-chart { display: grid; gap: var(--sp-2); margin-top: 6px; }
.power-chart-row { display: grid; grid-template-columns: 110px minmax(0, 1fr); gap: var(--sp-2); align-items: center; font-size: var(--fs-meta); }
.power-chart-row.active .power-chart-label { color: var(--accent); font-weight: 600; }
.power-chart-track { position: relative; height: 20px; background: var(--line); }
.power-chart-fill { position: absolute; left: 0; top: 0; height: 20px; background: var(--ink-3); }
.power-chart-row.active .power-chart-fill { background: var(--accent); }
.power-chart-value { display: block; margin-top: 2px; color: var(--ink-2); font-size: var(--fs-cap); }
.paired-composition { margin-top: 18px; }
.paired-caption, .diff-caption, .tornado-caption { margin: 0 0 8px; color: var(--ink-2); font-size: var(--fs-meta); }
.paired-row { display: grid; grid-template-columns: 140px minmax(0, 1fr); gap: var(--sp-2); align-items: center; margin-bottom: 10px; font-size: var(--fs-meta); }
.paired-tracks { display: grid; gap: 6px; }
/* C3-07: chip, track, gutter. The value never sits on the fill. Case B uses a lighter tint of the
   same category hue (color-mix), not a 45% opacity wash. */
.paired-track { display: grid; grid-template-columns: 22px minmax(0, 1fr) 56px; gap: 8px; align-items: center; min-height: 20px; }
.paired-tag { font-size: var(--fs-meta); font-weight: 600; color: var(--ink); letter-spacing: 0.02em; }
.paired-tag.b { color: var(--ink-2); }
.paired-fill { display: block; height: 14px; border-radius: 0; justify-self: start; }
.paired-value { font-size: var(--fs-meta); font-variant-numeric: tabular-nums; color: var(--ink-2); white-space: nowrap; text-align: right; }
.diff-chart { margin-top: 18px; }
.diff-row { display: grid; grid-template-columns: 140px minmax(0, 1fr) 64px; gap: var(--sp-2); align-items: center; margin-bottom: 6px; font-size: var(--fs-meta); }
.diff-track { position: relative; height: 10px; background: var(--line); }
.diff-mid { position: absolute; left: 50%; top: -3px; width: 1px; height: 16px; background: var(--line-2); }
.diff-fill { position: absolute; top: 0; height: 10px; }
/* L-07 (3-2 batch 2): B-A difference bars use the direction tokens (cheaper/dearer), not the
   cash-flow positive/negative pair. */
.diff-fill.down { background: var(--dir-down); }
.diff-fill.up { background: var(--dir-up); }
.diff-value { text-align: right; font-variant-numeric: tabular-nums; }
.tornado-legend { display: flex; gap: var(--sp-4); align-items: center; font-size: var(--fs-meta); color: var(--ink-2); margin-bottom: 10px; }
.tornado-legend .swatch { display: inline-block; width: 10px; height: 10px; margin-right: 4px; vertical-align: middle; }
/* L-01/L-09 (3-2 batch 2): the legend swatch is the exact fill the tornado bars themselves use
   (buildTornadoFragment) -- one direction-colour pair, not the old positive/negative or a
   category tint, so the legend never drifts from what is actually drawn. */
.tornado-legend .swatch.better { background: var(--dir-down); }
.tornado-legend .swatch.worse { background: var(--dir-up); }
/* G5 (3-2 R2): the sales-price caption sits 18px above its own row (.ladder-sales-caption), which
   is the first row of .ladder-chart. With only 14px of margin above that, the caption landed on
   top of the "Price ladder" toggle button just above it. */
.ladder-chart { margin-top: 30px; margin-bottom: 6px; }
/* R9 item 1: every rung is a full bar from zero on one shared scale, at every width. A value label
   sits at its own bar's end inside a fixed right-hand gutter (the bar itself never grows past 78%
   of the row, so the label's own column is never squeezed); a value label lives inside its own row
   only and can never land on a neighbouring row's bar or text, which is what R8's collision-avoidance
   logic (moving a label above its own bar) actually broke. */
.ladder-rows { position: relative; }
.ladder-caption-row { margin: 0 96px 6px 0; font-size: var(--fs-cap); font-weight: 600; color: var(--accent); }
.ladder-row { padding: var(--sp-2) 0; border-bottom: 1px solid var(--line); }
.ladder-row:last-child { border-bottom: 0; }
.ladder-row:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* R9 item 1 fix: flex-direction: column defaults align-items to stretch, which made the name/
   caption spans as wide as the whole row (a real getBoundingClientRect() box, not just their
   rendered glyphs) and produced false "overlap" reads against anything positioned over that row.
   flex-start keeps each span exactly as wide as its own text. */
.ladder-row-head { display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-1); }
.ladder-row-name { font-weight: 500; font-size: var(--fs-body); }
.ladder-row-caption { color: var(--ink-2); font-size: var(--fs-meta); }
/* C3-06: plot = track + value gutter. The sales guide is absolutely positioned over the
   tracks only (right: 88px), so it never enters the gutter. */
.ladder-plot { display: grid; grid-template-columns: minmax(0, 1fr) 88px; gap: 8px; align-items: center; margin-top: 8px; }
.ladder-row-track { position: relative; height: 24px; background: var(--line); border-radius: var(--r-sm); }
.ladder-row-guide { position: absolute; top: 0; bottom: 0; width: 0; border-left: 1.5px dashed var(--accent); pointer-events: none; z-index: 1; }
.ladder-row-bar { position: absolute; left: 0; top: 0; bottom: 0; background: var(--ink); border-radius: var(--r-sm); }
.ladder-row-value { font-size: var(--fs-sm); font-weight: 600; font-variant-numeric: tabular-nums; color: var(--ink-2); white-space: nowrap; }
.ladder-axis { position: relative; height: 16px; margin-top: 8px; margin-right: 96px; }
.ladder-axis-tick { position: absolute; top: 0; transform: translateX(-50%); font-size: var(--fs-cap); color: var(--ink-3); white-space: nowrap; }
/* R10 B(e): the centered -50% transform shifted the 0 tick half its own width past the container's
   left edge, where html's overflow-x: hidden clipped off the leading "0.00" and left only "USD/kg"
   visible. Anchor the first/last ticks at their own edges instead of centering them. */
.ladder-axis-tick.axis-start { transform: translateX(0); }
.ladder-axis-tick.axis-end { transform: translateX(-100%); }
.target-row { display: flex; flex-wrap: wrap; gap: var(--sp-4); align-items: end; margin-top: 12px; }
.target-row label { display: grid; gap: var(--sp-1); font-size: var(--fs-body); }
.target-row input { font-size: var(--fs-h3); font-family: var(--font-display); width: 140px; border: 0; border-bottom: 1px solid var(--ink); background: transparent; min-height: 44px; }
.target-row select { min-height: 44px; border: 1px solid var(--line-2); background: var(--surface); }
.target-row button { min-height: 44px; padding: 0 var(--sp-4); border: 1px solid var(--line-2); background: var(--surface); border-radius: var(--r-sm); }
#target-result { margin-top: 12px; font-size: var(--fs-body); }
#target-result p { margin: 0 0 6px; }
#target-result button { min-height: 36px; padding: 0 var(--sp-3); margin-left: 8px; border: 1px solid var(--line-2); background: var(--surface); }
.sources-panel {
  position: fixed; top: 0; right: 0; height: 100vh; width: min(420px, 96vw); background: var(--surface); border-left: 1px solid var(--line);
  box-shadow: var(--shadow-sheet); transform: translateX(100%); transition: transform var(--dur-panel) var(--ease-panel); z-index: 20; overflow-y: auto; padding: var(--sp-5);
}
.sources-panel.open { transform: translateX(0); }
.sources-panel-head { display: flex; justify-content: space-between; align-items: center; }
.sources-panel-head h2 { font-family: var(--font-display); font-weight: 500; font-size: var(--fs-title); margin: 0; }
.sources-panel-head button {
  position: relative; width: 44px; height: 44px; min-height: 0; min-width: 0; padding: 0; border: 0; border-radius: var(--r-round);
  background: transparent; color: var(--ink-2); font-size: var(--fs-input); display: flex; align-items: center; justify-content: center;
}
.sources-panel-head button::before {
  content: ""; position: absolute; inset: 6px; border-radius: var(--r-round); background: transparent; z-index: -1;
}
/* C2-02: every dismiss control keeps the 2 px accent ring. A later outline:none must not win. */
.sources-panel-head button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (hover: hover) and (pointer: fine) {
  .sources-panel-head button:hover::before { background: var(--surface-2); }
}
.sources-api-note { color: var(--ink-2); font-size: var(--fs-sm); padding: var(--sp-2) 0; border-bottom: 1px solid var(--line); }
/* H-A5 (3-3 R1D): the "N inputs without a source" view's own link back to the unfiltered list --
   a plain text-link row above the filtered rows, same button-as-link pattern .text-link uses in
   the header. Hidden by default (JS shows it only while state.sourcesFilter === "unsourced"). */
.sources-switch { margin: 0 0 var(--sp-2); }
.sources-switch button {
  min-height: 32px; padding: 0; border: 0; background: transparent; color: var(--accent);
  font-size: var(--fs-sm); text-decoration: underline; text-underline-offset: 2px;
}
.sources-switch button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Each unsourced-filter row's current value + unit, between the name/tag head and the citation
   line -- same quiet ink-3 the field rail's own "= 28,050 kW" mirror uses. */
.sources-value { margin: 4px 0 0; color: var(--ink-3); font-size: var(--fs-meta); }
.sources-row { padding: var(--sp-3) 0; border-bottom: 1px solid var(--line); }
.sources-row-head { display: flex; justify-content: space-between; gap: var(--sp-2); align-items: baseline; }
.sources-tag { font-size: var(--fs-meta); color: var(--ink-3); white-space: nowrap; }
.sources-tag.user { color: var(--accent); }
.sources-detail { margin: 6px 0 0; color: var(--ink-2); font-size: var(--fs-sm); }

/* 3-2 step2 (COMPONENTS.md 1): sliders. The input's own box is the 44px pointer hit area (a
   native range control can't have a click region bigger than its box, so the "visible rail 3px /
   hit 44px" split lives in the box's own height, not a padded overlay); the runnable-track pseudo
   inside it draws the thin 3px rail. --slider-color (category colour or --ink, set inline per
   field) and --pct (the live 0-100% fill position, kept in sync on every input/render) drive one
   shared gradient so the filled portion left of the thumb reads in the field's own colour without
   per-key CSS. .field-slider-wrap only exists to host the absolutely positioned default-value tick
   (a real form control can't render its own child elements). */
.field-slider-wrap { position: relative; height: 44px; margin: 2px 0 0; }
.field-slider {
  -webkit-appearance: none; appearance: none; width: 100%; height: 44px; background: transparent; margin: 0; padding: 0;
  --slider-color: var(--ink); --pct: 0%; position: relative; z-index: 1;
}
.field-slider::-webkit-slider-runnable-track {
  height: 3px; border-radius: var(--r-pill);
  background: linear-gradient(to right, var(--slider-color) 0%, var(--slider-color) var(--pct), var(--rail) var(--pct), var(--rail) 100%);
}
.field-slider::-moz-range-track { height: 3px; border-radius: var(--r-pill); background: var(--rail); }
.field-slider::-moz-range-progress { height: 3px; border-radius: var(--r-pill); background: var(--slider-color); }
.field-slider::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; width: 16px; height: 16px; border-radius: var(--r-round);
  background-color: var(--surface);
  border: 1px solid var(--line-2); box-shadow: 0 1px 2px var(--shadow-ink);
  margin-top: -6.5px; cursor: grab; transition: box-shadow var(--dur-press) ease-out;
}
.field-slider::-moz-range-thumb {
  width: 16px; height: 16px; border-radius: var(--r-round);
  background-color: var(--surface);
  border: 1px solid var(--line-2);
  box-shadow: 0 1px 2px var(--shadow-ink); cursor: grab; transition: box-shadow var(--dur-press) ease-out;
}
.field-slider.dragging::-webkit-slider-thumb,
.field-slider:active::-webkit-slider-thumb {
  box-shadow: 0 1px 2px var(--shadow-ink), 0 0 0 3px color-mix(in srgb, var(--slider-color) 25%, transparent);
}
.field-slider.dragging::-moz-range-thumb,
.field-slider:active::-moz-range-thumb {
  box-shadow: 0 1px 2px var(--shadow-ink), 0 0 0 3px color-mix(in srgb, var(--slider-color) 25%, transparent);
}
.field-slider:focus-visible { outline: none; }
.field-slider:focus-visible::-webkit-slider-thumb { outline: 2px solid var(--accent); outline-offset: 2px; }
.field-slider:focus-visible::-moz-range-thumb { outline: 2px solid var(--accent); outline-offset: 2px; }
.field-slider:disabled { opacity: 0.4; }
.field-slider:disabled::-webkit-slider-thumb, .field-slider:disabled::-moz-range-thumb { cursor: default; }
/* S-03: a purely decorative 2x8px rail marker at the default's own % position (COMPONENTS.md 1) --
   not part of the native range control, which cannot host real children, and not interactive
   itself (it used to be an 8x8px <button tabindex="-1">, unreachable by keyboard and an easy-to-miss
   pointer target, V-1-2/I-1-2). The actual reset control is .field-reset, a real button in the
   field's label row (see .field-label-actions below); aria-hidden here keeps this mark silent. */
.field-slider-tick {
  position: absolute; top: 50%; left: 0; width: 2px; height: 8px; margin: -4px 0 0 -1px; border-radius: var(--r-sm);
  background: var(--ink-3); z-index: 2; pointer-events: none;
}
.field-convert { margin: 2px 0 0; color: var(--ink-3); font-size: var(--fs-meta); }
.field-convert[hidden] { display: none; }

/* 3-2 step2 (COMPONENTS.md 2): segmented toggles (kW/MW, USD/kWh vs USD/MWh, Rate vs Project
   total, Capacity factor vs Annual output, t/day vs t/year). A 32px surface-2 tray holds 28px
   segments with 2px inner padding; the selected segment lifts onto --surface with a soft shadow
   and hairline border instead of the old black-fill toggle, so the same component reads as one
   family with the tech chips below (also no black fill) rather than two different vocabularies. */
.unit-pill { display: flex; gap: 2px; width: fit-content; height: 32px; padding: 2px; border-radius: var(--r-ctrl); background: var(--surface-2); }
.unit-pill button {
  min-height: 28px; padding: 0 10px; font-size: var(--fs-sm); font-weight: 500; border: 1px solid transparent; border-radius: var(--r-ctrl);
  background: transparent; color: var(--ink-2); transition: background var(--dur-panel) ease-out, box-shadow var(--dur-panel) ease-out, transform var(--dur-press) ease-out;
}
.unit-pill button[aria-pressed="true"] {
  background: var(--surface); color: var(--ink); border-color: var(--line2); box-shadow: 0 1px 2px var(--shadow-ink);
}
@media (hover: hover) and (pointer: fine) {
  .unit-pill button:hover { color: var(--ink); }
}
.unit-pill button:active { transform: scale(0.97); }
@media (prefers-reduced-motion: reduce) {
  .unit-pill button {
    transition-duration: 0s;
    transition-delay: 0s;
  }
}


/* S-07: on a coarse (touch) pointer, chips/segmented controls/legend rows/Reset grow their real hit
   area to >=44px; the desktop 32/28px visual size (mouse/trackpad, pointer:fine) is unchanged.
   .hero-bar-seg is deliberately excluded -- it stays data-proportional (V-1-3's own fix direction:
   keep the bar's true geometry as a mark, give the *legend row* the reliable touch target). */
@media (pointer: coarse), (max-width: 600px) {
  .tech button, .unit-pill button, .stages button, .stmt-tabs button { min-height: 44px; }
  .unit-pill { height: auto; }
  .skip:focus { min-height: 44px; }
  .control, .field .control input, input.inline-number { min-height: 44px; }
  /* .bar-row.bar-row-clickable (both classes) outranks the mobile media query's plain .bar-row
     min-height: 24px rule by specificity, so this still wins on an actual phone (narrow *and*
     coarse at once) regardless of which block appears later in the file. */
  .bar-row.bar-row-clickable { min-height: 44px; }
  /* min-height guarantees >=44 regardless of line-height rounding (12px --fs-meta's own line-height
     landed at 43.6px from padding math alone on some font stacks -- 0.4px short of the floor). */
  .field-reset { padding: 14px 8px; margin: -14px -8px; min-height: 44px; display: inline-flex; align-items: center; }
}

/* R4: CAPEX field with rate/total mode */
.capex-field-wrap { display: grid; gap: var(--sp-1); }
.capex-field-wrap > label { font-size: var(--fs-body); }
.capex-field-wrap .capex-inner { margin-top: 0; }
.capex-field-wrap .capex-inner label { display: none; }
.capex-total-row { margin-top: 8px; gap: var(--sp-2); }
.capex-total-row select { min-height: 44px; border: 1px solid var(--line-2); background: var(--surface); }
.capex-rate-row { display: flex; align-items: center; gap: var(--sp-2); margin-top: 6px; }
.capex-rate-row input { width: 90px; border: 0; border-bottom: 1px solid var(--line-2); background: transparent; min-height: 32px; }

/* R4: stack replacement basis */
.stack-basis-field select { min-height: 40px; border: 1px solid var(--line-2); background: var(--surface); margin-top: 4px; }
.stack-basis-field .field { margin-top: 10px; }

/* R4: cost row / tornado / ladder click targets */
/* H-06 (3-3, Hayeon: "the clickable box sticks to the first letter; looks bad"): the row's own
   background (hover, and .row-detail-open below) used to paint edge-to-edge with the label text
   flush against the row's left edge. Padding + an equal negative margin keeps every row's visual
   left edge exactly where it was (the composition figure's own left margin is unchanged) while
   giving the label 12px of breathing room inside its own highlighted box, with a small radius so
   the box itself reads as a control, not a raw strip. */
.bar-row-clickable {
  cursor: pointer; padding: 0 var(--sp-3); margin: 0 calc(-1 * var(--sp-3)); border-radius: var(--r-ctrl);
  transition: background var(--dur-panel) ease-out;
}
@media (hover: hover) and (pointer: fine) {
  .bar-row-clickable:hover:not(.row-detail-open) { background: color-mix(in srgb, var(--row-accent, var(--accent)) 5%, transparent); }
}
.bar-row-clickable:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.bar-row-clickable:hover .bar-label, .bar-row-clickable:focus-visible .bar-label { color: var(--accent); }
.tornado-hit { cursor: pointer; }
.tornado-hit:focus { outline: none; }
.tornado-hit:focus-visible { outline: none; }
.ladder-item-clickable { cursor: pointer; }
.ladder-item-clickable:hover, .ladder-item-clickable:focus-visible { background: var(--surface); }

/* R5/3-2 step2: inline detail description/formula/stats reused by the inline detail blocks.
   Sub-sections (Drivers, Source) carry their own top divider + 24px gap (COMPONENTS.md 6) so the
   panel reads as stacked sections rather than one continuous scroll of paragraphs. */
.detail-desc { color: var(--ink-2); font-size: var(--fs-body); margin: var(--sp-3) 0 0; }
.detail-formula { margin: var(--sp-3) 0 0; font-variant-numeric: tabular-nums; font-size: var(--fs-body); color: var(--ink); }
.detail-gap { margin: var(--sp-2) 0 0; color: var(--ink-2); font-size: var(--fs-sm); max-width: 62ch; }
.detail-stats { margin: var(--sp-5) 0 0; display: grid; gap: var(--sp-2); }
.detail-stats div { display: flex; justify-content: space-between; gap: var(--sp-3); border-bottom: 1px solid var(--line); padding-bottom: var(--sp-1); }
.detail-stats dt { color: var(--ink-3); font-size: var(--fs-meta); }
.detail-stats dd { margin: 0; font-variant-numeric: tabular-nums; font-size: var(--fs-body); text-align: right; white-space: nowrap; }
/* COMPONENTS.md 6: "Value" and "Share of discounted LCOH" sit as two headline cells, not a
   label-left/value-right list row like the rest of detail-stats. */
.detail-value-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-4); margin: var(--sp-5) 0 0; padding-top: var(--sp-5); border-top: 1px solid var(--line); }
.detail-value-cell { display: grid; gap: 2px; min-width: 0; }
.detail-value-cell dt { margin: 0; font-size: var(--fs-cap); text-transform: uppercase; letter-spacing: 0.05em; color: var(--ink-3); }
.detail-stats dt { letter-spacing: 0.06em; text-transform: uppercase; font-weight: 500; }
.detail-value-cell dd { margin: 0; font-family: var(--font-display); font-size: var(--fs-h2); font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.detail-subhead { margin: var(--sp-5) 0 var(--sp-2); padding-top: var(--sp-5); border-top: 1px solid var(--line); font-size: var(--fs-cap); text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-3); }
.detail-drivers { display: grid; gap: var(--sp-3); }
.detail-source { margin: 4px 0 0; color: var(--ink-2); font-size: var(--fs-sm); }

/* R4: section index */
.section-nav {
  display: flex; gap: var(--sp-1); flex-wrap: wrap; overflow: visible; position: static; top: auto; z-index: 1; background: var(--paper); padding: var(--sp-2) 0; border-bottom: 1px solid var(--line); margin-bottom: 4px;
}
.section-nav button { min-height: 32px; padding: 0 var(--sp-2); font-size: var(--fs-meta); border: 1px solid var(--line-2); background: var(--surface); color: var(--ink-2); border-radius: var(--r-sm); flex: none; }
.section-nav button[aria-current="true"] { background: var(--ink); color: var(--surface); border-color: var(--ink); }
#headline:focus, article:focus, h2:focus { outline: none; }
#headline, article[id^="sec-"], h2, .ladder-toggle { scroll-margin-top: 16px; }

/* R4: mobile inputs bottom sheet */
.edit-inputs-fab { display: none; }
.sheet-backdrop { display: none; }
.inputs-sheet-head { display: none; }

/* R4: finance statement notes */
.stmt-price-used { color: var(--accent); }
.stmt-funding { color: var(--warn); }

/* R4: advanced host panels */
.advanced-panel { margin-top: 24px; padding-top: var(--sp-4); border-top: 1px solid var(--line); }
.advanced-panel:first-child { margin-top: 0; border-top: 0; padding-top: 0; }
.advanced-panel-title, .advanced-panel-summary { display: none; }
.advanced-panel-body { min-width: 0; }
.advanced-jump { display: flex; flex-wrap: wrap; gap: var(--sp-1); margin: 10px 0 18px; }
.advanced-jump a {
  min-height: 32px; display: inline-flex; align-items: center; padding: 0 var(--sp-2); font-size: var(--fs-meta); color: var(--ink-2);
  border: 1px solid var(--line-2); background: var(--surface); text-decoration: none; border-radius: var(--r-sm);
}
.advanced-jump a:hover { color: var(--ink); }

/* R4: secondary/converted unit mini fields */
.field-mini { margin-top: -4px; opacity: 0.9; }
.field-mini label { font-size: var(--fs-meta); color: var(--ink-3); }
.field-mini .control { min-height: 36px; }
.field-mini .control input { font-size: var(--fs-body); }

/* R5: capacity-factor / output mode, CAPEX boundary, FX blocking */
.cf-field-wrap, .capex-field-wrap { display: grid; gap: var(--sp-1); }
.cf-field-wrap > label { font-size: var(--fs-body); }
.cf-field-wrap .cf-inner label { display: none; }
.cf-derived { margin: 2px 0 0; color: var(--ink-2); font-size: var(--fs-meta); }
.cf-derived.warn { color: var(--warn); }
.capex-boundary-row { display: flex; align-items: center; gap: var(--sp-2); margin-top: 6px; flex-wrap: wrap; }
.capex-boundary-row select { min-height: 36px; border: 1px solid var(--line-2); background: var(--surface); }
.capex-fx-prompt { margin: 6px 0 0; color: var(--warn); font-size: var(--fs-meta); }
.fx-prompt { margin: 8px 0 0; color: var(--warn); font-size: var(--fs-body); max-width: 62ch; }
.plausibility-warning { margin: 8px 0 0; color: var(--warn); font-size: var(--fs-body); max-width: 62ch; }
.number.fx-blocked { opacity: 0.4; }

/* 3-2 step2 (COMPONENTS.md 6): inline detail (replaces the edge drawer). No coloured left rail --
   the panel is identified by the caret pointing at its row instead. Radius 12 matches every other
   floating panel in the system (sources panel, sheet); the old flat left accent bar is dropped. */
.inline-detail {
  position: relative;
  /* R10 Move 3: 6px (down from 10px) keeps the gap between the tornado's top bar and its open
     detail within the spec's ~8px window ("detail top within 8px of the bar"). The caret (8px,
     positioned by JS at the triggering row's own x) sits inside that gap. */
  margin: 14px 0 6px; padding: var(--sp-5); border: 1px solid var(--line); border-radius: 0; background: var(--surface);
  opacity: 1; transform: translateY(0);
  transition: opacity var(--dur-panel) var(--ease-panel), transform var(--dur-panel) var(--ease-panel);
}
@starting-style { .inline-detail { opacity: 0; transform: translateY(4px); } }
@media (prefers-reduced-motion: reduce) {
  .inline-detail { transition: opacity var(--dur-panel) ease; transform: none; }
  @starting-style { .inline-detail { transform: none; } }
}
.inline-detail-caret {
  position: absolute; top: -5px; width: 8px; height: 8px; background: var(--surface);
  border-left: 1px solid var(--line); border-top: 1px solid var(--line); transform: translateX(-50%) rotate(45deg);
  transition: left var(--dur-panel) var(--ease-panel), transform var(--dur-panel) var(--ease-panel);
}
@media (prefers-reduced-motion: reduce) {
  .inline-detail-caret { transition: none; }
}
.inline-detail-head { display: flex; justify-content: space-between; align-items: center; gap: var(--sp-3); }
.inline-detail-title { font-family: var(--font-display); font-weight: 500; font-size: var(--fs-h3); margin: 0; }
/* 32px visible ghost circle inside a 44px hit box (same "visible small / hit 44" split as the
   slider thumb), so the close control clears the touch-target floor without looking oversized. */
.inline-detail-close {
  position: relative; width: 44px; height: 44px; min-height: 0; min-width: 0; padding: 0; border: 0;
  background: transparent; color: var(--ink-2); font-size: var(--fs-input); border-radius: var(--r-round);
  display: flex; align-items: center; justify-content: center; flex: none;
}
.inline-detail-close::before {
  content: ""; position: absolute; inset: 6px; border-radius: var(--r-round); background: transparent; z-index: -1;
  transition: background var(--dur-panel) ease-out;
}
@media (hover: hover) and (pointer: fine) {
  .inline-detail-close:hover::before { background: var(--surface-2); }
}
.inline-detail-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.row-detail-open { background: color-mix(in srgb, var(--row-accent, var(--accent)) 8%, transparent); }
.inline-detail.closing { opacity: 0; transform: translateY(4px); transition: opacity var(--dur-panel) var(--ease-panel), transform var(--dur-panel) var(--ease-panel); }
@media (prefers-reduced-motion: reduce) {
  .inline-detail.closing { transform: none; transition: opacity var(--dur-panel) ease; }
}
.ladder-detail-item { list-style: none; border-bottom: 0; padding: 0; }
.bar-row-clickable[aria-label] { scroll-margin-top: 80px; }
.trace-list { display: grid; gap: var(--sp-2); margin-top: 6px; }
.trace-row { display: grid; grid-template-columns: 1fr auto auto; gap: var(--sp-1) var(--sp-3); align-items: baseline; padding: var(--sp-2) 0; border-bottom: 1px solid var(--line); font-size: var(--fs-sm); }
.trace-name { font-weight: 500; }
.trace-val { font-variant-numeric: tabular-nums; text-align: right; }
.trace-tag { color: var(--ink-3); font-size: var(--fs-meta); grid-column: 2 / 3; text-align: right; }
.trace-tag.user { color: var(--accent); }
.trace-src { grid-column: 1 / -1; color: var(--ink-2); font-size: var(--fs-meta); }

/* R5: conventions note beside the ladder */
.conventions-note { margin-top: 20px; padding-top: var(--sp-4); border-top: 1px solid var(--line); }
.conventions-heading { margin: 0 0 8px; font-size: var(--fs-sm); color: var(--ink-2); text-transform: uppercase; letter-spacing: 0.04em; }
.conventions-list { display: grid; gap: var(--sp-2); margin: 0; }
.conventions-row { display: grid; gap: var(--sp-1); }
.conventions-row dt { font-weight: 500; font-size: var(--fs-sm); }
.conventions-row dd { margin: 0; color: var(--ink-2); font-size: var(--fs-sm); }
.conventions-caveat { margin: 10px 0 0; color: var(--ink-3); font-size: var(--fs-meta); }
.conventions-value { color: var(--ink); font-weight: 500; font-variant-numeric: tabular-nums; }
.conventions-diff { font-variant-numeric: tabular-nums; }

/* R5: cash chart off-scale note */
.cash-note { margin: 8px 0 0; color: var(--ink-3); font-size: var(--fs-meta); max-width: 62ch; }

/* R5: Case B full input set */
.caseb-heading { margin-top: 20px; }
.caseb-fields { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: var(--sp-3); margin-top: 10px; }
.caseb-field .control input { font-size: var(--fs-input); }
@media (pointer: coarse) {
.caseb-field .control input { min-height: 44px; }
}
@media (max-width: 599px) {
.caseb-field .control { height: 44px; min-height: 44px; }
.caseb-field .control input { height: 44px; min-height: 44px; }
}

/* R6 Move 4: two-column first screen for Standard/Pro. Left column keeps the headline + composition;
   right column carries that stage's signature figure (tornado in Standard, cash flow in Pro). */
/* R7: 45/55 left the composition chart's plot track at ~119px (R6 Opus/Sol: "squeezed to about
   110px"). Widening the left column to 55/45 and switching the fold-only composition rows to a
   stacked (label above, track+value below) layout together get the plot track to ~360px+ while the
   right column (tornado / cash flow) only narrows by about the same margin the left column gains. */
.result-fold-grid {
  display: grid; grid-template-columns: minmax(0, 1.45fr) minmax(260px, 0.75fr); gap: var(--sp-6); align-items: start; margin-top: 28px;
}
.result-fold-grid.fold-single { grid-template-columns: minmax(0, 1fr); }
.result-fold-left, .result-fold-right { min-width: 0; }
.result-fold-right > header { margin-bottom: 6px; }
/* H-10 (3-3, Hayeon: "1440 (narrow side column) ... consider giving the chart more width in
   Standard if the column is too narrow"): at the widest breakpoint this app has (>=1181px, above
   the 901-1180 fold ratio below) the tornado's own column was the *narrowest* of the three fold
   ratios app.css defines (0.75fr / 260px, versus 0.9fr / 220px in the medium band) even though the
   viewport itself is largest -- documented layout change, widening it here to give the tornado
   noticeably more room without touching the medium-width ratio or Pro's full-width cash-flow
   column (grid-template-columns is shared by both stages; Pro sets fold-single and hides this
   column entirely, so it never sees this ratio). */
@media (min-width: 1181px) {
  .result-fold-grid { grid-template-columns: minmax(0, 1.2fr) minmax(320px, 0.95fr); }
}
.result-fold-right .lede { margin-bottom: 4px; }
/* documented exception: 72px does not sit on the --fs-* scale. R6 Move 4 sized the two-column
     fold headline between --fs-h2 (28) and --fs-display (88) to fit the narrower left column
     without wrapping or overflow; changing it is a layout decision for a later 3-2 bundle, not
     this token pass. */
.number { font-size: var(--fs-figure); }
.result-fold-left .number { font-size: var(--fs-figure); }
.result-fold-left .number-row { min-height: 76px; }
.result-fold-left .metric-name { display: none; }
@media (min-width: 901px) {
  /* R9 4a: R7's stacked "label above track" layout inside this narrower fold column read as a
     different grammar from Quick's label-beside-track row (the R8/R9 review: ~50px gaps, a detached
     value column). Keep the same label / bar / value-at-bar-end row everywhere and narrow the two
     fixed columns instead of stacking them, to still recover width for the track. */
  .result-fold-left .bar-row { grid-template-columns: 176px minmax(0, 1fr); }
  .result-fold-left .bar-axis-row { grid-template-columns: 176px minmax(0, 1fr) 54px; }
  .result-fold-left .bar-label { white-space: nowrap; }
  /* Q1-2 (3-3 r1c): keep the Total row's label wrapping (not the cost rows' forced nowrap) at
     this narrower fold width too; the grid itself stays the normal 176px 2-column track shared
     with every cost row (see the base .bar-row-total rule above). */
  .result-fold-left .bar-row-total .bar-label { white-space: normal; }
}
@media (min-width: 901px) and (max-width: 1180px) {
  .result-fold-grid { grid-template-columns: minmax(0, 1.2fr) minmax(220px, 0.9fr); }
  .result-fold-left .bar-label { white-space: normal; }
}

/* R6 Move 3: electricity-sourcing figure ("If you bought power differently"). */
.power-figure { margin: 26px 0 0; }
.power-bars { display: grid; gap: var(--sp-2); margin-top: 4px; }
/* R8 1c: a visible legend line for the "your case" reference tick, since a hover-only title gives
   no visible label at all (and the tick repeats once per bar, so labelling it in place would be
   noisy). */
.power-figure-legend { margin: 2px 0 10px; font-size: var(--fs-meta); color: var(--ink-2); display: flex; align-items: center; gap: var(--sp-1); }
.power-figure-legend-swatch { display: inline-block; width: 2px; height: 12px; background: var(--accent); }
.power-bar-tag { margin-left: 8px; }
.power-bar-row {
  display: grid; grid-template-columns: 168px minmax(0, 1fr) 64px; gap: var(--sp-2); align-items: center; min-height: 34px;
  font-size: var(--fs-sm); cursor: pointer; border: 0; background: transparent; text-align: left; width: 100%; padding: 0; font: inherit; color: inherit;
}
.power-bar-row:hover .power-bar-label, .power-bar-row:focus-visible .power-bar-label { color: var(--accent); }
.power-bar-label { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.power-bar-meta { display: block; color: var(--ink-3); font-size: var(--fs-cap); margin-top: 1px; }
/* R10 Move 4: bars 18px, matching the composition legend rows above. */
.power-bar-track { position: relative; height: 18px; background: transparent; }
.power-bar-fill { position: absolute; left: 0; top: 0; height: 18px; background: var(--ink-3); }
@media (max-width: 600px) {
  .power-bar-track { background: var(--surface-2); border: 1px solid var(--line-2); box-sizing: border-box; }
}
.power-bar-row.current .power-bar-fill { background: var(--ink); }
.power-bar-row.current .power-bar-label { font-weight: 600; color: var(--ink); }
.power-bar-value { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: right; font-variant-numeric: tabular-nums; }
.power-bar-marker { position: absolute; top: -4px; width: 1px; height: 26px; background: var(--accent); }
@media (max-width: 600px) {
  .power-bar-row {
    grid-template-columns: minmax(0, 1fr) minmax(0, 64px);
    grid-template-areas: "head head" "track value";
    align-items: center;
    row-gap: 4px;
  }
  .power-bar-row > span:first-child { grid-area: head; display: block; }
  .power-bar-track { grid-area: track; width: 100%; }
  .power-bar-value { grid-area: value; }
  .power-bar-marker { top: 0; width: 2px; height: 100%; min-height: 18px; }
}
/* R7: the Build years panel reuses the power-bar-row component but its values (construction-year
   CAPEX, often 7-8 digits) must never truncate with an ellipsis the way a short preset price does. */
.power-bar-row.wide-value { grid-template-columns: 92px minmax(0, 1fr) auto; }
.power-bar-row.wide-value .power-bar-value { max-width: 64px; }
.power-bar-row.wide-value .power-bar-value { overflow: visible; text-overflow: clip; white-space: nowrap; }

/* R6 STOP: Advanced panels behind chips, one open at a time. */
.advanced-chips { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin: 10px 0 20px; }
.advanced-chip {
  min-height: 36px; padding: 0 var(--sp-3); font-size: var(--fs-sm); border: 1px solid var(--line-2); background: var(--surface); color: var(--ink-2); border-radius: var(--r-pill);
}
.advanced-chip[aria-pressed="true"] { background: var(--ink); color: var(--surface); border-color: var(--ink); }
.advanced-chip.unverified { border-style: dashed; }
/* R6 STOP: only the active panel renders; the other four stay mounted in the DOM (host contract
   unchanged, state preserved) but are not displayed, so a sighted user sees one panel, not five
   stacked. A real pointer click cannot land on suppressed pixels, so an automated check that drives
   a panel without first choosing its chip will not find it -- documented in the round report. */
.advanced-panel { margin-top: 0; padding-top: 0; border-top: 0; display: none; }
.advanced-panel.active { display: block; }


@media (max-width: 900px) {
  .top, main { padding-left: var(--sp-4); padding-right: var(--sp-4); }
  /* R9 (G5 fix): on a short mobile viewport, the fixed "Edit inputs" FAB always occupies the
     bottom ~52px of the screen. In Standard/Pro the compact jump-nav (item 2) plus the existing
     fold-grid and composition top margins pushed the last composition rows (e.g. "Water", "Stack
     replacement") down far enough to land under that fixed footer at the initial scroll position.
     Tightening these three spacings (safe on Quick too, which already had headroom to spare)
     reclaims enough height that composition stays clear of the FAB's rectangle in every stage. */
  .result-fold-grid { margin-top: 8px; }
  .composition { margin: 12px 0 0; }
  .headline-block { padding: var(--sp-1) 0 var(--sp-1); }
  .quick-preview { margin: 8px 0 0; padding: var(--sp-2); display: none; }
  .quick-preview-row { padding: var(--sp-1) 0; }
  .composition { margin: 0; }
  .bar-row { min-height: 44px; }
  .bar-row .bar-label { font-size: var(--fs-sm); line-height: 1.15; }
  figcaption { margin-bottom: 7px; }
  .bar-axis-tick { font-size: var(--fs-cap); }
  .condition, .was-line { margin-top: 4px; }
  .bar-value-num, .bar-share { font-size: var(--fs-meta); }
  /* R10 Move 1: mobile masthead -- crumb+title stay on one line, tabs wrap to their own row, and
     the text-link actions tuck to the right of the tab row (order 2/3) so the whole masthead still
     reads as one compact block instead of the old 3 full-width stacked rows. */
  .top { padding-top: var(--sp-2); }
  .masthead-row { flex-wrap: nowrap; row-gap: 0; min-height: 0; border-bottom: 0; align-items: stretch; flex-direction: column; }
  .masthead-left { flex: 1 1 auto; min-height: 44px; height: 44px; align-items: center; width: 100%; }
  .masthead-title { font-size: var(--fs-section); line-height: 44px; }
  .crumb { display: none; }
  .stages { order: 3; flex: 1 1 100%; height: 44px; min-height: 44px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
  .stages button { min-height: 44px; height: 44px; padding: 0 var(--sp-3); }
  .masthead-actions { order: 4; gap: 0; position: static; height: auto; width: 100%; flex-wrap: wrap; justify-content: flex-start; }
  .masthead-actions .text-link { min-height: 44px; height: 44px; padding: 0 var(--sp-2); font-size: var(--fs-meta); }
  .stages { padding-right: 0; }
  .masthead-row .status-line { display: none; }
  main { grid-template-columns: 1fr; gap: var(--sp-2); padding-top: var(--sp-4); }
  .inputs { position: static; }
  .result { order: -1; }
  .number { font-size: var(--fs-figure); }
  .number-row { min-height: 0; flex-wrap: wrap; }
  .number.pending { font-size: var(--fs-h3); max-width: none; }
  .field, .control, .control input, .control select { max-width: 100%; }
  .status-line { flex-wrap: wrap; gap: var(--sp-1) var(--sp-3); }
  .sell-row, .sell-row dl { grid-template-columns: 1fr; display: grid; }
  .headline-block { position: static; background: var(--paper); padding: var(--sp-2) 0 var(--sp-2); border-bottom: 1px solid var(--line); }
  .bar-row { grid-template-columns: 108px minmax(0, 1fr); }
  .bar-axis-row { grid-template-columns: 108px minmax(0, 1fr) 52px; }
  /* Q1-2 (3-3 r1c): keep the Total row's label wrapping at this narrower mobile width too; the
     grid itself stays the normal 108px 2-column track shared with every cost row. */
  .bar-row-total .bar-label { white-space: normal; }
  .power-bar-row { grid-template-columns: minmax(0, 1fr) minmax(0, 64px); grid-template-areas: "head head" "track value"; align-items: center; }
  /* R7: power-sourcing labels ("Grid, mid merit", "Surplus power") and their CF/price meta line
     were ellipsis-truncated at this width ("Grid, mid m...", "Surplus po..."); wrap to two lines
     instead so the full name and numbers stay readable without opening the detail. */
  .power-bar-label, .power-bar-meta { white-space: normal; overflow: visible; text-overflow: clip; }
  .power-bar-label { line-height: 1.2; }
  .power-bar-row.wide-value { grid-template-columns: minmax(0, 1fr) minmax(0, 64px); }
  .result-fold-grid { grid-template-columns: 1fr; gap: var(--sp-4); }
  .tornado-svg text { font-size: var(--fs-meta); }
  main, .result, .std, .pro, article, .table-scroll, .stmt-wrap, figure, .paired-row, .diff-row, .target-row { min-width: 0; }
  .table-scroll { width: 100%; }
  .sources-panel { padding: var(--sp-4); }
  .version-notice { padding: var(--sp-3) var(--sp-4); }

  /* R4: mobile inputs bottom sheet + FAB */
  .inputs {
    /* R7: 82vh let the sheet cover the headline and composition entirely, so dragging a slider gave
       no visible feedback (rubric "change and see immediately"). Half height leaves the top of the
       screen, including the composition chart, visible while editing; the pinned mini-headline
       below is a second, belt-and-suspenders way to see the live price even at half height. */
    position: fixed; left: 0; right: 0; bottom: 0; top: auto; max-height: 50vh; background: var(--surface);
    border-top: 1px solid var(--line); border-radius: var(--r-float) var(--r-float) 0 0; box-shadow: var(--shadow-sheet);
    transform: translateY(100%); transition: transform var(--dur-panel) var(--ease-panel); z-index: 30;
    padding: 0 var(--sp-4) calc(var(--sp-5) + env(safe-area-inset-bottom)); overflow-y: auto;
  }
  .inputs.sheet-open { transform: translateY(0); }
.inputs-sheet-head { display: flex; flex-direction: column; gap: var(--sp-1); padding: 18px 0 var(--sp-3); position: sticky; top: 0; z-index: 2; background: var(--surface); border-bottom: 1px solid var(--line); }
  .inputs-sheet-head::before {
    content: ""; position: absolute; top: 8px; left: 50%; width: 36px; height: 4px; margin-left: -18px;
    border-radius: var(--r-pill); background: var(--line-2);
  }
  .inputs-sheet-head-row { display: flex; justify-content: space-between; align-items: center; }
  /* R7: pinned live price + was\u2192now trace at the top of the sheet, so a slider drag is visible
     even while the sheet covers the main headline underneath it. */
  .inputs-sheet-price { margin: 0; display: flex; align-items: baseline; gap: var(--sp-2); font-family: var(--font-display); font-size: var(--fs-h3); font-variant-numeric: tabular-nums; }
  .inputs-sheet-price-was { font-family: var(--font-ui); font-size: var(--fs-meta); color: var(--ink-2); }
  /* R8 3b: the pinned sheet head also carries a compact one-line cost composition (a thin stacked
     bar of the same category colours as the main composition chart) so it stays visible next to the
     live price while a slider drag is in progress. Segments are plain spans with an inline width
     percentage; they need an explicit flex/block layout to actually take up that width. */
  .inputs-sheet-mini-bar { display: flex; height: 6px; border-radius: var(--r-sm); overflow: hidden; background: var(--line); margin-top: 2px; }
  .inputs-sheet-mini-seg { display: block; height: 100%; }
.sheet-done { min-height: 44px; padding: 0 var(--sp-4); border: 1px solid var(--line); background: var(--surface-2); border-radius: var(--r-ctrl); color: var(--ink); }
.sheet-done:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.sheet-backdrop { display: block; position: fixed; inset: 0; background: var(--scrim); z-index: 29; opacity: 0; pointer-events: none; transition: opacity var(--dur-panel) var(--ease-panel); }
  .sheet-backdrop.open { opacity: 1; pointer-events: auto; }
  body.sheet-locked { overflow: hidden; }
  html:has(body.sheet-locked) { overflow: hidden; }
  /* R6 Move 2: the floating pill is replaced by a full-width sticky bar that reserves its own space
     (body padding-bottom) so it never covers results underneath it. */
  body { padding-bottom: calc(52px + env(safe-area-inset-bottom)); }
  .toast { bottom: 66px; }
  .edit-inputs-fab {
    display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 15; width: 100%;
    min-height: 52px; padding: 0 var(--sp-4); border: 0; border-top: 1px solid var(--line-2);
    background: var(--surface-2); color: var(--ink); font-size: var(--fs-body); text-align: left;
    box-shadow: 0 -2px 10px var(--shadow-ink);
  }
  .edit-inputs-fab .fab-label { font-weight: 600; }
  .edit-inputs-fab .fab-price { color: var(--ink-2); font-variant-numeric: tabular-nums; }
  .section-nav { position: static; top: auto; overflow-x: auto; flex-wrap: nowrap; -webkit-overflow-scrolling: touch; }
  .section-nav button { flex: none; }
  /* R8 3a: right-edge fade + chevron cue while more chips sit off-screen to the right (Pro-only
     "Cash flow & statements" and "Advanced" chips were reachable but had no visible cue). */
  .section-nav.has-more::after {
    content: "\203a"; position: sticky; right: 0; display: flex; align-items: center; justify-content: center;
    flex: 0 0 28px; width: 28px; align-self: stretch; margin-left: -28px; font-size: var(--fs-input); color: var(--ink-2);
    background: linear-gradient(to right, var(--transparent) 0%, var(--paper) 65%);
    pointer-events: none;
  }
  /* R10 Move 4: the compact-breakpoint native <select> (R9 item 2) is replaced by the same chip row
     desktop uses, wrapped (not clipped, not a horizontal scroller) so every destination stays a
     directly visible, one-tap chip; still position: static (R9's own fix), so it scrolls away with
     the page and can never cover content beneath it. */
  .section-nav,
  .section-nav.section-nav-compact {
    position: static; top: auto; overflow-x: auto; flex-wrap: nowrap; white-space: nowrap; padding: var(--sp-1) 0; z-index: 1;
  }
  .section-nav.section-nav-compact button { flex: none; }
  .section-nav.section-nav-compact.has-more::after { content: "\203a"; }

.capex-total-row { flex-direction: column; align-items: stretch; }
  .inline-detail { padding: var(--sp-4); }
  /* S-04: 390px value cells stack (one column, not two) and the value never ellipsizes -- the old
     two-column 154px cell truncated "0.95 USD/kg" to "0.95 USD/..." (V-1-1/I-1-5). 24px matches the
     orchestrator's mobile value-font spec; --fs-h2 (28px) stays for 1440. */
  .detail-value-grid { grid-template-columns: 1fr; gap: var(--sp-2); padding-top: var(--sp-4); margin-top: var(--sp-4); }
  .detail-subhead { padding-top: var(--sp-4); margin-top: var(--sp-4); }
  .detail-value-cell dd { font-size: var(--fs-title); white-space: normal; overflow: visible; text-overflow: clip; }
  .trace-row { grid-template-columns: 1fr; }
  .trace-val, .trace-tag { text-align: left; grid-column: 1 / -1; }
  .caseb-fields { grid-template-columns: 1fr; }

  /* R10 Move 5: mobile 390 as the same instrument -- the hero bar sticks under the masthead once
     scrolled to it, and the four Quick sliders sit inline right beneath it (own sticky offset,
     measured in JS since the hero bar's height varies) so dragging one moves the sticky bar
     directly, with no "Edit inputs" sheet in the way. Both classes are only ever added by JS on
     Quick at this breakpoint (updateMobileStickyOffsets), so Standard/Pro and desktop are unaffected. */
  .hero-bar-wrap.mobile-sticky {
    position: sticky; top: 0; z-index: 12; background: var(--paper);
    margin: 0 -16px; padding: var(--sp-2) var(--sp-4) var(--sp-1); box-shadow: 0 2px 6px var(--shadow-ink);
  }
  .mobile-quick-sliders { display: none; }
  .mobile-quick-sliders:not([hidden]) { display: block; }
  .mobile-quick-sliders.mobile-sticky-follow {
    position: sticky; z-index: 11; background: var(--paper);
    margin: 0 -16px; padding: 0 var(--sp-4) var(--sp-1); border-bottom: 1px solid var(--line);
  }
  /* S-09: label+value share one line; the range gets its own full-width line beneath, instead of
     splitting one 390px row three ways (a 107.5px rail, V-1-7 -- a 10px drag swung the price
     0.041->0.047 USD/kWh). This also gives the row-clickable travel a coarse-pointer target hit its
     own 44px height via .field-slider-wrap, not a sliver of a shared row. */
  .mobile-slider-row { display: flex; flex-direction: column; gap: 2px; min-height: 56px; padding: var(--sp-1) 0; }
  .mobile-slider-meta { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-2); flex-wrap: wrap; }
  .mobile-slider-label { font-size: var(--fs-meta); color: var(--ink-2); line-height: 1.2; }
  .mobile-slider-value { min-width: 4.5em; text-align: right; font-variant-numeric: tabular-nums; font-size: var(--fs-sm); }
  .mobile-slider-input { width: 100%; }
}

@media (min-width: 901px) and (max-width: 1180px) {
  .masthead-row { flex-wrap: wrap; min-height: 0; row-gap: 0; }
  .masthead-left { flex: 1 1 100%; min-width: 0; height: 44px; }
  .masthead-title { font-size: var(--fs-section); }
  .crumb { display: none; }
  .masthead-row .status-line { display: none; }
  .stages { order: 3; flex: 1 1 auto; }
  .masthead-actions { order: 4; margin-left: auto; }
}

/* C3-04 cash flow: two stacked plots, titles off the grid, ticks inset. */
.cash-chart { width: 100%; height: auto; display: block; overflow: visible; }
.cash-plot-title { font-size: var(--fs-label); font-weight: 600; fill: var(--ink); }
.cash-chart text.cash-tick { font-size: var(--fs-cap); fill: var(--ink-3); }
.cash-grid { stroke: var(--line); stroke-width: 1; }
.cash-zero { stroke: var(--line-2); stroke-width: 1; }
.cash-bar.up { fill: var(--cash-pos); }
.cash-bar.down { fill: var(--cash-neg); }
.cash-bar[data-capped="true"] { fill: var(--cash-neg); }
.cash-break { stroke: var(--paper); stroke-width: 1.6; stroke-linecap: round; }
.cash-cap-value { font-size: var(--fs-meta); fill: var(--ink); font-variant-numeric: tabular-nums; }
.cash-key { display: flex; gap: 16px; align-items: center; margin: 0 0 6px; font-size: var(--fs-meta); color: var(--ink-2); }
.cash-key i { display: inline-block; width: 10px; height: 10px; margin-right: 6px; vertical-align: -1px; }
.cash-key .in { background: var(--cash-pos); }
.cash-key .out { background: var(--cash-neg); }
.sell-axis-cluster-note {
  position: absolute; left: 12px; top: -28px; white-space: nowrap;
  font-size: var(--fs-meta); color: var(--ink-2);
}
.cash-line { fill: none; stroke: var(--ink); stroke-width: 2; }
.cash-payback-dot { fill: var(--accent); }
.cash-payback-label { font-size: var(--fs-meta); font-weight: 600; fill: var(--accent); stroke: var(--paper); stroke-width: 4px; paint-order: stroke fill; }
.cash-cap-note { font-size: var(--fs-meta); fill: var(--ink-2); }
.cash-year-tick { stroke: var(--ink-3); stroke-width: 1; }
.cash-build { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line); }
.cash-build-title { margin: 0 0 6px; font-size: var(--fs-label); font-weight: 600; }
.cash-build-equal { margin: 0; font-variant-numeric: tabular-nums; color: var(--ink-2); }
.cash-build-row { display: grid; grid-template-columns: 64px minmax(0, 1fr) 72px; gap: 8px; align-items: center; margin-top: 6px; }
.cash-build-track { display: block; height: 10px; background: var(--line); border-radius: 0; }
.cash-build-fill { display: block; height: 10px; background: var(--ink-3); border-radius: 0; min-width: 3px; }

/* C3-05: sales price sits with the outcomes. One price axis under them. */
.sell-layout { display: block; margin-top: 12px; }
.sell-basis { grid-column: 1 / -1; margin: 0; color: var(--ink-2); font-size: var(--fs-meta); max-width: 68ch; }
.sell-price label { display: block; color: var(--ink-3); font-size: var(--fs-meta); }
.sell-price-control { display: flex; align-items: baseline; gap: 8px; }
.sell-price-control input { font-size: var(--fs-h2); font-family: var(--font-display); width: 140px; border: 0; border-bottom: 1px solid var(--ink); background: transparent; min-height: 44px; }
/* G5 (3-2 R2): the highest pin label sits 34px above the lane top (pin top 18px, label offset
   -52px). With only 28px of margin above the lane, that label could land on the "Real 2025 USD
   ..." condition line right above it. */
.sell-axis { position: relative; margin: 64px 0 36px; min-height: 148px; }
.sell-axis-lane { position: relative; height: 112px; margin: 0 8px; }
.sell-axis-pin { position: absolute; top: 14px; width: 6px; height: 14px; margin-left: -3px; }
.sell-axis-pin::before { content: ""; position: absolute; left: 0; top: 8px; width: 6px; height: 6px; border-radius: var(--r-round); background: var(--ink); }
.sell-axis-pin::after { content: ""; position: absolute; left: 2px; top: 0; width: 2px; height: 8px; background: var(--ink); }
.sell-axis-pin.sales { border-color: var(--accent); }
.sell-axis-pin.sales::before, .sell-axis-pin.sales::after { background: var(--accent); }
.sell-axis-pin.levered { border-color: var(--ink-2); }
.sell-axis-pin.levered::before, .sell-axis-pin.levered::after { background: var(--ink-2); }
.sell-axis-name { position: absolute; left: 6px; white-space: nowrap; font-size: var(--fs-meta); color: var(--ink-2); font-variant-numeric: tabular-nums; }
.sell-axis-pin.sales .sell-axis-name { color: var(--accent); font-weight: 600; }
.sell-axis-pin.pin-hollow::before {
  background: var(--paper);
  box-shadow: inset 0 0 0 2px currentColor;
  color: var(--ink);
}
.sell-axis-pin.pin-hollow.sales::before { color: var(--accent); }
.sell-axis-pin.pin-hollow.levered::before { color: var(--ink-2); }
/* C3-06/G5 (3-2 R2): the "below" pin labels in .sell-axis-lane can sit close to the axis-tick
   numbers directly under them (e.g. a required price near a round tick value). The gap below
   clears the label text before the tick row starts. */
.sell-axis-track { position: relative; height: 18px; margin: 18px 8px 0 0; border-top: 1px solid var(--line); }
.sell-axis-tick { position: absolute; top: 4px; transform: translateX(-50%); font-size: var(--fs-meta); color: var(--ink-3); white-space: nowrap; }
.sell-axis-tick.axis-start { transform: none; }
.sell-axis-tick.axis-end { transform: translateX(-100%); }
#sell-metrics { display: grid; grid-template-columns: 1fr 1fr; gap: 16px 24px; margin: 0; }
#sell-metrics > div { border-bottom: var(--table-rule); padding-bottom: 8px; }
#sell-metrics .sell-price { grid-column: 1; }
#sell-metrics dt { color: var(--ink-3); font-size: var(--fs-meta); }
#sell-metrics dd { margin: 0; font-variant-numeric: tabular-nums; font-size: var(--fs-input); font-family: var(--font-display); }
@media (max-width: 700px) {
  .sell-layout, #sell-metrics { grid-template-columns: 1fr; }
}

/* F round: reduced motion zeros transitions. Existing info/detail rules stay. */
@media (prefers-reduced-motion: reduce) {
  .field-slider::-webkit-slider-thumb,
  .field-slider::-moz-range-thumb,
  .unit-pill button,
  .tech button,
  .inline-detail,
  .inline-detail.closing,
  .inline-detail-caret,
  .inline-detail-close::before,
  .info-popover,
  .sources-panel,
  .sheet-backdrop,
  .inputs,
  button {
    transition-duration: 0s !important;
    transition-delay: 0s !important;
    animation-duration: 0s !important;
  }
}
