/* Balcony Weather Station — dashboard styles.
   Light and dark themes are driven entirely by the tokens on :root. */

:root {
    --bg: #f0f4f8;
    --card-bg: #ffffff;
    --text: #1a202c;
    --text-secondary: #64748b;
    --accent-temp: #e53e3e;
    --accent-hum: #3182ce;
    --accent-pres: #38a169;
    --accent-warn: #dd6b20;
    --border: #e2e8f0;
    --cell-text: rgba(0, 0, 0, 0.6);
    --row-highlight: rgba(49, 130, 206, 0.10);
    --shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.06);
    --radius: 12px;
    --gap: 16px;
}

@media (prefers-color-scheme: dark) {
    :root {
        --bg: #1a202c;
        --card-bg: #2d3748;
        --text: #e2e8f0;
        --text-secondary: #a0aec0;
        --border: #4a5568;
        --cell-text: rgba(255, 255, 255, 0.7);
        --row-highlight: rgba(99, 179, 237, 0.16);
        --shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
    }
}

* { margin: 0; padding: 0; box-sizing: border-box; }

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    background: var(--bg);
    color: var(--text);
    min-height: 100vh;
    padding: 20px;
}

.container { max-width: 900px; margin: 0 auto; }

header { text-align: center; padding: 24px 0 16px; }
header h1 { font-size: 1.8rem; font-weight: 700; }

/* Out of the way but on the first screen: the reader this exists for is one
   whose browser asked for the wrong language, and they should not have to
   scroll to disagree. Absolutely positioned so it cannot shift the title,
   which is centred; the header reserves the height it needs on a phone,
   where the corner is not far enough from a centred h1. */
header { position: relative; }
.lang-switch {
    position: absolute;
    top: 0;
    right: 0;
    display: flex;
    gap: 2px;
    font-size: 0.72rem;
    letter-spacing: 0.04em;
}
.lang-switch a {
    color: var(--text-secondary);
    text-decoration: none;
    padding: 3px 7px;
    border-radius: 6px;
}
.lang-switch a:hover { color: var(--text); background: var(--border); }
.lang-switch a[aria-current] {
    color: var(--bg);
    background: var(--text);
    font-weight: 600;
}
.lang-switch a:focus-visible { outline: 2px solid var(--accent-hum); outline-offset: 2px; }
header .subtitle { color: var(--text-secondary); font-size: 0.9rem; margin-top: 4px; }

/* ── Shared card chrome ───────────────────────────────────────────────── */

.card {
    background: var(--card-bg);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    border: 1px solid var(--border);
    /* Grid items default to min-width:auto, so a card whose content has a
       wide minimum (the sparkline canvas) would push the grid past the
       viewport instead of shrinking. */
    min-width: 0;
}

.card-title {
    font-size: 0.85rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-secondary);
    margin-bottom: 12px;
}

/* ── Banners (forecast, frost, stale) ─────────────────────────────────── */

.banner-row { text-align: center; margin-bottom: 16px; }
.banner-row:empty { margin-bottom: 0; }

.banner {
    display: inline-block;
    padding: 8px 18px;
    border-radius: 20px;
    font-size: 0.9rem;
    background: var(--card-bg);
    color: var(--text);
    box-shadow: var(--shadow);
    border: 1px solid var(--border);
}

.banner-alert {
    background: var(--accent-temp);
    color: #fff;
    font-weight: 600;
    border-color: transparent;
}

.banner-warn {
    background: var(--accent-warn);
    color: #fff;
    font-weight: 600;
    border-color: transparent;
}

/* ── The two predictions ──────────────────────────────────────────────── */

/* The banner row holds both, side by side, wrapping on a narrow screen. */
.banner-row { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }

/* Ordinary wrapping text, deliberately NOT a flex row. As flex items the
   label, the percentage and the horizon become side-by-side columns that
   each wrap internally, so on a narrow screen the "·" is orphaned onto its
   own line and "in 6 h" splits down the middle. English was short enough to
   hide it; German is not. */
