/* ==========================================================================
   Charging dashboard — THE BASE SHEET.

   This file is not a design. It is what every design has to stand on, and it
   is loaded under all six of them. The look lives in `themes/<slug>.css`, and
   `themes/classic.css` is today's design ported here unchanged.

   ── THE LINE THIS FILE DRAWS ──

   style.css makes the page WORK. A theme makes it LOOK like something.

   So this file holds, and a theme sheet must not need to repeat:

     · the reset;
     · the accessibility floor — .sr-only, a focus outline that survives a
       theme forgetting to define a colour, and --tap;
     · bidi and RTL correctness — the `unicode-bidi: plaintext` set, the
       Hebrew leading, and the physical `text-align` that a plaintext table
       cell and a plaintext stat value both need;
     · the `[hidden]` contract, because app.js hides real destinations with it;
     · the router's display contract — `.view` / `.view.is-active`;
     · the menu mechanics app.js toggles — the rail, the drawer, `.is-open`,
       the scrim, and the desktop gutter that comes back on `.nav[hidden]`;
     · overflow containment, so nothing can scroll the page sideways;
     · the geometry of every custom property a JS module sets — --start/--end
       on a tariff slice, --at on the now marker, --pct on the settle bar, and
       the whole --sp-* scale, which three view modules and the shell write
       into inline styles by name;
     · the figure-integrity mechanism on a stat tile (container query context,
       no break inside the number, one break opportunity before the unit);
     · every `content:` that carries MEANING rather than decoration — the two
       suspension glyphs, the ⚠/✓ prefixes, the delta arrows and the
       translated "archived" tag. The view modules are forbidden from writing
       those strings into the DOM, so the sheet is the only place they can
       exist, and a theme that dropped them would leave two states told apart
       by colour alone.

   It holds NO colour, NO shadow, NO font family, NO border, NO radius and NO
   type size. Those are a theme's, entirely. Where a rule here has to reference
   one it does so through a `var(…, fallback)` so a theme that has not defined
   the token still renders.

   Nothing here is a lock. Theme sheets are linked AFTER this one, so at equal
   specificity a theme wins; treat everything below as the default a theme may
   overrule and the floor it should not. See THEMES.md for the contract.

   State is expressed as classes / [data-*] / [hidden] that the view modules
   toggle. No JS lives here.
   ========================================================================== */

:root {
  /* — the spacing scale —
     In the base sheet because it is not only CSS: app.js sets
     `margin: var(--sp-4)` on its own chrome, and views/tariff.js,
     views/history.js and views/account.js build a property name by
     concatenation — `var(--sp-' + step + ')'` — because the CSP refuses a
     style="…" attribute. A theme may change these VALUES. A theme that
     removes a NAME makes those declarations invalid at computed-value time,
     which drops the spacing silently and reads as a layout bug. */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;

  /* The minimum interactive target. This dashboard is read one-handed while
     standing next to a charger, and one of its buttons opens a contactor. A
     theme may raise this. It may not lower it. */
  --tap: 44px;

  /* The rail's width. Here rather than in a theme because the desktop gutter
     below is computed from it and the drawer's off-canvas width is clamped
     against it — both are menu mechanics, not paint. */
  --nav-w: 224px;
}

/* ── reset ─────────────────────────────────────────────────────────────── */

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

html { -webkit-text-size-adjust: 100%; }

body {
  /* clip, not hidden: keeps the page from ever scrolling sideways without
     turning <body> into a scroll container. Wide content scrolls in .scroll-x. */
  overflow-x: clip;
}

img, svg { max-width: 100%; display: block; }
button, input, textarea, select { font: inherit; color: inherit; }

/* app.js hides real destinations with `el.hidden` — the contact menu item when
   the card is unconfigured, and the whole rail plus its handle on a signed-out
   page. ANY author declaration of `display` beats the UA sheet's `[hidden]`,
   whatever its specificity, so a theme that gives `.nav__link` a display value
   would silently resurrect a menu item leading to a section that is no longer
   in the document. One rule, stated loudly, so no theme has to remember. */
[hidden] { display: none !important; }

/* The outline colour is a theme's, but the OUTLINE is not optional. The
   fallback is what keeps focus visible in a theme that never defined --focus:
   currentColor always resolves, and it is by definition legible against the
   ground the text beside it is legible against. */
:focus-visible {
  outline: 2px solid var(--focus, currentColor);
  outline-offset: 2px;
}

/* Screen-reader-only text — used for the icon-button labels. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

/* Any container that may hold a table/strip wider than the phone. */
.scroll-x,
.table-wrap {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
}

