/*
    The shell's own styling. Everything it uses is a token from tokens.css.

    THE PHONE LAYOUT IS THE LAYOUT (D15): this file is written for a 380 px column with a 16 px gutter and
    no horizontal scroll, and the one media query at the end gives the desktop more room beside that column
    rather than a different arrangement.
*/

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

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

body {
    margin: 0;
    /* Explicit, never inherited: see tokens.css. */
    background: var(--portal-background);
    color: var(--portal-text);
    font-family: var(--portal-font);
    font-size: var(--portal-text-base);
    line-height: 1.5;
}

a { color: var(--portal-brand); }

/* Space Grotesk carries the headings; the tight tracking is what keeps it from reading as a wordmark. */
h1, h2, h3 {
    font-family: var(--portal-font-figure);
    letter-spacing: -0.018em;
}

:focus-visible {
    outline: 2px solid var(--portal-focus);
    outline-offset: 2px;
}

/* The router's FocusOnNavigate Selector="h1" moves focus to the page's heading after every route
   change, so a screen-reader or keyboard user lands on the new content instead of where they were.
   That focus is the app's, never the user's own - the heading is not in the tab order and cannot be
   reached by Tab - so it must not draw a ring: a box around the page title on arrival reads as a
   defect. Only the heading element itself; a control inside one (the site picker's button) keeps its
   indicator, which is what a keyboard user does reach. */
h1:focus,
h1:focus-visible {
    outline: none;
}

.portal-visually-hidden {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0; border: 0;
    clip-path: inset(50%);
    overflow: hidden;
    white-space: nowrap;
}

.portal-boot {
    padding: var(--portal-space-8) var(--portal-gutter);
    color: var(--portal-text-muted);
}

/* ---------------------------------------------------------------- shell

   ONE WHITE CARD ON A GREY DESK, which is the whole of the visual language: the desk is the only surface
   darker than the card, and nothing else on screen is. On a phone the card fills the width with a small
   inset; from --portal-rail-at it grows a rail down its left edge.

   The rail is ONE piece of markup in both layouts, laid out as a row on a phone and a column on a desktop -
   D15 forbids a different information architecture at the wide size, and two markups would be exactly that.
*/

.portal-shell {
    display: flex;
    flex-direction: column;
    margin: var(--portal-space-2);
    margin-left: max(var(--portal-space-2), env(safe-area-inset-left));
    margin-right: max(var(--portal-space-2), env(safe-area-inset-right));
    min-height: calc(100vh - var(--portal-space-4));
    background: var(--portal-surface);
    border-radius: var(--portal-radius);
    box-shadow: var(--portal-elevation);
    overflow: hidden;
}

.portal-rail {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--portal-space-2) var(--portal-space-3);
    padding: var(--portal-space-2) var(--portal-gutter);
    border-bottom: 1px solid var(--portal-rule);
}

/* The nav and the site switcher travel together: on a phone they take a line of their own under the mark,
   because the bar above cannot hold five things in 380 px, and on a desktop they are simply the next two
   children of the column. */
.portal-rail-middle {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--portal-space-2);
    flex-basis: 100%;
    min-width: 0;
}

/* The company's mark, then the product, then whose account this is - in that order, because somebody who
   arrived here from a redirect did not choose to be on this host. */
.portal-brand {
    display: flex;
    align-items: center;
    gap: var(--portal-space-3);
    min-width: 0;
    color: var(--portal-text);
    text-decoration: none;
}

.portal-brand-mark {
    display: block;
    height: 1.375rem;
    width: auto;
}

.portal-brand-product {
    padding-left: var(--portal-space-3);
    border-left: 1px solid var(--portal-border);
    font-size: var(--portal-label-size);
    font-weight: 600;
    letter-spacing: 0.17em;
    text-transform: uppercase;
    color: var(--portal-text-faint);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* One entry per page that exists. It is not hidden when there is only one: the rail is where a reader
   learns what this app has, and an app that shows nothing there reads as one page rather than as the first
   of several. */
.portal-nav {
    display: flex;
    gap: var(--portal-space-1);
    min-width: 0;
    overflow-x: auto;
}

.portal-nav a {
    display: flex;
    align-items: center;
    /* The design's own padding, with the 44 px touch floor under it: 10 px either side of a 13 px line is
       40 px, and a thumb does not reliably find that. */
    min-height: 2.75rem;
    padding: var(--portal-space-2) var(--portal-space-3);
    border-radius: var(--portal-radius-sm);
    font-size: var(--portal-text-sm);
    font-weight: 500;
    color: var(--portal-text-2);
    text-decoration: none;
    white-space: nowrap;
}

.portal-nav a:hover { background: var(--portal-sunk); color: var(--portal-text); }

/*
    The current entry: a tinted pill whose TEXT carries the brand, and no spine. The spine was a second
    marker for one state - the tint already says it - and it sat outside the pill's own padding, where it
    read as a stray mark beside the rail rather than part of the entry.
*/
.portal-nav a.active,
.portal-nav a[aria-current] {
    background: var(--portal-brand-tint);
    color: var(--portal-brand-deep);
}

.portal-rail-foot {
    display: flex;
    align-items: center;
    gap: var(--portal-space-2);
    margin-left: auto;
    min-width: 0;
}

.portal-signed-in {
    display: none;
    min-width: 0;
}

.portal-signed-in-who {
    display: block;
    font-size: var(--portal-text-sm);
    font-weight: 500;
    color: var(--portal-text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.portal-signed-in-what {
    display: block;
    font-size: var(--portal-label-size);
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--portal-text-faint);
}

.portal-language-problem {
    font-size: var(--portal-text-sm);
    color: var(--portal-text-muted);
}

.portal-body {
    position: relative;
    flex: 999 1 35rem;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

/* Shell chrome above the page: the day being shown and the language it is read in. Both belong to the app
   rather than to whichever screen is open, and both sit at the right edge, the language outermost. */
.portal-topbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: var(--portal-space-3) var(--portal-space-4);
    padding: var(--portal-space-3) var(--portal-gutter) 0;
}

.portal-main {
    flex: 1;
    width: 100%;
    max-width: var(--portal-column);
    margin: 0 auto;
    padding: var(--portal-space-4) var(--portal-gutter) var(--portal-space-8);
}

/*
    The desktop: the same column, with the rail beside it rather than above it. Nothing is added and nothing
    moves to a different place - the rail's own children simply stack.
*/
@media (min-width: 56rem) {
    .portal-shell {
        flex-direction: row;
        margin: var(--portal-space-6);
        min-height: calc(100vh - var(--portal-space-8) - var(--portal-space-4));
        border-radius: var(--portal-radius-card);
    }

    .portal-rail {
        flex: none;
        flex-direction: column;
        align-items: stretch;
        width: var(--portal-rail);
        gap: var(--portal-space-6);
        /* No padding at the foot: the person's block ends on the card's bottom edge, where the week strip
           beside it does, so the two are one band rather than two that nearly line up. */
        padding: var(--portal-space-6) 0 0 var(--portal-space-4);
        border-bottom: 0;
        border-right: 1px solid var(--portal-rule);
    }

    .portal-brand {
        flex-direction: column;
        align-items: flex-start;
        gap: var(--portal-space-3);
    }

    .portal-brand-mark { height: 1.625rem; }

    .portal-brand-product {
        padding-left: 0;
        border-left: 0;
        max-width: 100%;
    }

    .portal-rail-middle {
        flex-direction: column;
        align-items: stretch;
        flex-basis: auto;
        gap: var(--portal-space-4);
    }

    .portal-nav {
        flex-direction: column;
        overflow-x: visible;
        padding-right: var(--portal-space-3);
    }

    /* One band across the foot of the card: this and the week strip beside it share --portal-foot, so the
       bottom edge of the rail and the bottom edge of the content line up rather than nearly lining up. */
    .portal-rail-foot {
        flex-direction: column;
        align-items: flex-start;
        justify-content: center;
        gap: 1px;
        height: var(--portal-foot);
        margin-top: auto;
        /* Out of the rail's own left padding and back in again, so the rule above this block runs the full
           width of the rail and meets the week strip's - one line across the card, not two stubs. */
        margin-left: calc(-1 * var(--portal-space-4));
        margin-right: 0;
        padding-left: var(--portal-space-4);
        border-top: 1px solid var(--portal-rule);
    }

    .portal-signed-in { display: block; }

    /*
        Out of the flow at this size, so the page starts at the top of the card rather than below a band of
        chrome: in flow, the stacked language toggle made that band ~68 px tall and pushed every heading down
        by it. The page header reserves the width this occupies, so a long title wraps instead of running
        underneath.
    */
    .portal-topbar {
        position: absolute;
        top: var(--portal-space-4);
        right: var(--portal-space-8);
        z-index: 1;
        padding: 0;
    }

    .portal-page-header { padding-right: 20rem; }

    /*
        THE ROW CARRIES THE RESERVATION WHEN THERE IS ONE. .portal-page-actions puts the heading, a filter
        and an action on one line and zeroes the header's own right padding to do it, which took away the
        space the out-of-flow topbar above occupies, so the controls landed underneath the day and the
        language picker. The row is what sits at the top of the card now, so the row is what keeps clear.
    */
    .portal-page-actions { padding-right: 20rem; }

    .portal-main {
        /* No column clamp here: --portal-column is the PHONE's measure (D15), and keeping it on a desktop
           left the page header only 36rem wide, so the day and "today" sat beside the heading in the middle
           of the card rather than at its right edge. The card's own width is the measure at this size. */
        max-width: none;
        margin: 0;
        padding: var(--portal-space-6) var(--portal-space-8) var(--portal-space-8);
    }

    /*
        The text of a page still reads at a comfortable measure; it is the HEADER row that spans - and a
        block that has a SECOND COLUMN beside it, which the page declares with .portal-wide rather than the
        layout guessing from the route. Without that exclusion the site page's diagram and its detail panel
        had 36rem to share however wide the window was, so the panel wrapped underneath at every size
        (found 21-10-2026 at 1280 px).
    */
    .portal-main > :not(.portal-page-header):not(.portal-wide) { max-width: var(--portal-column); }
    .portal-main > .portal-wide { max-width: var(--portal-wide); }
}

/* ---------------------------------------------------------------- controls */

/*
    The one control that appears inside a card, so it is the one whose contrast has to hold in BOTH schemes.
    It used to be the brand colour on the surface colour, which is 1.6:1 in dark mode - and the screen it
    appears on is the "try again" one, so a customer whose API is down could not see the control that
    recovers it. Brand fill with its own contrast colour is legible in both, and the border is what keeps it
    distinguishable from body text in light mode rather than a rectangle of the same colour.
*/
.portal-button {
    font: inherit;
    padding: var(--portal-space-2) var(--portal-space-3);
    border: 1px solid var(--portal-brand);
    border-radius: var(--portal-radius-sm);
    background: var(--portal-brand);
    color: var(--portal-brand-contrast);
    /* 44 px, which is the smallest target a thumb finds reliably. */
    min-height: 2.75rem;
    cursor: pointer;
}

/* On the card rather than on a coloured band, so it is an outline in the page's own ink. */
.portal-button-quiet {
    background: transparent;
    border-color: var(--portal-border);
    color: var(--portal-text-2);
}

.portal-button-quiet:hover { background: var(--portal-sunk); }

/*
    Signing out is a link, not a button. It is the quietest thing in the rail and it sits under the person's
    own name: a bordered control there reads as the action of that block, which it is not - the actions of
    this app are on the page. A <button> still, because it DOES something rather than going somewhere, and
    the underline is what says it can be clicked.
*/
.portal-signout {
    font: inherit;
    font-size: var(--portal-text-xs);
    padding: 0;
    border: 0;
    background: none;
    color: var(--portal-text-muted);
    text-decoration: underline;
    text-align: left;
    cursor: pointer;
}

.portal-signout:hover { color: var(--portal-text); }

/* A select is a FIELD, so it takes the field's edge and corner rather than a button's: the same
   --portal-control-border and --portal-radius-control as the inputs it stands beside, on the form and in
   a page's own filter alike. It read as a button at 6 px on --portal-border, which is also 1.31:1. */
.portal-select {
    font: inherit;
    min-height: 2.75rem;
    max-width: 12rem;
    padding: var(--portal-space-1) var(--portal-space-2);
    border: 1px solid var(--portal-control-border);
    border-radius: var(--portal-radius-control);
    background: var(--portal-surface);
    color: var(--portal-text);
}

/* ---------------------------------------------------------------- page header */

.portal-page-header { margin-bottom: var(--portal-space-6); }

/*
    A page's heading WITH its filter and its action, on one row. Aligned on the baseline of the controls
    rather than the top, so the select and the button sit on the heading's own last line instead of
    floating beside the middle of it.
*/
.portal-page-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--portal-space-4);
    margin-bottom: var(--portal-space-6);
}

