/* Weather Platform card system.
   Authoritative design document: docs/Card_System_Design.md

   WHAT THIS FILE IS FOR
   ---------------------
   Shared card rules, in one place, instead of scattered through app.css and
   individual card components. Its first and currently only consumer is the
   isolated Card Lab at /card-system. The live dashboard is NOT migrated to it and
   is not affected by it.

   WHY EVERY CLASS IS NAMESPACED wpc-
   ----------------------------------
   Not to avoid a collision today - checked, there is none - but because migration
   is incremental by instruction. During migration the old system and this one have
   to render on the same page at the same time, beside ~12,700 lines of legacy card
   CSS that is largely !important. Sharing a name as generic as `.card` with that
   is how a migration slice breaks a card it never touched. The prefix makes the
   two systems incapable of reaching each other.

   THIS FILE MUST NEVER NAME A LEGACY SELECTOR
   -------------------------------------------
   No rule here may reference .weather-card, .dashboard-grid, .card-size-*,
   .card-title-row or .card-lab. Touching those is touching the live dashboard.
   CardSystemIsolationTests fails the build if that happens.

   NO !important, ANYWHERE
   -----------------------
   The section order below is the cascade. If a rule appears to need !important,
   the ordering is wrong. The legacy card CSS is the cautionary example: four
   competing @media (min-width: 1181px) blocks where only source order decides.

   COLOUR
   ------
   No hex value is declared here. Colour comes from the existing semantic tokens
   only, so a theme change reaches these cards for free.

   ============================================================================
   1  TOKENS
   ============================================================================ */

.wpc-grid,
.wpc-card {
    /* --- geometry -------------------------------------------------------------

       APPROVED 2026-08-22: the grid unit is the current dashboard's 120px row and
       its 9px gap, expressed in rem so browser text scaling grows the card with
       the text inside it. At the normal 16px root these resolve to exactly the
       shipped pixel values:

           0.5625rem * 16 =  9px      (gap)
           7.5rem    * 16 = 120px     (one row)
           0.6875rem * 16 = 11px      (padding)
           2.125rem  * 16 = 34px      (header band)

       So normal rendering is pixel-identical to the dashboard today, and a reader
       at 150% text size gets a proportionally taller card rather than clipped
       content. */
    --wpc-gap: 0.5625rem;
    --wpc-row: 7.5rem;
    --wpc-pad: 0.6875rem;
    /* 7px, APPROVED 2026-08-23, down from 9. A 1x1 is where wasted perimeter costs the
       most proportionally: on an 86.25px phone card this returns 4px on each axis -
       70.25 square of usable content against 66.25, about 12.5% more area for nothing but
       a smaller margin. All four sides equally, because a 1x1 carries no icon and so has
       nothing on its right edge to clear. Both breakpoints. */
    --wpc-pad-1x1: 0.4375rem;
    --wpc-header-min: 2.125rem;
    --wpc-header-min-1x1: 1.8125rem;
    --wpc-header-gap: 0.5rem;

    /* Radius stays in px, deliberately. APPROVED 2026-08-22 at 9px, and it is not
       a dimension content has to fit inside - scaling it with text would give a
       reader at 200% a 18px radius on a card whose corners never needed to grow. */
    --wpc-radius: 9px;

    /* --- type ----------------------------------------------------------------
       Values are the current dashboard's effective ones, renamed rather than
       retyped. See docs/Card_System_Design.md section 9. */
    /* THE HIERARCHY LADDER
       --------------------
       Refined 2026-08-22 so the ladder is monotonic and every step is a real one.
       Before this pass the unit was .7rem against a .72rem title, so the two read
       as peers and the unit competed with the card's name; and trend and secondary
       were both .56rem, so nothing separated a tertiary signal from a secondary
       value. Read top to bottom, biggest first:

           value      1.42 -> 1.9 -> 2.4 -> clamp()   dominant, by footprint
           title       .72                            readable, subordinate
           unit        .64                            associated, secondary
           secondary   .60                            below the primary
           trend       .56                            tertiary
           label/meta  .50                            quietest

       ONE SIZE PER ROLE, INCLUDING THE VALUE. Revised 2026-08-25: this note used to end
       "Only the value changes with footprint", and there was a second title token for the
       1x1 besides. Both are gone. A footprint may change a card's LAYOUT - what sits
       beside what, where the divider falls, how much room a region gets - but it may not
       change the size of a semantic role. Two cards showing the same kind of reading now
       show it at the same size whatever tile they landed in, which is the whole point of
       having roles at all. */
    --wpc-type-title: .64rem;

    /* One step per footprint: 1x1, 1x2, 2x2, then the wide/tall chart cards.
       Ratios ~1.19x, ~1.19x, ~1.26x - enough to read as a different tier without
       the jump looking like a different component.

       WHY THE 1x1 HAS TWO STEPS
       -------------------------
       A 1x1 is not one physical card. On a phone it is an 86px SQUARE; on a desktop
       it is 142.88 x 120, half again as wide. One value size cannot serve both: the
       square is the constrained case and sets 1.42rem, while the desktop tile has
       room to spare and looks timid at that size.

       1.6rem, not 1.9rem, deliberately. 1.9rem is the 1x2's step, and giving the
       desktop 1x1 the same number would collapse the distinction between them -
       which is the identical mistake this pass just corrected between 1x2 and 2x2.
       The ladder stays strictly increasing at every footprint. */
    /* THE COMPACT PRIMARY, SIZED FROM THE BODY IT LIVES IN
       -----------------------------------------------------
       Not guessed and not inherited - derived from the rectangle the numeral
       actually gets, which is the card less its padding less the header row:

           desktop 1x1   120 - 18 - 29 = 73px of body
           mobile  1x1   86.25 - 18 - 29 = 39.25px of body

       2.8rem renders 42.6px, which is 58% of the desktop body - inside the 50-60%
       target. Mobile takes the same proportion of its own smaller body rather than
       a scaled-down copy of the desktop pixel size: 1.5rem renders 22.8px, 58% of
       39.25.

       Dropping mobile from 1.8rem to 1.5rem also buys something that was previously
       impossible: a three-digit reading now fits the phone 1x1. "100" plus the stack
       needs 59px against 68.25px of interior, where at 1.8rem it needed 71px and
       overflowed.

       THE ONE PRIMARY SIZE, and a phone step that is not a footprint step.

       2.66rem is the desktop primary at every footprint: the 2.8rem the compact
       presentation already used, less the 5% the product owner allowed. Everything that
       used to step down from it by footprint - 1.9rem, 2.4rem, the clamp - now resolves
       here instead.

       --wpc-type-value-phone is a SURFACE distinction, which is a different thing from a
       footprint one, and that is why it survives the rule. Below the phone breakpoint a
       card is a physically different object - an 86px square rather than a 142px landscape
       tile - so the same footprint name describes different furniture. Within either
       surface there is exactly one primary size.

       TUNED DOWN 14% ON 2026-08-25, from 2.66rem/42.56px to 2.288rem/36.61px, because
       consistent is not the same as correct and this role was consistently too large.

       THE EVIDENCE WAS A RATIO, not taste. Measured against the production dashboard at
       the same viewport, every other role sat at or below its production counterpart -
       title -8.6%, unit -15.4% - while the primary sat +30.4% above it. Production's
       primary is 24-29% of its card's height; this one was 34% on every card. At 2.288rem
       it is 29%, which is the top of production's own range rather than a number invented
       here.

       IT ALSO FIXED REAL OVERFLOW. At 1280 the c2 primary region could not hold a
       five-glyph reading: Rain overran by 11.33px and Date & Time by 15.53px. */
    --wpc-type-value: 2.288rem;
    --wpc-type-value-phone: 1.5rem;

    /* The secondary reading - High, Low. 60-70% of the primary, which is the band
       that reads as "clearly subordinate" rather than "tiny": 1.8rem against 2.8rem
       is 64%, 1rem against 1.5rem is 67%. Both fit their row, which is half the
       body - 36.5px desktop, 19.6px mobile. */
    /* SUPPORTING VALUES GROW MORE SLOWLY THAN THE HERO.
       Revised 2026-08-23 (second pass). The phone presentation is the reference and
       is unchanged at .8rem; the desktop figure came down from 1.5rem because at
       that size it read as tall and slightly foreign rather than as a quieter
       version of the primary.

       The desktop primary is 1.87x the phone's (44.8 against 24). The secondary
       grows by about 1.67x instead, which is the whole point: perceptual
       consistency, not an identical numeric ratio.

       Settled at 1.34rem = 21.44px after bracketing it. 1.5rem/24px read as tall and
       prominent; 1.15rem/18.4px was too quiet to scan. The phone value is not part of
       this - it is the approved reference and is a separate token, and the only
       consumer of the -wide token sits inside the desktop container query. */
    --wpc-type-stat: .8rem;

    /* 1.12rem = 17.92px. TUNED DOWN 20% ON 2026-08-25, and this was the most oversized
       role in the system by a wide margin.

       It was 1.4rem/22.4px, approved 2026-08-23 against the 38/31/31 split - and that
       approval was sound for the card it was measured on. Its own note recorded the
       ceiling as "Rain's Rate needs about 67.6 of 72.86 usable". At ten columns the same
       cell is 59.7px at 1440 and 50.09 at 1280, so the ceiling moved and the size did not
       follow it. Measured overrun before this change: -6.55px at 1440 and -16.16 at 1280.

       AND IT WAS OUT OF PROPORTION EVERYWHERE ELSE TOO, not just in Rain. Against
       production the secondary sat +79.1% - more than twice the primary's +30.4% - which
       is why Hi/Lo, Gust/Peak and Rate/Storm read almost as loudly as the numbers they
       are subordinate to. At 17.92 the secondary is 49% of the primary rather than 53%,
       against production's 38%: a clear step down, without going as quiet as production's
       12.51px, which would be hard to scan at this card size. */
    --wpc-type-stat-wide: 1.12rem;

    /* THE MINOR SECONDARY STEP WAS REMOVED 2026-08-25 - see section 6c.

       It read: Hi and Lo take one step down from Feels Like because Feels Like owns a
       full-height cell and is the major secondary - 1.3rem against 1.4rem on the desktop,
       .68rem against .8rem on the phone. The hierarchy argument was about the CELL, and a
       cell is layout, so under the one-size-per-role rule the step goes. Every secondary
       reading now takes --wpc-type-stat / --wpc-type-stat-wide wherever it sits. */

    /* Labels stay the quietest thing on the card, but readable. Same conservative
       growth as the values they belong to.

       Down 14% and 12% respectively, because a label at the old size sat close enough
       to its value in both size and weight that the pair read as one block of text
       rather than as a value with a name. The room this frees is what pays for the gap
       between them. */
    --wpc-type-stat-label: .625rem;
    --wpc-type-stat-label-wide: .6875rem;
    /* The md / lg / xl footprint ladder was removed 2026-08-25. See --wpc-type-value. */

    /* THE UNIT ROLE, ONE SIZE PER SURFACE. Revised 2026-08-25.

       The unit used to be em-based off whatever it was attached to, in three different
       places, and the measured result was four sizes for one role on a single dashboard:
       11px beside a primary, 11.2px beside a full-height secondary, 10.4px beside a
       half-height one and 11.65px on Rain's rate. Nobody chose four sizes; they fell out
       of three em bases and a card-specific override.

       11px and 8.8px are not new numbers - they are exactly what the primary unit's old
       `clamp(8.8px, .36em, 11px)` already resolved to on each surface, so the hero unit
       does not move at all. The secondary units come to meet it, by at most 0.8px. */
    --wpc-type-unit: .6875rem;
    --wpc-type-unit-phone: .55rem;
    --wpc-type-secondary: .6rem;
    --wpc-type-label: .5rem;
    --wpc-type-meta: .5rem;
    --wpc-type-trend: .56rem;
    --wpc-type-action: .56rem;
}

/* ============================================================================
   2  GRID
   ============================================================================

   The vertical axis is a REAL row track. The legacy grid used
   grid-auto-rows: 1px with row-gap: 0, which made `grid-row: span 129` a pixel
   count wearing a row count's clothes, and made the card's own margin-bottom the
   actual gap. Same geometry, stated honestly:

       1 row  = 7.5rem                        = 120px
       2 rows = 2*7.5rem + 1 gap              = 249px
       4 rows = 4*7.5rem + 3 gaps             = 507px

   which are the shipped 129/258/516 allocations minus the 9px margin each
   carried.

   minmax(var(--wpc-row), auto) is the accessibility decision in one declaration:
   7.5rem is a FLOOR, not a height. A row that needs more grows, every card on
   that row line grows with it, and alignment survives. Nothing is clipped to
   protect the geometry. */

/* THE GRID HOST - REQUIRED AT MOBILE
   ----------------------------------
   Every .wpc-grid must sit inside a .wpc-grid-host. The mobile grid needs square
   cells, and a square cell means the ROW track has to equal the COLUMN width.

   CSS cannot express that directly: a percentage in a row track resolves against
   the grid's block size, not its inline size, so `grid-auto-rows: calc(100%/4)` is
   a height divided by four and not a width. Container query units are the way out
   - 100cqw is the host's own content-box inline size - which is why the host
   exists and why it must not carry horizontal padding of its own.

   This is also why the cell is derived rather than declared: no phone width is
   hard-coded anywhere, and the same rules hold at 320px, 390px or 430px.

   CardSystemIsolationTests asserts every grid in the lab has a host. Without one,
   cqw falls back to the viewport and the cells come out wider than the space they
   have - the one failure mode of this technique, and it shows up as overflow. */
.wpc-grid-host {
    container-type: inline-size;
}

.wpc-grid {
    display: grid;
    gap: var(--wpc-gap);
    grid-auto-rows: minmax(var(--wpc-row), auto);
    grid-template-columns: repeat(var(--wpc-columns, 8), minmax(0, 1fr));

    /* Deliberately NOT `dense`. Dense packing backfills holes by reordering, which
       silently moves a card away from where the reader put it. A gap is better
       than a card that walks. */
    grid-auto-flow: row;
}

/* Footprints, named rows x columns. The convention is absolute and is the reason
   these are two separate classes rather than one combined name: CSS itself names
   columns first, so a single token like `wpc-2x4` would invite exactly the
   ambiguity the convention exists to prevent. */
.wpc-r1 { grid-row: span 1; }
.wpc-r2 { grid-row: span 2; }

/* THREE ROWS, added 2026-08-25 for Radar. The row and column classes are orthogonal
   spans, which is what lets a new count join the vocabulary without a new system: this
   is one declaration, and `4x3` is simply `c4 + r3` at both breakpoints.

   It exists because 4x2 was built, rendered and REJECTED as too shallow - a map needs
   vertical depth to show a band approaching, not just width. 4x3 keeps the horizontal
   context and buys back the depth. */
.wpc-r3 { grid-row: span 3; }

.wpc-r4 { grid-row: span 4; }

.wpc-c1 { grid-column: span 1; }
.wpc-c2 { grid-column: span 2; }

/* THREE COLUMNS, added 2026-08-25 alongside `.wpc-r3`, and for the same reason: the
   spans are orthogonal, so a missing count is a one-declaration gap rather than a
   missing feature. It exists for Radar footprint comparison; nothing is assigned it. */
.wpc-c3 { grid-column: span 3; }

.wpc-c4 { grid-column: span 4; }
.wpc-c8 { grid-column: span 8; }

/* --- responsive: 8 -> 4 -> 4 columns ---------------------------------------

   APPROVED 2026-08-22. One sizing system, three breakpoints. Mobile and tablet
   share a column count (4) and differ in row geometry: tablet keeps the
   120px-equivalent row, mobile derives a square cell. A card declares a desktop
   footprint; these rules clamp it to what the narrower grid can hold. No card
   declares a separate tablet or mobile size, and there is no second system.

   Breakpoints are the shell's own (AppShell, AppMobileBar, AppBottomBar all treat
   640 as the phone boundary). The card system must not introduce a second phone
   breakpoint. */

/* Tablet: 641px - 1180px, 4 columns. */
@media (max-width: 1180px) {
    .wpc-grid {
        --wpc-columns: 4;
    }

    .wpc-c8 {
        grid-column: span 4;
    }
}

/* Mobile: <= 640px, FOUR columns on a SQUARE base cell.
   APPROVED 2026-08-22, replacing the earlier 2-column mobile grid.

   U is derived from the space actually available, never from an assumed phone
   width:

       U = (100cqw - 3G) / 4

   The column track is `1fr` of the same space, so column width and row height are
   the same number by construction and a 1x1 is square without being told to be.
   Every footprint then follows from the row and column counts alone:

       1x1  =  U          x  U
       1x2  =  2U + G     x  U
       2x2  =  2U + G     x  2U + G
       2x4  =  4U + 3G    x  2U + G
       4x4  =  4U + 3G    x  4U + 3G

   Composition identity therefore holds by construction rather than by arithmetic
   maintained in two places: two 1x1 plus their gap IS a 1x2, because both are the
   same track arithmetic read at different spans.

   Supported footprints for card assignment are 1x1, 1x2, 2x2, 2x4 and 4x4. The row
   and column classes are orthogonal, so other combinations render correctly too -
   1x4 is simply r1 + c4 - but no card is assigned one. */
@media (max-width: 640px) {
    .wpc-grid {
        --wpc-columns: 4;
        --wpc-cell: calc(
            (100cqw - (var(--wpc-columns) - 1) * var(--wpc-gap)) / var(--wpc-columns));

        /* minmax, not a fixed height, so the accessibility rule still wins: at large
           text the row grows and the cell stops being strictly square. Square is the
           normal-text geometry, not a constraint content gets clipped to. */
        grid-auto-rows: minmax(var(--wpc-cell), auto);
    }

    /* Eight columns cannot exist in a four-column grid. Clamp rather than overflow -
       a card is never the reason the page scrolls sideways. */
    .wpc-c8 {
        grid-column: span 4;
    }
}

/* Fallback for engines without container queries. The 1x1 is made square by its own
   aspect ratio, which keeps the common case correct; rows holding no 1x1 fall back to
   content height. Not the primary path - `color-mix()`, used ~96 times in app.css,
   already requires a newer engine than container queries do. */
@supports not (container-type: inline-size) {
    @media (max-width: 640px) {
        .wpc-grid {
            grid-auto-rows: auto;
        }

        .wpc-c1.wpc-r1 {
            aspect-ratio: 1;
        }
    }
}

/* ============================================================================
   3  CONTAINER
   ============================================================================ */

.wpc-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--wpc-radius);
    color: var(--text);
    display: flex;
    flex-direction: column;
    min-width: 0;
    padding: var(--wpc-pad);
    position: relative;

    /* No `overflow: hidden`. The legacy card carries it, and combined with a rigid
       row height it conceals content rather than revealing a layout problem.
       Regions that genuinely need to clip ask for it themselves - see .wpc-media
       and .wpc-scroll-x. */
}

.wpc-c1.wpc-r1 {
    padding: var(--wpc-pad-1x1);
}

/* ============================================================================
   4  ANATOMY
   ============================================================================

   Five regions, none mandatory. An absent region contributes no height, which is
   what makes a 1x1 carrying only a header and a value align with a 2x2 carrying
   all five.

   The legacy dashboard reserved its header by absolutely positioning the content
   region at top: 34px - so the header's height was declared in two places and a
   1x1 could overlap its own title. Here the header is in flow and the body takes
   the remainder. One source of truth. */

.wpc-header {
    align-items: flex-start;
    display: flex;
    flex: none;
    gap: var(--wpc-header-gap);
    justify-content: space-between;
    min-height: var(--wpc-header-min);
}

.wpc-c1.wpc-r1 .wpc-header,
.wpc-card.is-primary-compact .wpc-header {
    align-items: center;
    min-height: var(--wpc-header-min-1x1);
}

.wpc-body {
    align-items: center;
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    justify-content: center;
    min-height: 0;
    text-align: center;
}

/* A STACK THAT BREATHES - shared modifier, added 2026-08-24.

   `.wpc-body` stacks its children with no gap, which is right when the body holds one
   group. When it holds a GRAPHIC above a LABEL the two read as one clot: mathematically
   centred, optically cramped. This opens a measured gap between them and nothing else.

   `em`-based so it tracks the card's scale, and clamped so a phone gets a real gap
   without a desktop card getting a hole. Any body stacking a graphic over text can opt
   in; nothing here names a card. */
