/* app.css — styles for the /{locale}/app/ web app page.
   Minimal for now; Tasks 9-14 append their own sections here as each
   surface (locations, forecast, chart, settings, privacy banner) is built.
   Loaded only on the app page (see app.njk's head block) -- content pages
   never pay for it. */

#app {
  display: grid;
  gap: 1.5rem;
}

.app-noscript {
  padding: 1rem;
  border: 1px solid currentColor;
  border-radius: 0.5rem;
}

.privacy-banner[hidden] {
  display: none;
}

/* Task 14 -- dismiss-only privacy banner. Class names here are the
   contract with web/banner.ts's `emitBanner`, which is the only place
   these classes are set on real elements (Task 14W mounts it). No
   Accept/Reject/"manage preferences" affordance exists anywhere in this
   file or in banner.ts -- see banner.ts's header comment for why asking
   would misdescribe the product.

   TASK 8: the dismiss button is now built via web/ui.ts's `button()`, so it
   also carries `ui-button ui-button--secondary` (components.css) alongside
   `.privacy-banner-dismiss` below -- the contract is unbroken (this class
   is still literally emitted, by `emitBanner`, on the one real button),
   just no longer the ONLY styling that button gets. */
.privacy-banner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  padding: 0.75rem 1rem;
  border: 1px solid var(--text-secondary);
  border-radius: 0.5rem;
}

.privacy-banner-text {
  flex: 1 1 20rem;
  margin: 0;
}

.privacy-banner-link {
  color: var(--teal);
  white-space: nowrap;
}

.privacy-banner-dismiss {
  white-space: nowrap;
}