/* Inside the row the header gives up its own bottom margin and its right padding - that padding exists to
   keep the day out of a full-width heading, and here the row's own gap does the same job. */
.portal-page-actions > .portal-page-header {
    flex: 1 1 20rem;
    margin-bottom: 0;
    padding-right: 0;
}

/* A field that is one item of a row rather than a line of a form. */
.portal-field-inline { margin-bottom: 0; }

/* The mark before a section's title (Card.Icon), on the title's own baseline. */
.portal-card-icon {
    flex: none;
    margin-right: var(--portal-space-2);
    vertical-align: -0.15em;
}

.portal-page-header h1 {
    margin: 0;
    font-size: var(--portal-text-xl);
    line-height: 1.25;
}

/* The question, VISIBLE rather than behind a hover (D16): there is no hover on a touch screen. */
.portal-question {
    margin: var(--portal-space-2) 0 0;
    color: var(--portal-text-muted);
}

.portal-context {
    margin: var(--portal-space-1) 0 0;
    font-size: var(--portal-text-sm);
    color: var(--portal-text-muted);
}

/* ---------------------------------------------------------------- card */

.portal-card {
    background: var(--portal-surface);
    border: 1px solid var(--portal-border);
    border-radius: var(--portal-radius);
    box-shadow: var(--portal-elevation);
    margin-bottom: var(--portal-space-4);
    overflow: hidden;
}

/* The title and the figure the section's own question asks for, on one row. Baseline-aligned, because the
   two are different sizes and aligning their boxes would float the small one off the cap height. */
.portal-card-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--portal-space-2) var(--portal-space-4);
    padding: var(--portal-space-3) var(--portal-space-4);
    border-bottom: 1px solid var(--portal-border);
}

.portal-card-head > .portal-card-title {
    flex: 1 1 13rem;
    padding: 0;
    border-bottom: 0;
}

.portal-card-aside {
    margin: 0;
    font-size: var(--portal-text-sm);
    color: var(--portal-text-muted);
    font-variant-numeric: tabular-nums;
}

.portal-card-title {
    margin: 0;
    padding: var(--portal-space-3) var(--portal-space-4);
    font-size: var(--portal-text-sm);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--portal-text-muted);
    border-bottom: 1px solid var(--portal-border);
}

.portal-card-body > :first-child { margin-top: 0; }
.portal-card-body > :last-child { margin-bottom: 0; }

.portal-fact {
    margin: 0;
    padding: var(--portal-space-4);
    font-size: var(--portal-text-lg);
}

/* ---------------------------------------------------------------- list */

.portal-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.portal-list-item + .portal-list-item { border-top: 1px solid var(--portal-border-soft); }

.portal-list-link {
    display: flex;
    align-items: center;
    gap: var(--portal-space-3);
    /* Well past the 44 px floor (D21) now that the row carries a mark, a name and a line of facts. */
    min-height: 4.25rem;
    padding: var(--portal-space-3) var(--portal-space-4);
    color: var(--portal-text);
    text-decoration: none;
}

.portal-list-title { flex: 1; font-weight: 500; }

.portal-list-facts {
    font-size: var(--portal-text-sm);
    color: var(--portal-text-muted);
}

.portal-list-chevron { color: var(--portal-text-muted); }

/* ---------------------------------------------------------------- empty state */

.portal-empty {
    padding: var(--portal-space-6) var(--portal-space-4);
    background: var(--portal-surface);
    border: 1px solid var(--portal-border);
    border-radius: var(--portal-radius);
    text-align: center;
}

.portal-empty-title { margin: 0; font-weight: 600; }

.portal-empty-body {
    margin: var(--portal-space-2) 0 0;
    color: var(--portal-text-muted);
}

.portal-empty-action { margin-top: var(--portal-space-4); }

.portal-muted { color: var(--portal-text-muted); }

.portal-back { margin-top: var(--portal-space-6); }

/* ---------------------------------------------------------------- state pill (D18)
   The WORD is the meaning; the colour only agrees with it (D21), so this is readable in greyscale and the
   text colour is the page's own rather than one tuned per state. */

.portal-pill {
    display: inline-flex;
    align-items: baseline;
    gap: var(--portal-space-1);
    padding: 2px var(--portal-space-2);
    border: 1px solid var(--portal-state-border);
    border-radius: var(--portal-radius-sm);
    background: var(--portal-state-ground);
    font-size: var(--portal-text-xs);
    font-weight: 500;
    line-height: 1.5;
    color: var(--portal-state-ink);
    /* White-space held, because "not yet settled" wrapping mid-phrase reads as two states. */
    white-space: nowrap;
}

.portal-pill-detail { font-weight: 400; color: var(--portal-text-faint); }

/* Outlined and lightness-stepped rather than five pale hues, for two reasons. In a list a row of coloured
   pills is louder than its data; and red is the brand here, so a state that reached for it would read as an
   alarm on a settled figure. Each one is told apart by its WORD first (D21), then by its border and ground. */
.portal-pill-final { }

.portal-pill-provisional {
    background: var(--portal-state-held-ground);
    color: var(--portal-state-held-ink);
}

.portal-pill-unsettled {
    border-style: dashed;
    color: var(--portal-state-faint-ink);
}