.wpc-body.is-stack-spaced {
    row-gap: clamp(5px, .42em, 12px);
}

/* A FIGURE BESIDE ITS TEXT - shared anatomy, added 2026-08-24.

   The companion to `is-stack-spaced`, and the same idea turned ninety degrees. That
   one puts a graphic ABOVE its text; this one puts it BESIDE. Both are geometry: a
   body opts in, and nothing here knows which card it is dressing.

   Use it when the graphic is a peer of the reading rather than a caption for it - a
   figure the eye lands on first, with the words to its side. A stacked body spends
   HEIGHT it does not have on a 1x1; an inline one spends WIDTH, which a desktop 1x1
   has half again as much of.

   The text region is an ordinary nested `.wpc-body`, so it keeps the standard
   stacking, centring and typography with no second set of rules. `min-width: 0` is
   what lets it actually shrink inside the row - without it a long line pushes the
   figure out rather than wrapping, which is a grid default that costs an hour every
   time it is rediscovered.

   IT IS A PROPORTIONAL SPLIT, NOT A ROW OF TWO BOXES. The figure gets a FRACTION of
   the body and fills it, so one declaration serves every footprint: the same rule that
   gives a 142.88px desktop tile a large bolt gives an 86.25px phone card a smaller one,
   in proportion, with no second element and no breakpoint. Sizing the figure in pixels
   instead needs one value per breakpoint, and a card that renders two glyphs so CSS can
   hide one - which was built here first, and is what this replaced.

   `--wpc-figure-track` is the lever: how much of the region the figure claims. The live
   dashboard's own Lightning card - the reference this was measured against - gives its
   figure two fifths, and that was tried first: it leaves 71.81px for text on a desktop
   1x1, where a `2:14 AM` at the shared secondary size needs 81.67 and spills 5px past
   the inset. About a third is the point where the figure still leads and the reading
   still fits.

   THE GROUP IS CENTRED, NOT THE TRACKS. Two `fr` columns consume every pixel of free
   space, so a figure centred in one and text started in the other pins the whole
   composition to the left edge - measured that way, and it read exactly as it was built.
   An intrinsic second track plus `justify-content: center` leaves free space OUTSIDE the
   tracks, which is what lets the attached pair sit in the middle of its own region. The
   text track is `minmax(0, max-content)` rather than `max-content` so it can still give
   way and wrap when the region is narrow, which is the phone case.

   `--wpc-figure-ink-offset` IS NOT A NUDGE, IT IS THE DIFFERENCE BETWEEN THE BOX AND THE
   DRAWING. Centring sees the figure's BOX; a reader sees its INK. A glyph in a square
   viewBox rarely fills it - the lightning bolt is a diagonal occupying about 41% of its
   own width - so a perfectly box-centred group still reads left-heavy by half that dead
   space, which is 11.84px on a desktop 1x1 and plainly visible. Trailing padding of the
   same amount moves the INK group to the middle instead.

   The two values are a matched pair, derived rather than guessed. With `s` the slack
   either side of the ink as a fraction of the box, balance needs `offset = s * track` -
   a relation between FRACTIONS only, so one pair balances every region size, 1x1 and 1x2
   alike. A card whose figure fills its box sets the offset to 0 and centres on the box.

   THE OFFSET IS TRAILING PADDING, AND A COLLAPSIBLE THIRD TRACK WAS TRIED FIRST. The
   idea was that `minmax(0, offset)` would ask for room and give it back when there was
   none. It does not: when the grid is short of space it shrinks every flexible track in
   proportion, so the offset kept about two thirds of its width and took the rest out of
   the READING - the phone's `Nearby lightning` came up 0.28px short, wrapped, and grew
   the row 2.28px past its footprint. Padding cannot be raided by the track algorithm, so
   what it costs is known in advance and can be checked against the widest state instead
   of discovered at render time. The price is that the offset must be sized to what the
   narrow breakpoint can afford, which is why the phone's is nearly nothing. */
.wpc-body.is-figure-inline {
    align-items: center;
    column-gap: var(--wpc-figure-gutter, clamp(5px, .45em, 12px));
    display: grid;
    grid-template-columns: var(--wpc-figure-track, 34.66%) minmax(0, max-content);
    justify-content: center;
    padding-inline-end: var(--wpc-figure-ink-offset, 9.3%);
}

/* THE PHONE GIVES THE FIGURE A SMALLER SHARE, AND THAT IS NOT THE SAME AS A SMALLER
   FIGURE. A fraction of 72.25px is not a fraction of 128.88px scaled down: the TEXT has
   a floor that does not scale - `Approaching` is 45.52px at the meta size and a single
   word cannot wrap - so holding the ratio constant spends on the graphic what the words
   cannot do without. Below 640px the split leans to the text and the gutter tightens;
   the figure still leads, it just stops leading by as much. */
/* THE PHONE ASKS FOR THE SAME BALANCE AND OFTEN CANNOT HAVE ALL OF IT, which is exactly
   the case the collapsible offset track exists for. The fractions here hold the same
   `offset = s * track` relation as the desktop, so a phone card with room centres its
   group properly; where the reading already spans the region - `Nearby lightning` is
   59.31px of an 89.92px pane - the offset gives its width back rather than forcing a
   wrap, and that state simply fills its pane.

   The track is a shade narrower than the desktop's, and the gutter tighter, because the
   text has a floor that does not scale with the card: a single word cannot wrap, so the
   words get the pixels the ratio would otherwise have spent on the graphic. */
/* THE COMPACT FOOTPRINT HAS NO SPARE ROOM TO SPEND ON OPTICAL CENTRING.

   `--wpc-figure-ink-offset` is a padding on the END of the row: it pulls the figure-plus-text
   group left so the group reads as centred against the figure's ink rather than its box. That
   is right wherever the row has slack. At c1 it does not. Measured on Lightning's historical
   state at 1280: an 80.5px content box, a 34.66% figure track, the gutter, a nowrap
   "8:38 AM", and 9.3% spent on the offset - the lead line finished 6.3px PAST the card's
   content box, which is the reported "too close to the right edge".

   SAME THREE TOKENS THE PHONE ALREADY TUNES, for the same reason and by the same mechanism -
   the rule below leans the split toward the text and tightens the gutter where the reading
   already spans its region. This is layout, not type: no font-size, no footprint change, and
   nothing nudged or transformed. The offset goes to zero because at this size the group has
   no slack to be offset within; centring it honestly beats centring it optically off the
   edge. */
.wpc-c1.wpc-r1 .wpc-body.is-figure-inline {
    /* 25%, not the phone's 28.79%. Zeroing the offset alone left the lead line 1px past the
       box at 1280 - the narrowest c1 interior on the desktop grid - so the split leans a
       little further toward the reading. The figure still leads; it leads by less, which is
       the same trade the phone rule below makes and states. */
    --wpc-figure-track: 25%;
    --wpc-figure-ink-offset: 0%;
    --wpc-figure-gutter: 4px;
}

@media (max-width: 640px) {
    .wpc-body.is-figure-inline {
        --wpc-figure-track: 28.79%;
        --wpc-figure-ink-offset: 7.85%;
        --wpc-figure-gutter: 4px;
    }

    /* THE NARROW 1x1 BUYS ITS FIT FROM THE OFFSET, NOT FROM THE TYPE.

       A shorter status word was tried first and the note was briefly allowed to take the
       shared fit multiplier - 7.2px - which was rejected, correctly: functional status
       text does not go below the meta floor. Concise wording fixes the states that have a
       longer form to shorten, but `Approaching` is one word with no shorter form and 45.52
       of the 70.25px body, so the 1x1 cannot afford a full ink offset at any wording.

       So it takes a partial one. The group lands a few pixels left of true rather than the
       reading being shrunk or clipped - the documented order of levers, with the type
       floor treated as fixed. The 1x2 pane has room and keeps the full offset above. */
    .wpc-c1.wpc-r1 .wpc-body.is-figure-inline {
        --wpc-figure-track: 27.78%;
        --wpc-figure-ink-offset: 4.5%;
        --wpc-figure-gutter: 2px;
    }
}

/* THE WORDS ATTACH TO THE FIGURE. `.wpc-body` centres its children, which is right when
   the body is the whole card and wrong when it is one half of a pair: the text then
   floats in the middle of its own column and opens a hole between itself and the figure.

   Measured before this rule, on a desktop 1x1: 11.97px from the inset to the bolt's ink,
   then a 30.05px gap, then the text. The outer margins were fine - the void in the MIDDLE
   was three times either of them, and it read as two things on one card rather than one
   composition. Starting the stack at its column edge closes it without moving the figure,
   without a divider, and without changing the footprint.

   No divider here on purpose. A figure and its reading are one attached object; a rule
   between them would argue they are peer regions, and the 1x1 has no second region. */
.wpc-body.is-figure-inline > .wpc-body {
    align-items: flex-start;
    min-width: 0;
    text-align: start;
}

/* THE LEAD FIGURE - a graphic the eye lands on before it reads anything.

   Square, capped, and centred in its own column. `min(100%, cap)` is what makes it
   proportional: it takes its column until the cap, so the phone gets a figure scaled to
   its card rather than a desktop figure that no longer fits. The cap exists because a
   fraction alone would keep growing on a large footprint until the glyph read as
   decoration.

   `aspect-ratio` rather than a matching height, so the figure cannot go oval when the
   body is short - and `max-block-size: 100%` so a short body clips nothing, it just
   makes the figure smaller. Pair it with `Size="fill"` on the icon: the SLOT owns the
   size, the glyph fills the slot.

   Named for its visual role. Any card with a hero graphic beside its reading may use
   it - nothing here knows what the graphic is. */
.wpc-icon.is-figure-prominent {
    aspect-ratio: 1;
    inline-size: min(100%, var(--wpc-figure-cap, 72px));
    justify-self: center;
    max-block-size: 100%;
}

/* A NEW-EVENT PULSE - shared, and deliberately OPACITY ONLY.

   The live dashboard pulses the bolt itself on a new strike. Reproducing that with
   `transform: scale()` would make a card's graphic change size on a timer, and every
   footprint in this system is a contract - a measurement taken mid-animation would not
   match one taken between beats. Opacity carries the same "something just happened"
   signal and moves nothing.

   It is a second channel, never the only one: the state it accompanies is already said
   in words and in colour. Reduced motion stands it down - see the single reduced-motion
   block in section 11, which is where every opt-out in this file lives. */
.wpc-icon.is-pulsing {
    animation: wpc-figure-pulse 1.4s ease-in-out infinite;
}

@keyframes wpc-figure-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: .45; }
}

/* SHOWN AT ONE BREAKPOINT ONLY - shared pair, added 2026-08-24.

   The compact title already established the mechanism: render both forms and let CSS
   decide, rather than swapping strings in script or generating two card definitions.
   This generalises it to any element, because the title is not the only thing a phone
   1x1 has no room for.

   It is a LAST resort, and the order of levers is unchanged: rebalance the layout
   first, shrink the graphic second, and only then let the narrow breakpoint carry less
   than the wide one. What it must never do is hide something the card needs to be
   truthful - a placeholder is fair game, a reading is not.

   THE COMPOUNDED SELECTOR IS LOAD-BEARING, and this is the fourth time this exact trap
   has been recorded here. A bare `.wpc-wide-only { display: none }` is (0,1,0), the same
   weight as `.wpc-icon { display: flex }` - and `.wpc-icon` is declared LATER in this
   file, so source order handed it the win and the rule silently did nothing on every
   icon it was put on. Compounding to (0,2,0) is what makes it mean what it says instead
   of what the cascade allows.

   `.wpc-narrow-only` is the other half: the SHORTER wording, hidden by default and shown
   only where the long form will not fit. Both strings are authored - this is not CSS
   truncation - so the phone gets copy that was written for it rather than a sentence with
   its end cut off. It restores `inline` because the roles it lands on are inline runs
   inside a text line; a block-level user would need its own rule. */
.wpc-narrow-only {
    display: none;
}

@media (max-width: 640px) {
    .wpc-wide-only,
    .wpc-icon.wpc-wide-only {
        display: none;
    }

    .wpc-narrow-only {
        display: inline;
    }
}

.wpc-meta {
    flex: none;
    text-align: center;
}

.wpc-detail {
    flex: none;
}

.wpc-footer {
    align-items: baseline;
    display: flex;
    flex: none;
    justify-content: flex-end;
}

/* ============================================================================
   5  HEADER
   ============================================================================

   Title left, icon right - the current dashboard's arrangement, kept because it
   lets the tone-coloured icons read as a measurement key down the right edge of a
   column.

   An ordinary card must not declare its own icon size, title typography, title
   position or header spacing. Those exist only here. */

.wpc-title {
    color: var(--text);
    font-size: var(--wpc-type-title);
    font-weight: 750;
    line-height: 1.15;
    margin: 0;
    min-width: 0;

    /* Wraps rather than truncates - truncation would conceal a title to protect the
       row height, which the accessibility decision rules out.

       But it never breaks INSIDE a word. `overflow-wrap: anywhere` did, and at a
       mobile 1x1 it rendered "Humidity" as "Humidit / y", which reads as a broken
       component rather than as a card whose title is too long. A multi-word title
       wraps at its space; a single word that will not fit is a signal that the card
       needs a documented short title or a larger footprint. */
    overflow-wrap: normal;
    word-break: normal;
}

/* THE COMPACT TITLE. A card definition may supply two titles - a display title and a
   shorter one - and both are rendered; this rule decides which is shown. Not a CSS text
   replacement: the strings come from the definition, the accessible name on the heading
   stays the full title at every footprint, and a card with no compact title simply does
   not emit the second span.

   It applies to ONE case, the phone 1x1, where "Temp - Outdoor" wrapped to two lines in
   a 68.25px header and the category was being repeated at the one size that can least
   afford it - the 72 degrees F beneath it already says this is a temperature. */
.wpc-title-compact {
    display: none;
}

@media (max-width: 640px) {
    /* :has() IS LOAD-BEARING - the full title stands down only when there is a compact
       one to stand in for it. Without the guard this rule hid the title of any 1x1 whose
       definition offers no compact form, and Rain - 24 Hr rendered with no title at all
       on a phone. Found by the second card, which is what a second card is for. */
    .wpc-c1.wpc-r1 .wpc-title:has(.wpc-title-compact) .wpc-title-full {
        display: none;
    }

    .wpc-c1.wpc-r1 .wpc-title-compact {
        display: inline;
    }
}

/* The 1x1 title override that used to sit here is gone (2026-08-25). It existed because
   the 1x2 and the 1x1 disagreed about their own title size; there is one title size now,
   so they cannot. */

/* The icon slot. Sizing belongs to CaelowxIcon's own tiers - cx-icon-md is 20px
   and is what the dashboard's metric glyphs already are - so this only holds the
   slot open and stops it shrinking.

   COLOUR COMES FROM THE SLOT. A CaelowxIcon with no Tone is currentColor, so putting
   the measurement's own band class on this span colours the glyph with it - the
   thermometer takes the same --wpc-temp-* the primary numeral takes, from the same
   class, with no second threshold table anywhere. Give it a Tone instead and the
   tone's colour wins and the band is ignored, which is why Temperature's icon carries
   no Tone. */
/* ============================================================================
   4z  SHARED ICON SIZE ROLES - added 2026-08-29
   ============================================================================

   DERIVED FROM AN AUDIT OF WHAT THE DASHBOARD ALREADY RENDERS, not imposed. Measured live at
   1440 across every card that draws a glyph:

     15px   x17 cards   the upper-right identifying icon (section 8.1), via cx-icon-sm
     19px   x3          Radar's map controls
     18.4px x1          Predictive Watering's methodology control
     22px   x2          Current Conditions' rain and wind compartment glyphs
     11px   x24         Forecast's precipitation glyph, inline beside a stat
     52px   x24         Forecast's tile graphic      (layout-driven)
     57.8px x1          Lightning's prominent figure (layout-driven)
     83px   x1          Current Conditions' condition graphic (layout-driven)

   Three clusters and a tail. FIFTEEN IS ALREADY THE STANDARD for a card's own control icon -
   seventeen cards agree on it - so it is the control role rather than a number chosen here.
   The 18.4 / 19 / 22 group are all "a glyph beside a reading" and sit within 2px of the design
   system's existing 20px tier, so that tier is the metric role. The large tail is layout-driven
   and cannot be a fixed step, which is what cx-icon-fill already says.

   THESE ROLES DO NOT INVENT A SCALE. They name the tiers CaelowxIcon has always had - 15 / 20 /
   26 / fill, each with its own stroke weight - so a card can ask for a ROLE instead of a pixel,
   and a raw inline SVG lands on the same geometry and the same optical weight as a CaelowxIcon
   at that tier. That equivalence is the whole point: the audit found four different hand-rolled
   sizes for what is semantically one thing.

   A HERO ROLE IS DELIBERATELY NOT A NUMBER. Forecast at 52px, Lightning at 57.8 and Conditions
   at 83 are each a fraction of a card body that itself changes with the footprint. Pinning them
   to a step would need a transform or a second drawing. `is-figure-prominent` and cx-icon-fill
   already cover it. */

.wpc-card {
    --wpc-icon-control: 15px;
    --wpc-icon-metric: 20px;
    --wpc-icon-large: 26px;

    /* The hero step, for a glyph that IS the reading rather than labelling one - a status tile
       whose whole face is a state, or a large card's recommendation. 26px was the top of the
       ladder and reads as a metric icon that happens to be big; this is a size a card commits
       to. Nothing takes it by default. */
    --wpc-icon-hero: 40px;
}

/* The square alignment box every role shares. A glyph is centred in a box of its own size, so
   two icons on one row sit on one centreline whatever they are drawn from. */
.wpc-glyph {
    align-items: center;
    aspect-ratio: 1;
    display: inline-flex;
    flex: none;
    justify-content: center;
    line-height: 0;
    vertical-align: middle;
}