.banner-nowcast { line-height: 1.45; }
.banner-nowcast strong { font-weight: 700; }

/* The separator, the number and the horizon read as one thing: a line may
   break before them, never inside them. */
.nowcast-chance { white-space: nowrap; }

.predictions { text-align: center; margin-bottom: 16px; }

/* One card, two depths. The summary rules are child selectors on purpose:
   the technical details are a <details> nested inside this body, and a
   descendant selector would give that one the same ⓘ and centring as the
   card's own handle. */
.prediction-note {
    display: inline-block;
    /* Both halves matter. 82ch is the reading measure. 100% is the phone:
       an inline-block is sized shrink-to-fit, and shrink-to-fit floors at
       the content's *minimum* width — so one table too wide to compress
       makes this box wider than the page, and the page scrolls sideways
       instead of the table scrolling inside its own .table-scroll. Capping
       at the container hands the overflow back to the wrapper that is
       there to take it. */
    max-width: min(82ch, 100%);
    text-align: left;
    font-size: 0.8rem;
    color: var(--text-secondary);
}
.prediction-note > summary {
    cursor: pointer;
    text-align: center;
    padding: 2px 8px;
    border-radius: 12px;
    list-style: none;
}
/* The handle is the card's heading, so it holds an <h2>. Inline and at
   the summary's own size, or the browser's h2 defaults would turn a
   12.8px pill into a 19px block. */
.prediction-note > summary h2 { display: inline; font: inherit; }
.prediction-note > summary::-webkit-details-marker { display: none; }
.prediction-note > summary::before { content: "ⓘ "; }
.prediction-note > summary:hover { color: var(--text); }
.prediction-note[open] > summary { color: var(--text); }
.prediction-note-body {
    margin-top: 8px;
    padding: 14px 18px;
    background: var(--card-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
}
/* The card is as wide as the widest table; the prose inside it is not. */
.prediction-note-body dl { max-width: 62ch; }
.prediction-note-body dt {
    color: var(--text);
    font-weight: 600;
    margin-bottom: 3px;
}
.prediction-note-body dd {
    margin: 0 0 12px;
    line-height: 1.5;
}
.prediction-note-body dd:last-child { margin-bottom: 0; }
.prediction-note-body strong { color: var(--text); }
.prediction-provenance {
    font-size: 0.72rem;
    padding-top: 8px;
    border-top: 1px solid var(--border);
}

/* ── The deep dive ────────────────────────────────────────────────────── */

/* A section inside the explainer card rather than a second card beside it:
   no chrome of its own, just a rule separating it from the short answer. */
.prediction-deep {
    margin-top: 14px;
    padding-top: 12px;
    border-top: 1px solid var(--border);
}
.prediction-deep > summary {
    cursor: pointer;
    color: var(--text);
    font-weight: 600;
    list-style: none;
}
.prediction-deep > summary::-webkit-details-marker { display: none; }
.prediction-deep > summary::before {
    content: "\25b8";
    display: inline-block;
    width: 1em;
    transition: transform 0.15s;
}
.prediction-deep[open] > summary::before { transform: rotate(90deg); }
.prediction-deep-body { margin-top: 10px; }
.prediction-deep h3 {
    font-size: 0.86rem;
    color: var(--text);
    margin: 18px 0 6px;
}
.prediction-deep h3:first-child { margin-top: 0; }
.prediction-deep h4 {
    font-size: 0.8rem;
    color: var(--text);
    margin: 14px 0 4px;
}
.prediction-deep p { margin: 0 0 8px; line-height: 1.55; }
.prediction-deep p:last-child { margin-bottom: 0; }
.deep-live { color: var(--text); }
.deep-live:empty { display: none; }

/* A table narrower than its content should scroll inside its own box rather
   than push the page sideways — the same failure mode .card's min-width: 0
   guards against, one level down. */
.table-scroll { overflow-x: auto; margin: 0 0 10px; }

.deep-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.72rem;
    line-height: 1.4;
}
.deep-table th,
.deep-table td {
    padding: 4px 8px;
    text-align: left;
    border-bottom: 1px solid var(--border);
    vertical-align: top;
}
.deep-table thead th {
    color: var(--text);
    font-weight: 600;
    white-space: nowrap;
}
.deep-table tbody th { font-weight: 400; }
.deep-table .numeric {
    text-align: right;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}