.portal-pill-assumed {
    border-style: dotted;
    background: var(--portal-state-held-ground);
    color: var(--portal-state-held-ink);
}

/* The one state that keeps a hue: "we have no reading" is a caution somebody should act on, and a grey
   pill in a grey list is not read. Amber rather than red, because red is the brand. */
.portal-pill-silent {
    background: var(--portal-state-caution-ground);
    border-color: var(--portal-state-caution-border);
    color: var(--portal-state-caution-ink);
}

/* ---------------------------------------------------------------- KPI tile and row */

/*
    One column at 380 px, which is the real layout (D15). auto-fit rather than a fixed count so two tiles
    fill the width and four wrap to a grid; the 13rem minimum is what stops a number and its unit breaking
    across lines, which is the point at which a KPI stops being readable at a glance.
*/
.portal-kpi-row {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--portal-space-3);
    margin-bottom: var(--portal-space-4);
}

.portal-kpi {
    padding: var(--portal-space-4);
    background: var(--portal-surface);
    border: 1px solid var(--portal-border);
    border-radius: var(--portal-radius);
    box-shadow: var(--portal-elevation);
}

.portal-kpi-label {
    margin: 0;
    font-size: var(--portal-text-sm);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--portal-text-muted);
}

.portal-kpi-value {
    margin: var(--portal-space-2) 0 0;
    /* Space Grotesk, like every other figure on the page: a number set in the body face beside one set in
       the figure face is the drift the two-token type scale exists to prevent. */
    font-family: var(--portal-font-figure);
    font-size: var(--portal-text-xl);
    font-weight: 700;
    line-height: 1.15;
    /* The figures line up between tiles in a row, which is the only thing making two of them comparable
       at a glance - and it keeps a changing figure from shifting the characters beside it. */
    font-variant-numeric: tabular-nums;
}

/* Smaller and lighter than the figure: a unit that competes with the number is noise, and a unit that is
   absent is what §6.3 rule 3 forbids. */
.portal-kpi-unit {
    margin-left: var(--portal-space-1);
    font-size: var(--portal-text-base);
    font-weight: 400;
    color: var(--portal-text-muted);
}

.portal-kpi-state { margin: var(--portal-space-2) 0 0; }

/* The smallest step of the type scale, on the page rather than in a tooltip - the whole of D16. */
.portal-kpi-explanation {
    margin: var(--portal-space-2) 0 0;
    font-size: var(--portal-text-xs);
    color: var(--portal-text-muted);
}

.portal-kpi-more { margin-top: var(--portal-space-3); }

/* ---------------------------------------------------------------- disclosure */

.portal-disclosure {
    border-top: 1px solid var(--portal-border);
    padding-top: var(--portal-space-2);
}

.portal-disclosure-summary {
    /* 44 px including the padding, because this is a touch target and D21 sets that bar (the dashboard's
       hover-only trade-off does not carry over). */
    min-height: 2.75rem;
    display: flex;
    align-items: center;
    padding: var(--portal-space-2) 0;
    font-size: var(--portal-text-sm);
    font-weight: 600;
    color: var(--portal-brand);
    cursor: pointer;
}

.portal-disclosure-body {
    padding-bottom: var(--portal-space-2);
    font-size: var(--portal-text-sm);
    color: var(--portal-text-muted);
}

.portal-disclosure-body > :first-child { margin-top: 0; }
.portal-disclosure-body > :last-child { margin-bottom: 0; }

/* ---------------------------------------------------------------- chart card */

.portal-legend {
    display: flex;
    flex-wrap: wrap;
    gap: var(--portal-space-2) var(--portal-space-4);
    margin: var(--portal-space-3) 0 0;
    padding: 0 var(--portal-space-4);
    list-style: none;
    font-size: var(--portal-text-sm);
}

.portal-legend-entry {
    display: flex;
    align-items: center;
    gap: var(--portal-space-2);
}

.portal-legend-swatch {
    width: 0.75rem;
    height: 0.75rem;
    border-radius: var(--portal-radius-sm);
    flex: none;
}

.portal-chart-state {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--portal-space-2);
    margin: var(--portal-space-3) 0 0;
    padding: 0 var(--portal-space-4) var(--portal-space-4);
}

.portal-chart-asof {
    font-size: var(--portal-text-xs);
    color: var(--portal-text-muted);
}

.portal-chart-unavailable,
.portal-chart-more { padding: 0 var(--portal-space-4) var(--portal-space-4); }

.portal-chart-unavailable { color: var(--portal-text-muted); }

/* ---------------------------------------------------------------- period picker */

.portal-period {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--portal-space-3);
    margin-bottom: var(--portal-space-4);
}

.portal-period-step {
    display: flex;
    align-items: center;
    gap: var(--portal-space-2);
}

/* Fixed width so the label does not jump between "oktober 2026" and "mei 2026" and move the Next button
   out from under a thumb that is tapping through months. */
.portal-period-label {
    min-width: 9rem;
    text-align: center;
    font-weight: 600;
}

.portal-button-step {
    min-width: 2.75rem;
    font-size: var(--portal-text-lg);
    line-height: 1;
}

/* Visibly inert rather than merely unclickable: a disabled control that looks live reads as a broken app. */
.portal-button:disabled {
    opacity: 0.45;
    cursor: default;
}

.portal-period-custom {
    display: flex;
    flex-wrap: wrap;
    gap: var(--portal-space-3);
}

.portal-period-field-label {
    display: block;
    font-size: var(--portal-text-xs);
    color: var(--portal-text-muted);
}

.portal-input {
    font: inherit;
    min-height: 2.75rem;
    padding: var(--portal-space-1) var(--portal-space-2);
    border: 1px solid var(--portal-border);
    border-radius: var(--portal-radius-sm);
    background: var(--portal-surface);
    color: var(--portal-text);
}

/* ---------------------------------------------------------------- table, the desktop half of a pair
   DataTable renders this and DataList renders the phone half; exactly one of the two is displayed at any
   width, so a screen reader is offered one and never hears the collection twice (§6.3 rule 4). */

.portal-desktop-only { display: none; }

.portal-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--portal-text-sm);
}

.portal-table th,
.portal-table td {
    padding: var(--portal-space-3) var(--portal-space-4);
    text-align: left;
    border-bottom: 1px solid var(--portal-border-soft);
}

/* A column heading is a LABEL, and takes the label pair the rest of the app's labels take rather than a
   size of its own, the same 10 px at 0.11em as a field's. */
.portal-table th {
    font-size: var(--portal-label-size);
    font-weight: 600;
    letter-spacing: var(--portal-label-tracking);
    text-transform: uppercase;
    color: var(--portal-text-muted);
    border-bottom-color: var(--portal-border);
}

.portal-table td {
    /* Top, not middle: a cell can carry two lines (a name over its stamp) and a one-line cell beside it
       must start level with the first of them, not float against the pair. */
    vertical-align: top;
    font-size: var(--portal-text-base);
    font-variant-numeric: tabular-nums;
}

.portal-table tr:last-child td { border-bottom: 0; }

/* The one fact a reader scans the column for. */
.portal-table-name { font-weight: 500; }

/* The urgent section's rows carry their name heavier, because that section is the one somebody opens the
   page for and its rows are read before the others. */
.portal-section-urgent .portal-table-name { font-weight: 600; }

.portal-table-quiet { color: var(--portal-text-2); }

/* A stamp under the name it belongs to: its own line, and quieter than the name. */
.portal-window-stamp {
    display: block;
    font-size: var(--portal-text-sm);
    color: var(--portal-text-muted);
}

/* An action inside a row is not a page's action: it sits in a cell beside five others, so it takes the
   row's own type size. The 44 px floor stays - a thumb finds this one too. */
.portal-button-small {
    padding: 0 var(--portal-space-3);
    font-size: var(--portal-text-sm);
}

/* ---------------------------------------------------------------- the framework's own error strip */

#blazor-error-ui {
    display: none;
    position: fixed;
    bottom: 0; left: 0; right: 0;
    z-index: 1000;
    padding: var(--portal-space-3) var(--portal-gutter);
    background: #fde047;
    color: #15232e;
}

#blazor-error-ui .reload { color: #15232e; }

/* ---------------------------------------------------------------- desktop
   More room beside the same column, never a second information architecture. */

@media (min-width: 48rem) {
    .portal-main { padding-top: var(--portal-space-8); }
    .portal-signed-in { display: inline; }

    /* The tiles sit beside each other where there is room. Still the same tiles in the same order - the
       phone column with more width, not a second information architecture. */
    .portal-kpi-row { grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); }

    /* The pair swaps over here, and only here. A page that renders a table without a list beside it is
       blank on a phone, which is why the two are checked as a pair in the tests rather than left to
       whoever writes the next page. */
    .portal-desktop-only { display: block; }
    .portal-phone-only { display: none; }
}

/* ---------------------------------------------------------------- the day, and the week that selects it */

.portal-day { text-align: right; }

.portal-day-date {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: flex-end;
    gap: var(--portal-space-2);
    margin: 0;
    font-family: var(--portal-font-figure);
    font-size: var(--portal-text-base);
    font-weight: 500;
}