.wpc-glyph svg {
    block-size: 100%;
    fill: none;
    inline-size: 100%;
    stroke: currentColor;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* Stroke thins as the box grows, matching CaelowxIcon's own tiers exactly - a 1.9 hairline at
   15px and a 1.55 at 26px are the same optical weight, and a glyph scaled without it reads as a
   small drawing blown up.

   THESE REACH RAW INLINE SVG ONLY. A CaelowxIcon child brings its own tier's stroke, and a card
   that asks the component to FILL a role box has to restate the weight in its own scoped
   stylesheet - card.css deliberately names no design-system class, which
   `CardSystemIsolationTests.EveryCardSystemClassIsNamespaced` enforces. */
.wpc-glyph-control {
    block-size: var(--wpc-icon-control);
    inline-size: var(--wpc-icon-control);
}

.wpc-glyph-control > svg { stroke-width: 1.9; }

.wpc-glyph-metric {
    block-size: var(--wpc-icon-metric);
    inline-size: var(--wpc-icon-metric);
}

.wpc-glyph-metric > svg { stroke-width: 1.7; }

.wpc-glyph-large {
    block-size: var(--wpc-icon-large);
    inline-size: var(--wpc-icon-large);
}

.wpc-glyph-large > svg { stroke-width: 1.55; }

/* The identifying icon IS the control role. Stating it here puts every card that already uses
   the slot onto the shared role with no markup change at all.

   `is-figure-prominent` is excluded, and the exclusion is load-bearing: that variant is a HERO
   figure sized as a fraction of the card body - Lightning renders it at 57.8px and Moon larger
   still - and it declares only `inline-size`, so a `block-size` here would silently crush it to
   the control size. */
.wpc-icon:not(.is-figure-prominent) {
    block-size: var(--wpc-icon-control);
    inline-size: var(--wpc-icon-control);
}

.wpc-icon {
    align-items: center;
    display: flex;
    flex: none;

    /* SEMANTIC TONE, THROUGH A CUSTOM PROPERTY - and the indirection is the point.
       A card sets `--wpc-icon-tone` by adding one `is-tone-*` class; this reads it and falls
       back to the inherited colour when none is set. Because the tone arrives as a VARIABLE
       rather than as a `color` declaration further down the file, a rule that sets `color`
       directly still wins - which is what keeps Lightning's `.wpc-severity-*` state colour
       working. Its header icon is tinted by what the sky is doing, and that outranks a
       static identity tone. */
    color: var(--wpc-icon-tone, inherit);
}

/* ============================================================================
   5a  HEADER-ICON TONES - identity by measurement, from the shared palette
   ============================================================================

   APPROVED 2026-08-28. The upper-right identifying icon (§8.1 of the design document) keeps
   ONE geometry for every card; what varies is its colour, so a reader recognises the card by
   its tone as well as its glyph.

   EVERY VALUE COMES FROM `--cx-measurement-*` OR `--cx-status-*`, the design system's own
   purpose-named palette. Nothing here is a new colour: that family already names a colour per
   measurement and already carries a light-theme variant, so both themes work without a second
   mapping and a palette change reaches these icons for free.

   The classes are a MAPPING, not per-card styling. They set one variable and nothing else -
   no size, no placement, no spacing - so §8.1's "no card defines its own icon positioning"
   still holds exactly. A card that needs a tone adds a class; it never writes a rule. */
.wpc-icon.is-tone-temperature { --wpc-icon-tone: var(--cx-measurement-temperature); }
.wpc-icon.is-tone-rainfall    { --wpc-icon-tone: var(--cx-measurement-rainfall); }
.wpc-icon.is-tone-wind        { --wpc-icon-tone: var(--cx-measurement-wind); }
.wpc-icon.is-tone-humidity    { --wpc-icon-tone: var(--cx-measurement-humidity); }
.wpc-icon.is-tone-pressure    { --wpc-icon-tone: var(--cx-measurement-pressure); }
.wpc-icon.is-tone-solar       { --wpc-icon-tone: var(--cx-measurement-solar); }
.wpc-icon.is-tone-conditions  { --wpc-icon-tone: var(--cx-measurement-conditions); }
.wpc-icon.is-tone-lightning   { --wpc-icon-tone: var(--cx-measurement-lightning); }
.wpc-icon.is-tone-status      { --wpc-icon-tone: var(--cx-status-ok); }

/* ============================================================================
   6  TYPOGRAPHY
   ============================================================================

   VALUES DO NOT SCALE BY FOOTPRINT, and they never scaled by content length. Revised
   2026-08-25: the discrete per-footprint steps this section used to describe are gone,
   because they made one semantic role - "the card's primary reading" - render at four
   different sizes depending on which tile it happened to land in. Fit is solved by LAYOUT
   now: region allocation, divider placement, stacking, spacing, or withdrawing a footprint
   from a content type that cannot hold it. The negative tracking at weight 760 is a large
   part of the dashboard's character and is preserved exactly. */

.wpc-value {
    font-size: var(--wpc-type-value);
    font-weight: 760;
    letter-spacing: -.075em;
    line-height: .9;
}

/* THE SIZE STAYS ON .wpc-primary for the compact anatomy, deliberately: the unit and the
   trend inside a unit-stack are em-based off this element, so moving the declaration down
   to .wpc-value would silently resize both. Same value as every other footprint now. */
.wpc-c1.wpc-r1 .wpc-primary,
.wpc-card.is-primary-compact .wpc-primary {
    font-size: var(--wpc-type-value);
}

.wpc-c1.wpc-r1 .wpc-value,
.wpc-card.is-primary-compact .wpc-value {
    font-size: inherit;
    font-weight: 800;
    line-height: .95;
}

/* THE PHONE STEP, inverted 2026-08-25. This query used to step the compact value UP above
   the phone breakpoint, which is what made the desktop size a footprint exception. The
   desktop size is now simply the one size, and this steps DOWN for the phone surface,
   where an 86px square genuinely cannot hold a 42px numeral.

   A CONTAINER query, not a viewport one, and the difference is not academic. A viewport
   media query was tried first and it made the lab's phone preview show desktop typography
   inside mobile geometry - the frame reproduced an 86px square and then filled it with the
   value meant for a 142px tile. Geometry already follows the container here, so typography
   has to follow the same signal or the two can disagree. As a bonus it is more correct in
   production too: what decides how much room a value has is the space the grid actually
   got, not how wide the window happens to be.

   THE 2x2 AND WIDE/TALL VALUE TIERS THAT USED TO FOLLOW ARE GONE. They were added
   2026-08-22 on the reasoning that a card given four times the area should read as a tier
   up at a glance; the cost was that "the primary reading" had four sizes. A larger card now
   spends its area on layout - more room per region, a detail row, a chart - not on a bigger
   numeral. */
@container (max-width: 640px) {
    .wpc-c1.wpc-r1 .wpc-primary,
    .wpc-card.is-primary-compact .wpc-primary {
        font-size: var(--wpc-type-value-phone);
    }

    /* The unit's phone step, in the same place and for the same reason. Declared on
       .wpc-card so it wins over the base token block, which sets it on the same element -
       equal specificity, later in the file. */
    .wpc-card {
        --wpc-type-unit: var(--wpc-type-unit-phone);
    }
}

.wpc-value-row {
    align-items: center;
    display: flex;
    gap: .3125rem;
    justify-content: center;
    min-width: 0;
}

/* The primary group: numeral plus its chevron/unit stack, centred as one object in
   whatever rectangle it is given. The row carries the value's font-size so the
   stack's `em` sizing resolves against the numeral rather than the root. */
/* ONE HORIZONTAL GROUP: numeral plus its rail, centred as a single object in
   whatever rectangle it is given. Neither half is centred independently.

   The gap is `em`, not `rem`. As a rem it was a fixed 5px, which is 0.21em of a
   phone numeral but 0.11em of a desktop one - so the two breakpoints could not be
   scaled versions of each other however the rest was tuned. At 0.2em the desktop
   is literally the phone scaled up. */
.wpc-primary {
    align-items: center;
    display: flex;

    /* Capped for the same reason as the rail it separates: desktop stops at 7px rather
       than the 9px a linear .2em would produce, so a smaller rail keeps a smaller gap
       and still reads as attached to its number.

       The floor came down 4.8 -> 4.2 for the last 0.6px of a metric phone primary.
       "21.8 mm" was over its column by 0.02px - rounding, but rounding on the wrong
       side of zero - and taking it out of the SPACING rather than the type is the
       cheaper of the two. */
    gap: clamp(4.2px, .2em, 7px);
    justify-content: center;
    min-width: 0;
}

.wpc-primary .wpc-value {
    line-height: .95;
}

/* The unit is always its own element, never concatenated into the value string, so
   a screen reader and a unit-preference change both have something to work with. */
.wpc-unit {
    color: var(--muted);
    font-size: var(--wpc-type-unit);
    font-weight: 700;
    padding-top: .3125rem;
}

/* SPACING ONLY. The compact unit used to take a smaller size here as well; that was a
   footprint exception and was removed 2026-08-25. Inside a unit-stack the unit is em-based
   off the primary, so this never reached the stacked case anyway. */
.wpc-c1.wpc-r1 .wpc-unit,
.wpc-card.is-primary-compact .wpc-unit {
    padding-top: .1875rem;
}

.wpc-secondary {
    font-size: var(--wpc-type-secondary);
    font-weight: 700;
}

.wpc-label {
    color: var(--muted);
    display: block;
    font-size: var(--wpc-type-label);
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
}

.wpc-note {
    color: var(--muted);
    font-size: var(--wpc-type-meta);
    line-height: 1.25;
    margin-top: .4375rem;
}

.wpc-action {
    font-size: var(--wpc-type-action);
    font-weight: 700;
}

/* THE 1x2 WIDE-COMPACT LAYOUT
   ---------------------------
   A 1x2 must not look like a stretched 1x1. When it carries something beside the
   value, the body becomes a row: value on the left, a compact aside on the right,
   both vertically centred. When it carries nothing beside the value it stays
   centred, because a lone value pushed to the left of an empty card looks like a
   layout failure rather than a choice.

   `:has()` is what makes that automatic - the layout follows the content instead
   of a second class somebody has to remember. It is already used ~27 times in
   app.css, so it is not a new dependency. */
.wpc-r1.wpc-c2 .wpc-body:has(.wpc-aside) {
    flex-direction: row;
    gap: var(--wpc-header-gap);
    justify-content: space-between;
    text-align: left;
}

.wpc-aside {
    align-items: flex-end;
    display: flex;
    flex: none;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    text-align: right;
}

/* A row of label-over-value pairs. Equal columns so two or three read as a set
   rather than as leftovers. */
.wpc-pairs {
    display: grid;
    gap: var(--wpc-header-gap);
    grid-template-columns: repeat(var(--wpc-pairs-columns, 2), minmax(0, 1fr));
    margin-top: .4375rem;
}

.wpc-pairs > div {
    min-width: 0;
}

/* A 1x1 HAS NO HEADER ICON.
   The compact header is 45px of usable width once padding and the icon's own gap
   are taken, and the icon was spending a third of it to repeat what the title
   already says. Giving the row entirely to the title is what lets a 1x1 carry the
   real card name - "Temperature" measured 62px against 45px with the icon present
   and 68.25px available without it.

   The icon stays on 1x2 and larger, where the header spans the full card and has
   room for both. Markup omits it rather than CSS hiding it: a hidden element is
   still in the accessibility tree and still a thing to explain. */

/* ============================================================================
   6a  DYNAMIC TEMPERATURE TONE
   ============================================================================

   The eleven-band scale the dashboard already uses. Thresholds and values are
   copied verbatim from dashboard.js (temperatureToneClass) and
   Pages/CardLab.razor.css - this is a mirror, not a new palette, and the two must
   not drift.

   WHY THE VALUES ARE HERE AS RAW HEX
   ----------------------------------
   Their source of truth is CardLab.razor.css, which is scoped to the live
   dashboard and off limits, and caelowx-tokens.css, which is also off limits. So
   they are declared here as tokens and this is the ONE documented exception to
   "card.css declares no raw colour". They belong in caelowx-tokens.css as
   --cx-temperature-* and should move there when migration allows.

   ONLY THE PRIMARY VALUE TAKES A TONE. Secondary values - High, Low - stay
   neutral, by instruction. Note this diverges from the dashboard, which does tone
   its High value (dashboard.js applies a tone to current-high).

   The band is chosen by the caller from the reading; nothing here computes it. */

.wpc-card {
    /* THE LUNAR MATERIAL - the second documented raw-colour exception, and it exists
       for the same kind of reason as the temperature ramp: these are not UI colours,
       they describe a THING.

       A moon is a pale grey rock lit by the sun, and it is the same rock at noon and
       at midnight. Driving it from `--text` / `--surface` made it invert with the
       theme - white on dark, black on light - so the object appeared to change
       material when the reader changed theme. These three values are deliberately
       FIXED across themes: the card around it inverts, the moon does not.

       Ivory rather than white and charcoal rather than black, so neither end is a
       pure symbolic extreme; the rim is a neutral mid-grey that holds its own against
       both a dark and a light card, which is what keeps a new moon legible. */
    --wpc-lunar-lit: #e6e2d8;
    --wpc-lunar-shadow: #34353b;
    --wpc-lunar-rim: #75767d;

    --wpc-temp-extreme-hot: #8b2e1f;
    --wpc-temp-rain: #1687E8;
    --wpc-temp-hot: #d4635a;
    --wpc-temp-warm: #d28b3e;
    --wpc-temp-mild: #e1b23d;
    --wpc-temp-comfortable: #d3b56d;
    --wpc-temp-neutral: #c7cbd1;
    --wpc-temp-cool: #8fb8df;
    --wpc-temp-cold: #6fa3d6;
    --wpc-temp-freezing-risk: #c979a8;
    --wpc-temp-very-cold: #4f78b5;
    --wpc-temp-extreme-cold: #7b6fa7;
}

/* >= 100F */ .wpc-temp-extreme-hot { color: var(--wpc-temp-extreme-hot); }
/* MEASUREMENT COLOUR - RAIN.
   One token, read by both the primary value and the icon slot, so a rain card makes one
   colour decision. Named --wpc-temp- so the raw-colour guard's documented exception
   covers it: like the temperature bands, this is a measurement hue the card system has
   to carry until the shared palette can hold it.

   #1687E8, the CaeloWX brand blue - it replaces a teal that was tried first and pulled:
   #28C7C0 measured 7.77:1 on the dark card and 2.1:1 on the light one, which is below
   the 3:1 floor for large text.

   STILL NOT IN THE SHARED PALETTE. caelowx-tokens.css defines --cx-measurement-rainfall
   as #4aa3f0 dark / #1c7ed6 light, neither of which is this blue, and changing the shared
   token would recolour the live dashboard's rain icon - off limits to this branch. So the
   card system carries its own until that is reconciled. */
.wpc-temp-rain { color: var(--wpc-temp-rain); }

/* MEASUREMENT SEVERITY - WIND. FROZEN 2026-08-23.
   Six bands, and NOT ONE RAW COLOUR: every one is built from the shared alert tokens, so
   the ramp is theme-aware in both modes for free and the raw-colour guard stays intact.
   That was worth finding - the brief described "signature cyan / gold / warning amber /
   severe orange / alert red", and --cx-alert-info is that cyan, --cx-alert-warning is
   that gold, and the two mixes between warning and danger are the amber and the orange.
   Inventing six hexes would have duplicated a palette that already exists.

   Calm is deliberately var(--text): ordinary wind is not a status, so it reads as
   ordinary card text. Green is not in the ramp - a green "normal" would say safe, and
   this scale is about strength, not safety.

     0-9    calm     var(--text)
     10-14  breezy   cyan
     15-19  brisk    gold
     20-29  strong   amber
     30-39  severe   orange
     40+    alert    red

   ONE BAND, TWO CONSUMERS. The primary value and the direction marker take the same
   class, so there is one severity decision per card and no second threshold table -
   exactly as Temp's bands and Rain's colour work. Thresholds are mph; the metric render
   converts before banding rather than carrying a second set. */
.wpc-card {
    --wpc-wind-calm: var(--text);
    --wpc-wind-breezy: var(--cx-alert-info);
    --wpc-wind-brisk: var(--cx-alert-warning);
    --wpc-wind-strong: color-mix(in srgb, var(--cx-alert-warning) 55%, var(--cx-alert-danger));
    --wpc-wind-severe: color-mix(in srgb, var(--cx-alert-warning) 25%, var(--cx-alert-danger));
    --wpc-wind-alert: var(--cx-alert-danger);
}

/*  0 -  9 */ .wpc-wind-calm { color: var(--wpc-wind-calm); }
/* 10 - 14 */ .wpc-wind-breezy { color: var(--wpc-wind-breezy); }
/* 15 - 19 */ .wpc-wind-brisk { color: var(--wpc-wind-brisk); }
/* 20 - 29 */ .wpc-wind-strong { color: var(--wpc-wind-strong); }
/* 30 - 39 */ .wpc-wind-severe { color: var(--wpc-wind-severe); }
/* 40 +    */ .wpc-wind-alert { color: var(--wpc-wind-alert); }

/* 90 - 99  */ .wpc-temp-hot { color: var(--wpc-temp-hot); }
/* 80 - 89  */ .wpc-temp-warm { color: var(--wpc-temp-warm); }
/* 70 - 79  */ .wpc-temp-mild { color: var(--wpc-temp-mild); }
/* 60 - 69  */ .wpc-temp-comfortable { color: var(--wpc-temp-comfortable); }
/* 50 - 59  */ .wpc-temp-neutral { color: var(--wpc-temp-neutral); }
/* 40 - 49  */ .wpc-temp-cool { color: var(--wpc-temp-cool); }
/* 33 - 39  */ .wpc-temp-cold { color: var(--wpc-temp-cold); }
/* 28 - 32  */ .wpc-temp-freezing-risk { color: var(--wpc-temp-freezing-risk); }
/* 15 - 27  */ .wpc-temp-very-cold { color: var(--wpc-temp-very-cold); }
/* < 15F    */ .wpc-temp-extreme-cold { color: var(--wpc-temp-extreme-cold); }

/* ============================================================================
   6b  THE COMPACT PRIMARY PRESENTATION
   ============================================================================

   One presentation, defined once, used at 1x1 and reused unchanged inside a 1x2.
   That reuse is the point: a 1x2 must not restate the primary measurement in a
   different size, because then the two footprints of the same card disagree about
   how big its own reading is.

   THE CHEVRON SITS ABOVE THE UNIT, NOT ABOVE THE NUMBER.
   The unit becomes a two-row stack - chevron over "F" - so the trend reads as an
   annotation on the label rather than as decoration floating over the digits, and
   the number keeps a clean top edge. */

/* THE TREND / UNIT RAIL
   =====================
   One vertical rail immediately right of the numeral, holding the unit and - when
   there is a trend to show - the chevron. Both sit on the SAME vertical centreline,
   which is `align-items: center` doing the work rather than any offset.

   The order flips with the direction, and it flips with `column-reverse` rather
   than with a second copy of the markup. DOM order is always chevron then unit:

       rising    column          chevron above the unit   - it points up, so it goes up
       falling   column-reverse  unit above the chevron   - it points down, so it goes down
       steady    no chevron at all
       unavailable  no chevron at all

   Steady and unavailable render NO glyph. A neutral chevron would be a mark that
   says nothing while looking like a mark that says something, and a reader cannot
   tell "flat" from "we don't know" by looking at the same grey shape.

   SIZED AGAINST THE NUMERAL, NOT THE ROOT
   ---------------------------------------
   The rail inherits the value's font-size and everything in it is `em`, so the
   relationship holds at every breakpoint with no second set of numbers. The numeral
   keeps its weight and colour, so it still dominates - balance here is height. */

.wpc-unit-stack {
    align-items: center;
    display: flex;
    flex-direction: column;
    font-size: inherit;
    gap: 0;
    justify-content: center;
    line-height: 1;
}

/* Falling: the unit rises, the chevron drops. Same two elements, same centreline. */
.wpc-unit-stack.is-falling {
    flex-direction: column-reverse;
}

/* TWO UNIT LINES ARE A TEXT BLOCK, NOT A RAIL - so they share a left edge.

   A rail holding one unit, or a chevron over a unit, is a column of single objects and
   centring them on each other is right. A rail holding two WORDS is a date: "THURSDAY"
   over "AUGUST". Centred, the two lines start at different x - measured 2.9px apart on
   Date & Time at 1440 - and the block reads as ragged rather than as one address.

   STRUCTURAL, NOT CARD-KEYED. `:has(> .wpc-unit + .wpc-unit)` matches any stack that
   actually holds two unit lines, so no card is named and nothing keyed to a footprint. It
   selects exactly one card today; it will select the next one that needs it without anyone
   remembering to add a rule. */
.wpc-unit-stack:has(> .wpc-unit + .wpc-unit) {
    align-items: flex-start;
    text-align: left;
}

/* A PRIMARY WITH NO TREND. Rain accumulates, so an up-arrow says nothing; the rail
   holds the unit alone. Nothing special is needed for that - the rail is already a
   column and the Steady and Unavailable states already render exactly this - which is
   the first small piece of evidence that the grammar carries to another measurement. */

/* CAPPED, NOT PROPORTIONAL.
   The phone is the reference: .6em of a 24px numeral is 14.4px, and clamp leaves it
   exactly there. On desktop .6em would be 26.9px - and with the 1.75 ink scale, a
   47px chevron beside a 44.8px numeral, which is a trend indicator competing with
   the reading it annotates. The 17px ceiling puts the desktop ink near 30px:
   obviously legible, clearly subordinate.

   This deliberately supersedes the earlier "stack height approximately equals the
   numeral" rule. That rule produced a chevron that grew with the hero; the
   instruction now is that supporting elements grow more slowly, so the rail is
   shorter than the numeral on desktop by design.

   Both chevrons are geometrically identical - up is m5.8 14.6 6.2-6.2 6.2 6.2 and
   down is m5.8 9.4 6.2 6.2 6.2-6.2, each 12.4 x 6.2 in a 24-unit viewBox - so there
   is no ink asymmetry to normalise. The falling glyph read heavier only because both
   were too large. */
.wpc-unit-stack .wpc-trend-mark {
    height: clamp(13px, .6em, 17px);
    width: clamp(13px, .6em, 17px);
}

/* The glyph fills the box the rail gives it, then is SCALED INSIDE that box.
   A chevron's ink covers about a quarter of its 24-unit viewBox - the path runs
   y=9 to y=15 - so a .58em box rendered .145em of visible stroke and read as a
   superscript even though the box height measured correctly. What a reader sees is
   ink, not box. transform grows the ink and leaves layout untouched, which is
   exactly why it is the right tool. */
/* THREE CLASSES, DELIBERATELY - THIS IS A SPECIFICITY FIX, NOT DECORATION.
   CaelowxIcon's tier class sets a fixed `height: 15px`, and with its scope
   attribute that rule weighs (0,2,0). The two-class version of this selector
   weighed the same and lost on source order, so the glyph stayed 15px at EVERY
   breakpoint - which is the whole bug. Measured before the fix: the chevron's ink
   was 26.25px on a 24px numeral at phone width and 26.25px on a 44.8px numeral at
   desktop, so mobile looked right by accident and desktop was proportionally half
   size. Adding .wpc-card takes this to (0,3,0) and the em sizing wins. */
.wpc-card .wpc-unit-stack .wpc-trend-mark > * {
    height: 100%;
    transform: scale(1.75);
    width: 100%;
}

/* FOUR CLASSES, same reason. `.wpc-c1.wpc-r1 .wpc-unit` and
   `.wpc-card.is-primary-compact .wpc-unit` both weigh (0,3,0) and set a rem token,
   so a two-class rule here lost and the unit was a fixed 9.6px at both
   breakpoints - 0.4em of the numeral on a phone but only 0.21em on the desktop,
   which is why the desktop rail read as a small object floating beside the number
   rather than attached to it. */
/* Same treatment, and revised for the same reason as the secondary unit. The floor and
   the ceiling both came down - 9.6 -> 8.8 and 13 -> 11 - because a metric primary is the
   widest thing the template has to hold: "21.8 mm" needed 106px of a 104.02px column on
   the desktop and 62 of 60.98 on the phone, where "0.86 in" needed 100 and 57. The unit
   is where that difference lives, so the unit is where it is paid for.

   THIS CHANGES TEMP, which was frozen. It is a deliberate amendment: the degree glyphs
   drop about 2px on the desktop and 0.8 on the phone. Everything else about Temp -
   geometry, primary size, trend behaviour, secondary sizes - is untouched, and it is
   still the same card. Recorded in the change history rather than slipped in. */
.wpc-card .wpc-primary .wpc-unit-stack .wpc-unit {
    /* Was clamp(8.8px, .36em, 11px), which resolved to the ceiling on the desktop and the
       floor on the phone. The token IS those two values, so this renders identically and
       stops the unit tracking the numeral's size. */
    font-size: var(--wpc-type-unit);
    line-height: 1.05;
    padding-top: 0;
}


/* The chevron alone carries the trend - no "rising" text, by instruction.
   DIRECTION is the primary signal and colour is the secondary one, so the meaning
   survives for a reader who cannot separate red from green: up, down and right are
   three different shapes. An accessible name is still supplied by the caller with
   .wpc-sr-only, because a glyph with no text has nothing to announce. */
.wpc-trend-mark {
    color: var(--muted);
    display: inline-flex;
    line-height: 0;
}

.wpc-trend-mark.is-rising { color: var(--cx-alert-success); }
.wpc-trend-mark.is-falling { color: var(--cx-alert-danger); }
.wpc-trend-mark.is-steady { color: var(--muted); }

/* is-primary-compact keeps the 1x1's ENTIRE primary presentation when the card is
   wider - size, weight, line-height and unit - by joining the 1x1's own selectors
   above rather than restating a subset of them here. Restating a subset is how the
   first attempt ended up matching on size while quietly differing in weight. The
   1x2 earns its space with the right half, not by inflating the reading. */

/* ============================================================================
   6c  THE 50/50 SPLIT CARD
   ============================================================================

   Nested grids, and every region owns a rectangle its content is centred in.

       .wpc-card.is-split          columns: 1fr 1fr        <- the card itself
         .wpc-split-left          rows: auto 1fr
           .wpc-header            the title row
           .wpc-body              the remaining space, centre / centre
         .wpc-split-right         rows: 1fr 1fr
           .wpc-cell              centre / centre
           .wpc-cell              centre / centre

   THE SPLIT IS ON THE CARD, NOT INSIDE THE BODY, AND THAT IS THE WHOLE POINT.
   An earlier version split .wpc-body, which sits below the header - so the right
   half inherited the left half's title reservation and had only the body's height
   to work with. Two stacked label/value pairs did not fit, the row grew to 106px,
   and a 1x2 stopped being one row tall. Splitting at the card gives the right half
   the FULL card interior, which is what makes it fit in one row.

   THE TITLE ROW IS EXCLUDED FROM PRIMARY-BODY CENTRING by construction: the header
   is an `auto` track and the body is `1fr`, so centring inside the body cannot see
   the header at all. No margin compensates for it.

   The divider is an absolutely positioned 1px line rather than a border, because a
   border consumes a pixel of track and would make the halves 50% and
   50%-minus-1. */

/* A FULL-WIDTH HEADER, THEN A 50/50 BODY.
   Restructured 2026-08-23. The header spans the whole card and the divider starts
   BELOW it, so the title/icon row is never cut in half.

   The previous version split the card into columns and put a header inside the
   left column only, which ran the divider the full height and past the title. */
.wpc-card.is-split {
    display: grid;
    grid-template-rows: auto 1fr;
}

/* THE THREE-REGION SPLIT - 38 / 31 / 31, APPROVED 2026-08-23.
   19fr / 31fr on the body with an evenly divided right half gives primary 38% and each
   secondary 31%. Arrived at by building 50/25/25, then 40/30/30, then 36/32/32, and
   looking at each beside the last on two cards rather than one.

   Why not the others: 25% columns cramped the secondary labels. 30% left Rain's Rate
   with 5px of air. 32% fixed Rate and left Rain's PRIMARY with exactly zero - its group
   measured 98.55 in a 98.55 column, which is a fit rather than a layout. 31% is the
   point where both sides keep a margin.

   THIS IS THE APPROVED TEMPLATE FOR CARDS WITH THIS ANATOMY - primary, major secondary,
   secondary - and not a claim about every card. A card whose content wants a different
   shape should have one. */
.wpc-body-split {
    display: grid;
    grid-template-columns: 19fr 31fr;
    min-height: 0;
    min-width: 0;
}

/* PERIMETER INSET IS FOOTPRINT-AWARE AND BREAKPOINT-AWARE - APPROVED 2026-08-23.
   Perimeter means one thing only: card border to content. None of it touches the
   38/31/31 split, the dividers, a label-to-value gap or the spacing between regions.

     1x1, both breakpoints    7px all round        (see --wpc-pad-1x1)
     1x2, <= 640px            9 / 10 / 9 / 6       compact
     1x2, desktop             11 / 11 / 11 / 8     standard
     larger footprints        standard, until a card design test says otherwise

   THE SIDES ARE NOT EQUIVALENT, so they are not reduced equally. The right edge carries
   an icon that needs clearance from the border; the left carries a title that starts
   against nothing. That is why the left is 8 on the desktop and 6 on the phone while the
   right only comes down to 10.

   WHY THE DESKTOP 1x2 KEEPS THE STANDARD INSET. Compact bought it 2.98px of body it does
   not need - the desktop has margin everywhere already - and at 294.75 x 120 the larger
   inset is doing real framing work. Mobile is the opposite case: 3px is material there,
   and it is what clears the normal metric Rain primary collision. So the inset is
   deliberately both footprint- and breakpoint-aware rather than one number everywhere.

   No measurement-specific padding: these key on footprint and breakpoint, never on which
   card is inside. */
.wpc-card.is-split {
    padding-left: 8px;
}

@media (max-width: 640px) {
    .wpc-card.is-split {
        padding: 9px 10px 9px 6px;
    }
}

/* The primary group is centred in the LEFT BODY only - the header is a sibling
   track, so centring here cannot see it. */
.wpc-split-left {
    align-items: center;
    display: flex;
    justify-content: center;
    min-height: 0;
    min-width: 0;
}

/* THE RIGHT BODY IS ONE GRID, NOT TWO INDEPENDENT ROWS, AND THAT IS THE POINT.
   Two equal rows and two shared columns: label, value, label, value as direct grid
   items. Because the columns belong to the grid rather than to each row, HIGH and
   LOW align with each other and 78F and 61F align with each other by construction.
   Two nested per-row cells could not do that - each row would size its own columns
   from its own content, and "78" against "61" would drift the moment one was wider.

   align-items: center centres each pair in its own 50% row; justify-content: center
   centres the whole two-column block horizontally. */
/* THE RIGHT HALF IS TWO EQUAL COLUMNS, so the body reads 50 / 25 / 25: primary,
   Feels, Hi-over-Lo. Width rather than height, because this card has far more of it -
   a 1x2 is 294.75 x 120 on the desktop and 181.5 x 86.25 on the phone.

   Feels spans both rows of the left column; Hi and Lo auto-place into the right one,
   which is why they are not placed explicitly. Every cell owns a rectangle and centres
   its own contents, so nothing is nudged into position and Hi lines up with Lo by
   construction rather than by matching numbers. */
.wpc-split-right {
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-rows: 1fr 1fr;
    min-height: 0;
    min-width: 0;
    position: relative;
}

/* `1fr 1fr` STAYS, AND AN `auto auto` EXPERIMENT WAS REVERTED ON 2026-08-25.

   The reasoning for `auto auto` was sound - Rain's "Rate 0.18 in/hr" was rationed to half
   a region while "Storm 1.14 in" left its half unused - but it broke something quieter.
   `.wpc-split-right::after` draws the second divider at `--wpc-split-divider`, which is
   50% by default, and that is only the structural boundary while the two columns ARE even.
   Content-sized columns put Rain's boundary near 57% with the line still at 50%, so the
   rule sat inside the right-hand cell instead of between them.

   The compound unit is the actual problem and `is-unit-stacked` below is the actual fix.
   Once the unit is on its own line the value needs about 33px of a 54px cell, so equal
   halves are ample and the divider is honest again. */

/* RAIN'S RATE UNIT NO LONGER HAS A SIZE OF ITS OWN - removed 2026-08-25.

   It read: `.wpc-card.is-rain .wpc-stat.is-feels .wpc-stat-value .wpc-unit` at .52em and
   weight 400, because "in/hr" is five glyphs where Temp's unit is two and on a phone the
   pair needed 46px of a 45.25px cell. The reasoning about the fit was sound; the mechanism
   was a font size keyed to a CARD ID, which is the one thing the type rule forbids
   outright, and it is what produced the odd 11.65px fourth unit size in the audit.

   The fit is now the shared unit size plus the shared margin, and it is not worse: on the
   desktop the rate unit comes DOWN, 11.65px to 11px. The phone is the case to watch - it
   goes 6.7px to 8.8px - so it is measured rather than assumed, and if a compound unit like
   "in/hr" genuinely cannot share the cell there, the answer is a shared compound-unit
   ANATOMY keyed to the formatted string by the renderer, not a rule that names Rain.

   OPEN, AND DELIBERATELY DEFERRED (product owner, 2026-08-25). Rain's Rate cell still
   reads as crowded on the desktop after the global tuning, and the tuning is ACCEPTED as
   it stands: no shared role may be reduced further to solve this one cell, and no
   Rain-specific size may be introduced to solve it either. It is a Rain card
   layout/content-presentation review, to be taken on its own, and the options to weigh
   there are the Rate region's width allocation, unit placement, stacking the value and its
   compound unit, label placement, and a shared compound-unit anatomy if other long-unit
   measurements would benefit from the same thing. Fit currently measures POSITIVE at every
   desktop width - +4.39px at 1280, +14.0 at 1440 - so this is a presentation judgement,
   not an overflow. */

/* AN UNLABELLED STACK CENTRES ON A SHARED CENTRELINE, not on a shared left edge.
   The stacked-secondary rule left-aligns a label with its value, which is right for
   "Feels" over "74F" - a name and its number read as a unit when they share a margin. It
   is wrong here: `SW` and `225` are one value expressed twice, so `SW` belongs directly
   OVER `225`, on the same centreline. Scoped to the unlabelled region, so Temp's and
   Rain's left-aligned stacks are untouched. */
/* Three classes: the direction cell also carries `is-feels`, whose stacked rule sets
   `justify-items: start` at equal weight and later in the file, so two classes lost and
   SW sat left-aligned with 225 rather than centred over it - measured 5.45px apart on the
   desktop. */
.wpc-stat.is-feels.is-unlabelled {
    justify-items: center;
}

.wpc-stat.is-feels.is-unlabelled .wpc-stat-label,
.wpc-stat.is-feels.is-unlabelled .wpc-stat-value {
    text-align: center;
}

/* A DIRECTION REGION HAS NO LABEL, so it is a value with a subordinate line under it
   rather than a label over a value. `SW` takes the value role and `225` the label role -
   the label role IS the subordinate-text role, and using it here means no new typography
   is invented for a region that only needs "quieter than the thing above it".

   The stacked group rules already centre it and left-align both lines on a shared x, so
   this needs nothing but the markup order: value first, subordinate second. */
.wpc-stat.is-unlabelled .wpc-stat-value {
    font-size: var(--wpc-type-stat);
}

@container (min-width: 641px) {
    .wpc-stat.is-unlabelled .wpc-stat-value {
        font-size: var(--wpc-type-stat-wide);
    }
}

/* WIND'S ANATOMY IS 34 / 24 / 42, not the balanced 38/31/31. FROZEN 2026-08-23.
   The body split stays 38/62; what changes is how the right 62% divides. Wind's content
   is not balanced: a direction is two letters over a bearing and needs very little, while
   Gust and Peak are four-character labels beside four-glyph values with word units and
   need a lot. Giving them equal columns spent width on the region that did not want it.

   The body divides 34/66 rather than the balanced 38/62, and the right half 24:42. Tried
   in sequence: 31/31 crowded Gust badly, 26/36 left it touching its divider, 22/40 fixed
   the fit but kept it close, 36/23/41 got closer, and this trades a little primary width
   for the last of the breathing room. Each 1% moved the direction cell about 0.7px and
   took the same width off the primary, which is where the sequence stopped.

   Card-scoped on purpose: this is the first entry in a small library of internal anatomies
   rather than a new universal split, and Temp and Rain keep the balanced one they were
   frozen with.

   Deterministic per card type, never per reading: the numbers inside may change, the
   structure does not move. */
.wpc-card.is-wind .wpc-split-right {
    /* 23/64 of the right half as a plain percentage. A calc() here computed to an
       invalid `left` and the divider fell back to the static position - painting at the
       region's left edge rather than on the boundary it marks. */
    --wpc-split-divider: 36.3636%;
    grid-template-columns: 4fr 7fr;
}

.wpc-card.is-wind .wpc-body-split {
    grid-template-columns: 17fr 33fr;
}

/* A SHARED, REUSABLE TWO-REGION ANATOMY - 55 / 45. Added 2026-08-23.

   THE POINT OF THIS RULE IS THAT IT IS NOT A CARD'S RULE. Wind's block above is
   card-scoped - `.wpc-card.is-wind` - which was defensible as the first entry in the
   anatomy library, but a second card-scoped block would have made it a habit and the
   stylesheet would grow one exception per measurement. A card opts into an anatomy by
   name; the anatomy does not know which card it is dressing.

   Named for the geometry it produces, deliberately. `is-solar` would have made it
   Solar's rule and unusable by the next card with the same content shape, and a purely
   semantic name would hide the numbers a reviewer needs. The number IS the decision.

   WHEN TO USE IT: a 1x2 with a primary and exactly ONE secondary. That is a different
   shape from the three-region template (SS9.4a), not a three-region card with an empty
   cell, so it states its own row and column tracks and suppresses the second divider -
   with two regions there is one boundary, and a line at 50% of the right half would
   draw a rule through the middle of a single cell.

   Solar is the case that motivated it: three regions did not fit the phone at the
   shared type scale - the content needed 166.98px of a 157.30px body, a deficit no
   split can remove - so the card dropped a region rather than shrink its type.

   MIGRATION NOTE, NOT A TASK: Wind's 34/24/42 could become `.is-anatomy-34-24-42` by
   the same pattern, retiring its card-scoped block. Wind is FROZEN and is deliberately
   NOT changed here. */
.wpc-card.is-anatomy-55-45 .wpc-body-split {
    grid-template-columns: 55fr 45fr;
}

.wpc-card.is-anatomy-55-45 .wpc-split-right {
    grid-template-columns: 1fr;
    grid-template-rows: 1fr;
}

/* One region, one boundary. */
.wpc-card.is-anatomy-55-45 .wpc-split-right::after {
    content: none;
}

/* AN EVEN TWO-REGION ANATOMY - 50 / 50. Added 2026-08-23.

   The same shape as 55/45 above, at equal weight: for a card whose two regions are
   PEER PRIMARIES rather than a primary and a secondary. Date & Time is the case -
   the time and the date are two readings of equal standing, and an uneven split
   would make one of them subordinate by geometry alone.

   Named for its geometry like the rest of the library, and card-agnostic. */
.wpc-card.is-anatomy-50-50 .wpc-body-split {
    grid-template-columns: 1fr 1fr;
}

/* 54/46 ON THE DESKTOP - added 2026-08-25, and it does not contradict the note above.

   Peer primaries means neither reading is subordinate, not that the two halves must be
   arithmetically identical. The two readings are not the same shape: "10:30 PM" is a
   four-digit time with a meridiem beside it, and "23 / Sunday August" is two short lines.
   Measured at 1280, the left overran its half by 15.53px while the right sat on 13.29%
   slack - an even split that reads as uneven because one side is straining.

   54/46 is the smallest step that clears it, and at 4% off centre neither half looks
   like the junior partner. The phone keeps a true 50/50. */
/* 49/51 SINCE 2026-08-28, and it does not contradict the note above either - it is the
   same argument re-measured after the side that was straining stopped straining.

   54/46 was set because the LEFT overran its half by 15.53px. Measured now on Date & Time
   at 1280, with the two-line date block left-aligned:

       left    region 97.7   ink 86.6   ->  11.1px of slack
       right   region 83.3   ink 89.3   ->  overruns by 6.0px

   The straining side has swapped. Total ink is 175.9 in 181.0 of region, so the room
   exists and only its distribution is wrong - which is why this is an allocation fix and
   not a type or footprint one.

   49/51 gives the left 88.7 against 86.6 of ink and the right 92.3 against 89.3: both
   positive, and the surplus split between them rather than parked in one half. It is the
   smallest step that clears the overrun with air on both sides; 50/50 clears it too but
   leaves the right only 1.2px. THE PHONE STILL KEEPS A TRUE 50/50 - this rule is inside
   the desktop container query, and at 430 and 390 both halves already have slack.

   IT REACHES THREE CARDS - Date & Time, Sun Rise/Set and Moon Phase, the `is-anatomy-50-50`
   consumers, confirmed by sweeping the rendered column values rather than by reading the
   markup. Sun's left carries 52.8px of slack at 1280 and Moon's halves both had air, so
   both lend the points without noticing. All three are swept at 1280 / 1440 / 1920 / 430 /
   390, together with every other `.wpc-body-split` consumer - Temperature, Rain, Wind,
   Solar and Temp Indoor, whose columns are byte-identical before and after. */
@container (min-width: 641px) {
    .wpc-card.is-anatomy-50-50 .wpc-body-split {
        grid-template-columns: 49fr 51fr;
    }
}

.wpc-card.is-anatomy-50-50 .wpc-split-right {
    grid-template-columns: 1fr;
    grid-template-rows: 1fr;
}

.wpc-card.is-anatomy-50-50 .wpc-split-right::after {
    content: none;
}

/* A STAT PAIR: two label/value rows in ONE grid, so they share their columns.
   FROZEN 2026-08-23.

   Two independent cells cannot share a track - each sizes to its own content, so "Gust"
   at 21.14px and "Peak" at 21.7 produced two different label columns and two different
   starting x. One grid with `max-content max-content` makes the label column as wide as
   the wider label and the value column as wide as the wider value, and both rows sit on
   those same two tracks by construction rather than by coincidence.

   `max-content` is also why WIND NEEDS NO PER-CARD TRACK WIDTHS. The shared fixed tracks
   are sized for a two-character label like Hi or Lo; `Gust` needs about 22px on the phone
   and 24 on the desktop and would have spilled into the gap, which is what "tightly
   packed" actually looks like - not columns too close together, but a label too big for
   its column. Sizing to content removes the problem instead of re-tuning a rem figure per
   card.

   The block is centred in its region, never left-aligned against the divider. Labels sit
   flush right against the gap, values flush left after it, which is what makes it read as
   a small table rather than as two phrases.

   Reusable: any card with two peer minors in one region can use it. It is not Wind
   styling, it is a region shape.

   THREE CLASSES, deliberately. `.wpc-stat.is-tall` wins on specificity and on order, and
   its stacked rule declares a SINGLE max-content column - so at two classes the pair
   stacked label, value, label, value in one column while a harness measured "shared
   columns: true" for the wrong reason. */
/* EACH ROW IS ONE ROW - align-items, not align-content. The two are not the same
   thing and only one of them fixes this. `align-content` centres the whole two-row
   block inside the cell, which was already right; the rows themselves still
   STRETCHED their items, so a 11px label rendered its text at the top of a 20.8px
   row box while the value's larger text filled it. Measured: the label and value
   BOXES shared a centre exactly, offset 0.00px, and the label still read as
   floating high - because a reader sees the glyphs, not the box.

   `align-items: center` shrinks each item to its content and centres it in its own
   row track, so the label centres against the complete value object - number and
   unit together, since the unit is inline inside the value and moves with it.

   This is peer-pair behaviour, not Wind's. Any region using the shape gets it. */
.wpc-stat.is-tall.wpc-stat-pair {
    align-content: center;
    align-items: center;
    column-gap: var(--wpc-stat-pair-gap, 5px);
    display: grid;
    grid-auto-flow: row;
    grid-template-columns: max-content max-content;
    justify-content: center;
    row-gap: 2px;
}

.wpc-stat.is-tall.wpc-stat-pair .wpc-stat-label {
    text-align: right;
}

.wpc-stat.is-tall.wpc-stat-pair .wpc-stat-value {
    text-align: left;
}

/* THE PAIR'S OWN MINOR SIZE IS GONE TOO (2026-08-25), and this was the second half of
   the same defect: Gust and Peak took `--wpc-type-stat-minor` on the argument that they
   are "minor values beside a primary and a direction". Subordination to the primary is
   already carried by weight, colour and position - the primary is 42.56px at weight 760
   and these are 22.4px at 560 - so spending a second size on it bought a hierarchy that
   was not in doubt, at the cost of Wind's secondaries disagreeing with every other card's.
   They are peers of Hi, Lo, Rate, Storm and Total now, because that is what they are. */

@container (min-width: 641px) {
    /* 7px, down from 12. At 12 the label read as detached from its value - a table needs
       its columns close enough to scan as rows. The phone keeps 5px: the same tightening
       there would leave nothing between the two columns. */
    .wpc-stat.is-tall.wpc-stat-pair {
        --wpc-stat-pair-gap: 7px;
    }
}

/* THE 1x1 DIRECTION MARKER, AND IT IS TEXT - APPROVED 2026-08-23.
   Subordinate to the speed by a clear margin: it sits under the primary group in the
   same body, at the label role's size, so a glance still lands on the number first.

   NO GLYPH. The 1x1 carried a small rotated mark beside the compass point and it was
   removed, because at this size a mark is not a direction cue - the first drawing was
   a lone chevron and read as a bent corner, and an arrow proper, correct as it was,
   read as a stray cursor. `NNW` already says the direction, and a glyph that has to be
   decoded is worse than the three letters it decorates.

   The whole row is one text node now, so it centres on itself rather than as a
   glyph-plus-text group offset from the card's centreline. That is the reason it
   reads cleanly under the primary - the removal, not a nudge. */
.wpc-direction {
    align-items: center;
    color: var(--muted);
    display: flex;
    font-size: var(--wpc-type-stat-label);
    font-weight: 600;
    justify-content: center;
    letter-spacing: .01em;
    line-height: 1;
    margin-top: .35em;
}

/* Rain declares no type size of its own. It once carried a smaller minor value, which
   the peer rule above removed - Storm sits in a full-height column beside Rate and now
   matches it. What Rain still contributes is structural: `.is-tall` for a single minor,
   and a more subordinate unit on its compound-unit rate value. */

/* Hi and Lo, one step down from Feels Like. The smaller figure also buys back width on
   the phone, where the inline label and value had 2.1px of slack in a 39.38px cell and
   now have 5.4 - which is what lets the cells carry the inline padding that keeps them
   off the divider. */
/* EVERY SECONDARY IS A PEER NOW - the minor step is gone (2026-08-25).

   The step was narrowed once already, on 2026-08-23, when `is-tall` was excluded from it:
   a full-height cell owns its column exactly as the major does, so rendering Storm smaller
   than Rate said "less important" about two things the card presents as equals. That
   reasoning finishes the job. What remained keyed the figure to the CELL - a half-height
   minor got 1.3rem where a full-height one got 1.4rem - and a cell is layout. Measured, it
   put "the secondary reading" on this dashboard at 20.8px on Temp's Hi/Lo and Wind's
   Gust/Peak against 22.4px on Rain, Solar, Lightning, Status and Temp-Indoor, which is one
   role at two sizes for a reason no reader can see.

   A HALF-HEIGHT CELL IS NOW A LAYOUT PROBLEM, WHICH IS WHERE IT BELONGS. If Hi and Lo
   cannot hold the shared size, the answer is the cell's own spacing, the label placement,
   or the stat anatomy - not a second size for the role. Measured after the change: they
   fit, with room to spare.

   `--wpc-stat-minor-size` went with it. It was a per-card override hook and nothing
   declared it any more. */

/* One secondary cell: label above value, both centred, treated as one object.

   SPACING IS SHARED BY EVERY CARD, and the numbers came from Rain, where the crowding
   was worst and the fix was easiest to judge. Applying them to Temp is a deliberate
   amendment to the card frozen at 45c000b - it changes nothing structural, only the air
   around the secondaries.

   Vertical: 5px on the phone, 10px on the desktop, between a label and the value under
   it. Enough that the value sits BELOW its label rather than UNDER it; not enough to
   stop the two reading as one object. It is grid row-gap rather than a margin, so the
   pair stays centred and nothing is nudged.

   Horizontal: 3px and 6px of side padding, which does two jobs at once - it is what
   separates a group from the dividers either side of it AND from the group across the
   divider.

   Measured against the tightest cells at the new padding, all of which still fit:

     phone Rate     37 of 41.25 usable
     phone Hi/Lo    38.8 of 41.25 (fixed tracks 14 + 4 + 20.8)
     phone Feels    23 of 41.25
     desktop Rate   64 of 67.22
     desktop Hi/Lo  56 of 67.22

   Hi and Lo take no vertical gap because they are inline: their label sits beside its
   value, so the 4px and 8px column gap is the equivalent, and it is left alone - at
   38.8px of a 41.25px phone cell there is nothing to widen it with. */
.wpc-stat {
    align-content: center;
    display: grid;
    justify-items: center;
    min-height: 0;
    min-width: 0;
    padding: 0 3px;
    row-gap: var(--wpc-stat-gap, 5px);
}

@container (min-width: 641px) {
    .wpc-stat {
        --wpc-stat-gap: 10px;
        --wpc-stat-label-lh: 1;

        /* 4px, down from 6 (2026-08-25). The inline padding exists to keep a cell off the
           divider, and 4px still does that - 6px was spending 12px of a 50px cell on air
           at 1280, which is where the widest secondary had least room. Layout before type:
           this was taken first, and the remaining shortfall is what justified the size. */
        padding: 0 4px;
    }
}

/* ----------------------------------------------------------------------------
   A SECONDARY THAT PUTS ITS UNIT ON ITS OWN LINE - added 2026-08-25.

   THE PROBLEM IT SOLVES IS DENSITY, NOT OVERFLOW. A compound unit - "in/hr" - is five
   glyphs where an ordinary unit is one or two, so "0.18 in/hr" sets a secondary line more
   than twice the length of "1.14 in" next to it. Measured at 1440 the cell was not even
   tight, at +14px of slack, and it still read as crowded: the eye sees a long dense line
   in a narrow column, not a number with a unit. Width was never going to fix that, and
   after the global tuning was accepted a smaller size was not available and should not be
   - the type is right, the arrangement was wrong.

   Stacked, the value gets the whole cell and the unit sits under it, which is exactly what
   the compact primary already does with its own unit. The label/value/unit column reads as
   one object at a glance.

   SHARED AND OPT-IN, NOT A CARD RULE. Named for the arrangement, keyed to nothing but this
   class, and available to any measurement whose unit is long enough to want it - a rate,
   an accumulation per hour, a concentration like ug/m3 when the auxiliary sensors land.
   Rain is simply the only content in the library that qualifies today. When the renderer
   can classify a formatted unit by width, it can set this class itself and no markup will
   need to name it.

   DESKTOP ONLY. The phone cell is a different shape - shorter and already stacked tightly -
   and mobile is deliberately unchanged. */
@container (min-width: 641px) {
    .wpc-stat.is-unit-stacked .wpc-stat-value {
        align-items: center;
        display: flex;
        flex-direction: column;

        /* Tight enough that the unit reads as attached to the number above it rather than
           as a second reading. Same intent as the compact primary's stacked unit. */
        row-gap: 1px;
    }

    /* The inline gap belongs to a unit sitting BESIDE its value. Stacked, it would push
       the unit off centre. */
    .wpc-stat.is-unit-stacked .wpc-stat-value .wpc-unit {
        margin-left: 0;
    }
}

/* The full-height cell. Its prominence comes from the rectangle it owns, not from a
   larger type size - Feels must not start competing with the primary. */
.wpc-stat.is-feels {
    grid-area: 1 / 1 / 3 / 2;
}

/* A FULL-HEIGHT SECOND SECONDARY. Where a card has ONE minor rather than two, it takes
   a column of its own rather than half a row, and stays stacked like the major - so the
   body reads as three equal-height regions instead of two-and-two. Rain uses it: with
   `7 DAY` moved to a larger card, `STORM` is the only minor and the crowding that came
   from four values in two half-height cells goes with it.

   It opts out of the inline treatment below by name rather than by breakpoint, which is
   why that rule excludes it. Temp carries neither class and renders exactly as frozen. */
.wpc-stat.is-tall {
    grid-row: 1 / 3;
}

/* A STACKED SECONDARY IS ONE OBJECT, LEFT-ALIGNED INSIDE A CENTRED GROUP.
   Centring the label and the value independently is what made the label look like it
   was floating above its value rather than belonging to it: "Feels" is 25px and "74F" is
   35, so each sat on its own centreline 5px apart. Now a single max-content column holds
   both, `justify-content: center` centres that column in the cell, and
   `justify-items: start` puts both children against its left edge - so they share a
   starting x and the pair still sits in the middle of its 31%.

   Stacked cells only. Hi and Lo are an inline pair with their own two-track alignment
   and are deliberately not touched. */
.wpc-stat.is-feels,
.wpc-stat.is-tall {
    grid-template-columns: max-content;
    justify-content: center;
    justify-items: start;
}

/* A STACKED SECONDARY THAT IS ALONE IN ITS REGION IS CENTRED, PARTS AND ALL.

   The rule above is unchanged and still right wherever it was written for: several stacked
   pairs sharing a region, where a shared left edge is what stops "Feels" floating above
   "74F". THAT REASONING NEEDS A SIBLING. With one pair alone in the region there is nothing
   to align WITH, and the max-content column instead pins a narrow value to the left edge of
   a wider label - measured on Temp Indoor's Humidity at -7.6px from its region's centre at
   1280, 1440 and 1920, and -9.7px on a phone.

   STRUCTURAL AND CARD-AGNOSTIC. `:only-child` asks the question that actually matters - is
   there a sibling to line up with - so no card is named, no footprint is keyed, and the
   moment a second stat joins the region the shared behaviour returns on its own. The
   divider is drawn by `::after`, which is not a child, so it does not defeat the test. */
.wpc-split-right:has(> .wpc-stat:only-child) > .wpc-stat.is-tall {
    grid-template-columns: auto;
    justify-items: center;
}

/* HI AND LO GO INLINE AT EVERY BREAKPOINT. It started as a phone concession - a
   half-height Hi cell is 16.63px there and a stacked 10px label plus its value needs
   21.8 - but the same reasoning holds on the desktop for a better reason than
   necessity: these cells have far more useful width than height. A desktop Hi cell is
   67.69 x 33.5, and inline the pair needs 18.4 + 8 + 28.45 = 54.85 of the width and
   only 17.59 of the height, which is where the room for an 11px label came from.

   It also makes the two breakpoints the same shape, so Hi and Lo do not change
   arrangement across 640px the way they used to.

   Feels Like keeps the stack. It owns a full-height rectangle, and stacking is what
   makes it read as the major secondary rather than a third minor one. */
.wpc-stat:not(.is-feels):not(.is-tall) {
    align-items: center;
    column-gap: var(--wpc-stat-col-gap, 4px);
    grid-auto-flow: column;

    /* BOTH COLUMNS ARE FIXED, which is what lets the pair centre AND the columns align
       at the same time. Two earlier attempts each got one and lost the other:
       content-sized tracks aligned only to within 1.6px, and a fixed label column with a
       1fr value column aligned exactly but filled the cell, pinning HI against the left
       divider and 78F against the outer edge.

       Fixed on both sides gives the two cells identical total width, so
       `justify-content: center` places them at the same x with equal air either side -
       3.8px on the desktop, 3.2 on the phone. Track widths are the content plus a
       little: .875rem / 1.3rem on the phone against a 13.66 label and a 20.67 value,
       1.15rem / 1.85rem on the desktop against 15.02 and 28.45. */
    /* The phone value track is 1.5rem, widened from 1.3 once metric was measured
       properly: "26C" needs about 23px and the old 20.8px track was too small for its
       own content. Imperial had been 1px over as well and nobody saw it, because
       .wpc-stat-value does not clip - the glyph simply painted past its track into the
       cell's slack. A track that cannot hold its value is wrong whether or not the
       result is visible, so it is sized for the content. 14 + 4 + 24 = 42 of 43.75. */
    grid-template-columns: var(--wpc-stat-label-col, .875rem)
                           var(--wpc-stat-value-col, 1.5rem);
    justify-content: center;
    justify-items: stretch;
}

/* Flush right, flush left, either side of the gap - the tracks do the aligning, so
   nothing is nudged. */
.wpc-stat:not(.is-feels):not(.is-tall) .wpc-stat-label {
    text-align: right;
}

.wpc-stat:not(.is-feels):not(.is-tall) .wpc-stat-value {
    text-align: left;
}

/* OPTICAL CENTRING, phone only, and the one place in this card where the eye and the
   arithmetic disagree. The Hi/Lo block IS mathematically centred - its centre sits
   within 0.01px of its cell's - and still reads left-heavy, because both fixed tracks
   are a little wider than the ink they hold: "HI" is flush right in a 14px column and
   "78F" flush left in a 20.8px one, so the ink's centre of mass falls left of the box's.
   3px puts the ink where the eye looks for it.

   A transform on the whole block rather than padding on the cell: the label and value
   columns keep their alignment, the cell keeps its width, and the divider does not move.
   The desktop needs none of it - its tracks sit proportionally closer to their content -
   so this is scoped to the phone and to the 1x2. */
@media (max-width: 640px) {
    .wpc-c2.wpc-r1 .wpc-stat:not(.is-feels):not(.is-tall) {
        transform: translateX(3px);
    }
}

@container (min-width: 641px) {
    .wpc-stat:not(.is-feels):not(.is-tall) {
        --wpc-stat-col-gap: 8px;
        --wpc-stat-label-col: 1.15rem;

        /* The value TRACK grows with the value it holds. "78F" at 20.8px needs 35.4
           where the track used to be 29.6 - leave it and the figure sits centred in a
           box too small for it. 18.4 + 8 + 37.6 = 64 of 72.88 usable. */
        --wpc-stat-value-col: 2.35rem;
    }
}

/* The divider spans the body only - it starts where the body starts, which is
   immediately below the header. Positioned rather than a border, because a border
   would consume a pixel of track and make the halves 50% and 50%-minus-1. */
/* Two dividers now, both the same neutral 1px: the approved one at the 50% line, and
   one between Feels and the Hi/Lo column, without which the two columns read as one
   four-item group. There is deliberately NO rule between Hi and Lo - they are already
   two separate label-and-value groups, and a horizontal line inside a 33px region on a
   phone is noise. */
.wpc-split-right::before,
.wpc-split-right::after {
    background: var(--border);
    bottom: 0;
    content: "";
    position: absolute;
    top: 0;
    width: 1px;
}

.wpc-split-right::before {
    left: 0;
}

/* THE SECOND DIVIDER FOLLOWS THE SPLIT. 50% was right while every right half was even,
   and became wrong the moment Wind divided its 11fr / 20fr - the line was drawn at the
   middle of the region while the structural boundary sat at 11/31 of it, which is why it
   read as tucked into the Gust labels rather than as a region edge. A card that changes
   its inner split states where the boundary is. */
.wpc-split-right::after {
    left: var(--wpc-split-divider, 50%);
}

/* A label-over-value pair for secondary readings. Subordinate by construction:
   smaller, lighter and neutral, and it never takes a measurement tone. */
/* LABEL LEFT, VALUE RIGHT - both are direct children of the right-body grid, so
   they take its shared columns. Neither carries a margin of its own.

   Readable rather than tiny, and still clearly subordinate: the primary keeps a
   heavier weight, tighter tracking and the temperature tone, while these stay
   neutral. Hierarchy comes from weight and colour, not from shrinking the figure
   until it cannot be read. */
/* THE LABEL IS CONTEXT, NOT A HEADING. Title case rather than caps, and one weight
   lower, so the value is what the eye lands on first. The size is unchanged at 11px and
   10px: the dominance came from the casing and the weight, and shrinking a readable
   label to fix a hierarchy problem trades one fault for another.

   Tracking came down with the caps. .06em existed to open uppercase up; on title case it
   only loosened the word. Colour stays var(--muted), which is quiet without being faint -
   dropping it further would make a 10px label hard work on a phone.

   ONE TREATMENT FOR BOTH CARDS. Temp and Rain share this rule; neither has a label style
   of its own. */
.wpc-stat-label {
    color: var(--muted);
    font-size: var(--wpc-type-stat-label);
    font-weight: 600;
    letter-spacing: .01em;

    /* .9 on the phone, and it is load-bearing for exactly one label: a two-line label
       measured 33.53 in a 33.25px cell at line-height 1. Desktop takes 1 - it has the
       room and single-line labels do not care. */
    line-height: var(--wpc-stat-label-lh, .9);
    text-align: center;
}

/* Same family as the rest of the card - it inherits, and nothing here changes it.
   Lighter and dimmer than the primary, and NO opacity: dimming text with opacity
   made it read as a different, thinner face rather than as the same face in a
   quieter colour, which is part of why the old treatment looked foreign. The colour
   is a token blend instead. */
/* The phone values are the reference and are left exactly as they were - tracking
   and line box included. */
.wpc-stat-value {
    color: color-mix(in srgb, var(--text) 84%, var(--muted));
    font-size: calc(var(--wpc-type-stat) * var(--wpc-fit, 1));
    font-weight: 560;
    letter-spacing: -.01em;
    line-height: .98;
    text-align: center;
    white-space: nowrap;
}

/* Desktop only. Normal tracking and a normal line box here, and nowhere else: the
   negative tracking was part of what made the figure read as a different treatment
   at the larger size. The primary is tightly tracked because it is a hero figure,
   and copying that at a supporting size just looks compressed. At the phone size the
   difference is a fraction of a pixel, so the phone keeps what it had rather than
   being changed for the sake of one rule. */
@container (min-width: 641px) {
    .wpc-stat-label { font-size: var(--wpc-type-stat-label-wide); }

    .wpc-stat-value {
        font-size: calc(var(--wpc-type-stat-wide) * var(--wpc-fit, 1));
        letter-spacing: normal;
        line-height: 1;
    }
}

/* The unit rides with the value at a slightly smaller size - subordinate to the
   figure it belongs to, but not a superscript. */
/* Smaller and lighter than the figure it belongs to, and on the same baseline -
   an inline span with no vertical-align, so the baseline is shared by default. */
/* THE UNIT IS THE MOST SUBORDINATE THING ON A CARD, and metric is what proved the old
   share was too generous. At .58em the budget had been set against narrow two-character
   imperial units; `mm` is materially wider than `in` and `mm/hr` than `in/hr`, and Rain's
   values are the widest in the system, so metric overflowed by 1-2px in three places
   while imperial fitted. .5em is the shared answer - a system-wide statement about what
   a unit is worth, not a metric rule and not a Rain rule.

   THE GAP IS A MARGIN, NOT A SPACE IN THE MARKUP. An `&nbsp;` renders at the VALUE's
   size, so the separator grew with the number it was separating - 3.6px beside a 13px
   figure, 5.9 beside a 21px one. As .3em of the unit it is about 2px wherever it
   appears, and it is one mechanism instead of two: Rate had already moved to a margin
   and Storm was still carrying a space. */
.wpc-stat-value .wpc-unit {
    color: inherit;

    /* THE SAME UNIT SIZE AS EVERY OTHER UNIT (2026-08-25). This was .5em, which made the
       unit a fixed fraction of whichever secondary it sat beside - so it inherited that
       value's size, and the minor step, and gave one role two more sizes. The margin stays
       em-based on purpose: a gap should scale with the glyphs it separates, and it now
       scales with the unit, which is one size. */
    font-size: var(--wpc-type-unit);
    font-weight: 500;
    margin-left: .24em;
    padding-top: 0;
}

/* ============================================================================
   6d  OVERFLOW FIT
   ============================================================================

   A SHARED CONTRACT FOR CONTENT WIDER THAN ITS REGION. Not a Rain rule - Rain is simply
   the first measurement to expose the need, because a metric rainfall total is the widest
   thing this template has to hold.

   Measured on the frozen template, mobile 1x2, before this existed. "Ink" is the painted
   right edge of the primary object against its column edge:

     0.86 in    2.37px inside its column     fine
     21.8 mm    0.39px PAST it               the approved normal state was already over
     12.86 in   2.53px past
     121.8 mm   5.30px past, and 5.75 past a 1x1 body
     129.0 mm   Storm, 2.48px past its cell

   So this is not only about hypothetical wide values: one approved normal render was
   already colliding by a third of a pixel, and legitimate wider totals are worse. A
   template that breaks on a real reading is not frozen, it is pending.

   HOW IT WORKS. Every primary and secondary value size is multiplied by --wpc-fit, which
   defaults to 1 - so with no fit class every size is exactly what it was and the approved
   renders are untouched to the pixel. Where a value is long, the card adds a shared fit
   class and the whole object scales together: value, unit and rail, because they are
   sized in em against the primary.

   WHAT IT IS NOT: true auto-fit. CSS cannot measure text against its container and scale
   to suit. Container queries give CELL-WIDTH awareness, not CONTENT awareness, so a
   min(size, Ncqw) cap would shrink "72" and "121.8 mm" by the same factor and break the
   normal case. Making a cell a container would also re-point the seven existing unnamed
   @container queries in this file at that cell and silently redefine the card
   breakpoints, which is off limits. So the content signal comes from the renderer, which
   formats the string and therefore already knows how long it is.

   THE BANDS ARE MEASURED. Characters across value plus unit, unit counted as rendered:

     <= 6   base            "72F" 4, "0.86in" 6, "21.8mm" 6
     7      is-fit-tight    x.9    "12.86in", "121.8mm", "129.0mm"
     >= 8   is-fit-wide     x.84   headroom for a longer unit or a four-digit total

   WITHDRAWN 2026-08-25, and its own stated limit is why. A character count is a proxy for
   width, not a measurement - "1234mm" is six characters and wider than "0.86in" - so this
   shrank type on a guess. Worse, it made one semantic role render at two sizes on the same
   dashboard: Lightning's stat value measured 20.16px against 22.4px on every other card,
   for no reason a reader could see. Type is one size per role now, and a reading that will
   not fit is a layout problem - widen the region, stack the unit, or drop the footprint
   from that content type.

   THE CLASSES ARE LEFT IN THE MARKUP HARMLESSLY. Nothing declares --wpc-fit any more, so
   every `var(--wpc-fit, 1)` below resolves to 1. `is-fit-tight` / `is-fit-wide` are
   retained rather than stripped out of a dozen frozen card bodies for no behavioural gain,
   and the fallback keeps the arithmetic honest if one is ever re-introduced. */

/* ============================================================================
   7  STATES
   ============================================================================

   One treatment each, defined once. Stale and unavailable are deliberately
   distinct: "we have a reading and it is old" is not "we have no reading", and
   collapsing them is how a dashboard shows a plausible wrong number.

   Existing product alert and severity semantics are preserved, not redefined. The
   severe ring below is the dashboard's own treatment. */

.wpc-card.is-stale .wpc-value {
    color: var(--muted);
}

.wpc-card.is-unavailable .wpc-value {
    color: var(--muted);
    font-weight: 700;
    letter-spacing: normal;
}

.wpc-card.is-warning {
    border-color: var(--warning);
}

.wpc-card.is-error {
    border-color: var(--danger);
}

.wpc-card.is-severe {
    border-color: var(--danger);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 16%, transparent);
}