/* Task 11 — the 48-hour chart. renderChart.ts (web/renderChart.ts) builds a
   full-width SVG (bars, grid, bands) plus a visually-hidden `<table>` that
   carries the same values as real text — the SVG is `aria-hidden`, so the
   table is not decoration, it is the chart's only accessible form until
   Task 12 adds a scrub gesture (which still won't help a screen-reader
   user, hence the table existing at all). */

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* A TABLE IGNORES `width: 1px` without this, and the chart's accessible table
   is a table.
   A table's used width is `max(specified, min-content)`, and `white-space:
   nowrap` above makes every cell unbreakable — so min-content was the sum of
   all 8 rows' widest cells: 1054px. `overflow: hidden` cannot clip that,
   because the BOX itself is that wide, and being absolutely positioned it then
   pushed the document's scrollWidth past the viewport. Measured on
   /th/app/: 1485 against a 1470 viewport, a 15px horizontal scroll on every
   locale, caused by an element nobody can see.
   `table-layout: fixed` makes the specified width authoritative. Chosen over
   `display: block` (which drops table semantics in some screen readers, and
   this table IS the chart's whole text alternative) and over dropping
   `white-space: nowrap` (which is load-bearing for the rest of the pattern). */
table.visually-hidden {
  table-layout: fixed;
}

.chart-wrapper {
  width: 100%;
}

.chart-svg {
  width: 100%;
  height: auto;
  display: block;
}

/* The chart's key. Sits under the plot rather than inside it, so it cannot
   scroll away from the marks it explains, and is real text so it is the one
   place the coral grammar and the clock are readable — the SVG above is
   aria-hidden and its marks carry no words. */
.chart-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 0.75rem;
  margin: 0.5rem 0 0;
  padding: 0;
  list-style: none;
  font-size: 0.75rem;
  color: var(--text-secondary);
}

.chart-legend li {
  display: flex;
  align-items: center;
  gap: 0.25rem;
}

/* Fixed box so the four swatches sit on one baseline whatever they draw. */
.chart-legend-swatch {
  width: 20px;
  height: 12px;
  flex: none;
  overflow: visible;
}

/* The user's own limits. DASHED coral — the third treatment in the coral
   grammar, and the only dashed one: thin frame means conditions match, thick
   frame means recommended open window, and a dash means "this is the value you
   chose". Weight and pattern mirror iOS's thresholdLineWidth / thresholdDash. */
.chart-threshold {
  stroke: var(--coral);
  stroke-width: 1;
  stroke-dasharray: 4 4;
}

/* Night: the hours the sun is below the horizon. Black rather than a token
   colour, and at iOS's own 0.45, because this darkens whatever is behind it
   rather than tinting toward a hue — the bars, the gridlines and the coral
   wash all have to stay readable through it. Deliberately NOT coral: the coral
   grammar is the window vocabulary (thin frame / thick frame / dashed limits)
   and night is context, like the gridlines. */
.chart-night {
  fill: #000;
  opacity: 0.45;
}

/* The "conditions match" wash, behind every row. QUALIFYING spans, not open
   ones — see renderChart.ts's buildBandLayer. 0.12 mirrors iOS's
   windowRangeLayer, and lives here rather than in TypeScript so a test reads
   the value that actually ships. It must stay an order of magnitude lighter
   than the frames' stroke, or fill and frame stop reading as two things. */
.chart-band {
  fill: var(--coral);
  opacity: 0.12;
}

/* The two frames. THIN = conditions meet your criteria (Layer 1). THICK =
   recommended open window (Layer 2), nested inside the thin one. Both solid
   and the same colour deliberately: what separates them is weight and extent,
   because dashed coral already means something else — the user's own limits.
   Stroke WIDTH is set per-rect in renderChart.ts, which insets each rect by
   half of it (SVG centres a stroke where SwiftUI's strokeBorder insets it). */
.chart-band-qualifying,
.chart-band-open {
  fill: none;
  stroke: var(--coral);
}

.chart-band-qualifying {
  stroke-opacity: 0.9;
}

.chart-band-open {
  stroke-opacity: 1;
}

/* The open window's length, inside its own frame. Coral like the frame it
   labels, and smaller than a row title because it sits INSIDE a 17-unit band
   rather than beside one. Removed at render time when it does not fit — see
   renderChart.ts's fitOpenBandLabels. */
.chart-band-duration {
  fill: var(--coral);
  font-size: 9px;
}

.chart-band-strip-label {
  fill: var(--teal);
  font-size: 13px;
}

.chart-grid-line {
  stroke: var(--text-secondary);
  stroke-opacity: 0.25;
  stroke-width: 0.5;
}

/* Chart text sizes are viewBox USER UNITS, not CSS pixels -- renderChart.ts
   sets viewBox="0 0 CHART_WIDTH totalHeight", so these scale with the
   rendered width rather than being 8/9 device px. Raised 8/9 -> 11/12 -> 13/14; the
   first bump was deployed but invisible, because buildHash only covered
   app.js so a CSS-only change never busted the shell cache. ROW_HEIGHT is 64
   user units, so 14 still leaves the row label clear of its bars. */
.chart-tick-label {
  fill: var(--text-secondary);
  font-size: 13px;
}

/* Row titles read as the same KIND of label as the band strip's "Window",
   because they are: each names a horizontal band of the chart. Matched to
   `.chart-band-strip-label` on colour, size and weight (2026-08-21) — before
   this the strip was small teal and the rows were bold near-white, so one
   chart had two label voices.

   Only the placement still differs, and that is a constraint rather than a
   choice: "Window" is right-aligned inside the 126-unit gutter, which the row
   titles cannot use — the widest of them is Malayalam's at 160.4 units, so
   right-aligning at the gutter's edge would run it off the left of the canvas.
   They keep their own full-width band above each plot. */
.chart-row-label {
  fill: var(--teal);
  font-size: 13px;
}

/* The window status line — "Open now — close at 3 PM." or "Next window opens
   at …" — in amber, the same token `.card.caution` uses in aerzeit.css.
   UNCONDITIONAL as of 2026-08-20, by request. It was previously scoped to the
   "next window" state alone, on the reasoning that "open now" is not a caution;
   the owner asked for the line itself to be yellow, which makes it one colour
   carrying one piece of information rather than a three-way state encoding.

   A stable class, not `p:nth-of-type(2)`: an order-dependent selector would
   silently repaint the wrong paragraph the next time a line is added to or
   removed from this card — which just happened, when "Currently 68°F" was
   removed. Emitted with classList.add so ui.test.ts's class-parity scanner can
   see it; it reads `classList.add("literal")` but truncates
   `setAttribute("class", expr)` at the first `)`. */
/* QUALIFIED as `.site-main p.forecast-window-status` (0-2-1), NOT the bare
   class (0-1-0). aerzeit.css carries `.site-main p { color: var(--text-secondary) }`
   at 0-1-1, which out-specifies a lone class no matter which stylesheet loads
   last — so the first version of this rule shipped and the text stayed GRAY,
   with nothing failing anywhere, because a colour does not throw. Do not
   "simplify" this selector back: chartColorSpecificity.test.ts fails if you do. */
.site-main p.forecast-window-status {
  color: var(--amber);
}

.chart-bar {
  fill: var(--teal);
}

/* A no-data bar is drawn at zero height (see chartModel.ts) — this rule
   exists only in case that ever changes; the REAL no-data signal is
   `.chart-no-data-marker`'s hatch, drawn regardless of bar height, which
   is what keeps "no data" from ever reading as "measured zero" (#536). */
.chart-bar--no-data {
  fill: var(--text-secondary);
  opacity: 0.3;
}

/* Already-elapsed slots read as history, not forecast — the same three-way
   split iOS's iconCell uses (past gray / in-window teal / plain secondary).
   Ported opacity 0.55 from `.gray.opacity(0.55)` there.

   Placed AFTER --no-data deliberately: both are single-class selectors, so
   source order decides, and a past no-data slot should gray. Its bar is
   zero-height anyway and `.chart-no-data-marker` -- a SEPARATE element, not
   restyled here -- is what carries "no data", so #536's rule that a missing
   reading must never look like a measured zero is untouched.

   Only the BARS gray. The coral window bands deliberately do not: iOS draws
   qualifying periods in full including any elapsed part, and records that a
   faded-elapsed treatment was tried and superseded. */
.chart-bar--past {
  fill: var(--text-secondary);
  fill-opacity: 0.55;
}

.chart-no-data-marker {
  opacity: 0.5;
}

/* Task 12 — scrub-to-inspect. `.chart-plot-host` is what carries
   `touch-action: pan-y`: a horizontal scrub must not fight the page's own
   vertical scroll on touch (renderChart.ts's header comment; the SVG
   inside `.chart-plot-content` is rebuilt on every render and inherits
   nothing of its own, so the rule lives on the stable host instead). */

.chart-plot-host {
  position: relative;
  touch-action: pan-y;
}

.chart-plot-content {
  width: 100%;
}

/* Purely visual, `aria-hidden` (renderChart.ts) — the tooltip below is the
   real, announced readout. Positioned in percent because `.chart-svg`'s
   viewBox (0..CHART_WIDTH) maps 1:1 onto `.chart-plot-host`'s own rendered
   width. */
/* The present moment. Neutral-bright, deliberately NOT coral: coral is window
   vocabulary in this chart and nothing else may wear it.
   (This comment used to justify itself against "coral already means two things
   ... window bands and the scrub crosshair" — the crosshair WAS the second
   meaning, and it is red now, so coral means one thing again.)
   Both this and the crosshair are dashed, so DASH no longer separates them;
   COLOUR does, and it has to, because they overlap exactly when the user
   scrubs to now. */
.chart-now-line {
  stroke: var(--teal);
  stroke-width: 1.5;
  stroke-dasharray: 4 4;
}

/* The clock at the present moment, in the icon zone above the band strip —
   iOS's own marker, same colour and same place. Teal, not coral: the coral
   grammar is the window vocabulary (thin frame / thick frame / dashed limits)
   and the present moment is not part of it. */
.chart-now-marker {
  fill: var(--teal);
}

/* The axis's timezone, in the gutter beside the time ticks. The chart's x-axis
   is the LOCATION's clock, never the viewer's, and this is what says so. */
.chart-axis-zone {
  fill: var(--text-secondary);
  font-size: 10px;
}

/* x-axis times. A midnight tick shows the weekday instead, and is emphasised,
   because crossing into a new day is the one thing on this axis worth finding
   at a glance. */
.chart-tick-time {
  fill: var(--text-secondary);
  font-size: 11px;
}

.chart-tick-time--day {
  fill: var(--text-primary);
  font-weight: 600;
}

/* The scrub position: a dashed red line the full height of the chart, as iOS
   draws it ("Full-height dashed red line across all visible rows" —
   crosshairOverlay: .red.opacity(0.75), lineWidth 1.5, dash [5, 4]).
   NOT coral, which it used to be. The crosshair is CONTEXT — where your finger
   is — not window vocabulary, so it belongs with the neutral now-marker rather
   than with the bands. Coral wearing a fifth meaning is what the grammar
   forbids, and coralGrammar.test.ts now enforces it.
   Dashed via a repeating gradient because this is an HTML overlay <div>, not
   an SVG line: 5 on / 4 off reproduces iOS's dash array exactly. */
.chart-crosshair {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1.5px;
  background: repeating-linear-gradient(
    to bottom,
    var(--danger) 0 5px,
    transparent 5px 9px
  );
  opacity: 0.75;
  pointer-events: none;
}

.chart-scrub-slider {
  width: 100%;
  accent-color: var(--teal);
}

/* `.chart-tooltip` itself carries NO visual chrome — it is a `role="status"`
   live region that stays in the DOM, unhidden, from first render (see
   renderChart.ts's `build()` comment: hiding it would risk the AT missing
   the very first announcement). Empty, it must render as nothing; the box
   only appears once renderTooltip() adds `--active`, i.e. after a real
   interaction.

   FIX ROUND 1: this file previously carried a deliberately-empty
   `.chart-tooltip {}` rule so web/ui.test.ts's class-parity gate would see
   the class as "checked." A reviewer correctly called that allowlisting
   relocated to the stylesheet -- an empty rule is dropped by any minifier,
   is indistinguishable from a real missing style, and is one `{}` among 47
   real rules, uncountable. The exemption now lives in web/ui.test.ts's own
   `IDENTITY_ONLY` map, where it is visible and named instead of hidden in
   CSS; see that map's comment for why `.chart-tooltip` genuinely has none
   of its own styling. */
.chart-tooltip--active {
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--text-secondary);
  border-radius: 0.5rem;
}

.chart-tooltip-time {
  margin: 0 0 0.25rem;
  font-weight: 600;
}

.chart-tooltip-values {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.15rem 0.5rem;
  margin: 0;
}

.chart-tooltip-values dt {
  color: var(--text-secondary);
}

.chart-tooltip-values dd {
  margin: 0;
}

/* Search results are two lines: "Rome (IT)" then "41.89°, 12.51°".
   `white-space: pre-line` is what renders the \n renderLocations.ts puts in the
   button's text; without it the label collapses to one line and the coordinates
   run straight into the name. Left-aligned because two centred lines of
   different widths read as ragged. */
/* Both lists show the same two-line place label, so they share the rule — a
   saved location that rendered on one line while its search result rendered on
   two would look like two different records. The remove "✕" is excluded: it is
   a single glyph and left-aligning it would drag it away from its row's edge. */
.search-results button,
.saved-locations button:not(.ui-button--destructive) {
  white-space: pre-line;
  text-align: left;
}

/* QUALIFIED as `.site-main p.units-footer` (0-2-1), NOT the bare class
   (0-1-0), for the same reason `.forecast-window-status` above is: aerzeit.css
   carries `.site-main p { color: var(--text-secondary) }` and class-plus-element
   out-specifies a lone class regardless of stylesheet order. A colour does not
   throw, so an inert rule here would simply never appear. */
.site-main p.units-footer {
  color: var(--text-secondary);
  font-size: 0.85rem;
  margin: 0.5rem 0 0;
}

/* THE ICON STRIP — one cell per 3 hours, above each row's plot.
   Ported from iOS's icon cell. The three-way colour split is grey (elapsed),
   teal (in a recommended window), muted (everything else) — and the coral bar
   below is what keeps "in window" from being carried by colour alone (#211). */
.chart-cell-icon {
  fill: var(--text-secondary);
}

.chart-cell-label {
  font-size: 8px;
  fill: var(--text-secondary);
}

/* Elapsed. Dimmer than "future" rather than a different hue: the past is
   context you can no longer act on, not a separate category of thing. */
.chart-cell--past {
  fill: var(--text-secondary);
  opacity: 0.45;
}

/* In a recommended open window. Teal, matching the now-marker's role as the
   chart's non-coral accent — coral stays window-FRAME vocabulary. */
.chart-cell--window {
  fill: var(--teal);
}

.chart-cell--future {
  fill: var(--text-secondary);
}

/* Nothing was measured in these three hours. Faint, so a row with a coverage
   gap reads as a gap rather than as a row of real low readings. */
.chart-cell--nodata {
  opacity: 0.4;
}

/* The non-colour cue for "in window": a shape, in the colour this chart
   already uses for window vocabulary. See coralGrammar.test.ts. */
.chart-cell-mark {
  fill: var(--coral);
}

/* PANE HEADING ICON — iOS's `sectionCard(header:icon:)` puts a 12x12 teal
   glyph 6 units before its 12 pt title. Both numbers are expressed here as
   fractions of the heading's own size (1em, 0.5em) so the pair keeps iOS's
   proportion if the heading's font-size ever changes.
   `currentColor`, not var(--teal): aerzeit.css already colours `.site-main h2`
   teal, and inheriting means the icon cannot drift from the text beside it. */
.pane-heading-icon {
  width: 1em;
  height: 1em;
  margin-right: 0.5em;
  fill: currentColor;
  /* The glyph's optical centre sits above the text baseline; -0.08em lines the
     two up without affecting the heading's line box. */
  vertical-align: -0.08em;
  flex: none;
}

/* A subsection's own explanatory line, under its title and above its fields —
   iOS puts `windowDurationFooter` in exactly that position. Qualified as
   `.site-main p.settings-subcard-footer` (0-2-1) for the same reason
   `.units-footer` is: a lone class loses to aerzeit.css's `.site-main p`. */
.site-main p.settings-subcard-footer {
  color: var(--text-secondary);
  font-size: 0.85rem;
  margin: 0 0 0.75rem;
}

/* A subsection whose TITLE names its only control: the control spans the card
   with its value readout beneath, where a multi-control subsection would put a
   label to its left. iOS's Precipitation and Humidity subsections have the
   same shape — title, then the wave, no field label. */
.settings-bare-control {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

/* FULL SUBCARD WIDTH, lifting `.ui-wave`'s shared `max-width: 420px`.
   That cap is right for a wave sitting in a row's control column, where it is
   one of two things sharing the line. This wave is not that: it is the
   Temperature card's own band — read-only, `aria-hidden`, a depiction of the
   range the Min/Max fields below it set — so it behaves like the forecast
   chart and takes the width it is given.
   Safe to widen because the control MEASURES itself: `waveSelector.ts`'s
   ResizeObserver re-renders on the observed width, and 420 there is only the
   pre-layout fallback, not the control's own size. */
.settings-bare-control .ui-wave {
  max-width: none;
}

.site-main p.settings-bare-hint {
  color: var(--text-secondary);
  font-size: 0.85rem;
  margin: 0;
}

/* THE TIME-RANGE EDITOR — the one variable-length control in the panel.
   Each row is From/Until plus a remove button; iOS lays the same three out
   horizontally with a `minus.circle` at the end. */
.settings-time-ranges {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.settings-time-range {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.settings-time-edge {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  color: var(--text-primary);
}

/* A native time input: the browser's own picker and keyboard handling, which
   no hand-built control matches for time entry. `color-scheme: dark` is what
   makes its built-in chrome (the clock icon, the spinners) legible on this
   background — without it the glyphs render for a light page and vanish. */
.settings-time-input {
  font: inherit;
  color-scheme: dark;
  background: var(--field-background);
  color: var(--text-primary);
  border: 1px solid var(--navy-secondary);
  border-radius: 0.25rem;
  padding: 0.25rem 0.375rem;
}

/* Square, so a bare minus reads as a button rather than as a hyphen. */
.settings-time-remove {
  padding: 0.25rem 0.625rem;
  line-height: 1;
}

.settings-time-add {
  align-self: flex-start;
}

.site-main p.settings-time-status {
  color: var(--text-secondary);
  font-size: 0.85rem;
  margin: 0.5rem 0 0;
}

/* THE DURATION DIAGRAMS — static pictures above Min open / Max open / Min gap,
   ported from iOS's WindowWaveIndicator and CooldownWaveIndicator. They are
   `aria-hidden` (the field and its sentence carry the meaning), so everything
   here is appearance only. */
.settings-wave-indicator {
  max-width: 320px;
}

.wave-indicator {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

.wave-indicator-frame {
  fill: none;
  stroke: var(--coral);
  stroke-width: 3;
}

.wave-indicator-sine {
  fill: none;
  stroke: var(--teal);
  stroke-width: 2;
}

.wave-indicator-arrow {
  fill: var(--teal);
}

/* The gap diagram's two windows are context, not subject — iOS draws them at
   0.5 opacity for exactly that reason. */
.wave-indicator--muted {
  opacity: 0.5;
}

/* Grey, deliberately NOT teal or coral: the gap is the absence of a window,
   so it must not wear window vocabulary. iOS uses `Color(white: 0.8)`. */
.wave-indicator-gap {
  stroke: #cccccc;
  stroke-width: 1.5;
  stroke-dasharray: 4 3;
}

.wave-indicator-arrow--gap {
  fill: #cccccc;
}

/* THE BITMASK TOGGLE CLUSTER — wind directions, sun positions. Wraps because
   eight labelled toggles do not fit one line on a phone, and each toggle is
   `ui-toggle` (a <label> around its checkbox), so the text is part of the hit
   target rather than beside it. */
.settings-bit-group {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 0.625rem;
  justify-content: flex-end;
}