.portal-day-today {
    font-family: var(--portal-font);
    font-size: var(--portal-label-size);
    font-weight: 600;
    letter-spacing: var(--portal-label-tracking);
    text-transform: uppercase;
    color: var(--portal-text-faint);
}

.portal-day-note {
    margin: var(--portal-space-1) 0 0;
    font-size: var(--portal-text-xs);
    color: var(--portal-text-faint);
}



/*
    The week along the foot of the card. Seven equal columns so the shape is the same every week, and the
    two steps are outside them so a thumb reaching for Sunday does not land on "next".
*/
.portal-week {
    display: flex;
    align-items: stretch;
    height: var(--portal-foot);
    border-top: 1px solid var(--portal-rule);
}

.portal-week-step,
.portal-week-day {
    font: inherit;
    background: none;
    border: 0;
    border-right: 1px solid var(--portal-rule);
    color: var(--portal-text-2);
    cursor: pointer;
}

.portal-week-step {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 3rem;
    color: var(--portal-text-muted);
}

.portal-week > :last-child { border-right: 0; }

.portal-week-day {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 1px;
    /* Fills the band, which is itself well over the 44 px D21 asks of the control a thumb uses most here. */
    height: 100%;
    padding: var(--portal-space-2) var(--portal-space-1);
}

.portal-week-day:hover:not(:disabled) { background: var(--portal-sunk); }

.portal-week-dow {
    font-size: var(--portal-label-size);
    font-weight: 600;
    letter-spacing: var(--portal-label-tracking);
    text-transform: uppercase;
    color: var(--portal-text-faint);
}

.portal-week-dom {
    font-family: var(--portal-font-figure);
    font-size: var(--portal-text-lg);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.portal-week-day.is-on { background: var(--portal-brand-tint); }
.portal-week-day.is-on .portal-week-dow,
.portal-week-day.is-on .portal-week-dom { color: var(--portal-brand); }

/* A day that has not happened: shown, so the week keeps its shape, and plainly not on offer. */
.portal-week-day:disabled,
.portal-week-step:disabled {
    cursor: default;
    opacity: 0.45;
}

/* ---------------------------------------------------------------- language */

/*
    Two choices, both on screen. A dropdown would hide the one a reader in the wrong language is looking for,
    which is the only reason this control exists.
*/
.portal-language {
    display: inline-flex;
    flex-direction: column;
    padding: 2px;
    border: 1px solid var(--portal-border);
    border-radius: var(--portal-radius-control);
    background: var(--portal-sunk);
}

.portal-language-choice {
    font: inherit;
    font-size: var(--portal-text-xs);
    font-weight: 600;
    letter-spacing: 0.06em;
    /* Narrower than 44 px on purpose, and defensible: it is two letters in a pair, the pair itself is a
       44 px target, and a tap on the wrong half is undone by a tap on the other. */
    min-width: 2.25rem;
    min-height: 2rem;
    padding: 0 var(--portal-space-2);
    border: 0;
    border-radius: var(--portal-radius-sm);
    background: none;
    color: var(--portal-text-muted);
    cursor: pointer;
}

.portal-language-choice:hover { color: var(--portal-text); }

.portal-language-choice.is-on {
    background: var(--portal-surface);
    border: 1px solid var(--portal-border);
    color: var(--portal-text);
}

/* ===========================================================================
   The site diagram, its picker, and the panel a row opens.

   The diagram is a flowchart whose branch axis runs DOWN the page. The
   dashboard's fans sideways, which is right on a wide screen and impossible at
   390 px: three children abreast leaves about 100 px each, which holds neither
   a name nor a figure. Turned ninety degrees every row is full width, the same
   graph is drawn, and the desktop is this column with the detail beside it
   rather than a second information architecture (D15).
   =========================================================================== */

/* The small-caps label, which four places were spelling out inline. */
.portal-label {
    margin: 0;
    font-size: var(--portal-label-size);
    font-weight: 600;
    letter-spacing: var(--portal-label-tracking);
    text-transform: uppercase;
    color: var(--portal-text-muted);
}

/* ---------------------------------------------------------------- the picker */

.portal-sitepicker {
    position: relative;
}

.portal-sitepicker-title {
    margin: var(--portal-space-1) 0 0 0;
    line-height: 1;
}

.portal-sitepicker-button {
    display: inline-flex;
    align-items: center;
    gap: var(--portal-space-2);
    min-height: 44px;
    padding: 2px var(--portal-space-3);
    margin-left: calc(var(--portal-space-3) * -1);
    border: 1px solid var(--portal-border);
    border-radius: var(--portal-radius-control);
    background: var(--portal-surface);
    font-family: var(--portal-font-figure);
    font-weight: 700;
    font-size: var(--portal-text-xl);
    letter-spacing: -0.01em;
    color: var(--portal-text);
    cursor: pointer;
}

/* One site is not a choice, so the control stops looking like one rather than
   offering a list of itself. */
.portal-sitepicker-button:disabled {
    border-color: transparent;
    background: transparent;
    cursor: default;
}

.portal-sitepicker-button:not(:disabled):hover {
    border-color: var(--portal-brand);
    color: var(--portal-brand-deep);
}

.portal-sitepicker.is-open .portal-sitepicker-button {
    border-color: var(--portal-brand);
    background: var(--portal-brand-tint);
    color: var(--portal-brand-deep);
}

.portal-sitepicker-chevron {
    display: inline-flex;
    color: var(--portal-text-muted);
}

.portal-sitepicker.is-open .portal-sitepicker-chevron {
    color: var(--portal-brand-deep);
}

/* Covers the page so a tap anywhere dismisses the list. A real button, so the
   keyboard reaches it; Escape on the picker does the same thing. */
.portal-scrim {
    position: fixed;
    inset: 0;
    z-index: 20;
    border: 0;
    padding: 0;
    background: transparent;
    cursor: default;
}

.portal-sitepicker-menu {
    position: absolute;
    z-index: 21;
    top: calc(100% + var(--portal-space-2));
    left: calc(var(--portal-space-3) * -1);
    width: min(22rem, calc(100vw - (var(--portal-gutter) * 2)));
    max-height: min(60vh, 26rem);
    overflow-y: auto;
    border: 1px solid var(--portal-border);
    border-radius: var(--portal-radius);
    background: var(--portal-surface);
    box-shadow: 0 10px 28px rgba(26, 34, 38, 0.14);
}

.portal-sitepicker-count {
    margin: 0;
    padding: var(--portal-space-2) var(--portal-space-3);
    border-bottom: 1px solid var(--portal-border-soft);
    background: var(--portal-sunk);
    font-size: var(--portal-label-size);
    font-weight: 600;
    letter-spacing: var(--portal-label-tracking);
    text-transform: uppercase;
    color: var(--portal-text-muted);
}

.portal-sitepicker-search {
    padding: var(--portal-space-2) var(--portal-space-3);
    border-bottom: 1px solid var(--portal-border-soft);
}

.portal-input {
    width: 100%;
    box-sizing: border-box;
    min-height: 44px;
    padding: var(--portal-space-2) var(--portal-space-3);
    border: 1px solid var(--portal-control-border);
    border-radius: var(--portal-radius-control);
    background: var(--portal-surface);
    font: inherit;
    font-size: var(--portal-text-base);
    color: var(--portal-text);
}

.portal-sitepicker-item {
    display: flex;
    align-items: center;
    gap: var(--portal-space-3);
    width: 100%;
    min-height: 48px;
    padding: var(--portal-space-2) var(--portal-space-3);
    border: 0;
    border-bottom: 1px solid var(--portal-border-soft);
    background: var(--portal-surface);
    font: inherit;
    font-size: var(--portal-text-base);
    text-align: left;
    color: var(--portal-text);
    cursor: pointer;
}

.portal-sitepicker-item:last-child {
    border-bottom: 0;
}

.portal-sitepicker-item:hover {
    background: var(--portal-sunk);
}

/* PisteOp's own current-venue pill, in its own colours: the tint, no visible
   border and the deep accent at weight 600. */
.portal-sitepicker-item.is-on {
    background: var(--portal-brand-tint);
    color: var(--portal-brand-deep);
    font-weight: 600;
}

.portal-sitepicker-item-name {
    flex: 1 1 auto;
    min-width: 0;
}

.portal-sitepicker-none {
    margin: 0;
    padding: var(--portal-space-4) var(--portal-space-3);
    font-size: var(--portal-text-sm);
    color: var(--portal-text-muted);
}

/* ---------------------------------------------------------------- the page */

.portal-site {
    display: flex;
    flex-wrap: wrap;
    gap: var(--portal-space-6);
    align-items: flex-start;
    margin-top: var(--portal-space-4);
}

 /* The diagram is the page's subject, so it takes its full measure BEFORE the panel grows: nested rows
    indent twice, and at 20rem the second level had about 130 px left for a name and a figure. It does not
    grow past 28rem either - a wider column only stretches the rows. */
.portal-site-diagram {
    flex: 0 1 28rem;
    min-width: 0;
}

.portal-site-detail {
    flex: 1 1 18rem;
    min-width: 0;
}

.portal-live {
    display: flex;
    align-items: center;
    gap: var(--portal-space-2);
    margin: 0 0 var(--portal-space-3) 0;
    padding: var(--portal-space-2) var(--portal-space-3);
    border: 1px solid var(--portal-border);
    border-radius: var(--portal-radius-control);
    background: var(--portal-sunk);
    font-size: var(--portal-text-sm);
    color: var(--portal-text-2);
}

.portal-live-dot {
    flex: none;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--portal-flow-out);
}

