/* Amproteq Studio — application shell.
 *
 * This file owns shell mechanics only: reset, the scroll law, the splash, the
 * four-region grid (rail, header, left, centre, right) and the rail/header
 * regions themselves. Reusable UI — buttons, panels, forms, tables, the
 * landing, toasts, the modal — lives in components.css, built through
 * web/js/ui/kit.js. Keyframes live in motion.css.
 *
 * ── SCROLLING, and the law that used to be here ───────────────────────────
 * This file used to open with a rule that the application never scrolls,
 * anywhere. That was a misreading, and it is revoked. The customer wanted the
 * COVER PAGE not to scroll, because its overlay depends on there being nothing
 * to scroll behind it. Applying that to every screen broke the product — "now
 * I can't scroll down anywhere" — and caused the two worst layout defects of
 * that round: panels clipped at a pane edge with nothing to say content was
 * missing, and fractional grid rows collapsing to zero so panels printed on
 * top of each other. Neither was a layout fault. Both were the lock.
 *
 * What holds now: the shell is pinned to the viewport so the rail and header
 * stay put and a map can fill its stage, and the three content columns scroll.
 * `:root[data-route='start']` locks the cover page, and nothing else does.
 *
 * The real quality bar was never the absence of a scrollbar. It is that
 * nothing overlaps and nothing is clipped.
 *
 * ── STRUCTURE, and why it is enforced ─────────────────────────────────────
 * Every base rule appears BEFORE the "Responsive" section at the bottom, and
 * every responsive override lives in that one section, in descending
 * max-width order. Two rules of equal specificity resolve by source order,
 * not by which one looks like the "override" — a media-query rule for `.foo`
 * written above a bare `.foo{...}` later in the file LOSES to it at every
 * viewport width, silently. That is not hypothetical: it is exactly how
 * `.app-rail`'s `flex-direction: row` for narrow screens was once overridden
 * back to `column` by the desktop rule sitting below it, and exactly how
 * `.app-shell.is-blank` once beat a media query's bare `.app-shell` on
 * specificity instead. Keeping every override physically last is what makes
 * "last one wins" and "the override wins" the same statement. */

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

html, body {
  height: 100%;
  /* The document itself does not scroll — the shell is pinned to the viewport
   * so the rail and header stay put. Scrolling happens inside the content
   * columns; see the Scrolling section at the top of this file. */
  overflow: hidden;
  margin: 0;
  background: var(--bg-deep);
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--fs-md);
  line-height: var(--lh-normal);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overscroll-behavior: none;
}

/* Display leading plus the trim that contains Jost's glyph box. See the long
 * note on --lh-trim in tokens.css: without the padding, every heading in the
 * product overflows its own element by ~0.3em and contract §1's self-check
 * flags it on every screen. Overriding `padding` (rather than
 * `padding-inline`) on a heading removes the trim and brings it back. */
h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-tight);
  line-height: var(--lh-tight);
  padding-block: var(--lh-trim);
  margin: 0;
}
p { margin: 0; }

button { font: inherit; color: inherit; }
a { color: inherit; text-decoration: none; }

::selection { background: rgba(60, 204, 252, 0.3); color: #fff; }

:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 3px;
  border-radius: var(--r-1);
}

.skip-nav {
  position: absolute;
  left: -9999px;
  top: var(--sp-3);
  z-index: var(--z-toast);
  padding: var(--sp-3) var(--sp-5);
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--r-3);
}
.skip-nav:focus { left: var(--sp-3); }

/* ── The lockup ─────────────────────────────────────────────────────────────
 * The real Amproteq mark — a folded, faceted bolt in cyan to azure — exactly
 * as the marketing site builds it. It is NOT redrawn as an SVG, NOT
 * recoloured and NOT put in a rounded square: the placeholder "A" in a
 * gradient tile that used to sit in the rail is the single thing the customer
 * called out by name.
 *
 * `b` is Jost 300 at .3em. `s` is JetBrains Mono 9px at .2em, muted, behind a
 * 1px rule. Two sizes: the full lockup in the header, the mark alone in the
 * 76px rail, where a wordmark cannot fit and should not be squeezed. */