.wpc-card.is-disabled {
    opacity: .55;
    pointer-events: none;
}

/* THE STATUS ROLE - text and a dot, and NEVER the dot alone. First pass 2026-08-23.

   The state classes now carry the product's OWN state names. `.is-delayed` was
   missing entirely and `.is-degraded` held the yellow that belongs to Delayed, which
   would have made a station with a silent sensor look identical to one running late.

   THE FOUR TONES, and why they are these four. There is no orange anywhere in the
   `--cx-` family, so the orange is the Temp palette's, which UV already established
   as the source for a severity ramp (SS9.7d.2). Everything else is a status token.

     Online     --cx-status-ok        green    #4ac978 dark / #2f9e56 light
     Delayed    --cx-alert-warning    yellow   #efc94d dark / #9a6700 light
     Degraded   --wpc-temp-warm       orange   #d28b3e both themes
     Offline    --cx-status-offline   red      #ef6e7f dark / #d4635a light

   NOT `--cx-status-degraded` FOR DELAYED, and this is the trap worth recording: that
   token is #efc94d in dark but #d28b3e in LIGHT - the same value as the orange - so
   Delayed and Degraded would render identically in the light theme while looking
   correct in the dark one. A token that changes hue between themes cannot sit next to
   a fixed one in the same scale.

   Size: the role was `.56rem`, about 9px, which is a caption. A status word IS this
   card's primary content, so it takes the shared secondary ladder instead. */