.num { font-variant-numeric: tabular-nums; }

/* ── bidi: figures are LTR runs inside RTL prose ───────────────────────── */
/* Every box below holds a measurement, not a sentence: "31.10", "₪ 14.00",
   "5.1 kW", "17:04", "3 h old". Dropped into a Hebrew paragraph those are weak
   runs fenced by neutrals, and the neutrals take the paragraph's direction —
   which is how "6 h 03 m" comes out as "m 03 h 6".
   `plaintext` gives each of these boxes its own bidi paragraph whose direction
   comes from its first STRONG character. A bare figure has none, so it stays
   LTR; a value that does start with Hebrew still reads RTL. Crucially it does
   not touch the `direction` property, so `text-align: start/end` keeps
   resolving in the page direction and a numeric column still hugs the table's
   inline-end (right in LTR, left in RTL).
   MEASURED, not assumed: without this the duration cell "6 h 03 m" renders its
   glyph runs out of logical order at dir="rtl". It is the column WITHOUT .num
   that broke, so the rule is on the cell, not on the numeric class — all six
   columns are mixed-script, not just the three numeric ones. A cell that really
   does start with Hebrew ("10 בספט׳") still gets RTL.
   THIS IS THE BASE SHEET'S because it is correctness, not taste, and because a
   theme has no way to discover it failed: the glyphs are all present and all
   legible, in the wrong order. */
.num,
.table th,
.table td,
.updated__abs,
.stat__value,
.suspend__amount,
.comment__time,
.stale__age,
/* A user-agent string is Latin, punctuation-heavy and displayed verbatim inside an
   RTL row. Without its own bidi paragraph the brackets and slashes take the page
   direction and the whole line reassembles itself in front of the reader. */
.session__ua,
.contact__value,
.tariff__flip strong { unicode-bidi: plaintext; }

/* Hebrew has no ascenders or descenders: the letters fill the x-height band
   almost solidly, so identical leading reads noticeably denser than Latin. A
   script fact rather than a taste, which is why it is the base's default — a
   theme is free to set its own body leading, and every one of them will. */
[dir="rtl"] body { line-height: 1.65; }

/* The one place a PHYSICAL value is the right answer, and the reason is worth
   the four lines. Table cells are `unicode-bidi: plaintext` so their glyph runs
   cannot scramble; the cost is that `text-align: start/end` then resolves
   against each CELL's own content direction. Measured at dir="rtl": the Hebrew
   date column aligned right and the Latin duration column aligned left, in the
   same table — a column edge that moves with the row's content is not a column.
   Inside a block that is already scoped to one direction, `right` is not an
   un-migrated physical property; it is the only value plaintext cannot
   reinterpret. Every column, numeric or not, meets the same edge. */
[dir="rtl"] .table th,
[dir="rtl"] .table td,
[dir="rtl"] .table th.num,
[dir="rtl"] .table td.num { text-align: right; }

/* The same ruling, for the same reason, on the one other box that holds a bare
   figure: `.stat__value` is in the plaintext set above, so `start` resolves
   against the FIGURE's own direction — and a figure that opens with a digit has
   no strong character at all, resolves LTR, and slides to the far side of its
   tile while the Hebrew `.stat__label` under it stays put. The two halves of one
   tile ending up on opposite edges is not a theme's mistake and a theme has no
   way to discover it: every glyph is present and legible. So it is the base's,
   next to the cell rule it is a copy of. */
[dir="rtl"] .stat__value { text-align: right; }

/* ── the router's display contract ─────────────────────────────────────── */
/* <main> holds seven `.view` sections and exactly one wears `.is-active`;
   app.js moves that class from `location.hash`. `display: none` takes a flex
   item out of layout gap and all, so the inactive six cost no space and no
   stray `gap`. A theme may change what an active view IS (a grid, say) but not
   whether an inactive one is on screen. */

.view { display: none; min-width: 0; }
.view.is-active { display: flex; flex-direction: column; }

/* ── the menu mechanics ────────────────────────────────────────────────── */
/* Fixed rail from 900px up, off-canvas drawer below it. All of it is wired to
   app.js — `drawer()` toggles `.is-open`, `paintAuthRequired()` sets
   `nav.hidden` — so it is mechanism rather than paint and it lives here. What
   it looks like (ground, edge, type, the scrim's tint) is a theme's, and a
   theme that wants a different shape entirely — a bottom tab bar, say —
   overrides these rules from a sheet that loads after this one.

   The page is ALWAYS dir="rtl", so `inset-inline-start: 0` puts the rail on
   the right; the drawer therefore has to slide out to the right, and
   `translateX` is physical, not logical — hence the positive 100%. Flip the
   page to LTR and this one value is what needs revisiting. */