.lockup {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  flex: none;
  transition: opacity var(--dur-fast) var(--ease);
}
.lockup:hover { opacity: 0.82; }

.lockup img {
  width: 26px;
  height: 26px;
  display: block;
  object-fit: contain;
  /* The mark is bright on a near-black ground; a faint cyan bloom seats it
   * on the surface instead of leaving it floating. */
  filter: drop-shadow(0 2px 10px rgba(24, 152, 252, 0.45));
}

.lockup b {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: 14.5px;
  letter-spacing: 0.3em;
  color: var(--text);
  line-height: var(--lh-jost);
  white-space: nowrap;
}

/* "Studio", in the V7 voice: sentence case, Jost, legible — it was 9 px mono
 * capitals, the one uppercase micro-label left in the shell, on every screen. */
.lockup s {
  text-decoration: none;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 13px;
  letter-spacing: 0.02em;
  text-transform: none;
  color: var(--text-secondary);
  line-height: var(--lh-jost);
  padding-left: 10px;
  margin-left: -2px;
  border-left: 1px solid var(--border-soft);
  white-space: nowrap;
}

/* Rail variant: the mark alone, bigger, centred. */
.lockup-mark { gap: 0; }
.lockup-mark img { width: 30px; height: 30px; }

/* ── Splash ─────────────────────────────────────────────────────────────────
 * Held until the shell is mounted, so nobody sees an unstyled skeleton. A
 * splash that never lifts is a broken product — boot.js backstops this with
 * an 8s timeout regardless of what else happens during boot. */

#splash {
  position: fixed;
  inset: 0;
  z-index: var(--z-onboarding);
  display: grid;
  place-items: center;
  background:
    radial-gradient(60% 50% at 50% 42%, rgba(24, 152, 252, 0.10), transparent 70%),
    var(--bg-deep);
  transition: opacity var(--dur-slow) var(--ease), visibility var(--dur-slow);
}
#splash[hidden] { display: grid; opacity: 0; visibility: hidden; pointer-events: none; }

.splash-inner { display: grid; gap: var(--sp-5); justify-items: center; }

.splash-mark {
  width: 56px;
  height: 56px;
  object-fit: contain;
  filter: drop-shadow(0 6px 28px rgba(24, 152, 252, 0.55));
}

.wordmark { display: grid; justify-items: center; gap: var(--sp-2); }
.wordmark b {
  font-family: var(--font-display);
  font-size: var(--fs-2xl);
  font-weight: 300;
  letter-spacing: 0.3em;
  line-height: 1.4; /* gradient-clipped text must contain its own glyph box */
  /* The wordmark is the one place the gradient runs through type. */
  background: var(--grad);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  text-indent: 0.3em; /* recentre: letter-spacing adds a trailing gap */
}
.wordmark span {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--ls-wider);
  color: var(--text-faint);
  background: none;
  -webkit-text-fill-color: var(--text-faint);
  text-align: center;
}

.splash-bar {
  width: 170px;
  height: 2px;
  background: var(--border-soft);
  overflow: hidden;
}
.splash-bar i {
  display: block;
  height: 100%;
  width: 0%;
  background: var(--grad-r);
  box-shadow: 0 0 12px rgba(60, 204, 252, 0.6);
  transition: width var(--dur) linear;
}

.splash-pct {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  letter-spacing: var(--ls-wider);
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}
.splash-status {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--ls-wide);
  color: var(--text-faint);
  text-transform: lowercase;
}

/* ── Shell grid ─────────────────────────────────────────────────────────────
 * Viewport-locked. `100dvh` over `100vh` so a mobile URL bar collapsing does
 * not leave the last row under the chrome. Every region gets `min-height: 0`
 * and `overflow: hidden` — see the scroll law at the top of this file. */

.app-shell {
  display: grid;
  height: 100vh;
  height: 100dvh;
  max-height: 100dvh;
  overflow: hidden;
  grid-template-columns: var(--w-rail) var(--w-left) minmax(0, 1fr) var(--w-right);
  grid-template-rows: var(--h-header) minmax(0, 1fr);
  grid-template-areas:
    'rail header header header'
    'rail left   center right';
}
.app-shell[hidden] { display: none; }