.portal-site-footnote {
    margin: var(--portal-space-3) 0 0 0;
    font-size: var(--portal-text-sm);
    color: var(--portal-text-muted);
}

.portal-site-close {
    margin: var(--portal-space-3) 0 0 0;
}

.portal-link-button {
    min-height: 44px;
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    font-size: var(--portal-text-sm);
    color: var(--portal-brand-deep);
    text-decoration: underline;
    cursor: pointer;
}

/* ---------------------------------------------------------------- the flowchart */

.pf-chip {
    display: flex;
    align-items: center;
    gap: var(--portal-space-2);
    padding: var(--portal-space-2) var(--portal-space-3);
    border-radius: var(--portal-radius-control);
    background: var(--portal-border-soft);
    font-size: var(--portal-text-sm);
    font-weight: 500;
    color: var(--portal-text-2);
}

.pf-chip-icon {
    display: inline-flex;
    color: var(--portal-text-2);
}

/* The connector from the public grid down to the connection, with the figure
   it carries beside it rather than on top of it. */
.pf-trunk {
    display: flex;
    align-items: center;
    gap: var(--portal-space-3);
    padding: var(--portal-space-2) 0 var(--portal-space-2) 18px;
}

.pf-node {
    padding: var(--portal-space-3);
    border: 1px solid var(--portal-border);
    border-radius: var(--portal-radius);
    background: var(--portal-surface);
}

.pf-node-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--portal-space-2);
}

.pf-node-name {
    font-family: var(--portal-font-figure);
    font-weight: 500;
    font-size: var(--portal-text-base);
}

.pf-node-tag {
    font-size: var(--portal-label-size);
    font-weight: 600;
    letter-spacing: var(--portal-label-tracking);
    text-transform: uppercase;
    color: var(--portal-text-muted);
}

.pf-node-gap {
    margin: var(--portal-space-2) 0 0 0;
    padding: var(--portal-space-2) var(--portal-space-3);
    border: 1px solid var(--portal-state-caution-border);
    border-radius: var(--portal-radius-sm);
    background: var(--portal-state-caution-ground);
    font-size: var(--portal-text-sm);
    line-height: 1.4;
    color: var(--portal-state-caution-ink);
}

/* The bracket: one quiet vertical line the rows hang off. Structure, so it
   never moves. */
.pf-kids {
    position: relative;
    margin-left: 10px;
    padding: var(--portal-space-2) 0 0 14px;
    border-left: 2px solid var(--portal-flow-bracket);
    display: flex;
    flex-direction: column;
    gap: var(--portal-space-2);
}

.pf-row {
    display: flex;
    align-items: center;
    gap: 0;
}

.pf-sub {
    flex: 1 1 auto;
    min-width: 0;
}

.pf-asset {
    display: flex;
    align-items: center;
    gap: var(--portal-space-2);
    flex: 1 1 auto;
    min-width: 0;
    min-height: 56px;
    padding: var(--portal-space-2) var(--portal-space-3);
    border: 1px solid var(--portal-border);
    border-radius: var(--portal-radius-control);
    background: var(--portal-surface);
    font: inherit;
    text-align: left;
    color: var(--portal-text);
    cursor: pointer;
}

.pf-asset:hover {
    border-color: var(--portal-control-border);
}

.pf-asset.is-on {
    border-color: var(--portal-brand);
    background: var(--portal-brand-tint);
}

/* A row the customer has declared out of service. Three signals and the colour is never one of them on its
   own (D21): the amber outline with a heavier left edge, the triangle in the chip, and the words in it. The
   figure stays exactly as it was measured (D9) - the window says what the customer expects, the meter says
   what happened, and a row still moving power while it is declared out is the one thing worth seeing. Amber
   rather than the brand red, which is the brand here and would read as an alarm. */
.pf-asset.is-out {
    border-color: var(--portal-state-caution-border);
    border-left-width: 4px;
    padding-left: calc(var(--portal-space-3) - 3px);
}

/* Opening such a row must not cost it the marker: the brand outline says which row is open, and the heavy
   left edge stays amber to say what it is. */
.pf-asset.is-out.is-on {
    border-color: var(--portal-brand);
    border-left-color: var(--portal-state-caution-border);
}

.pf-asset-icon {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    border-radius: var(--portal-radius-sm);
    background: var(--portal-border-soft);
    color: var(--portal-text-2);
}

.pf-kind-battery { background: #eef3f7; color: var(--portal-flow-in); }
.pf-kind-solar { background: var(--portal-state-caution-ground); color: var(--portal-state-caution-ink); }

.pf-asset-what {
    flex: 1 1 auto;
    min-width: 0;
}

.pf-asset-name {
    display: block;
    font-size: var(--portal-text-sm);
    font-weight: 500;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.pf-asset-note {
    display: block;
    font-size: var(--portal-text-xs);
    color: var(--portal-text-muted);
}

/* Said in words, in its own outline, because a figure that is a forecast and
   one that is a reading must never look the same (section 7.2). */
.pf-asset-estimated {
    display: inline-block;
    margin-top: 3px;
    padding: 0 7px;
    border: 1px solid var(--portal-state-caution-border);
    border-radius: 999px;
    background: var(--portal-state-caution-ground);
    font-size: var(--portal-text-xs);
    color: var(--portal-state-caution-ink);
}

/* The chip itself. Shaped like .pf-asset-estimated because both are a word said about the row above the
   figure - and told apart by the triangle and by the row's own outline, which that one never carries, so
   "estimated" and "out of service" can sit side by side without reading as the same badge twice. */
.pf-asset-out {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    margin-top: 3px;
    margin-right: 4px;
    padding: 0 7px;
    border: 1px solid var(--portal-state-caution-border);
    border-radius: 999px;
    background: var(--portal-state-caution-ground);
    font-size: var(--portal-text-xs);
    font-weight: 600;
    color: var(--portal-state-caution-ink);
    white-space: nowrap;
}

.pf-asset-out svg { flex: none; }

.pf-figure {
    flex: none;
    text-align: right;
}

.pf-figure-value {
    display: block;
    font-family: var(--portal-font-figure);
    font-weight: 700;
    font-size: var(--portal-text-lg);
    white-space: nowrap;
}

.pf-figure-unit {
    display: block;
    font-size: var(--portal-text-xs);
    color: var(--portal-text-muted);
}

.pf-figure.is-in .pf-figure-value { color: var(--portal-flow-in); }
.pf-figure.is-out .pf-figure-value { color: var(--portal-flow-out); }
.pf-figure.is-idle .pf-figure-value,
.pf-figure.is-unknown .pf-figure-value { color: var(--portal-flow-idle); }

/* The trunk figure is the headline on the diagram, so it is spelled out. */
.pf-trunk .pf-figure { text-align: left; }
.pf-trunk .pf-figure-value { font-size: var(--portal-text-xl); }
.pf-trunk .pf-figure-unit { font-size: var(--portal-text-sm); font-weight: 500; }
.pf-trunk .pf-figure.is-in .pf-figure-unit { color: var(--portal-flow-in); }
.pf-trunk .pf-figure.is-out .pf-figure-unit { color: var(--portal-flow-out); }

/* ---------------------------------------------------------------- the moving part

   A gradient on a plain element, not an SVG path: the layout is fluid, and an
   SVG connector needs the pixel positions of the two boxes it joins, which a
   flex column does not have. The dashes travel the way the power does, and the
   arrow, the colour and the word all say the same thing, so the motion is the
   fourth statement of direction and never the only one (D21). */

.pf-link {
    flex: none;
    display: block;
    --pf-speed: 1.6s;
    --pf-dash: var(--portal-flow-idle);
}

.pf-link.is-in { --pf-dash: var(--portal-flow-in); }
.pf-link.is-out { --pf-dash: var(--portal-flow-out); }

.pf-link-v {
    width: 3px;
    height: 44px;
    margin-left: 4px;
    background-image: repeating-linear-gradient(
        to bottom,
        var(--pf-dash) 0 9px,
        transparent 9px 24px);
    background-size: 3px 24px;
    animation: pf-travel-down var(--pf-speed) linear infinite;
}

.pf-link-h {
    width: 40px;
    height: 3px;
    background-image: repeating-linear-gradient(
        to right,
        var(--pf-dash) 0 9px,
        transparent 9px 24px);
    background-size: 24px 3px;
    animation: pf-travel-right var(--pf-speed) linear infinite;
}

/* An export runs the other way, so the same gradient is animated backwards
   rather than a second one being written. */
.pf-link.is-out { animation-direction: reverse; }

/* Idle and unknown do not move: a still line and a slow one read differently,
   and only one of them is true. Unknown is also faded rather than solid. */
.pf-link.is-idle,
.pf-link.is-unknown { animation: none; }

.pf-link.is-unknown {
    --pf-dash: var(--portal-flow-unknown);
    opacity: 0.55;
}

@keyframes pf-travel-down {
    from { background-position-y: 0; }
    to { background-position-y: 24px; }
}

@keyframes pf-travel-right {
    from { background-position-x: 0; }
    to { background-position-x: 24px; }
}

@media (prefers-reduced-motion: reduce) {
    .pf-link { animation: none; }
}

/* ---------------------------------------------------------------- the detail panel */

.pf-detail {
    padding: var(--portal-space-4);
    border: 1px solid var(--portal-border);
    border-radius: var(--portal-radius);
    background: var(--portal-surface);
}

.pf-detail-name {
    margin: 0;
    font-family: var(--portal-font-figure);
    font-weight: 700;
    font-size: var(--portal-text-lg);
}

.pf-detail-kind {
    margin: var(--portal-space-1) 0 0 0;
    font-size: var(--portal-text-sm);
    color: var(--portal-text-muted);
}

.pf-detail-now {
    margin-top: var(--portal-space-4);
    padding: var(--portal-space-3);
    border: 1px solid var(--portal-border);
    border-radius: var(--portal-radius-control);
    background: var(--portal-sunk);
}

.pf-detail-figure { margin-top: var(--portal-space-2); }
.pf-detail-figure .pf-figure { text-align: left; }
.pf-detail-figure .pf-figure-value { font-size: 1.9rem; letter-spacing: -0.02em; }
.pf-detail-figure .pf-figure-unit { font-size: var(--portal-text-sm); }

.pf-detail-says {
    margin: var(--portal-space-2) 0 0 0;
    font-size: var(--portal-text-sm);
    line-height: 1.4;
    color: var(--portal-text-2);
}

.pf-detail-seen,
.pf-detail-note {
    margin: var(--portal-space-1) 0 0 0;
    font-size: var(--portal-text-sm);
    color: var(--portal-text-muted);
}

.pf-detail-charge {
    margin-top: var(--portal-space-4);
    padding: var(--portal-space-3);
    border: 1px solid var(--portal-border);
    border-radius: var(--portal-radius-control);
}

.pf-detail-charge-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
}

.pf-detail-charge-value {
    font-family: var(--portal-font-figure);
    font-weight: 700;
    font-size: var(--portal-text-lg);
}

/* An empty outline for an unknown charge, which is why the fill is a child
   rather than a background: no fill at all is drawn when there is no reading,
   and an empty battery is a different statement. */
.pf-bar {
    position: relative;
    height: 9px;
    margin-top: var(--portal-space-2);
    border-radius: 999px;
    background: var(--portal-rule);
    overflow: hidden;
}

.pf-bar-fill {
    position: absolute;
    inset: 0 auto 0 0;
    border-radius: 999px;
    background: var(--portal-text);
}

.pf-bar-edge {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 1px;
    background: var(--portal-control-border);
}

.pf-facts {
    margin: var(--portal-space-4) 0 0 0;
    border: 1px solid var(--portal-border);
    border-radius: var(--portal-radius-control);
    overflow: hidden;
}

.pf-fact {
    display: flex;
    justify-content: space-between;
    gap: var(--portal-space-3);
    padding: var(--portal-space-2) var(--portal-space-3);
    border-bottom: 1px solid var(--portal-border-soft);
}

.pf-fact:last-child { border-bottom: 0; }

.pf-fact dt {
    font-size: var(--portal-text-sm);
    color: var(--portal-text-muted);
}

.pf-fact dd {
    margin: 0;
    font-size: var(--portal-text-sm);
    font-weight: 500;
    text-align: right;
}

.pf-fact-figure { font-family: var(--portal-font-figure); }

/* ---------------------------------------------------------------- the edit mode */

.pf-detail-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--portal-space-3);
}