.deep-table tfoot td {
    border-bottom: none;
    border-top: 1px solid var(--border);
    color: var(--text);
}
.deep-table th[title] { cursor: help; }

/* A reliability bin holding only a handful of hours is shown rather than
   hidden — the Hours column says how few — but it is dimmed so it does not
   read with the same weight as a bin backed by several hundred.
   Opacity rather than a colour: `.deep-table td` already sets the colour on
   the cell, so a rule on the row would lose the cascade and do nothing. */
.reliability-table tr.is-thin td { opacity: 0.55; }

/* The rung the outlook is standing on, and the model's own row. */
.deep-table .is-active { background: var(--row-highlight); }
.deep-table .is-active td,
.deep-table .is-active th { color: var(--text); font-weight: 600; }

/* The condition is the long cell; without a hint the other two take their
   natural width and squeeze it into a six-line column on a phone. */
.rule-ladder td:first-child { width: 48%; }
.rule-ladder .tier-phrase { white-space: nowrap; }
.rule-ladder .tier-note { font-style: italic; white-space: normal; }

/* The other models: the rule each is measured against sits under its score,
   in the same italic as the ladder's notes. The scores keep to one line and
   the question gets the width it needs, so on a phone the table scrolls in
   its box rather than folding the question into a six-line column. */
.other-models .tier-note { display: block; font-style: italic; }
.other-models td:first-child { min-width: 9rem; }
.other-models td.numeric { white-space: nowrap; }

/* Below this the three columns cannot all keep their natural width, so the
   phrase is allowed to wrap rather than the condition going to six lines. */
@media (max-width: 30rem) {
    .rule-ladder td:first-child { width: 42%; }
    .rule-ladder .tier-phrase { white-space: normal; }
}

/* Raising or lowering the chance of rain. Its own pair of tokens rather than
   the temperature deltas' classes: those mean warmer and cooler, and a shared
   class would drag this column along the next time that palette moves. */
.feature-table .weight-up { color: var(--accent-warn); }
.feature-table .weight-down { color: var(--accent-hum); }

.model-card {
    margin: 0 0 10px;
    display: grid;
    grid-template-columns: max-content 1fr;
}
/* The rows are <div> wrappers so each dt/dd pair stays together; subgrid
   keeps the values on one column anyway, which a flex row cannot do. */
.model-card > div {
    display: grid;
    grid-column: 1 / -1;
    grid-template-columns: subgrid;
    gap: 4px 12px;
    padding: 3px 0;
    border-bottom: 1px solid var(--border);
}
.model-card > div:last-child { border-bottom: none; }
.model-card dt { margin: 0; }
.model-card dd { margin: 0; color: var(--text); }

/* ── Current conditions ───────────────────────────────────────────────── */

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

.current-card {
    padding: 16px 20px 12px;
    text-align: center;
    position: relative;
}

.current-card .label {
    font-size: 0.8rem;
    color: var(--text-secondary);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    margin-bottom: 4px;
}

.current-card .value { font-size: 2.2rem; font-weight: 700; line-height: 1.2; }
.current-card .unit {
    font-size: 0.9rem;
    font-weight: 400;
    color: var(--text-secondary);
    margin-left: 4px;
}
.current-card .detail { font-size: 0.8rem; color: var(--text-secondary); margin-top: 2px; }
.current-card .detail-compare { margin-top: 2px; font-size: 0.75rem; }