.app-shell > * { min-width: 0; min-height: 0; }

.app-rail   { grid-area: rail;   border-right: 1px solid var(--border-soft); overflow: hidden; }
.app-header { grid-area: header; border-bottom: 1px solid var(--border-soft); position: relative; overflow: hidden; }
.app-left   { grid-area: left;   border-right: 1px solid var(--border-soft); }
.app-center { grid-area: center; position: relative; }
.app-right  { grid-area: right;  border-left: 1px solid var(--border-soft); }

/* ── Scrolling ──────────────────────────────────────────────────────────────
 *
 * CORRECTED 20 September 2026. This file previously locked the entire
 * application against scrolling, on my reading of "no scrolling allowed". The
 * customer meant the cover page only, and the global lock broke the product:
 * "now I can't scroll down anywhere."
 *
 * It also caused the two worst layout defects of that round. Panels were
 * clipped at a pane edge with nothing on screen to say content was missing,
 * and fractional grid rows resolved to zero height so panels printed on top of
 * one another. Neither was a layout problem. Both were the lock.
 *
 * So: the shell stays pinned to the viewport, which is what keeps the rail and
 * the header in place and lets a map fill its stage — but the three content
 * columns scroll. Navigation never leaves, and content is as tall as it is.
 *
 * The cover page is the single exception, below. */

.app-left, .app-center, .app-right { overflow: auto; overscroll-behavior: contain; }

/* Content stacks from the top and grows past the fold when it needs to. The
 * `min-height: 100%` keeps a short screen filling the column rather than
 * floating in the middle of it. */
.app-left > *, .app-center > *, .app-right > * { min-height: 0; }

/* A stage that must be exactly one viewport — a full-bleed map is the case
 * this exists for. Opt in; do not make it the default, or every long screen
 * silently clips again. */
.stage.is-fill { height: 100%; min-height: 0; overflow: hidden; }

/* The cover page. Its overlay depends on there being nothing to scroll, and it
 * is the only screen that asked for this. boot.js publishes the route. */
:root[data-route='start'] .app-center { overflow: hidden; }
:root[data-route='start'] .stage { height: 100%; min-height: 0; }

/* The panel hosts stack from the top and grow past the fold when they need
 * to; their column scrolls. They used to be `height: 100%; overflow: hidden`,
 * which quietly cut off the last panel in a long stack. */
#app-left-panels,
#app-right-panels {
  min-height: 100%;
  display: grid;
  align-content: start;
  gap: var(--row-gap);
  padding: var(--row-gap);
}

/* Before a project exists (or once every side panel is empty — see
 * chrome.js's setPanels/syncBlank) the side rails have nothing to hold, so
 * the stage takes the full width rather than framing two empty columns. */
.app-shell.is-blank { grid-template-columns: var(--w-rail) 0 minmax(0, 1fr) 0; }
.app-shell.is-blank .app-left,
.app-shell.is-blank .app-right { display: none; }

/* ── Rail ───────────────────────────────────────────────────────────────── */

.app-rail {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(6px, 1.2vh, 14px);
  padding: var(--sp-4) 0 var(--sp-3);
  background: linear-gradient(180deg, rgba(24, 152, 252, 0.05), transparent 220px);
}

.rail-mark {
  flex: none;
  display: grid;
  place-items: center;
  padding: 2px;
}

.rail-items {
  display: grid;
  gap: 2px;
  width: 100%;
  min-height: 0;
  align-content: start;
}