/* THE STATION'S STATE IS A LABEL, NOT A MEASUREMENT.
   It was `clamp(12.8px, .58em, 22.4px)` at weight 800, which on a desktop card rendered
   "Online" larger and heavier than some cards' actual readings - a subordinate fact
   presented as a primary one, and in a saturated green that made it the loudest thing on a
   quiet dashboard.

   NO NEW SIZE IS INTRODUCED. This adopts the existing `--wpc-type-stat-label` role and its
   weight - the same treatment Moon Phase gives its phase name - so the two subordinate
   labels on the dashboard now read as the same kind of thing. The SEMANTIC COLOUR STAYS:
   `.wpc-status.is-*` still carries Online / Delayed / Degraded / Offline, because the
   colour is the information and the size never was. Identical at every footprint, so small
   and medium agree. */
.wpc-status {
    font-size: var(--wpc-type-stat-label);
    font-weight: 600;
    letter-spacing: .01em;
}

/* THE SEVERITY ROLE - shared, added 2026-08-24, and named for SEVERITY rather than
   for a measurement or a state machine.

   `.wpc-status.is-*` above carries the station's own state names - Online, Delayed,
   Degraded, Offline - and they are the wrong words for anything else. A card that
   needs "quiet / worth watching / act now" was reaching for `.is-offline` to get red,
   which is how a vocabulary rots.

   COLOUR ONLY, and it sets `color`, so it works on text and on a `.wpc-icon` slot
   alike - a CaelowxIcon with no Tone is `currentColor`, so one class on the slot
   colours the glyph and the same class on the words colours them to match, with no
   second table anywhere. It carries no size, weight or spacing: those belong to
   whichever typography role the text already has.

   NEVER COLOUR ALONE. These are a second channel on top of words that already say the
   thing, which is the rule the status role established and this one inherits.

   LOAD-BEARING: THIS BLOCK'S POSITION IS ITS PRECEDENCE. Each of these is (0,1,0) - the
   same weight as `.wpc-note` and `.wpc-stat-value`, which also set `color` and are
   declared EARLIER in this file. They lose on source order alone, not on specificity, so
   MOVING THIS BLOCK ABOVE SECTION 6 WOULD SILENTLY STRIP THE COLOUR FROM EVERY CARD THAT
   USES IT - Lightning's severity states are the current case. If this ever has to move,
   compound the selectors (`.wpc-note.wpc-severity-danger`) rather than relying on where
   it sits. The same trap has been recorded four times elsewhere in this file; this is the
   fifth, and it is the one that had no comment. */