.nav {
  position: fixed;
  inset-block: 0;
  inset-inline-start: 0;
  width: var(--nav-w);
  z-index: 60;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* ── THE DRAWER HANDLE, AND WHY IT IS SELECTED THROUGH `.btn` ──
   The handle is `button.btn.btn--icon.navtoggle`. A bare `.navtoggle` is
   specificity (0,1,0) — exactly what `.btn` is — and every theme sheet loads
   AFTER this one, so the moment a theme gives `.btn` a `display` value it wins
   on source order and the ☰ comes back on desktop, beside a rail that is
   already on screen, opening a drawer that duplicates it. Measured at 1440px
   under `classic`: visible, 44px and clickable. Three of the six themes hit
   this independently and each restated the rule in its own sheet.
   `.btn.navtoggle` is (0,2,0), which outranks `.btn` without spending an
   `!important` — the base reserves that for `[hidden]`, where no selector can
   help because ANY author `display` beats the UA sheet's rule whatever its
   weight. Here specificity is the cheaper tool and it is enough.
   A theme that genuinely wants a different handle can still reach it by
   matching `.btn.navtoggle` too. It just cannot do it by accident. */
.btn.navtoggle, .navscrim { display: none; }

@media (min-width: 900px) {
  /* The gutter goes on `.shell`, not on <body>, so that the sibling selector
     below can take it back when app.js hides the rail on a signed-out page —
     no `:has()`, no class on <html>. `.navscrim` sits between the two, so the
     general sibling combinator is the one that reaches. `nav` / `navscrim` /
     `.shell` must stay siblings in that order. */
  .shell { padding-inline-start: var(--nav-w); }
  .nav[hidden] ~ .shell { padding-inline-start: 0; }
}

@media (max-width: 899.98px) {
  .nav {
    width: min(var(--nav-w), 78vw);
    transform: translateX(100%);
    transition: transform .2s ease;
  }
  .nav.is-open { transform: none; }

  /* Inside the header rather than floating over the page: the header is
     sticky, so the menu is reachable from the bottom of a long history table
     without scrolling back up, and nothing has to reserve a corner for it. */
  /* `.btn.navtoggle` for the same reason the default above is: at (0,1,0) a
     theme's `.btn { display: … }` outranks it on source order, and below 900px
     that hides the only way into the menu. */
  .btn.navtoggle { display: inline-flex; order: -1; }

  .nav.is-open ~ .navscrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 55;
  }
}

/* The rail scrolls with nothing; the header must not scroll away either, or the
   ☰ leaves the screen on the one view that is taller than a phone. */
.shell__header {
  position: sticky;
  top: 0;
  z-index: 40;
}

/* ── geometry the view modules write into ──────────────────────────────── */
/* Each of these consumes a custom property a JS module sets through the style
   object (a style="…" attribute is refused by the CSP). The property names and
   what they mean are a contract between the module and whatever sheet is on;
   the SIZE, the colour and the shape are a theme's. */

/* RULING: the tariff band is a TIME AXIS and does not mirror under dir="rtl".
   Midnight is at the left edge and the day runs rightwards in both page
   directions — like a clock face, a scrubber or a Gantt row, none of which
   mirror in Hebrew. The scale labels (00 06 12 18 24) are the proof: mirrored,
   they read 24 → 00 left-to-right, and a reader has to invert the axis before
   reading the price. Pinning `direction` here also pins the CONTRACT:
   `inset-inline-start` on a box whose own direction is ltr always resolves to
   `left`, so --start/--end/--at keep meaning "percent of the day from
   midnight" with no change to the view module and no dependence on what the
   page direction happens to be.
   The legend and .tariff__flip are prose and are deliberately left to mirror. */
.tariff .scroll-x,
.tariff__band,
.tariff__scale { direction: ltr; }

.tariff__band { position: relative; overflow: hidden; }

.tariff__slice {
  position: absolute;
  inset-block: 0;
  inset-inline-start: var(--start, 0%);
  width: calc(var(--end, 0%) - var(--start, 0%));
}

.tariff__now {
  position: absolute;
  inset-block: 0;
  inset-inline-start: var(--at, 0%);
  z-index: 2;
}