.rail-item {
  position: relative;
  display: grid;
  gap: 4px;
  justify-items: center;
  padding: clamp(5px, 0.9vh, 9px) 0;
  background: none;
  border: 0;
  cursor: pointer;
  color: var(--text-faint);
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.12em;
  line-height: var(--lh-mono);
  text-transform: uppercase;
  transition: color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
/* The active indicator is one of the few places the gradient is allowed to
 * appear — a signature spent sparingly, not a flat cyan border. */
.rail-item::before {
  content: '';
  position: absolute; left: 0; top: 8%; bottom: 8%; width: 3px;
  border-radius: 0 var(--r-1) var(--r-1) 0;
  background: var(--grad-r);
  opacity: 0;
  transform: scaleY(0.4);
  transform-origin: center;
  transition: opacity var(--dur-fast) var(--ease), transform var(--dur) var(--ease);
}
.rail-item:hover { color: var(--text-dim); background: rgba(255, 255, 255, 0.022); }

/* THE ACTIVE STATE, made unmistakable.
 *
 * `aria-current` takes a token and the token for navigation is "page" — the
 * rail used to set the STRINGS "true" and "false" on every item at once,
 * which both announced every item as current and left this selector matching
 * the literal text. It is now `page`, set on one item and absent elsewhere.
 *
 * Three signals rather than one, because a cyan tint on a near-black ground
 * at 76px wide was not carrying: a 3px gradient bar on the leading edge, a
 * wash, and the label going to full brightness and medium weight. Colour is
 * never the only difference between two states in this product. */
.rail-item[aria-current='page'] {
  color: var(--cyan);
  background: linear-gradient(90deg, var(--plasma-wash), transparent 84%);
}
.rail-item[aria-current='page']::before { opacity: 1; transform: scaleY(1); }
.rail-item[aria-current='page'] .rail-item-l {
  color: var(--text);
  font-weight: var(--fw-medium);
  letter-spacing: 0.14em;
}
.rail-item[aria-current='page'] svg { stroke-width: 1.7; filter: drop-shadow(0 0 7px var(--plasma-glow)); }

/* RELATED, not current.
 *
 * `#/numbers` and `#/resource` are ways into a standing module rather than
 * modules beside it, so the rail points at the neighbour — Bill and Site
 * respectively — to keep itself from looking dead. It used to do that by
 * setting `aria-current="page"` on it, which told a screen reader you were on
 * Bill while you were plainly on the numbers screen.
 *
 * The affordance was right and the assertion was wrong, so this is the same
 * signal at a lower volume: the marker bar at half height, the wash gone, the
 * label lifted but not to full. It reads as "the module this belongs to"
 * rather than "you are here", which is what it always meant. */
.rail-item[data-related] { color: var(--text-dim); }
.rail-item[data-related]::before { opacity: 0.55; transform: scaleY(0.5); }
.rail-item[data-related] .rail-item-l { color: var(--text-dim); }

.rail-item-l { line-height: var(--lh-mono); transition: color var(--dur-fast) var(--ease); }

.rail-item svg {
  width: clamp(16px, 2.2vh, 19px);
  height: clamp(16px, 2.2vh, 19px);
  stroke: currentColor;
  fill: none;
  stroke-width: 1.4;
  transition: stroke-width var(--dur-fast) var(--ease);
}

/* Rail footer: the density switch. An instrument earns the right to be
 * denser for whoever has done this a hundred times already. */
.rail-foot {
  margin-top: auto;
  padding-top: var(--sp-3);
  display: grid;
  justify-items: center;
  gap: var(--sp-2);
  width: 100%;
  flex: none;
}

.rail-density {
  display: grid;
  gap: 3px;
  justify-items: center;
  padding: var(--sp-2) 0;
  width: 100%;
  background: none;
  border: 0;
  border-top: 1px solid var(--border-soft);
  cursor: pointer;
  color: var(--text-faint);
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.12em;
  line-height: var(--lh-mono);
  text-transform: uppercase;
  transition: color var(--dur-fast) var(--ease);
}
.rail-density:hover { color: var(--cyan); }
.rail-density b {
  font-weight: var(--fw-medium);
  color: var(--text-dim);
  letter-spacing: 0.14em;
}
.rail-density:hover b { color: var(--cyan); }

/* ── Header ─────────────────────────────────────────────────────────────────
 * ONE dense row. It was two, and it was the second biggest complaint after
 * the placeholder mark: a 112px band of mostly nothing across the top of a
 * screen that is not allowed to scroll. */

/* `minmax(0, auto)` on the chips and the KPI strip, not bare `auto`.
 *
 * A bare `auto` track refuses to go below its min-content width, so when the
 * row ran short the overflow went somewhere — and because `.app-header` is a
 * shell region with `overflow: hidden`, "somewhere" was off the right edge.
 * MEASURED at 1185px wide before this change: the Assumptions and Full report
 * buttons were clipped away entirely, with nothing on screen to say they
 * existed. Allowing those two tracks to shrink means the strip truncates (and
 * the media queries below drop whole cells) instead of the actions vanishing. */
.app-header {
  display: grid;
  grid-template-columns: auto minmax(0, auto) minmax(0, 1fr) minmax(0, auto) auto;
  align-items: center;
  gap: clamp(10px, 1.4vw, 22px);
  padding: 0 clamp(12px, 1.4vw, 20px);
}

.hdr-lockup { padding-right: clamp(10px, 1.2vw, 18px); border-right: 1px solid var(--border-soft); }

/* `min-width: 0` on the track AND `overflow: hidden` here: a chip carries a
 * customer's trading name, which can be any length, and a status line that
 * pushes the actions off the right edge of the header is worse than one that
 * truncates a name it also shows in full on the screen behind it. Each chip
 * ellipsises at 22ch (components.css) before this ever engages. */
.hdr-chips { display: flex; gap: var(--sp-2); align-items: center; min-width: 0; overflow: hidden; }

/* The key, as a prefix inside the chip, printed from `data-k`. Keeping it in
 * an attribute rather than a child element is deliberate: two code paths
 * write the chip's value with `textContent`, which would destroy a child, and
 * the key must survive both. */
.chip[data-k]::before {
  content: attr(data-k);
  font-size: 0.86em;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-faint);
  padding-right: 7px;
  margin-right: 1px;
  border-right: 1px solid var(--border-soft);
  flex: none;
  /* This replaces the status dot that used to live on this pseudo-element.
   * The dot said "filled / unfilled"; an em dash beside a named slot says the
   * same thing and says WHICH slot, in the same width. */
  width: auto; height: auto; border-radius: 0; background: none; opacity: 1;
}
.chip-live[data-k]::before { color: var(--cyan); opacity: 0.72; border-right-color: var(--border-neon); }