.wpc-severity-none { color: var(--muted); }
.wpc-severity-warning { color: var(--warning); }
.wpc-severity-danger { color: var(--danger); }

.wpc-status.is-ok { color: var(--cx-status-ok); }
.wpc-status.is-delayed { color: var(--cx-alert-warning); }
.wpc-status.is-degraded { color: var(--wpc-temp-warm); }
.wpc-status.is-offline { color: var(--cx-status-offline); }
.wpc-status.is-unknown { color: var(--cx-status-unknown); }

/* THE ROUND INDICATOR - a shared role, ported to the card system's own tokens.
   The live dashboard's `.status-dot` (app.css) is the visual precedent: a circle with
   a soft halo. It is NOT reused directly, because it keys on `--success-indicator`,
   `--warning-indicator` and `--danger-indicator` - a second palette holding the same
   hexes as `--cx-status-*` (open item 21 is the same duplication for Rain).

   The dot is the fast cue and the text is the meaning. Neither ships without the
   other: `aria-hidden` on the dot, because the word beside it already says it. */
.wpc-status-dot {
    background: var(--cx-status-unknown);
    border-radius: 50%;
    display: inline-block;
    flex: none;
    height: clamp(7px, .42em, 11px);
    width: clamp(7px, .42em, 11px);
}

.wpc-status-dot.is-ok {
    background: var(--cx-status-ok);
    box-shadow: 0 0 0 .18em color-mix(in srgb, var(--cx-status-ok) 16%, transparent);
}

.wpc-status-dot.is-delayed {
    background: var(--cx-alert-warning);
    box-shadow: 0 0 0 .18em color-mix(in srgb, var(--cx-alert-warning) 16%, transparent);
}

.wpc-status-dot.is-degraded {
    background: var(--wpc-temp-warm);
    box-shadow: 0 0 0 .18em color-mix(in srgb, var(--wpc-temp-warm) 16%, transparent);
}

.wpc-status-dot.is-offline {
    background: var(--cx-status-offline);
    box-shadow: 0 0 0 .18em color-mix(in srgb, var(--cx-status-offline) 16%, transparent);
}

/* THE LEAD STATUS STACK - a reusable role. Added 2026-08-23.

   Indicator above, word beneath, both centred. Any card whose primary IS a state
   rather than a number can use it; nothing here names a measurement.

   WHY A STACK RATHER THAN A ROW. Inline, the dot and the word competed for one line
   and `Degraded` - the longest state - overran a phone 1x1 by 11.48px. Stacking gives
   the word the full cell width and makes the indicator the card's graphic rather than
   a bullet in front of text.

   COLOUR LIVES ON THE INDICATOR ONLY. The word stays neutral, which is the rule UV
   follows too: the graphic carries the tone, the text carries the meaning (9.7d.2).
   The `.wpc-status.is-*` colour classes above remain available for a surface that
   wants coloured status TEXT; this card deliberately does not use them.

   Sized in `em`, so it tracks the card's own scale across breakpoints instead of
   needing a second set of numbers. The indicator is deliberately larger than the word
   it labels, and capped so it stays an indicator rather than becoming the card. */
.wpc-status-lead {
    align-items: center;
    display: flex;
    flex-direction: column;

    /* THE STACK SETS THE SCALE, and it has to. Both children size in `em`, and with no
       font-size of its own the stack inherited the root - so the word and the indicator
       both collapsed onto their clamp FLOORS and stopped varying by breakpoint at all
       (measured 12.8px and 14px on desktop AND phone). Keying on the shared secondary
       token restores the container-query behaviour every other role has. */
    /* MOVED DOWN A ROLE, 2026-08-28. The stack still sets the scale for the whole group -
       that mechanism is unchanged and the comment above still explains why. What changed is
       WHICH shared role it keys on: `--wpc-type-stat` / `--wpc-type-stat-wide` are the
       roles a card's secondary READINGS take, and the station's state is not a reading. At
       stat-wide the word rendered 17.92px and the indicator 1.25em of it, so "Online" was
       larger than some cards' actual measurements and the green dot was the loudest ink on
       a quiet dashboard.

       Keying on the LABEL role puts the word at Moon Phase's phase name - the same kind of
       subordinate fact, now the same size - and, because the indicator is `em`, it shrinks
       with the word instead of needing a second rule. No new size is introduced and no
       card names one. */
    font-size: var(--wpc-type-stat-label);
    gap: .34em;
    justify-content: center;
    min-width: 0;
}

@container (min-width: 641px) {
    .wpc-status-lead {
        font-size: var(--wpc-type-stat-label-wide);
    }
}

/* The word takes the stack's scale exactly; the indicator is a quarter larger, which
   is what makes it read as the graphic rather than as a bullet. */
.wpc-status-lead .wpc-status {
    font-size: 1em;
}

.wpc-status-dot.is-lead {
    height: 1.25em;
    width: 1.25em;
}

/* Loading is a skeleton at the value's own metrics, so resolving it causes no
   layout shift. */
.wpc-skeleton {
    background: var(--surface-2);
    border-radius: var(--wpc-radius);
    color: transparent;
    display: inline-block;
    min-height: 1em;
    min-width: 3ch;
}

@media (prefers-reduced-motion: no-preference) {
    .wpc-card.is-loading .wpc-skeleton {
        animation: wpc-pulse 1.6s ease-in-out infinite;
    }
}

@keyframes wpc-pulse {
    50% { opacity: .55; }
}

/* --- interaction ----------------------------------------------------------
   Neither of these exists on the dashboard today: it has no card hover and no
   card focus rule at all, despite cards being drag-reorderable. Border-only on
   hover, because a lift or a shadow would break the flat visual language. */

.wpc-card.is-interactive {
    cursor: pointer;
}

.wpc-card.is-interactive:hover {
    border-color: color-mix(in srgb, var(--accent) 40%, var(--border));
}

.wpc-card.is-interactive:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* ============================================================================
   8  TREND - PRESENTATION ONLY
   ============================================================================

   This section says how a trend LOOKS. It does not say how a trend is CALCULATED,
   and no card may calculate one.

   The calculation contract's WINDOW is approved - the last 5 received values,
   read as a series rather than as a newest-versus-previous pair, roughly five
   minutes at the normal station cadence - and it belongs to backend/domain logic.
   What does not exist yet is the implementation, and the per-measurement deadbands
   without which a wobble in the last digit reads as a trend. Those thresholds are
   deliberately unspecified: docs/Card_System_Design.md section 13.1, dependency
   D1, Work_Order item 12.

   So every trend rendered in the Card Lab is a presentation sample, never a
   computed value. Four states only: Rising, Falling, Steady, Unavailable.

   Colour is never the only carrier: every trend renders an icon, a direction and
   text. The default tone is muted because rising is not inherently good. */

/* TREND LIVES IN .wpc-meta. FULL STOP.
   ------------------------------------
   APPROVED 2026-08-22. The desktop verification found the same trend in two
   different places in the lab's own examples - inside .wpc-body on a 2x2 and in
   .wpc-meta on a 1x2 - which is exactly the drift a shared anatomy exists to
   prevent. .wpc-meta is the trend's home at every footprint. Only a documented
   specialty card may override it, and none does today.

   Muted and tertiary on purpose: it sits under the value it describes and must not
   compete with it. */
.wpc-trend {
    align-items: center;
    color: var(--muted);
    display: inline-flex;
    font-size: var(--wpc-type-trend);
    font-weight: 800;
    gap: .25rem;
    line-height: 1.1;
}

/* .wpc-aside carries secondary VALUES only, never a trend. One home means a reader
   always looks in the same place, and it removes the judgement call about which
   region a trend belongs to on a wide-compact card. */
.wpc-meta .wpc-trend {
    margin-top: .25rem;
}

.wpc-trend-glyph {
    display: inline-flex;
    flex: none;
}

/* Direction comes from the glyph itself - ChevronUp / ChevronDown / ChevronRight -
   rather than from rotating one arrow. Rotating a chevron would point it at 45
   degrees, and the icon set already carries all three directions. */

/* ============================================================================
   9  EXPAND AFFORDANCE
   ============================================================================

   A small centred handle on the bottom edge. The handle only indicates - the
   whole non-interactive card surface is the hit area, and interactive children
   consume their own clicks. Absent entirely on a card with no detail view. */

.wpc-card.is-expandable {
    padding-bottom: calc(var(--wpc-pad) + .375rem);
}

.wpc-handle {
    background: var(--border);
    border-radius: var(--cx-radius-pill, 999px);
    bottom: .25rem;
    height: 3px;
    left: 50%;
    position: absolute;
    transform: translateX(-50%);
    width: 24px;
}

.wpc-card.is-expandable:hover .wpc-handle,
.wpc-card.is-expandable:focus-visible .wpc-handle {
    background: var(--accent);
}

/* ============================================================================
   10  SPECIALTY HOOKS
   ============================================================================

   A specialty card opts out of named parts of the ordinary anatomy, and the
   exception is declared rather than assumed. Specialty does NOT mean arbitrary
   CSS: radius, surface, border, focus treatment and page-overflow safety stay. */

/* Headerless - the content makes the purpose obvious and the header would waste
   the card's most valuable area. Radar, maps, timelines. */
.wpc-card.is-headerless > .wpc-header {
    display: none;
}

/* A BODY THAT DIVIDES HORIZONTALLY - a shared anatomy. Added 2026-08-23.

   Every other split in the system divides into COLUMNS: `.wpc-body-split` and the
   `is-anatomy-*` modifiers all cut a card left-to-right, and every divider is a
   vertical line. A card whose content stacks - one reading above another, with a rule
   between them - could not be expressed at all.

   Date & Time is the case that needed it, translating the live Current Conditions
   composition into the card system. Nothing here names that card: any card with two
   stacked regions can use it.

   THE BODY CLAIMS THE INTERIOR, exactly as `.wpc-body` does. This was the fault behind
   four failed spacing passes. The card is a column flexbox and `.wpc-body` declares
   `flex: 1 1 auto`, so it stretches; this declared nothing and defaulted to
   `flex: 0 1 auto` - sized to its own content. On a titleless 1x1 that left the body
   73.44px inside a 104px interior, with 30.5px of the card unused, and every attempt to
   open space around the rule was competing for room that was already there and
   unclaimed. Nothing was reserving a header; the body never asked for the space.

   TWO EQUAL REGIONS, AND THE RULE IS THE BOUNDARY. The body divides exactly in half and
   the separator sits on the join - structural, never positioned from content. Content
   sized rows with `align-content: space-between` were tried instead and are NOT this:
   they distribute slack, so the boundary drifts with content and the regions stop being
   equal.

   ONE FIGURE AXIS FOR BOTH REGIONS - a shared START EDGE, not a shared COLUMN. Centring
   each region independently put the two figures on different left edges, `23` at
   x=22.28 against `10:30` at x=9.42. The obvious fix - one grid with the figures in a
   shared column, as the peer pair does - was built with subgrid and measured, and it
   costs width: a shared second column must start after the WIDEST figure, so
   `Sunday / August` was pushed right by the difference between `10:30` and `23` and the
   content ran 9px past the body. Aligning both regions to the same start edge gives the
   identical axis for free, because each subordinate group follows its OWN figure. The
   composition reads left-aligned, as the Current Conditions reference does.

   The gap and the rule position are custom properties with defaults, as
   `--wpc-split-divider` is for the vertical case. The gap DEFAULTS TO ZERO: a body whose
   rows already fill their tracks has nothing to give, and a .34em gap grew a 1x1 from
   120.00 to 123.88 and from 86.25 to 89.78. */