.pf-detail-edit {
    flex: none;
    display: inline-flex;
    align-items: center;
    gap: var(--portal-space-2);
    min-height: 44px;
    padding: 0 var(--portal-space-3);
    border: 1px solid var(--portal-control-border);
    border-radius: var(--portal-radius-control);
    background: var(--portal-surface);
    font: inherit;
    font-size: var(--portal-text-sm);
    font-weight: 500;
    color: var(--portal-text);
    cursor: pointer;
}

/* The mode says so for as long as it lasts, in the accent, across the top of the panel. */
.pf-edit-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--portal-space-3);
    margin: calc(var(--portal-space-4) * -1) calc(var(--portal-space-4) * -1) var(--portal-space-4);
    padding: var(--portal-space-2) var(--portal-space-4);
    border-bottom: 1px solid var(--portal-brand);
    background: var(--portal-brand-tint);
    border-radius: var(--portal-radius) var(--portal-radius) 0 0;
}

.pf-edit-mode {
    display: inline-flex;
    align-items: center;
    gap: var(--portal-space-2);
    font-size: var(--portal-text-sm);
    font-weight: 600;
    color: var(--portal-brand-deep);
}

.pf-field {
    margin-top: var(--portal-space-4);
}

.pf-field label,
.pf-field-label {
    display: block;
    margin-bottom: var(--portal-space-1);
    font-size: var(--portal-text-sm);
    font-weight: 500;
}

.pf-field-help {
    margin: var(--portal-space-1) 0 0 0;
    font-size: var(--portal-text-sm);
    color: var(--portal-text-muted);
    line-height: 1.4;
}

/* Under the field it belongs to, not in one block at the top: a form that lists its problems somewhere
   else makes the reader match them up. */
.pf-field-problem,
.pf-edit-problem {
    margin: var(--portal-space-1) 0 0 0;
    font-size: var(--portal-text-sm);
    line-height: 1.4;
    color: var(--portal-state-caution-ink);
}

.pf-edit-problem {
    padding: var(--portal-space-2) var(--portal-space-3);
    border: 1px solid var(--portal-state-caution-border);
    border-radius: var(--portal-radius-sm);
    background: var(--portal-state-caution-ground);
}

.pf-field-pair {
    display: flex;
    flex-wrap: wrap;
    gap: var(--portal-space-3);
}

.pf-field-pair > div {
    flex: 1 1 8rem;
    min-width: 0;
}

.pf-field-row {
    display: flex;
    align-items: center;
    gap: var(--portal-space-2);
    width: 100%;
    min-height: 44px;
    padding: var(--portal-space-2) var(--portal-space-3);
    border: 1px solid var(--portal-control-border);
    border-radius: var(--portal-radius-control);
    background: var(--portal-surface);
    font: inherit;
    font-size: var(--portal-text-base);
    text-align: left;
    color: var(--portal-text);
    cursor: pointer;
}

.pf-field-row-value {
    flex: 1 1 auto;
    min-width: 0;
}

.pf-field-row-action {
    flex: none;
    font-size: var(--portal-text-sm);
    font-weight: 500;
    color: var(--portal-brand-deep);
}

/* Everything that is not editable, named once with a way to ask - rather than as a column of disabled
   boxes that invite the question one at a time. */
.pf-edit-rest {
    margin-top: var(--portal-space-4);
    padding: var(--portal-space-3);
    border: 1px solid var(--portal-border);
    border-radius: var(--portal-radius-control);
    background: var(--portal-sunk);
}

.pf-edit-rest p {
    margin: 0;
    font-size: var(--portal-text-sm);
    line-height: 1.45;
    color: var(--portal-text-2);
}

/* In the flow at the foot of the panel, never floating over the field being typed into. */
.pf-edit-commit {
    display: flex;
    gap: var(--portal-space-3);
    margin: var(--portal-space-4) calc(var(--portal-space-4) * -1) calc(var(--portal-space-4) * -1);
    padding: var(--portal-space-3) var(--portal-space-4);
    border-top: 1px solid var(--portal-rule);
}

.pf-edit-commit .portal-button-quiet { flex: 1 1 0; }
.pf-edit-commit .portal-button { flex: 2 1 0; }

/*
    The phone, where the diagram's indent is the whole budget. Each nesting level costs the bracket's
    indent plus a connector, and at the default sizes a second level left about 90 px for a name and a
    figure: the name ellipsised to "Jelle Heid..." and the estimated pill broke over three lines inside
    its own rounded outline (found 21-10-2026 at 375 px, which is the layout D15 calls the real one).
    Nothing is removed and nothing moves - the same connector is simply drawn shorter.
*/
@media (max-width: 30rem) {
    .pf-kids {
        margin-left: 6px;
        padding-left: 10px;
    }

    .pf-link-h { width: 22px; }

    .pf-asset { padding-inline: var(--portal-space-2); }

    /* .pf-asset.is-out outranks .pf-asset whatever the order, so the narrow padding has to be restated
       here or an out-of-service row keeps the wide one and loses width the name needs at 380px. */
    .pf-asset.is-out { padding-left: calc(var(--portal-space-2) - 3px); }

    .pf-asset-icon {
        width: 26px;
        height: 26px;
    }
}