/* The spacer column. Empty on purpose: it is what keeps the KPI strip
 * anchored to the right edge as the chips change width. */
.hdr-spacer { min-width: 0; }

/* ── The KPI strip ──────────────────────────────────────────────────────────
 * The label now sits ABOVE the figure, in the DOM as well as on screen. Four
 * figures under four labels read as one strip; four figures over four
 * captions read as four unrelated things, which is most of why this band was
 * judged placeholder text.
 *
 * Hairline separators rather than a 26px gap: the same information in ~40 %
 * less width, which is what stops the actions being pushed off the edge of a
 * 1280px laptop. */
.hdr-kpis {
  display: flex;
  align-items: stretch;
  gap: 0;
  min-width: 0;
  padding-left: clamp(10px, 1.2vw, 18px);
  border-left: 1px solid var(--border-soft);
}
.kpi {
  display: grid;
  justify-items: start;
  align-content: center;
  gap: 1px;
  padding: 0 clamp(9px, 0.85vw, 15px);
  border-left: 1px solid var(--border-soft);
  min-width: 0;
}
.kpi:first-child { border-left: 0; padding-left: 0; }
.kpi:last-child { padding-right: 0; }
.kpi b {
  font-family: var(--font-mono);
  font-size: var(--fs-figure-sm);
  font-weight: var(--fw-medium);
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1, 'zero' 1;
  /* Figures do not track — see --ls-figure in tokens.css. The -0.01em that
   * was here pulled four tabular columns out of alignment with each other. */
  letter-spacing: var(--ls-figure);
  line-height: var(--lh-mono);
  padding-block: var(--lh-trim); /* mono glyph box is 1.25em — see tokens.css */
  color: var(--text);
  white-space: nowrap;
}
/* An unset KPI is a dash the eye skips, not a grey-on-grey number it tries to
 * read. The label above it stays legible, because "this figure does not exist
 * yet" is itself a piece of state worth showing. */
.kpi b.is-empty { color: var(--not-modelled); }
.kpi > span {
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.16em;
  line-height: var(--lh-mono);
  text-transform: uppercase;
  color: var(--text-faint);
  white-space: nowrap;
}

.hdr-actions { display: flex; gap: var(--sp-2); align-items: center; flex: none; }