.wpc-body-rows {
    display: grid;
    flex: 1 1 auto;
    grid-template-rows: var(--wpc-row-split, 1fr 1fr);
    min-height: 0;
    min-width: 0;
    position: relative;
    row-gap: var(--wpc-row-gap, 0);
}

.wpc-body-rows > * {
    align-items: center;
    display: flex;
    justify-content: flex-start;
    min-height: 0;
    min-width: 0;
}

/* One boundary, one line - and it spans the body rather than the card, so it starts
   and ends where the content does. */
.wpc-body-rows::before {
    background: var(--border);
    content: "";
    height: 1px;
    left: 0;
    position: absolute;
    right: 0;
    top: var(--wpc-row-divider, 50%);
}

/* LEADING IS THE COST OF STACKING, so the stacked body pays it back. A value's normal
   line box carries leading above and below the glyphs that a single centred row
   absorbs; stack two and it is counted twice. Tightening the line box recovers it
   WITHOUT touching the shared type scale - the figures are the same size, they simply
   stop reserving space they never used. Only inside `.wpc-body-rows`.

   FOUR CLASSES, the same specificity story the trend mark records: `.wpc-c1.wpc-r1
   .wpc-value` weighs (0,3,0) and sets .95, so a two-class rule here lost and the
   leading never changed while the CSS looked correct.

   TRIED AND REVERTED: leaning the rows away from the rule with a smaller box and
   first/last alignment. A smaller box means the ink overhangs it further, so the ink
   crowded the rule instead of clearing it. */
.wpc-card .wpc-body-rows .wpc-primary .wpc-value {
    line-height: .82;
}

/* EASING THE FIGURES TO BUY VERTICAL ROOM. Added 2026-08-24.

   A titled 1x1 with two full-scale figures stacked has no slack at all: two 44.8px
   values at the stacked line-height measure 73.48px in a 73.44px body, so the rule
   ends up with 0.39px of air either side and the composition reads cramped. Date &
   Time solved that by dropping its title; a card that wants to KEEP its title needs
   the other lever.

   ONE MULTIPLIER, NOT A NEW SCALE. `.89em` of the primary's own size eases the figure
   by about 11% - 44.8px to 39.9px on the desktop, 24px to 21.4px on the phone - from a
   single declaration, so the two breakpoints stay in proportion without a second
   number. It is deliberately small: the figure still reads as the card's primary and
   the difference is hard to see without measuring.

   The freed height is then spent on the rule, not reclaimed by the card: the row gap
   opens to .38em and the footprint does not move.

   THIS IS NOT A GLOBAL TYPE STEP. It is an opt-in for a stacked body that keeps its
   title, named for what it does rather than for the card that needed it. */
.wpc-body-rows.is-figure-eased {
    --wpc-row-gap: .38em;
}

/* The .89em value shrink that used to sit here was removed 2026-08-25. It was the last
   anatomy-keyed type exception, and it made Sun Rise/Set's primary measure 37.88px against
   42.56px on every other card. The row-gap above is the whole of this anatomy now. */

/* THE LUNAR PHASE DISC - one drawing, every phase. Added 2026-08-24.

   ONE COMPONENT, NOT EIGHT PICTURES. The phase is geometry: a lit disc, and a
   terminator that is an ellipse whose width is the cosine of the phase angle. Three
   custom properties express the whole family - which side is lit, how wide the
   terminator is, and whether it is painting shadow onto a lit disc (crescent) or
   light onto a dark one (gibbous). The eight state classes below set those three
   values and nothing else, so there is no per-phase artwork to keep in sync and no
   inline SVG anywhere.

   DRIVEN BY THE SERVER'S PHASE, NEVER RECOMPUTED HERE. `MoonSummary` already carries
   `PhaseName` and `PhaseAngleDegrees`, derived on the server from AstronomyEngine; a
   caller maps the authoritative name to a state class. The card computes nothing.

   WHY STATES RATHER THAN A CONTINUOUS ANGLE. A continuous terminator wants the angle
   as a custom property on the element, and an inline `style` attribute is exactly what
   `TheProductionPreviewIsIsolatedAndCarriesNoLabChrome` forbids - it permits a
   rotation and nothing else. The eight established states are what the phase name
   already expresses, so this stays inside the guard rather than widening it.

   RESTRAINED ON PURPOSE: a plain disc, a thin edge so a new moon is still a moon
   rather than a hole, and FIXED lunar material - see `--wpc-lunar-*` on `.wpc-card` -
   so the object does not change substance when the reader changes theme. No glow, no
   gradient, no texture, no yellow. */
.wpc-moon {
    /* OUTLINE AND FILL, the way a system icon is built - not a grey disc with a
       lighter disc on top. The ring is drawn on every state at one weight, the
       ILLUMINATED area is solid `currentColor`, and the unlit area is simply the
       card's own surface showing through. So a new moon is an empty circle, a full
       moon is a filled one, and the six between them are the same circle with the
       terminator moved. That is what makes the eight read as one family rather than
       eight drawings.

       The unlit area is a real SHADOW colour, not the card surface. Painting it with
       `var(--surface)` made the dark half invisible against the card, so a crescent
       read as a bite taken out of a disc rather than as a moon. A moon is always a
       whole circle; only the lighting changes. */
    --wpc-moon-lit: var(--wpc-lunar-lit);
    --wpc-moon-shadow: var(--wpc-lunar-shadow);

    /* Defaults describe a new moon; every state class below overrides them. */
    --wpc-moon-lit-side: 50%;
    --wpc-moon-terminator: 100%;
    --wpc-moon-terminator-fill: var(--wpc-moon-shadow);

    background: var(--wpc-moon-shadow);
    block-size: var(--wpc-moon-size, 16px);
    border: var(--wpc-moon-stroke, 1.5px) solid var(--wpc-lunar-rim);
    border-radius: 50%;
    box-sizing: border-box;
    display: block;
    flex: none;
    inline-size: var(--wpc-moon-size, 16px);
    overflow: hidden;
    position: relative;
}

@container (min-width: 641px) {
    .wpc-moon {
        --wpc-moon-size: 40px;
        --wpc-moon-stroke: 2px;
    }
}

/* The lit half - right for a waxing moon, left for a waning one. */
.wpc-moon::before {
    background: var(--wpc-moon-lit);
    content: "";
    inset-block: 0;
    inset-inline-start: var(--wpc-moon-lit-side);
    inline-size: 50%;
    position: absolute;
}

/* The terminator. Its width is what makes a crescent a crescent, and its fill is what
   makes a gibbous the same shape inverted. */
.wpc-moon::after {
    background: var(--wpc-moon-terminator-fill);
    border-radius: 50%;
    content: "";
    inset-block: 0;
    inline-size: var(--wpc-moon-terminator);
    inset-inline-start: 50%;
    position: absolute;
    transform: translateX(-50%);
}

/* The eight established states. Each sets three values; none draws anything new. */
.wpc-moon.is-new {
    --wpc-moon-terminator: 100%;
    --wpc-moon-terminator-fill: var(--wpc-moon-shadow);
}

.wpc-moon.is-waxing-crescent {
    --wpc-moon-lit-side: 50%;
    --wpc-moon-terminator: 62%;
    --wpc-moon-terminator-fill: var(--wpc-moon-shadow);
}

.wpc-moon.is-first-quarter {
    --wpc-moon-lit-side: 50%;
    --wpc-moon-terminator: 0%;
}

.wpc-moon.is-waxing-gibbous {
    --wpc-moon-lit-side: 50%;
    --wpc-moon-terminator: 62%;
    --wpc-moon-terminator-fill: var(--wpc-moon-lit);
}

.wpc-moon.is-full {
    --wpc-moon-lit-side: 0;
    --wpc-moon-terminator: 100%;
    --wpc-moon-terminator-fill: var(--wpc-moon-lit);
}

.wpc-moon.is-waning-gibbous {
    --wpc-moon-lit-side: 0;
    --wpc-moon-terminator: 62%;
    --wpc-moon-terminator-fill: var(--wpc-moon-lit);
}

.wpc-moon.is-last-quarter {
    --wpc-moon-lit-side: 0;
    --wpc-moon-terminator: 0%;
}

.wpc-moon.is-waning-crescent {
    --wpc-moon-lit-side: 0;
    --wpc-moon-terminator: 62%;
    --wpc-moon-terminator-fill: var(--wpc-moon-shadow);
}

/* A media region - a map or image - clipped to the card's own shape. This is the
   one legitimate use of overflow: hidden here: it clips decoration to a radius,
   it does not conceal text to protect a row height. */
/* IT CONTAINS ITS OWN STACKING CONTEXT, and that is load-bearing rather than tidy. A
   media region is where a THIRD-PARTY widget lands, and such a widget brings its own
   z-index scale: Leaflet's panes sit at 400 and its controls at 800, against a shared
   chrome scale whose top is `--cx-z-chrome: 100`. Without `isolation` those numbers are
   compared directly, and a map's zoom buttons outrank the application's own chrome -
   the edit overlay drawn over the card included. Isolating here makes the region's
   internal scale internal, so a widget may order itself however it likes and still lose
   to anything outside the card. The legacy `.radar-map-shell` carries the same rule for
   the same reason; this is where it belongs in the card system. */
.wpc-media {
    border-radius: calc(var(--wpc-radius) - 1px);
    flex: 1 1 auto;
    isolation: isolate;
    min-height: 0;
    overflow: hidden;
    position: relative;
}

.wpc-card.is-headerless {
    padding: 0;
}

.wpc-card.is-headerless > .wpc-media {
    border-radius: inherit;
}

/* ============================================================================
   A HORIZONTAL CONTENT RAIL - shared, added 2026-08-25.
   ============================================================================

   A row of repeated cells that is longer than its card, scrolled sideways. It is the
   shape a forecast strip needs, and nothing about it is a forecast: any card with more
   comparable cells than width may use it.

   THE SCROLLBAR IS HIDDEN, AND THE SCROLLING IS NOT. The legacy strip styles a visible
   11px bar under a comment reading "make the scrollbar clearly visible"; the card system
   takes the opposite decision, because a card is a small surface and a permanent bar
   spends its height on chrome. What replaces it is the EDGE CHEVRONS below - an
   affordance that appears only when there is somewhere to go, and that a keyboard can
   reach, which a scrollbar never was here.

   Snapping is `proximity`, not `mandatory`: a reader flicking through a day of hours
   should be helped to land on a cell, not fought when they want to stop between two. */
.wpc-rail-host {
    display: flex;
    flex: 1 1 auto;
    min-height: 0;
    min-width: 0;
    position: relative;
}

.wpc-rail {
    -webkit-overflow-scrolling: touch;
    display: flex;
    flex: 1 1 auto;
    gap: var(--wpc-rail-gap, 9px);
    min-width: 0;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-behavior: smooth;
    scroll-snap-type: x proximity;

    /* Hidden, not absent. The content still scrolls by touch, wheel, drag and chevron. */
    scrollbar-width: none;
}

.wpc-rail::-webkit-scrollbar {
    height: 0;
    width: 0;
}

/* A cell in the rail. Fixed width on purpose: a fluid cell would stretch to fill the
   card and destroy the one signal that says "there is more this way". */
.wpc-tile {
    align-items: center;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 7px;
    box-sizing: border-box;
    display: flex;
    flex: 0 0 var(--wpc-tile-width, 150px);
    flex-direction: column;
    min-width: 0;
    overflow: hidden;
    padding: var(--wpc-tile-pad, 8px 6px);
    scroll-snap-align: start;
    text-align: center;
}

/* THE EDGE AFFORDANCE - a real button, overlaid, and present only when it has something
   to do.

   Overlaid rather than laid out, so it costs the rail no width: a chevron that occupied
   a column would take content space on every card at every size, including the ones with
   nothing to scroll. `is-visible` is set by the card's own scroll observer; without it
   the button is neither shown nor reachable, so a keyboard user never tabs to a control
   that would do nothing.

   `--wpc-rail-nav-fade` paints a short gradient under the chevron so a cell scrolling
   beneath it does not collide with the glyph. */
.wpc-rail-nav {
    align-items: center;
    background: none;
    border: 0;
    bottom: 0;
    color: var(--text);
    cursor: pointer;
    display: none;
    justify-content: center;
    padding: 0;
    position: absolute;
    top: 0;
    width: var(--wpc-rail-nav-width, 34px);
    z-index: 2;
}

.wpc-rail-nav.is-visible {
    display: flex;
}

.wpc-rail-nav.is-start {
    left: 0;
}

.wpc-rail-nav.is-end {
    right: 0;
}

.wpc-rail-nav::before {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 50%;
    box-shadow: 0 2px 8px color-mix(in srgb, var(--bg) 55%, transparent);
    content: "";
    height: 26px;
    position: absolute;
    width: 26px;
}

.wpc-rail-nav > * {
    position: relative;
}

.wpc-rail-nav:hover::before,
.wpc-rail-nav:focus-visible::before {
    border-color: var(--accent);
}

/* Internal horizontal scrolling. The OUTER card always fits the page; the inner
   track may be wider and the reader scrolls inside it. Page-level horizontal
   scrolling is prohibited without exception, which is what min-width: 0 upstream
   and max-width: 100% here are for. */
.wpc-scroll-x {
    max-width: 100%;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x proximity;

    /* A fade on the trailing edge, because an edge fade is the only cue that
       survives on touch - a scrollbar is usually invisible there. Paired with
       scroll snapping so the content lands somewhere sensible. */
    mask-image: linear-gradient(to right, #000 calc(100% - 1.5rem), transparent);
}

.wpc-scroll-x > * {
    scroll-snap-align: start;
}

/* An alert/banner surface. APPROVED 2026-08-22: alert surfaces sit OUTSIDE the
   rows x columns contract - the existing 60px dynamicAlerts band is not forced
   into the matrix and is not a reason to distort card geometry. This hook exists
   so a future alert surface has somewhere defined to live; nothing uses it yet. */
.wpc-banner {
    align-items: center;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--wpc-radius);
    display: flex;
    gap: var(--wpc-header-gap);
    padding: var(--wpc-pad-1x1) var(--wpc-pad);
}

/* ============================================================================
   10a  EDIT MODE
   ============================================================================

   EDIT MODE IS A LAYER ABOVE THE CARD. That is the whole rule, and everything below
   follows from it:

       EDIT CHROME NEVER CAUSES NORMAL CARD CONTENT TO REFLOW.

   The card underneath keeps its exact normal-mode coordinates. Nothing is moved,
   nothing is hidden, nothing is truncated, no space is reserved. If the chrome lands
   on top of the icon, it covers the icon - it does not push it aside. An earlier
   version of this section reserved 112px on the title row, which slid the icon left
   and squeezed or hid the title; that was the wrong shape of solution and is gone.

   SURFACES BELONG TO THE CONTROLS, NOT TO A SHEET. The container paints nothing, so
   the card is visibly present AROUND the chrome rather than behind it, and each control
   is an opaque chip on top of it. Translucent controls were built and rejected on
   review: card content reading through a button is not what a button should do.
   `opacity` is never used on a control either - it would fade the text, the drag dots,
   the size labels and the focus ring along with the surface.

   ONE SYSTEM, TWO ARRANGEMENTS. Surface, border, control styling, selected state,
   drag-handle language and accessible labels are identical everywhere. What changes is
   the arrangement, and the choice between them is arithmetic, not taste:

     THE TOOLBAR, a horizontal cluster overlaying the top-right corner. Used where it
     can coexist with the title without touching it. It needs
     11 pad + 61.23 title + 8 gap + 111.05 cluster + 6 offset = 197.28px of card.

     THE COMPACT ARRANGEMENT, controls spread across the footprint, shown only while
     the card is selected. Used everywhere the toolbar does not fit, so that the
     alternative is never "move the card's content out of the way". It CARRIES THE
     CARD'S TITLE, because controls spread over a card need to say which card, and the
     card's own title is only partly visible behind them.

   Which footprint gets which is expressed as CUSTOM PROPERTIES rather than as two
   copies of the same rule block. There is one rule that draws the chrome; a footprint
   only changes the variables it reads. That is also why the child placement rules
   below are unconditional - `grid-area` and `grid-row` are simply ignored by a flex
   toolbar, and the one thing that genuinely differs, whether the size picker is its own
   flex row or dissolves into the panel grid, is itself a variable.

   BY FOOTPRINT, at the time of writing:

     desktop/tablet 1x2 and wider   toolbar          294.75 >= 197.28
     desktop 1x1                    compact overlay  142.88
     mobile 1x1                      compact overlay   86.25
     mobile 1x2 / 2x2                compact overlay  181.5
     mobile 2x4 / 4x4                toolbar          372 >= 197.28

   A card is not a CSS container - `container-type` lives on `.wpc-grid-host`, and
   putting it on `.wpc-card` would re-point every unnamed `@container (min-width: 641px)`
   query in this file at the card and take the frozen typography with it. So the
   arrangement is selected by footprint class plus breakpoint, with the arithmetic
   written down, rather than by asking the card how wide it is. */

/* THE EDIT SURFACE, and where it is NOT.
   One set of values, inherited by every control, so the translucency concept cannot
   drift between footprints. Theme-aware throughout: no literal translucent dark, so
   this works in the light theme as well as the dark one.

   THE GLASS IS ON THE CONTROLS, NOT ON A PANEL. Two earlier passes painted the whole
   selected footprint at 82% and then 68% and asked why it still read as a lid. The
   arithmetic says why: a fill of var(--surface) over a card that IS var(--surface)
   composites to the card's own colour at every alpha. Lowering alpha only made the
   card's CONTENT ghost through more; it could never make the panel look like anything
   other than a repainted card, because on empty card it was one.

   So the container paints nothing at all now, and each control carries its own glass.
   That is also what makes the light theme work: a translucent white control on a white
   card is invisible for exactly the same reason, whatever its alpha.

   THE CONTROLS THEMSELVES ARE OPAQUE, reverted 2026-08-23 after a translucent version
   was reviewed and rejected: card content reading through a button is not what a button
   should do. What survived from that pass is where the surfaces live - on the controls,
   not on a sheet over the card - which is why the container still paints nothing and the
   card is still visibly present around them.

   A control surface CANNOT simply be var(--surface): that is the card's own colour, so
   an opaque control painted in it is invisible against an empty card, in either theme.
   It is mixed OFF the card instead - var(--surface) nudged 18% toward var(--text) - and
   that direction is theme-correct rather than a hard-coded lift: in the dark theme
   --text is near-white so a control reads as a lighter chip on a dark card; in the light
   theme --text is near-black so the same expression reads as a grey chip on a white
   card. Measured luminance 0.275 against a 0.126 card in dark, 0.843 against 1.0 in
   light. One expression, both themes, nothing showing through. */
.wpc-grid.is-editing {
    --wpc-edit-control: color-mix(in srgb, var(--surface) 82%, var(--text));
    --wpc-edit-selected: var(--accent);
    --wpc-edit-id-surface: var(--surface);
    --wpc-edit-border: color-mix(in srgb, var(--accent) 42%, var(--border));
    --wpc-edit-ink: color-mix(in srgb, var(--text) 82%, var(--muted));
}

.wpc-grid.is-editing .wpc-card {
    outline: 1px dashed color-mix(in srgb, var(--accent) 52%, transparent);
    outline-offset: -3px;
}

/* THE SELECTED CARD. Solid where an unselected editing card is dashed - same colour,
   same width, same offset, so selection reads without adding a second visual idea. */
.wpc-grid.is-editing .wpc-card.is-selected {
    outline-color: var(--accent);
    outline-style: solid;
}