/* --pct is set inline by the view module, 0–100.
   RULING: this one DOES mirror — under dir="rtl" the fill grows from the right,
   which is what it already does for free (a block box narrower than its
   container hugs the inline-start edge). Opposite call to the tariff band, and
   deliberately so: the band is an axis with labelled coordinates that have a
   fixed reading order, this is an unlabelled proportion, and every native RTL
   progress indicator (UIProgressView, Android ProgressBar, Material) mirrors.
   It also sits in a flex row that mirrors around it, so a fill that grew
   leftwards would grow away from its own "Settling…" label. */
.settle__fill { width: calc(var(--pct, 0) * 1%); }

/* ── a figure never breaks inside itself ───────────────────────────────── */
/* `1,111.3` over `3₪` reads as two numbers, in the one panel this dashboard
   exists to show. So:
     1. the figure is atomic — `overflow-wrap: normal`, no break opportunity
        inside it;
     2. the single break opportunity is between the figure and its unit (the
        ZWSP below);
     3. it never has to break, because it always FITS — which is a theme's job:
        size `.stat__value` as `min(<your size>, 19cqi)` against the container
        context this rule establishes. 1cqi is 1% of the tile's content box, and
        the longest realistic figure ("12,345.67", 9 grouped glyphs) is 4.95em
        wide, so 19cqi holds it on one line at ANY tile width.
   `overflow-wrap: anywhere` used to sit here and is what split the digits once
   he-IL grouping widened the figure by a glyph. `break-word` is not the fix:
   measured at 375px it renders identically — the track min is a definite 140px,
   so min-content never sized the column, and both keywords break an overflowing
   word just the same.
   ponytail: calibrated for 9 glyphs. A 10-glyph figure (₪123,456.78 — this
   charger would have to deliver ~500,000 kWh) overflows its tile rather than
   splitting. Lower the 19 if that day comes; do not restore `anywhere`. */
.stat { container-type: inline-size; }
.stat__value { overflow-wrap: normal; }

/* The one legal break in a stat value, and the reason it is in CSS: there is no
   character between the figure and its unit in the DOM, so "12,345.67₪" is a
   single word. U+200B is a soft-wrap opportunity and nothing else — zero width,
   bidi class BN, so it cannot reorder the Hebrew around it, and generated
   content is not picked up by copy. */
.stat__unit::before { content: "\200B"; }

/* ── generated content that carries meaning ────────────────────────────── */
/* Not decoration. Every string below is a fact the view modules are forbidden
   from writing into the DOM, so the sheet is the only place it exists. A theme
   may restyle these or swap the glyph; a theme that simply drops them leaves
   two states told apart by colour alone, which is the defect these exist to
   prevent. */

.form-error::before { content: "⚠ "; }
.expiry__title::before { content: "🔒"; }
.expiry__error::before { content: "⚠ "; }
.expiry__ok::before { content: "✓ "; }
.stale__flag::before { content: "⏳"; }

.stat__delta--up::before { content: "▲"; }
.stat__delta--down::before { content: "▼"; }
.stat__delta--flat::before { content: "="; }

/* Two variants of one underlying state. They must never be told apart by
   colour alone: each carries its own glyph from here and its own copy from the
   view module. Do not reuse one variant's wording under the other class. */
.suspend--price::before { content: "⏱"; }
.suspend--panel::before { content: "⚡"; }
.suspend--unknown::before { content: "?"; }

/* The views are forbidden from writing this word into the DOM, so the sheet is
   the only place it can be translated. Keyed on :lang, not on [dir] — it is a
   language fact, not a layout one. :lang(he) also matches lang="he-IL". */
.comment--archived .comment__author::after { content: "archived"; }
:lang(he) .comment--archived .comment__author::after { content: "בארכיון"; }

/* ── motion floor ──────────────────────────────────────────────────────── */
/* Transitions are killed for everyone who asked, here, once, so no theme can
   forget. ANIMATIONS are deliberately NOT killed globally: `.btn.is-busy`
   wants to slow down rather than freeze — a frozen spinner on a busy button
   reads as a stuck command — so each theme owns its own animation's reduced
   state. THEMES.md makes that an obligation and test/theme.test.mjs fails a
   theme that declares an animation and never mentions the query. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: .01ms !important; }
}

/* ── print ─────────────────────────────────────────────────────────────── */
/* The menu is chrome, and its desktop gutter would print as a blank margin.
   The ACTIVE view is what prints — printing the history table means being on
   the history view, which is the only reason anyone prints this page. The rest
   of the print sheet (what a panel looks like on paper) is a theme's. */
@media print {
  /* `.btn.navtoggle` here too: a media query changes nothing about specificity
     or source order, so a theme's `.btn` display would follow the handle onto
     paper as well. */
  .nav, .btn.navtoggle, .navscrim { display: none; }
  .shell { padding-inline-start: 0; }
}