/* A thin indeterminate bar under the header — never a blocking overlay.
 * Solid cyan, not the gradient: the gradient is spent on the primary
 * button, the rail indicator and one hero number, nowhere else. */
.hdr-busy {
  position: absolute; left: 0; right: 0; bottom: -1px; height: 2px;
  overflow: hidden;
  background: transparent;
  pointer-events: none;
}
.hdr-busy::after {
  content: '';
  position: absolute; top: 0; bottom: 0; left: -30%; width: 30%;
  background: var(--cyan);
  box-shadow: 0 0 10px rgba(60, 204, 252, 0.7);
  opacity: 0;
}
.hdr-busy.is-on::after {
  opacity: 1;
  animation: hdr-busy-sweep 1.1s var(--ease) infinite;
}
@keyframes hdr-busy-sweep {
  0% { left: -30%; }
  100% { left: 100%; }
}

/* ── Stage ──────────────────────────────────────────────────────────────────
 * The stage is exactly one viewport tall, always. Its single child is given
 * the full box and told to fit inside it; nothing here grows to its content.
 * A flow that needs more room segments, or goes sideways. */

/* The stage fills its column when the screen is short and grows past it when
 * the screen is long; the column scrolls. It was `height: 100%; overflow:
 * hidden` with a single fractional row, which clipped a 1,969px results stack
 * into 856px and gave no sign the rest existed.
 *
 * `min-content` rows rather than `1fr`: a fractional row resolves to ZERO when
 * the content is taller than the container, which is how panels ended up
 * printed on top of one another. Use `.stage.is-fill` for a stage that really
 * must be exactly one viewport — a full-bleed map. */
.stage {
  min-height: 100%;
  display: grid;
  grid-template-rows: min-content;
  align-content: start;
  padding: var(--stage-pad-y) var(--stage-pad-x);
}
.stage > * { min-height: 0; min-width: 0; }

/* A screen that owns its own edges opts out: the landing, whose canvas has to
 * reach the corners, and a full-bleed map. Mark the screen's ROOT node
 * `is-bleed` and the stage drops its padding for it. */
.stage:has(> .is-bleed) { padding: 0; }

/* Pro density trims the instrument panel's padding without changing type —
 * a denser read for someone who has done this a hundred times already. */
.app-shell[data-density='pro'] { --stage-pad-y: clamp(10px, 2vh, 20px); --row-gap: clamp(6px, 1vh, 12px); }
.app-shell[data-density='pro'] .panel { padding: var(--sp-4); }

/* ── V6: the header carries the truth, in the right colours ─────────────────
 * A negative KPI (a saving that is a loss) is warm, never white or cyan. */
.kpi b.is-loss { color: var(--loss); }
/* The SITE chip has to be able to hold a coordinate — "28.448°S 21.256°E"
 * plus its key is ~26 characters, and every chip used to ellipsise at 22ch,
 * so a site with no town beside it showed "SITE | 28.448°S 21…". */
#chip-site { max-width: 30ch; }
#chip-project, #chip-tariff { max-width: 28ch; }


/* ══════════════════════════════════════════════════════════════════════════
   V7 SHELL — docs/EXPERIENCE_V7.md §1.1, §1.3, §2.4
   The rail and the header in the V7 language: sentence-case Inter labels
   (the uppercase, letter-spaced mono micro-labels are retired here), one
   project line, four KPIs from one source. Placed after the V6 rules it
   supersedes and before the responsive section, so the media queries below
   still win where they apply.
═══════════════════════════════════════════════════════════════════════════ */

/* ── Rail ───────────────────────────────────────────────────────────────── */

.app-rail { gap: 10px; padding-top: 14px; }

.rail-item {
  gap: 5px;
  padding: 9px 4px 8px;
  font-family: var(--font-body);
  font-size: 11.5px;
  font-weight: 500;
  letter-spacing: 0;
  line-height: 1.2;
  text-transform: none;
  color: var(--text-secondary);
}
.rail-item:hover { color: var(--text); background: rgba(255, 255, 255, 0.03); }
.rail-item-l {
  display: block;
  max-width: 72px;
  text-align: center;
  white-space: normal;
  line-height: 1.2;
}
.rail-item[aria-current='page'] { color: var(--cyan); }
.rail-item[aria-current='page'] .rail-item-l { color: var(--text); font-weight: 600; letter-spacing: 0; }
.rail-item[data-related] .rail-item-l { color: var(--text-dim); }