/* ---------------------------------------------------------------- a site, in a list or a menu

    One mark and one line of facts, used by the sites list and by the picker, because a site described two
    ways in two places is a site the customer has to recognise twice.
*/

.portal-site-icon {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    border-radius: var(--portal-radius-sm);
    background: var(--portal-brand-tint);
    color: var(--portal-brand-deep);
}

.portal-site-icon.is-large {
    width: 38px;
    height: 38px;
    border-radius: var(--portal-radius);
}

.portal-site-facts {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--portal-space-2);
    font-size: var(--portal-text-sm);
    font-weight: 400;
    color: var(--portal-text-muted);
}

/* The separator is drawn rather than written, so no markup carries a character that means nothing on its
   own and that a screen reader would read out between every pair of facts. */
/* Between two text facts only. A separator before the icons was orphaned at the start of the line
   whenever they wrapped, which on a 375 px row is every time; the icons are their own group and need no
   punctuation to say so. */
.portal-site-fact + .portal-site-fact::before {
    content: "·";
    margin-right: var(--portal-space-2);
    /* The facts' own colour, softened - at --portal-border it measured 1.2:1 against the card and read as
       a plain gap, which left "Liander" and "2 connections" looking like two unrelated words. */
    opacity: 0.55;
}

.portal-site-kit {
    display: flex;
    align-items: center;
    gap: var(--portal-space-2);
}

.portal-site-kit-item {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    font-variant-numeric: tabular-nums;
}

.portal-site-kit-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
    border-radius: 5px;
    background: var(--portal-border-soft);
    color: var(--portal-text-2);
}

.portal-site-kit-icon svg {
    width: 12px;
    height: 12px;
}

/* ---------------------------------------------------------------- the list row, with a mark and a line */

.portal-list-icon {
    flex: none;
    display: flex;
}

.portal-list-what {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.portal-list-detail { min-width: 0; }

.portal-list-go {
    flex: none;
    display: flex;
    align-items: center;
    gap: var(--portal-space-1);
    color: var(--portal-brand);
    font-size: var(--portal-text-sm);
    font-weight: 500;
}

/* Words on the desktop, the chevron alone on a phone: there the row is plainly tappable and the label is
   one more thing competing with the facts for a 380 px line. */
.portal-list-go-label { display: none; }

.portal-list-chevron { color: inherit; }

.portal-list-link:hover .portal-list-go-label { text-decoration: underline; }

@media (min-width: 34rem) {
    .portal-list-go-label { display: inline; }
}

/* ---------------------------------------------------------------- the picker's own rows */

.portal-sitepicker-item-what {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

/* The row carries two lines now, so it needs the height; .is-on keeps its weight on the name alone, since
   the facts under it are the same muted line on every row. */
.portal-sitepicker-item { min-height: 58px; }

.portal-sitepicker-item.is-on .portal-site-facts { font-weight: 400; }

/* ---------------------------------------------------------------- status chip
   Where a WINDOW stands, which is not what a FIGURE is: see Components/StatusChip.razor for why this is its
   own component rather than a sixth FigureState. Shaped like .portal-pill on purpose, the two are read in
   the same places and a second shape would say they are different kinds of thing. */

.portal-chip {
    display: inline-flex;
    align-items: baseline;
    padding: 2px var(--portal-space-2);
    border: 1px solid var(--portal-state-border);
    border-radius: var(--portal-radius-sm);
    background: var(--portal-state-ground);
    font-size: var(--portal-text-xs);
    font-weight: 500;
    line-height: 1.5;
    color: var(--portal-state-ink);
    white-space: nowrap;
}

/* Scheduled is the plain chip: .portal-chip already is it, so there is no rule here. Named in the component
   all the same, because a modifier that silently does not exist is how a fourth state would render as a
   third one. */

/* The one that keeps a hue, for the reason --portal-state-caution-* exists: it is the state somebody may
   have to act on, and a grey chip in a grey list is not read. Amber rather than red, because red is the
   brand here and would read as an alarm. */
.portal-chip-ongoing {
    background: var(--portal-state-caution-ground);
    border-color: var(--portal-state-caution-border);
    color: var(--portal-state-caution-ink);
    font-weight: 600;
}

.portal-chip-ended {
    background: var(--portal-state-held-ground);
    color: var(--portal-state-held-ink);
}

/* The section a customer opens this page for. Bordered rather than tinted throughout: the rows inside it
   carry their own chips, and a coloured ground behind coloured chips is louder than either. */
.portal-section-urgent { border: 2px solid var(--portal-state-caution-border); }

.portal-section-urgent > .portal-card-title {
    background: var(--portal-state-caution-ground);
    border-bottom-color: var(--portal-state-caution-border);
    color: var(--portal-state-caution-ink);
}

/* One window in the list: a name and its chip, then the facts under it. Not .portal-list-link, because a
   row here opens nothing, everything it has to say is already on it. */
.portal-window { padding: var(--portal-space-3) var(--portal-space-4); }

.portal-window + .portal-window { border-top: 1px solid var(--portal-border-soft); }

.portal-window-head {
    display: flex;
    align-items: center;
    gap: var(--portal-space-2);
}

.portal-window-name { flex: 1; font-weight: 500; }

.portal-window-site,
.portal-window-note {
    margin: var(--portal-space-1) 0 0;
    font-size: var(--portal-text-sm);
    color: var(--portal-text-muted);
}

.portal-window-when {
    margin: var(--portal-space-2) 0 0;
    font-size: var(--portal-text-sm);
    color: var(--portal-text-2);
}

.portal-window-action { margin-top: var(--portal-space-2); }

/* The form's fields. One column on a phone; the date and its time share a row because they are one answer. */
.portal-field { margin-bottom: var(--portal-space-6); }

.portal-field-label {
    display: block;
    margin-bottom: var(--portal-space-2);
    padding: 0;
    font-size: var(--portal-label-size);
    font-weight: 600;
    letter-spacing: var(--portal-label-tracking);
    text-transform: uppercase;
    color: var(--portal-text-muted);
}

/* A select that is an ANSWER fills its column; one that FILTERS a page stays compact beside the heading,
   which is why this is a modifier rather than a change to .portal-select. */
.portal-select-block {
    width: 100%;
    max-width: none;
}

.portal-field-pair { display: flex; gap: var(--portal-space-2); }
.portal-field-pair > :first-child { flex: 2; }
.portal-field-pair > :last-child { flex: 1; }

.portal-field-hint {
    margin: var(--portal-space-2) 0 0;
    font-size: var(--portal-text-sm);
    color: var(--portal-text-muted);
}

/* A refusal, in the brand's own red. The token sheet names no error colour, and inventing a second red
   beside the accent would be two reds doing one job; the field's own border says it too, so the colour is
   never the only signal (D21). */
.portal-field-problem {
    margin: var(--portal-space-2) 0 0;
    font-size: var(--portal-text-sm);
    color: var(--portal-brand);
}

.portal-input-problem { border-color: var(--portal-brand); }

/*
    A consequence stated where the decision is made (D16). FILLED rather than outlined: an outline of the
    same weight as the fields above it reads as another field, and this is the one thing on the page that
    is not asking for an answer. The caution tokens are the sheet's, so it is the same amber the "ongoing"
    chip and the out-of-service banner use - one hue for one meaning.
*/
.portal-notice {
    margin: 0 0 var(--portal-space-6);
    padding: var(--portal-space-3) var(--portal-space-4);
    border: 1px solid var(--portal-state-caution-border);
    border-radius: var(--portal-radius-control);
    background: var(--portal-state-caution-ground);
    color: var(--portal-state-caution-ink);
    font-size: var(--portal-text-sm);
}

.portal-form-actions { display: flex; gap: var(--portal-space-2); }
.portal-form-actions > * { flex: 1; }

/*
    An <a> that is one of a form's actions. Cancel GOES somewhere, so it stays an anchor - but beside a
    <button> it has to be the same object: a button centres its own text and carries no underline, and an
    anchor does neither, so Cancel read as a link loose inside a box.
*/
a.portal-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    text-decoration: none;
}

/* ---------------------------------------------------------------- invoices
   A month you open. The row is one button across its whole width, so the target is the row rather than a
   chevron somebody has to aim at, and the drawer is two columns that stack on a phone. */

.portal-window-open {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--portal-space-2) var(--portal-space-4);
    width: 100%;
    box-sizing: border-box;
    min-height: 2.75rem;
    padding: var(--portal-space-3) var(--portal-space-4);
    border: 0;
    background: none;
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
}

.portal-window-open:hover { background: var(--portal-sunk); }

.portal-window-month {
    flex: 1 1 12rem;
    font-family: var(--portal-font-display, inherit);
    font-weight: 600;
}

/* "€ 37 200 earned", the word is part of the figure, so the pair wraps as a unit and a screen reader hears
   what the number IS without a header to associate it with. */
.portal-window-figure {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.3em;
    flex: 0 1 11rem;
    color: var(--portal-text-body);
}

.portal-window-figure-value { font-variant-numeric: tabular-nums; }