.current-card.temp .value { color: var(--accent-temp); }
.current-card.hum .value { color: var(--accent-hum); }
.current-card.pres .value { color: var(--accent-pres); }

/* Warmer/wetter than the comparison reading reads red; cooler/drier blue.
   For humidity the pairing is inverted, hence the separate modifiers. */
.delta-up { color: var(--accent-temp); }
.delta-down { color: var(--accent-hum); }
.delta-up-hum { color: var(--accent-hum); }
.delta-down-hum { color: var(--accent-temp); }

.trend-rising { color: var(--accent-temp); }
.trend-falling { color: var(--accent-hum); }
.trend-steady { color: var(--text-secondary); }
/* The window the arrow measured over, recessive beside the number. */
.trend-window { color: var(--text-secondary); font-size: 0.92em; }

/* Chart.js sizes a responsive canvas from its parent, so the parent carries
   the dimensions. Sizing the canvas directly does not work: Chart.js writes
   an inline width onto it, which overrides the stylesheet and stops the card
   from shrinking. */
.current-card .sparkline-wrap {
    position: relative;
    margin: 6px auto 0;
    width: 100%;
    min-width: 0;
    max-width: 200px;
    height: 40px;
}

.current-card .sparkline { display: block; }

.updated {
    text-align: center;
    font-size: 0.8rem;
    color: var(--text-secondary);
    margin-bottom: 24px;
}

/* ── Period selector ──────────────────────────────────────────────────── */

.period-selector {
    display: flex;
    justify-content: center;
    gap: 4px;
    margin-bottom: 20px;
    padding: 4px;
}

.period-btn {
    /* Shrink before the row can overflow a narrow phone. */
    padding: 8px clamp(8px, 3vw, 20px);
    min-width: 0;
    border: none;
    background: transparent;
    border-radius: 8px;
    cursor: pointer;
    font: inherit;
    font-size: 0.9rem;
    color: var(--text-secondary);
    transition: background 0.15s, color 0.15s;
    white-space: nowrap;
}

.period-btn:hover { color: var(--text); background: var(--border); }
.period-btn.active { background: var(--text); color: var(--bg); }
.period-btn:focus-visible { outline: 2px solid var(--accent-hum); outline-offset: 2px; }

/* ── Charts ───────────────────────────────────────────────────────────── */

.chart-card { padding: 20px; margin-bottom: var(--gap); }
.chart-card canvas { max-height: 220px; }
.chart-card.climate canvas { max-height: 240px; }

.chart-note {
    font-size: 0.72rem;
    color: var(--text-secondary);
    text-align: center;
    margin-top: 8px;
    min-height: 1em;
}

.chart-fallback {
    text-align: center;
    color: var(--text-secondary);
    font-size: 0.85rem;
    padding: 24px 12px;
}

/* It only has anything to say when Chart.js failed to load. Its padding used
   to sit invisibly at the bottom of a card; now that the climate card has the
   year pages under it, 48px of empty paragraph would show as a hole. */
.chart-fallback:empty { display: none; }

/* ── Stats and records ────────────────────────────────────────────────── */

.stats-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--gap);
    margin-bottom: 24px;
}

.stats-card { padding: 20px; }
/* Inside .stats-grid the grid gap does the spacing; a stats-card placed
   directly in the container needs its own margin. */
.container > .stats-card { margin-bottom: var(--gap); }

.stat-row {
    display: flex;
    align-items: baseline;
    gap: 8px;
    padding: 6px 0;
    border-bottom: 1px solid var(--border);
    font-size: 0.88rem;
}