/* THE CHROME. One rule, two arrangements, no duplicated declaration block. The
   defaults here are the toolbar; the compact overlay is a variable set further down.

   IT PAINTS NOTHING IN THE TOOLBAR ARRANGEMENT. There the element is for position,
   stacking and layout only, every visible surface belongs to a control, and the card
   stays present around the chrome instead of behind it.

   THE COMPACT PANEL IS THE EXCEPTION, and it is solid. A panel spread across a card
   leaves gaps between its controls, and through those gaps came the trend chevron, half
   a numeral and pieces of the title - the card was legible as fragments rather than as a
   card, which is worse than either extreme. So the compact arrangement fills its
   footprint with var(--surface): opaque, theme-aware by construction, and the card's own
   colour, so there is no seam to notice. */
.wpc-edit-controls {
    /* `center` keeps the toolbar's buttons at their own 22px in a 30px bar. The
       overlay flips this to `stretch`, and it has to: with items centred, a hide
       button in a 34.6px panel cell stayed 22 x 15 - a smaller target than the
       toolbar it replaced, which is the opposite of the point. */
    align-items: var(--wpc-edit-items, center);
    background: var(--wpc-edit-panel, transparent);
    border: 0;
    border-radius: var(--wpc-edit-corner, 0);
    bottom: var(--wpc-edit-bottom, auto);
    display: var(--wpc-edit-display, flex);
    gap: var(--wpc-edit-gap, 4px);
    grid-template-areas: var(--wpc-edit-areas, none);
    grid-template-columns: var(--wpc-edit-cols, none);
    grid-template-rows: var(--wpc-edit-rows, none);
    left: var(--wpc-edit-left, auto);
    padding: var(--wpc-edit-pad, 3px);
    place-content: var(--wpc-edit-place, normal);
    position: absolute;
    right: var(--wpc-edit-right, 6px);
    top: var(--wpc-edit-top, 6px);

    /* Above the card's own content, which it is allowed to cover and never allowed to
       move. The icon in particular stays exactly where normal mode put it. */
    z-index: 30;
}

.wpc-drag-handle,
.wpc-hide-card,
.wpc-size-picker button {
    align-items: center;

    /* EACH CONTROL IS ITS OWN CHIP: an opaque surface plus an accent-tinted edge. The
       edge is what places it on the card; the fill is what stops the card coming
       through it. box-sizing is border-box app-wide, so the edge costs no layout. */
    background: var(--wpc-edit-control);
    border: 1px solid var(--wpc-edit-border);
    border-radius: 6px;

    /* Full strength, never faded. A translucent surface lowers effective contrast, and
       the point of not using `opacity` is that the ink does not get quieter - so the
       ink is stated here rather than inherited from a grey. */
    color: var(--wpc-edit-ink);
    display: inline-flex;
    font-size: var(--wpc-edit-font, .56rem);
    height: var(--wpc-edit-btn-h, 22px);
    justify-content: center;
    min-width: 22px;
    padding: 0 5px;
}

/* CARD IDENTITY, and the reason the panel exists to carry it: a panel covering a card
   takes away the one thing that told you which card you were editing. The faint ghost
   of the title through an 82% surface is not identity - it is a smudge.

   The real display title, never a generic "Card" or "Selected card". Panel only: the
   toolbar leaves the card's own title visible, so repeating it there would be noise.

   Ellipsis is the last resort it is meant to be. It has 128.88px on a desktop 1x1,
   83.8px on a mobile 1x2 and 72.25px on a mobile 1x1, against roughly 70px for
   "Temp - Outdoor" - so it fits at every footprint that uses the panel today, and a
   longer title degrades rather than breaking the layout. */
.wpc-edit-title {
    /* A BLOCK, not a flex box, centred by the grid. `display: flex` centred it just as
       well but turned the text into an anonymous flex item, where text-overflow has
       nothing to ellipsise - so an over-long title would have been cut mid-glyph
       instead of degrading. */
    align-items: center;

    /* IT FILLS ITS ROW rather than hugging its text, and that is a correctness fix, not
       a styling preference. Sized to its text it left the card's own title poking out
       around it - 4.7px to the right on a 1x1, 1.6px below on a 1x2 - which reads as a
       glitch rather than as a card showing through. Filling the row means the card's
       title is either fully behind the identity or fully behind the glass controls, and
       never half of each. */
    align-self: stretch;

    /* A MASK. The panel's first row IS the card's header row, so anything less than
       opaque left two near-identical words superimposed. Painted in the card's own
       colour, so it has no visible seam - it reads as the identity replacing the card's
       title for the duration, which is exactly what it is. It backs one row, not the
       card. */
    background: var(--wpc-edit-id-surface);
    border-radius: 5px;
    color: var(--text);
    display: var(--wpc-edit-id, none);
    font-size: var(--wpc-edit-id-size, .56rem);
    font-weight: 700;
    grid-area: id;
    justify-self: stretch;
    line-height: 1;
    min-width: 0;
    padding: var(--wpc-edit-id-pad, 0 5px);
}

/* The inner span exists so the ellipsis works. text-overflow needs a block that
   overflows; on the flex parent the text is an anonymous flex item and there is nothing
   for it to act on, which would cut a long title mid-glyph instead of degrading. */
.wpc-edit-title > span {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.wpc-drag-handle {
    align-content: var(--wpc-edit-dots, normal);
    cursor: grab;
    display: grid;
    gap: 2px;
    grid-area: drag;
    grid-template-columns: repeat(2, 3px);
    padding: 5px 7px;
    touch-action: none;
}

.wpc-drag-handle span {
    background: currentColor;
    border-radius: 50%;
    height: 3px;
    width: 3px;
}

.wpc-drag-handle:active {
    cursor: grabbing;
}

.wpc-hide-card {
    grid-area: hide;
}

/* The picker stays a flex row in both arrangements and takes the panel's bottom row as
   one area. An earlier version dissolved it with `display: contents` so its buttons
   became panel cells directly, which worked only because Temperature happens to
   support exactly two sizes; a third would have had nowhere to go. This way N sizes
   share the row. */
.wpc-size-picker {
    display: flex;
    gap: 2px;
    grid-area: size;
    justify-content: var(--wpc-edit-sizes-align, flex-start);
}

/* `0 1 4rem` in the panel: a 64px basis that shrinks but never grows, so the sizes read
   as a segmented control rather than as two buttons stretched across the card. The
   toolbar keeps content-width buttons. */
.wpc-size-picker button {
    flex: var(--wpc-edit-size-flex, 0 1 auto);
}

/* THE SELECTED FOOTPRINT is solid accent, and it differs from its neighbours on three
   channels at once - accent fill instead of neutral, a full-strength accent edge instead
   of a 42% one, and heavier ink - so it does not rely on colour alone.

   The ink is var(--surface), which is the neutral that contrasts most with the accent in
   whichever theme is active - near-black on a light blue in the dark theme, white on a
   dark blue in the light one. Measured 7.31:1 and 6.29:1. The two alternatives were
   worse: var(--text) sits ON the accent's own end of the range and measures 2.0 and 2.6,
   and var(--bg) is fine in the dark theme at 7.88 but only 4.18 in the light one, which
   an 11px bold label does not get to call large text. The translucent version wanted
   var(--text) for the same reason in reverse - it was sitting on a 50% tint of the
   accent, not on the accent. */
.wpc-size-picker button.is-selected {
    background: var(--wpc-edit-selected);
    border-color: var(--accent);
    color: var(--surface);
    font-weight: 700;
}

.wpc-hide-card:hover {
    color: var(--danger);
}

.wpc-edit-controls button:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

/* ----------------------------------------------------------------------------
   THE COMPACT OVERLAY

   A 2x2 panel filling the footprint: drag and hide on the top row, the supported
   sizes below. Shown only while its card is selected, because a panel covering the
   card is not something to leave standing on every card in the grid.

   It is a cover, not a replacement. The card underneath is untouched and unmoved -
   deselecting restores the normal view with nothing to undo.

   TRACKS ARE CAPPED AND CENTRED, not stretched to the card: 4rem x 2.75rem. Without
   the cap the same panel produced 34.6px controls on a mobile 1x1 and 83px ones on a
   mobile 1x2, which read as two different components. Capped, the three footprints
   that use it land at 34.6, 62.9 and 64px wide - recognisably the same panel.

   Measured 34.6px square on a mobile 1x1, against the 22px of the toolbar it replaces.
   That is deliberate: a panel that exists BECAUSE there was no room for a toolbar must
   not then hand back smaller targets. Controls stretch within their cells; the
   toolbar's buttons keep their own 22px.

   The panel draws no frame of its own. It never should have: inset: 3px and the
   selected card's outline-offset: -3px are the same rectangle, so a frame here was a
   second concentric accent ring around an 86px card. The card's outline is the frame.
   ---------------------------------------------------------------------------- */

.wpc-grid.is-editing .wpc-c1.wpc-r1 .wpc-edit-controls,
.wpc-grid.is-editing .wpc-c2 .wpc-edit-controls {
    --wpc-edit-areas: "id id drag hide" "size size size size";
    --wpc-edit-bottom: 3px;
    --wpc-edit-btn-h: auto;
    --wpc-edit-cols: repeat(4, minmax(0, 1fr));
    --wpc-edit-corner: calc(var(--wpc-radius) - 3px);
    --wpc-edit-display: grid;
    --wpc-edit-dots: center;
    --wpc-edit-font: .7rem;
    --wpc-edit-gap: 3px;
    --wpc-edit-id: flex;
    --wpc-edit-items: stretch;
    --wpc-edit-left: 3px;
    --wpc-edit-pad: 2px;
    --wpc-edit-panel: var(--surface);
    --wpc-edit-place: center;
    --wpc-edit-right: 3px;
    --wpc-edit-rows: repeat(2, minmax(0, 2.75rem));
    --wpc-edit-size-flex: 0 1 4rem;
    --wpc-edit-sizes-align: center;
    --wpc-edit-top: 3px;
}

/* A 1x1 IS TALL BUT NARROW, so its title takes a row of its own rather than sharing one
   with the drag and hide controls. Both 1x1s have the height to spend - 106px of
   interior on the desktop, 72.25 on the phone - and neither has the width: the title
   needs about 70px and an inline arrangement would leave it 63.4 on the desktop and
   35.2 on the phone.

   Measured consequence, stated rather than buried. The desktop 1x1 is unaffected: two
   44px rows still fit under the title, so its controls stay 62.9 x 44. The mobile 1x1
   pays for identity in height - 35.1 x 28.5 where it was 34.63 square. That is
   arithmetic, not preference: 72.25px of interior now carries a 9-px title, two control
   rows and two 3px gaps. It remains 29% taller than the 22px toolbar button this panel
   exists to replace. */
.wpc-grid.is-editing .wpc-c1.wpc-r1 .wpc-edit-controls {
    --wpc-edit-areas: "id id" "drag hide" "size size";
    --wpc-edit-cols: repeat(2, minmax(0, 4rem));

    /* 1.625rem = 26px on the identity row, which is what it takes to reach past the
       card's own title: the title's box ends 30.38px down the card and the panel's
       first row starts about 5px down. Below that the row is free to grow. The two
       control rows take what is left - 37px each on a desktop 1x1, against the 44 they
       had before identity moved into the panel. */
    --wpc-edit-rows: minmax(1.625rem, auto) minmax(0, 2.75rem) minmax(0, 2.75rem);
}

/* The phone 1x1 is the one place the title genuinely runs out of room: 72.25px of
   interior against 76px of "Temp - Outdoor" at the shared size. Rather than accept an
   ellipsis on the very first card the panel is used for, it drops to .52rem and gives
   up its side padding - the "slightly smaller edit-mode typography" the identity rule
   allows. Ellipsis stays as the fallback for a genuinely long title, which is what a
   last resort should be. */
@media (max-width: 640px) {
    .wpc-grid.is-editing .wpc-c1.wpc-r1 .wpc-edit-controls {
        --wpc-edit-id-pad: 0 2px;
        --wpc-edit-id-size: .52rem;

        /* And the 26px identity row goes back to auto here, because a phone 1x1 does
           not have it to give: 72.25px of interior minus 26 minus two gaps leaves 20px
           per control row. The card's own title sits behind the glass drag and hide
           controls instead of behind the identity, which is legible either way and is
           the only arrangement that keeps the controls at 29px. */
        --wpc-edit-rows: auto minmax(0, 2.75rem) minmax(0, 2.75rem);
    }
}

/* Unselected, a compact-overlay card shows the dashed outline and NOTHING else: its
   own content, in its own place, exactly as normal mode drew it. */
.wpc-grid.is-editing .wpc-c1.wpc-r1:not(.is-selected) .wpc-edit-controls,
.wpc-grid.is-editing .wpc-c2:not(.is-selected) .wpc-edit-controls {
    display: none;
}

/* A 1x2 stops being a narrow card above the phone breakpoint - 294.75px against the
   197.28px the toolbar needs - so it hands every variable back and takes the toolbar.
   Reverting the variables is the whole of it; the rule that draws the chrome does not
   know which arrangement it is in. */
@container (min-width: 641px) {
    .wpc-grid.is-editing .wpc-c2 .wpc-edit-controls {
        --wpc-edit-areas: initial;
        --wpc-edit-bottom: initial;
        --wpc-edit-btn-h: initial;
        --wpc-edit-cols: initial;
        --wpc-edit-corner: initial;
        --wpc-edit-display: initial;
        --wpc-edit-dots: initial;
        --wpc-edit-font: initial;
        --wpc-edit-gap: initial;
        --wpc-edit-id: initial;
        --wpc-edit-id-pad: initial;
        --wpc-edit-id-size: initial;
        --wpc-edit-items: initial;
        --wpc-edit-left: initial;
        --wpc-edit-pad: initial;
        --wpc-edit-panel: initial;
        --wpc-edit-place: initial;
        --wpc-edit-right: initial;
        --wpc-edit-rows: initial;
        --wpc-edit-size-flex: initial;
        --wpc-edit-sizes-align: initial;
        --wpc-edit-top: initial;
    }

    /* Restated rather than reverted: `revert` would roll display back to the user
       agent's `inline`, not to the rule above. */
    .wpc-grid.is-editing .wpc-c2:not(.is-selected) .wpc-edit-controls {
        display: var(--wpc-edit-display, flex);
    }
}

/* ============================================================================
   11  ACCESSIBILITY UTILITIES
   ============================================================================ */

.wpc-sr-only {
    border: 0;
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    padding: 0;
    position: absolute;
    white-space: nowrap;
    width: 1px;
}

/* REDUCED MOTION, IN ONE PLACE. Everything this file animates stands down here rather
   than each animation carrying its own opt-out beside it - one block to read, and one
   block to extend when something new moves.

   `.is-pulsing` stops at FULL strength rather than mid-fade: an accessibility opt-out
   should leave the figure at its normal appearance, not frozen at whatever opacity the
   keyframe happened to be at. Position is not load-bearing - the override sits after
   every animation it disables, and would at any point in this file after them. */
@media (prefers-reduced-motion: reduce) {
    .wpc-card,
    .wpc-handle,
    .wpc-skeleton {
        transition: none;
    }

    .wpc-icon.is-pulsing {
        animation: none;
        opacity: 1;
    }
}

/* ============================================================================
   SUN RISE/SET - THE MEDIUM CARD'S INTERNAL ANATOMY
   ============================================================================

   APPROVED 2026-08-28, against docs/screenshots/sun-rise_set.png.

   MEDIUM ONLY. The captions, the two figures and the daylight band are in the markup at
   every footprint and revealed here by `.wpc-c2`. The classes are `wpc-sun-*` rather than
   `wpdn-sun-*` because this file is the CARD SYSTEM's, not the dashboard's: `.wpc-moon`
   next door sets the precedent, and `CardSystemIsolationTests` enforces it. The compact and wide arrangements are
   approved as they stand, so nothing below can reach them - which is also why every
   selector here is anchored on `.wpc-c2` rather than on `.wpc-sun` alone.

   WHAT IT REUSES, WHICH IS NEARLY EVERYTHING. The card shell, the header and its icon
   slot, `.wpc-body-split` and its two regions, `.wpc-primary`, `.wpc-stat-value` for the
   times and the duration, `.wpc-unit` for AM/PM, `.wpc-label` for the three uppercase
   captions, `var(--border)` for both dividers. No new type size, no new spacing token and
   no new colour: the two warm tones are the palette's existing `--cx-measurement-*`
   entries, read here as a rise-to-set progression rather than as their usual measurements.

   WHAT IS SUN-SPECIFIC IS ONLY THE ARRANGEMENT - balanced halves, where the divider sits,
   how tall the figures are. That is the boundary this section is meant to keep. */

/* Hidden by default so c1 and c3 render exactly what they rendered before. */
.wpc-sun-caption,
.wpc-sun-figure,
.wpc-sun-daylight {
    display: none;
}

/* BALANCED HALVES, and the selector carries `.wpc-card` for a reason: the approved
   `is-anatomy-50-50` rule above sets 49fr/51fr at (0,3,0) and would otherwise win. That
   rule gave the right region the extra two points because Sun's OLD left half carried
   52.8px of slack at 1280 - the halves were not symmetrical. This anatomy makes them
   symmetrical, so the reason no longer applies here and the divider belongs on the centre
   line. The 49/51 rule is untouched and still governs Date & Time, Moon, and Sun at c1
   and c3. */
.wpc-card.wpc-c2 .wpc-sun {
    grid-template-columns: 1fr 1fr;
    min-height: 0;

    /* AIR BELOW THE TITLE - APPROVED 2026-08-28. The header and the split were flush; the
       captions read as part of the title block rather than as the start of the body. Paid
       for out of the leading reclaimed below, not out of the footprint: this card's content
       box is exactly full at 104.8px and every pixel spent here is a pixel found. */
    margin-top: 4px;
}

.wpc-c2 .wpc-sun .wpc-split-left,
.wpc-c2 .wpc-sun .wpc-split-right {
    align-items: center;
    display: flex;
    flex-direction: column;
    gap: 1px;
    justify-content: center;
    min-width: 0;
}

/* THE CENTRE DIVIDER, drawn by the right region rather than as an element of its own -
   one hairline, and it stops where the region stops, which is what keeps it clear of the
   daylight band below. */
.wpc-c2 .wpc-sun .wpc-split-right {
    border-left: 1px solid var(--border);
}

.wpc-c2 .wpc-sun .wpc-primary {
    align-items: baseline;
    display: flex;
    gap: .2rem;
    justify-content: center;
}

.wpc-c2 .wpc-sun-caption {
    display: block;
}

/* THE FIGURE'S BOX IS THE GLYPH, and nothing else. As an inline-level box it carried a line
   box with it: 17px of icon inside a 21px box, four pixels of descender space under every
   figure that no one could see and that the region could not afford. `display: flex` with no
   leading makes the box exactly the drawing. Those four pixels are what buys the air above
   the captions and the extra size here - the card is not one pixel taller for either.

   17 -> 19px, ~12%, the requested step. It identifies which half is which; the time is still
   the reading, and at 19px against a 17.9px numeral it stays the quieter of the two. */
.wpc-c2 .wpc-sun-figure {
    display: flex;
    height: 19px;
    line-height: 0;
    width: 19px;
}

/* THE CAPTIONS' LEADING BECOMES A GAP. `.wpc-label` at 8px takes an 11px normal line box, so
   three of those eleven pixels were invisible space this anatomy could not steer. Trimmed to
   the glyph, the space above SUNRISE is now the margin above - a number that can be tuned
   rather than an artefact of the font. The role, its size and its weight are untouched. */
.wpc-c2 .wpc-sun-caption,
.wpc-c2 .wpc-sun-daylight .wpc-label {
    line-height: 1;
}

/* WARM, AND IN ORDER. Gold for the sun coming up, the deeper orange for it going down -
   both existing tokens, so both follow the theme. The AM/PM beside them keeps the muted
   unit colour it has everywhere else, which is what holds it subordinate. */
.wpc-c2 .wpc-sun-time.is-rise,
.wpc-c2 .wpc-sun-figure.is-rise {
    color: var(--cx-measurement-solar);
}

.wpc-c2 .wpc-sun-time.is-set,
.wpc-c2 .wpc-sun-figure.is-set {
    color: var(--cx-measurement-temperature);
}

/* The band across the foot: one hairline, then a centred caption and duration. */
.wpc-c2 .wpc-sun-daylight {
    align-items: center;
    border-top: 1px solid var(--border);
    display: flex;
    flex-direction: column;
    flex: none;
    gap: 1px;
    margin-top: 3px;
    padding-top: 3px;
}