.portal-window-figure-word { color: var(--portal-text-muted); }

/* The word that says what opening does, with the chevron after it: one control, so the word is the
   affordance rather than a label beside an icon. */
.portal-window-toggle {
    display: inline-flex;
    align-items: center;
    gap: var(--portal-space-2);
    flex: none;
    font-size: var(--portal-text-sm);
    color: var(--portal-text-muted);
}

.portal-window-chevron { flex: none; color: var(--portal-text-muted); transition: transform 120ms ease; }

.portal-window-chevron.is-open { transform: rotate(180deg); }

@media (prefers-reduced-motion: reduce) {
    .portal-window-chevron { transition: none; }
}

/* The month still running, marked on the row as well as in the chart, the fade in the chart is never the
   only signal (D21). */
.portal-window-running { background: var(--portal-state-caution-ground); }

.portal-window-drawer {
    display: flex;
    flex-wrap: wrap;
    gap: var(--portal-space-6);
    padding: 0 var(--portal-space-4) var(--portal-space-5);
    background: var(--portal-sunk);
}

.portal-window-drawer-side { flex: 1 1 17rem; min-width: 0; }
.portal-window-drawer-main { flex: 2 1 24rem; min-width: 0; }

.portal-invoice + .portal-invoice {
    margin-top: var(--portal-space-4);
    padding-top: var(--portal-space-4);
    border-top: 1px solid var(--portal-border);
}

/* A cancelled invoice is SHOWN and left out of every total: a customer holding its PDF should find it here
   saying what became of it. Quietened rather than struck through, which is unreadable at this size. */
.portal-invoice.is-cancelled { opacity: 0.7; }

.portal-invoice-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--portal-space-2);
    margin: 0 0 var(--portal-space-2);
}

.portal-invoice-number { font-weight: 500; font-variant-numeric: tabular-nums; }

.portal-invoice-totals { width: 100%; border-collapse: collapse; }

.portal-invoice-totals td { padding: 3px 0; font-size: var(--portal-text-sm); }

.portal-invoice-amount { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }

.portal-invoice-subtotal td { padding-top: var(--portal-space-2); border-top: 1px solid var(--portal-border); }

.portal-invoice-total td { padding-top: var(--portal-space-2); font-size: var(--portal-text-base); font-weight: 600; }

/* Which site a line is for, under the line rather than beside it: a site name is longer than the amount
   column leaves room for. */
.portal-invoice-site {
    display: block;
    font-size: var(--portal-text-sm);
    color: var(--portal-text-muted);
}

/* What a month was made up of, as one bar of shares. */
.portal-composition {
    display: flex;
    height: 16px;
    border-radius: var(--portal-radius-sm);
    overflow: hidden;
}

.portal-composition-part { height: 100%; }

.portal-legend-value {
    margin-left: auto;
    font-variant-numeric: tabular-nums;
    color: var(--portal-text-2);
}

/* The horizon note, which is a statement rather than a caution: nothing is wrong, the readings are simply
   older than we keep them. */
.portal-notice-quiet {
    display: flex;
    align-items: flex-start;
    gap: var(--portal-space-3);
    margin-top: var(--portal-space-4);
    border-color: var(--portal-border);
    background: var(--portal-surface);
    color: var(--portal-text-2);
}

.portal-notice-quiet svg { flex: none; margin-top: 2px; color: var(--portal-text-muted); }

/* ---------------------------------------------------------------- out-of-service banner
   The top of a site page when something on it is out of service. Louder than a chip on a row, because the
   question it answers ("is anything wrong here?") is the one somebody opens the page with, and it is a
   link, so the answer is one tap away rather than something to go and look for. */

.portal-banner {
    display: flex;
    align-items: flex-start;
    gap: var(--portal-space-3);
    margin-bottom: var(--portal-space-4);
    padding: var(--portal-space-3) var(--portal-space-4);
    border: 2px solid var(--portal-state-caution-border);
    border-radius: var(--portal-radius-control);
    background: var(--portal-state-caution-ground);
    color: var(--portal-state-caution-ink);
    text-decoration: none;
}

.portal-banner:hover { color: var(--portal-state-caution-ink); }

.portal-banner-icon { flex: none; margin-top: 2px; }

.portal-banner-title { display: block; font-weight: 600; }

.portal-banner-note {
    display: block;
    margin-top: var(--portal-space-1);
    font-size: var(--portal-text-sm);
}

/* ---------------------------------------------------------------- the invoice page's own blocks

   THE GRANULARITY CONTROL, THE YEARS OVERVIEW AND THE DAY SPARKLINE
   The three pieces the "Portal, invoice summary" sketch has that nothing else on the portal needs. They
   live here rather than in a page's own file because D17 puts every look in one sheet; a second page that
   wants one of them gets it from here. */

.portal-segmented { display: flex; flex: none; }

.portal-segment {
    min-height: 2.75rem;
    padding: 0 var(--portal-space-4);
    border: 1px solid var(--portal-field-border);
    background: var(--portal-surface);
    font: inherit;
    font-size: var(--portal-text-sm);
    color: var(--portal-text-body);
    cursor: pointer;
}

.portal-segment:first-child { border-radius: 6px 0 0 6px; }
.portal-segment:last-child { border-radius: 0 6px 6px 0; }
.portal-segment:not(:first-child) { border-left: 0; }

.portal-segment:hover:not(:disabled) { background: var(--portal-sunk); }

.portal-segment.is-on {
    background: var(--portal-accent-wash);
    border-color: var(--portal-accent);
    color: var(--portal-accent-ink);
    font-weight: 600;
}

/* A granularity this period cannot draw. Kept in place rather than removed, with its title saying what
   would have to be true, a control that comes and goes reads as a page that is missing something. */
.portal-segment:disabled { color: var(--portal-text-muted); cursor: not-allowed; opacity: 0.6; }

/* The is-on border must win over the neighbour whose left border was removed, or the selected segment
   loses an edge. */
.portal-segment.is-on + .portal-segment { border-left: 1px solid var(--portal-field-border); }

/* One bar per year, label above and year beneath, so the shape of every year reads at a glance. */
.portal-years {
    display: flex;
    align-items: flex-end;
    justify-content: flex-start;
    gap: var(--portal-space-5);
    padding: var(--portal-space-4) var(--portal-space-4) 0;
    max-width: 40rem;
}

/* CAPPED, so one year and four look like the same chart. Uncapped, flex: 1 gives a customer in their
   first year a single bar the width of the card, which reads as a different component rather than as the
   same one with less in it. */
.portal-year {
    flex: 1 1 0;
    max-width: 8.5rem;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    gap: var(--portal-space-2);
    min-width: 0;
    height: 11.25rem;
}

.portal-year-value,
.portal-year-label {
    text-align: center;
    font-size: var(--portal-text-sm);
    color: var(--portal-text-muted);
    font-variant-numeric: tabular-nums;
}

.portal-year-value { color: var(--portal-text-body); }

.portal-year-value.is-chosen,
.portal-year-label.is-chosen { color: var(--portal-accent-ink); font-weight: 600; }

.portal-year-bar {
    border-radius: 4px 4px 0 0;
    background: var(--portal-series-1);
    box-sizing: border-box;
}

.portal-year-bar.is-chosen { background: var(--portal-accent); }

/* The running year: an outline, because its figure can still move. The caption says so in words, the
   shape is never the only signal (D21). */
.portal-year-bar.is-running {
    background: var(--portal-surface);
    border: 1px dashed var(--portal-field-border);
}

/* One bar per day of a month: a sparkline inside a drawer, not a chart, no axes, no zoom, no tooltip of
   its own, and every figure on the hover and in the aria-label. */
.portal-days {
    display: flex;
    align-items: flex-end;
    gap: 2px;
    height: 5rem;
    padding-top: var(--portal-space-2);
}

/* NOT ".portal-day", which is DayContext's own wrapper at the top right of every page: that class has
   existed since the shell was built, and giving it a flex width and a blue fill painted the date block
   solid blue. A new global class is checked against the sheet before it is added. */
.portal-daybar {
    flex: 1 1 0;
    min-width: 2px;
    border-radius: 2px 2px 0 0;
    background: var(--portal-series-1);
}

/* A day something was taken off. A DIFFERENT LIGHTNESS as well as a different hue, so it is told apart
   without colour vision, and the note beneath names the day in words. */
.portal-daybar.has-deduction { background: var(--portal-accent); }

.portal-days-axis {
    display: flex;
    justify-content: space-between;
    margin-top: var(--portal-space-2);
    font-size: var(--portal-text-sm);
    color: var(--portal-text-muted);
}

.portal-field-label-spaced { margin-top: var(--portal-space-5); }

/* The years overview and the day sparkline are both a row of bars, and at phone width a month of them is
   under 4 px each. They stay readable because nothing is read off a height: the figures are on the hover,
   in the label above each year, and in the aria-label. */
@media (max-width: 30rem) {
    .portal-years { gap: var(--portal-space-3); }
    .portal-year { height: 8rem; }
    .portal-days { height: 4rem; }
}