/* A screen with something to show for this project carries a small filled
 * dot at the icon's shoulder (V7 §1.1). */
.rail-item[data-has]::after {
  content: '';
  position: absolute;
  top: 7px;
  left: calc(50% + 7px);
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--cyan);
  box-shadow: 0 0 0 2px var(--bg-deep), 0 0 8px rgba(60, 204, 252, 0.55);
}
/* An optional step for this project reads quieter; its tooltip says why. */
.rail-item[data-optional]:not([aria-current='page']) svg { opacity: 0.55; }
.rail-item[data-optional]:not([aria-current='page']) .rail-item-l { color: var(--text-faint); }

/* ── Header ─────────────────────────────────────────────────────────────── */

.app-header {
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  gap: clamp(12px, 1.6vw, 26px);
}
.app-header .hdr-spacer { display: none; }

/* The project line: who · tariff · where. One line, truncating before the
 * KPIs or the actions ever move. */
.hdr-project {
  margin: 0;
  min-width: 0;
  display: flex;
  align-items: baseline;
  gap: 0;
  overflow: hidden;
  white-space: nowrap;
  font: 400 13.5px/1.3 var(--font-body);
  color: var(--text-secondary);
}
.hdr-project-empty { color: var(--text-faint); }
/* Only the name may shrink (and ellipsise, as a last resort); the tariff and
 * the town are dropped whole by chrome.js when the line does not fit. */