.stat-row:last-child { border-bottom: none; }
.stat-row .stat-label { color: var(--text); min-width: 0; }
.stat-row .stat-val { font-weight: 600; white-space: nowrap; }
.stat-row .stat-val.hi { color: var(--accent-temp); }
.stat-row .stat-val.lo { color: var(--accent-hum); }
.stat-row .stat-time {
    font-size: 0.78rem;
    color: var(--text-secondary);
    margin-left: auto;
    white-space: nowrap;
}
.stat-row .stat-range { font-size: 0.7rem; color: var(--text-secondary); }
.stat-row .stat-muted { font-size: 0.8rem; color: var(--text-secondary); }
.stat-row .stat-spacer {
    flex: 1;
    border-bottom: 1px dotted var(--border);
    min-width: 8px;
    align-self: center;
}

.stat-row.stat-summary {
    font-weight: 600;
    border-bottom: 2px solid var(--border);
    padding-bottom: 8px;
    margin-bottom: 8px;
}

.subcard-title {
    font-size: 0.8rem;
    color: var(--text-secondary);
    margin-bottom: 8px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

/* The months used to be a table under the chart, in a card of its own with a
   row of year buttons. They are read off the chart now; each page of the
   climate card carries only what a month cannot answer — the whole year. */
.climate-year { padding: 0 2px; }
.climate-year .stat-summary { margin-bottom: 0; border-bottom: none; padding-bottom: 0; }

/* ── Calendar heatmap ─────────────────────────────────────────────────── */

.heatmap-card { padding: 20px; margin-bottom: 24px; }

/* ── The pager: one page in view, arrows and swipe ────────────────────── */
/* Shared by the temperature calendar and the climate year; the behaviour
   that goes with it is static/js/pager.js. */

.pager-nav {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 12px;
}

.pager-nav button {
    background: var(--bg);
    border: 1px solid var(--border);
    color: var(--text);
    border-radius: 8px;
    width: 32px;
    height: 32px;
    flex: none;
    font-size: 1.1rem;
    line-height: 1;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background 0.15s, opacity 0.15s;
}

.pager-nav button:hover:not(:disabled) { background: var(--border); }
.pager-nav button:disabled { opacity: 0.3; cursor: default; }
.pager-nav button:focus-visible { outline: 2px solid var(--accent-hum); outline-offset: 2px; }

.pager-label { font-size: 1rem; font-weight: 600; text-align: center; flex: 1; }

.pager-track {
    display: flex;
    align-items: flex-start;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    -ms-overflow-style: none;
}

.pager-track::-webkit-scrollbar { display: none; }
.pager-track:focus-visible {
    outline: 2px solid var(--accent-hum);
    outline-offset: 4px;
    border-radius: 6px;
}

.pager-page { flex: 0 0 100%; min-width: 0; scroll-snap-align: center; }
.heatmap-grid {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 6px;
    /* Room for the hover below to grow into. The track clips on both axes —
       overflow-y is hidden, and overflow-x cannot scroll left of zero — so
       without this inset the top row and the edge columns lose the part of a
       hovered cell that sticks out, which reads as the day overlapping the
       card's border. Measured: the growth is 2.3px a side at the widest the
       card gets, so 4px leaves the edge column a little over 1.5px to spare. */
    padding: 4px;
}

.heatmap-header {
    font-size: 0.7rem;
    color: var(--text-secondary);
    text-align: center;
    padding: 2px 0;
}

.heatmap-cell {
    height: clamp(38px, 8vw, 62px);
    border-radius: 5px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 1px;
    font-size: 0.7rem;
    color: var(--cell-text);
    cursor: default;
    transition: transform 0.1s;
}

/* The grow is bounded by the gutter it grows into. At the widest the card
   ever gets — a 900px container — a cell is about 117px, so 4% is 2.3px a
   side against a 3px half-gap. It was 8%, which is 4.7px a side: enough to
   put a hovered day on top of both its neighbours. */
.heatmap-cell:hover { transform: scale(1.04); z-index: 1; }
/* No opacity: it used to fade the day number to 80% over a background
   already too close in luminance, costing contrast that heatmap.js now
   works to provide. The size difference carries the hierarchy. */
.heatmap-cell .cell-day { font-size: 0.68rem; }
.heatmap-cell .cell-temp { font-size: 0.8rem; font-weight: 600; }
.heatmap-cell.heatmap-empty { background: transparent; }
.heatmap-cell.heatmap-empty:hover { transform: none; }

.heatmap-cell.heatmap-nodata {
    background: transparent;
    border: 1px dashed var(--border);
    color: var(--text-secondary);
}

.heatmap-cell.is-today { box-shadow: inset 0 0 0 2px var(--text); }

/* A day whose numbers can be asked for says so on the way past. */
.heatmap-cell.is-pickable { cursor: pointer; }

/* Inset, like is-today, so it cannot spill into the gutter the hover grow
   is already measured against. */
.heatmap-cell.is-picked { box-shadow: inset 0 0 0 3px var(--text); }

.heatmap-cell:focus-visible {
    outline: 2px solid var(--text);
    outline-offset: 2px;
    z-index: 2;
}

/* Where a tapped day's numbers land. It holds its height whether or not a
   day is picked, so choosing one does not shove the month summary and the
   legend down the page. */
.heatmap-detail {
    min-height: 1.25rem;
    margin-top: 0.6rem;
    text-align: center;
    font-size: 0.78rem;
    color: var(--text-secondary);
}

.heatmap-month-summary {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 14px;
    margin-top: 12px;
    font-size: 0.78rem;
    color: var(--text-secondary);
}

.heatmap-month-summary .hi { color: var(--accent-temp); font-weight: 600; }
.heatmap-month-summary .lo { color: var(--accent-hum); font-weight: 600; }
.heatmap-month-summary .avg { color: var(--text); font-weight: 600; }

.heatmap-note { text-align: center; color: var(--text-secondary); padding: 20px; width: 100%; }

/* Said once, under the legend, when the archive is older than the pager
   goes. Quiet: it is a footnote about scope, not a warning. */
.heatmap-truncated {
    margin: 10px 0 0;
    font-size: 0.78rem;
    color: var(--text-secondary);
    text-align: center;
}

.heatmap-legend {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    margin-top: 12px;
    font-size: 0.75rem;
    color: var(--text-secondary);
}

.heatmap-legend-bar { width: 120px; height: 8px; border-radius: 4px; }

/* ── Empty state ──────────────────────────────────────────────────────── */

.no-data { text-align: center; padding: 60px 20px; color: var(--text-secondary); }
.no-data p { font-size: 1.1rem; }
.no-data .hint { margin-top: 8px; font-size: 0.85rem; }

footer { text-align: center; padding: 16px; color: var(--text-secondary); font-size: 0.75rem; }

/* ── Responsive ───────────────────────────────────────────────────────── */

/* The column stopped at 900px whatever the screen, so on a desktop the
   charts got 858px of a 1920px display and read as a 4:1 strip. Width is
   the axis a time series actually wants. Only the things that gain from it
   grow: the prose keeps its own ch caps, and the calendar keeps the ~117px
   cell the hover grow was measured against (see .heatmap-grid) rather than
   inflating a day to the size of a card. */
@media (min-width: 1240px) {
    .container { max-width: 1180px; }
    .chart-card canvas { max-height: 300px; }
    .chart-card.climate canvas { max-height: 320px; }
    .heatmap-grid { max-width: 860px; margin: 0 auto; }
}

@media (max-width: 600px) {
    .current-grid { grid-template-columns: 1fr; }
    .stats-grid { grid-template-columns: 1fr; }
    .current-card .sparkline-wrap { max-width: 100%; }
}

@media (max-width: 480px) {
    .heatmap-cell .cell-temp { display: none; }
    .heatmap-cell .cell-day { font-size: 0.72rem; }
}

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
    .heatmap-cell:hover { transform: none; }
}