.hdr-part { flex: none; white-space: nowrap; }
.hdr-part-name { color: var(--text); font-weight: 500; flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.hdr-part.is-dropped { display: none; }
.hdr-part:not([hidden]):not(.is-dropped) ~ .hdr-part:not([hidden]):not(.is-dropped)::before {
  content: '·';
  padding: 0 8px;
  color: var(--text-faint);
}
.hdr-part[hidden], .hdr-project-empty[hidden] { display: none; }

/* KPIs: a sentence-case label over a figure — no more mono capitals. */
.hdr-kpis { padding-left: clamp(12px, 1.3vw, 20px); }
.kpi { gap: 2px; padding: 0 clamp(10px, 1vw, 16px); }
.kpi > span {
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0;
  line-height: 1.3;
  text-transform: none;
  color: var(--text-secondary);
}
.kpi b {
  font-family: var(--font-heading);
  font-size: 16.5px;
  font-weight: 500;
  line-height: 1.25;
  padding-block: 0;
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.kpi b.is-empty { color: var(--text-faint); }
.kpi b.is-loss { color: var(--loss); }
/* "A300 · 3,0 MWh": the product id rides in front of the energy figure, in
 * a quieter weight, and is not part of the figure's own text. */
.kpi-battery { display: inline-flex; align-items: baseline; gap: 0; }
.kpi-battery-id { font-style: normal; font-weight: 400; color: var(--text-secondary); }
.kpi-battery-id:not([hidden])::after { content: '·'; padding: 0 6px; color: var(--text-faint); }
.kpi-battery-id[hidden] { display: none; }
.kpi-battery > span.is-empty { color: var(--text-faint); }

.hdr-actions { gap: 8px; }
.hdr-actions .btn-sm { height: 34px; padding: 0 16px; font-family: var(--font-body); font-size: 13px; border-radius: 999px; }
.hdr-actions .btn-primary:not(:disabled) { font-weight: 600; }

/* ══════════════════════════════════════════════════════════════════════════
   RESPONSIVE
   Every rule below this line is the only place a max-width query is allowed
   to live. Nothing above this line may be repeated below with a plain,
   equal-specificity selector — see the note at the top of the file.
═══════════════════════════════════════════════════════════════════════════ */

/* The header gives up information in a stated order as the row gets tight,
 * one cell at a time, rather than clipping whatever happened to be last.
 *
 * The order is by how much each figure is worth at a glance: Solar goes
 * first (it is null on the bill path anyway), then Storage, then the whole
 * strip, then the chips. Nothing is lost — every one of these figures is on
 * the screen behind the header — and the two ACTIONS never move, because a
 * control you cannot reach is the one failure that is not recoverable by
 * looking somewhere else. */
@media (max-width: 1440px) {
  .kpi { padding: 0 10px; }
}
/* The figure inside a KPI is never a label: whatever the label rules say. */
.kpi b span { font: inherit; letter-spacing: inherit; text-transform: none; color: inherit; }

@media (max-width: 1200px) {
  .kpi:nth-child(4) { display: none; }   /* Solar */
}

@media (max-width: 1100px) {
  .kpi:nth-child(3) { display: none; }   /* Battery */
  .lockup s { display: none; }
}

@media (max-width: 960px) {
  .hdr-project { display: none; }
  .app-header { grid-template-columns: auto minmax(0, 1fr) auto; }
  .hdr-kpis { justify-self: end; }
}

/* Narrow screens drop to rail + centre.
 *
 * `.app-shell.is-blank` must be named here too. It is a two-class selector
 * and therefore beats a bare `.app-shell` inside a media query no matter
 * which comes later in the file — so without it the blank state kept its
 * FOUR-column template while this block set TWO-column areas. The named
 * areas could not resolve against the mismatched track list and the centre
 * column collapsed to zero width: the entire interface rendered blank on
 * every laptop and phone under 1100px, with the markup present and correct
 * the whole time. Any new responsive rule added below this one must keep
 * naming both classes. */
@media (max-width: 1100px) {
  .app-shell,
  .app-shell.is-blank {
    grid-template-columns: var(--w-rail) minmax(0, 1fr);
    grid-template-areas: 'rail header' 'rail center';
  }
  .app-left, .app-right { display: none; }
}

@media (max-width: 860px) {
  .hdr-kpis { display: none; }
}

@media (max-width: 600px) {
  .app-shell,
  .app-shell.is-blank {
    grid-template-columns: 0 minmax(0, 1fr);
    grid-template-rows: auto var(--h-header) minmax(0, 1fr);
    grid-template-areas: 'rail rail' 'header header' 'center center';
  }

  .app-rail {
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    gap: var(--sp-4);
    border-right: 0;
    border-bottom: 1px solid var(--border-soft);
    padding: var(--sp-2) var(--sp-4);
    background: none;
  }
  .rail-items { display: flex; flex-direction: row; gap: var(--sp-2); width: auto; }
  .rail-item { grid-auto-flow: column; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); border-bottom: 2px solid transparent; }
  .rail-item::before { display: none; } /* the desktop indicator assumes a vertical rail */
  .rail-item[aria-current='page'] { border-bottom-color: var(--cyan); background: none; }
  .rail-item span { display: none; }
  /* V7: nine steps and a mark do not fit 390 px; the header already carries the
   * lockup, so the rail drops its mark, and the icon row scrolls inside
   * itself rather than ever pushing the page sideways. */
  .rail-mark { display: none; }
  .rail-items { overflow-x: auto; scrollbar-width: none; }
  .rail-item { padding: 6px 7px; }
  .rail-item[data-has]::after { top: 3px; left: calc(50% + 6px); }
  .rail-foot { margin-top: 0; margin-left: auto; padding-top: 0; width: auto; }
  /* Only the state, not the word for it: "DENSITY" over "GUIDED" is two mono
   * lines at .12em, which ran off the right edge of a 375px rail. */
  .rail-density { border-top: 0; padding: var(--sp-2) var(--sp-3); }
  .rail-density span { display: none; }

  .app-header { padding: 0 var(--sp-4); grid-template-columns: auto minmax(0, 1fr); }
  .hdr-actions { justify-self: end; }
  /* The assumptions stay one tap away on a phone, as a compact i. */
  .hdr-actions #btn-assumptions { width: 34px; padding: 0; font-size: 0; justify-content: center; }
  .hdr-actions #btn-assumptions::before { content: "i"; font: 600 15px/1 var(--font-heading); color: var(--text-dim); }
  .hdr-lockup { border-right: 0; padding-right: 0; }
  .lockup s { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .hdr-busy.is-on::after { animation: none; opacity: 0.7; left: 0; width: 100%; }
}

