﻿* {
    box-sizing: border-box;
}

/* Keyboard focus. Only six elements on this page had a focus style — the calendar's 366 cells,
   the scope control, the month nav, the target switch, every form field and the wage disclosure
   had none at all, so tabbing through it was invisible. One ring, stated once, on anything
   focusable. :focus-visible rather than :focus, so it never fires on a mouse click. */
:focus-visible {
    outline: 2px solid #7dd3fc;
    outline-offset: 2px;
    border-radius: 4px;
}

.heat-cell:focus-visible {
    outline-offset: 1px;
    /* Above its neighbours, or the ring is clipped by the next cell in the grid. */
    position: relative;
    z-index: 3;
}

body {
    margin: 0;
    font-family: Inter, "Segoe UI", Tahoma, sans-serif;
    background: radial-gradient(circle at top, #1e293b 0%, #0f172a 45%, #020617 100%);
    color: #e2e8f0;
    min-height: 100vh;
}

.hours-app {
    max-width: 1500px;
    margin: 0 auto;
    padding: 20px;
    /* Above the sky layer, which is body's first child. */
    position: relative;
    z-index: 1;
}

/* ══ The page's sky ═══════════════════════════════════════════════════════════
   The whole tool sits inside the weather. The layers composite in the order light
   actually reaches a lens: a wash (the sky's colour at this hour), the glow off
   the horizon, a light source, its rays, the cloud decks, the haze in front of
   them, the lens's own flare, fog, the particle canvas, then the scrim and one
   grain over everything. The scrim is not decoration — this is a page people read
   hours and money off, so the atmosphere is kept dark, slow and behind glass.
   Every condition is a data-attribute rule, so the JS only ever sets two strings
   and a handful of numbers. */

/* Registered so they can be ANIMATED and multiplied. An unregistered custom property is an
   untyped string: it cannot tween, so a ray sweep or a lightning ramp written against one would
   snap between keyframes instead of moving. Where @property is unsupported the declarations below
   still apply, they simply hold still — the sky degrades to a static version of itself. */
@property --wx-ray-angle {
    syntax: '<angle>';
    inherits: true;
    initial-value: 0deg;
}

@property --wx-flash {
    syntax: '<number>';
    inherits: true;
    initial-value: 0;
}

.wx-page-sky {
    position: fixed;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    overflow: hidden;
    /* Wind speed writes this; the drifts read it. */
    --wx-drift: 90s;
    /* ZENITH → mid → HORIZON, in that order down the frame. These are the pre-condition defaults,
       on screen for the frame or two before the first forecast lands, and they were left in the
       old bright-at-the-top order when the ramp was inverted — which showed up as the page
       flashing an upside-down sky on load. */
    --wx-wash-a: #0b1220;
    --wx-wash-b: #16203a;
    --wx-wash-c: #2b3a55;

    /* HOW MUCH SKY IS COVERED, 0-1. This is the fix for the sky that reported "Clear" and then
       drew cloud over the whole page: cover used to be an accident of the palette — every
       condition, clear included, painted all four decks and only varied their tint — so there was
       no value anywhere in the system meaning "there is no cloud up there". It multiplies the
       deck alphas below, so 0 paints nothing at all and the decks cost nothing to composite. */
    --wx-cover: 1;

    /* Colours are held as RGB TRIPLETS with a separate alpha, so every gradient can fade to the
       SAME colour at zero alpha. Fading to `transparent` instead is what made the clouds look
       like squares: `transparent` is rgba(0,0,0,0), so the ramp travels through darkening greys
       and the tile's own rectangle shows up as a grey halo around every soft edge. */
    --wx-cloud-rgb: 148, 163, 184;
    --wx-cloud-a: 0.05;
    --wx-veil-rgb: 148, 163, 184;
    --wx-veil-a: 0;

    /* The sun / moon disc. Radius drives the corona too, so one number resizes the whole body. */
    --wx-orb-core: 255, 255, 255;
    --wx-orb-glow: 255, 255, 255;
    --wx-orb-r: 5vmax;
    --wx-orb-x: 80%;
    --wx-orb-y: 12%;
    --wx-orb-o: 0;
    --wx-orb-core-a: 0.9;

    /* Crepuscular rays: on for a visible sun, off for everything else. Shafts of light need cloud
       gaps or haze to be visible at all, so this is a per-condition switch and not a constant. */
    --wx-rays: 0;
    --wx-ray-a: 0.05;

    /* Declared here as well as in @property, for browsers that do not implement @property. There
       the registration is skipped, the initial-value with it, and a var() with no value anywhere
       makes the whole declaration using it invalid — which would have taken the rays' background
       and the storm's brightness filter out entirely rather than merely freezing them. */
    --wx-ray-angle: 0deg;
    --wx-flash: 0;

    /* The lens itself. A veiling flare and one ghost, only ever as strong as the light making it. */
    --wx-flare: 0;

    /* The highlight on the cloud tops. It should be the colour of whatever is lighting the sky —
       warm under sun, cold under moon, near-nothing under a closed lid — not a generic white. */
    --wx-lit-rgb: 255, 255, 255;
    --wx-lit-a: 0.035;

    /* Horizon lift: how much brighter the sky gets toward the bottom of the frame. */
    --wx-horizon-rgb: 148, 163, 184;
    --wx-horizon-a: 0.05;

    /* The dome of light standing over the ground at the bottom of the frame. By day it is haze;
       after dark it is the port itself — Dundalk, Seagirt and the yards throw a warm sodium glow
       into the overcast that anyone who has worked a night shift here has looked at. */
    --wx-glow-rgb: 148, 163, 184;
    --wx-glow-a: 0.04;
    --wx-glow-h: 26%;

    /* Fog banks, and how thick they sit. */
    --wx-fog: 0;
    --wx-fog-rgb: 226, 232, 240;

    /* Cloud masks. Set here as SVG turbulence so the sky is right on the very first frame, then
       REPLACED by JS with a genuinely seamless baked tile — see the seam note on the decks. */
    --wx-mask-far: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='900' height='900'%3E%3Cfilter id='c'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.0045 0.009' numOctaves='5' seed='11' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 1.1 0 0 0 -0.28'/%3E%3C/filter%3E%3Crect width='900' height='900' filter='url(%23c)'/%3E%3C/svg%3E");
    --wx-mask-evolve: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='900' height='900'%3E%3Cfilter id='c'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.0052 0.0085' numOctaves='5' seed='83' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 1.1 0 0 0 -0.3'/%3E%3C/filter%3E%3Crect width='900' height='900' filter='url(%23c)'/%3E%3C/svg%3E");
    --wx-mask-near: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='900' height='900'%3E%3Cfilter id='c'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.011 0.02' numOctaves='4' seed='29' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 1.25 0 0 0 -0.46'/%3E%3C/filter%3E%3Crect width='900' height='900' filter='url(%23c)'/%3E%3C/svg%3E");
    /* One tile width and height for each deck. The drift keyframes travel exactly this far, so a
       baked tile of a different aspect only has to update these two numbers to stay seamless. */
    --wx-tile-far-w: 1760px;
    --wx-tile-far-h: 1180px;
    --wx-tile-near-w: 1080px;
    --wx-tile-near-h: 780px;
}

.wx-page-sky > * {
    position: absolute;
    inset: 0;
}

/* Stated explicitly, because the particle canvas is appended by JS and would otherwise land on
   top of the grain simply by being last in the DOM. The order is the order light reaches a lens:
   sky, ground glow, light source, its shafts, cloud, the haze in front of cloud, the flare the
   glass itself adds, fog at eye level, precipitation, then the readability scrim and one grain
   over the whole exposure. */
.wx-page-sky__wash { z-index: 0; }
.wx-page-sky__glow { z-index: 1; }
.wx-page-sky__light { z-index: 2; }
.wx-page-sky__disc { z-index: 2; }
.wx-page-sky__rays { z-index: 3; }
.wx-page-sky__clouds--far { z-index: 4; }
.wx-page-sky__clouds--evolve { z-index: 4; }
.wx-page-sky__clouds--near { z-index: 5; }
.wx-page-sky__clouds--lit { z-index: 6; }
.wx-page-sky__scatter { z-index: 7; }
.wx-page-sky__flare { z-index: 8; }
.wx-page-sky__fog { z-index: 9; }
.wx-page-sky__veil { z-index: 10; }
.wx-page-sky__canvas { z-index: 11; }
.wx-page-sky::after { z-index: 12; }
.wx-page-sky__grain { z-index: 13; }

/* A real sky is DARKEST at the zenith and brightest at the horizon — you are looking through far
   more atmosphere sideways than straight up. This wash ran the other way: a radial anchored above
   the top edge put its lightest colour at the top and fell to near-black at the bottom, which is
   the one thing about a sky everybody has seen ten thousand times and will feel is wrong without
   being able to say why. It also left the page with almost no tonal range at all — the same flat
   navy from corner to corner, which is what made every condition here look like the same picture
   in a different tint.
   So: --wx-wash-a is the ZENITH, --wx-wash-c is the HORIZON, and they run top to bottom in that
   order. Every condition's three colours were re-picked around that, dark overhead to light along
   the bottom, because inverting the ramp without inverting the palette would just have moved the
   same mistake to the other end of the page. */
.wx-page-sky__wash {
    background:
        linear-gradient(180deg,
            rgba(var(--wx-horizon-rgb), 0) 48%,
            rgba(var(--wx-horizon-rgb), calc(var(--wx-horizon-a) * 0.45)) 78%,
            rgba(var(--wx-horizon-rgb), var(--wx-horizon-a)) 100%),
        linear-gradient(180deg,
            var(--wx-wash-a) 0%,
            var(--wx-wash-b) 52%,
            var(--wx-wash-c) 100%);
    transition: background 1.8s ease;
}

/* THE GROUND'S OWN LIGHT. Every sky here was lit from the top down and from nowhere else, which
   is why the bottom of the frame died into flat black — and the bottom of the frame is where a
   real horizon is brightest. By day this is haze standing over the water; after dark it is the
   port throwing sodium light up into the cloud base, which is what the sky over Dundalk actually
   looks like at 3 AM and is the single cheapest thing that makes a night sky read as a PLACE
   rather than a gradient. Elliptical and centred below the frame, so only its top edge shows. */
.wx-page-sky__glow {
    background: radial-gradient(ellipse 78% var(--wx-glow-h) at 50% 104%,
        rgba(var(--wx-glow-rgb), var(--wx-glow-a)) 0%,
        rgba(var(--wx-glow-rgb), calc(var(--wx-glow-a) * 0.45)) 45%,
        rgba(var(--wx-glow-rgb), 0) 100%);
    transition: background 1.8s ease;
}

/* CREPUSCULAR RAYS. Shafts thrown from the sun through the haze — the thing that separates a
   photograph of a sky from a blue rectangle with a dot on it. A repeating conic gradient centred
   on the orb IS a fan of shafts; the mask is what keeps them from being a bicycle wheel — nothing
   at the disc itself (where the glare would drown them), strongest a few radii out, gone by the
   time they cross the page.
   The sweep animates the gradient's own start angle rather than rotating the element: rotating it
   would swing the sun across the page with it. Small on purpose — a couple of degrees over a
   minute, so it is never caught moving and never quite the same twice. */
.wx-page-sky__rays {
    /* Four shafts of DIFFERENT widths and strengths per period, not one repeated evenly. An even
       fan is a bicycle wheel — the eye finds the rhythm instantly and the whole thing collapses
       into a starburst decal. Uneven spacing at a shallow angle reads as light coming through
       gaps that happen to be where they are. */
    background: repeating-conic-gradient(from var(--wx-ray-angle) at var(--wx-orb-x) var(--wx-orb-y),
        rgba(var(--wx-orb-glow), 0) 0deg,
        rgba(var(--wx-orb-glow), var(--wx-ray-a)) 1.4deg,
        rgba(var(--wx-orb-glow), 0) 3.1deg,
        rgba(var(--wx-orb-glow), 0) 5.6deg,
        rgba(var(--wx-orb-glow), calc(var(--wx-ray-a) * 0.5)) 6.4deg,
        rgba(var(--wx-orb-glow), 0) 8.2deg,
        rgba(var(--wx-orb-glow), 0) 11.5deg,
        rgba(var(--wx-orb-glow), calc(var(--wx-ray-a) * 0.78)) 12.6deg,
        rgba(var(--wx-orb-glow), 0) 14.1deg,
        rgba(var(--wx-orb-glow), 0) 17.9deg,
        rgba(var(--wx-orb-glow), calc(var(--wx-ray-a) * 0.34)) 18.6deg,
        rgba(var(--wx-orb-glow), 0) 21.4deg,
        rgba(var(--wx-orb-glow), 0) 26.7deg);
    -webkit-mask-image: radial-gradient(circle at var(--wx-orb-x) var(--wx-orb-y),
        rgba(0, 0, 0, 0) 0,
        rgba(0, 0, 0, 0.85) calc(var(--wx-orb-r) * 2.6),
        rgba(0, 0, 0, 0.55) calc(var(--wx-orb-r) * 7),
        rgba(0, 0, 0, 0) calc(var(--wx-orb-r) * 14));
    mask-image: radial-gradient(circle at var(--wx-orb-x) var(--wx-orb-y),
        rgba(0, 0, 0, 0) 0,
        rgba(0, 0, 0, 0.85) calc(var(--wx-orb-r) * 2.6),
        rgba(0, 0, 0, 0.55) calc(var(--wx-orb-r) * 7),
        rgba(0, 0, 0, 0) calc(var(--wx-orb-r) * 14));
    mix-blend-mode: screen;
    opacity: calc(var(--wx-orb-o) * var(--wx-rays));
    animation: wxRaySweep 190s linear infinite;
    transition: opacity 1.8s ease;
}

@keyframes wxRaySweep {
    from { --wx-ray-angle: 0deg; }
    to   { --wx-ray-angle: 26.7deg; }
}

/* THE LENS. Everything above this point is the sky; this is the glass in front of it, and it is
   what says "photographed" rather than "drawn". Two artifacts, both anchored to the light source:
   a veiling streak drawn out horizontally (an anamorphic front element does this to any bright
   point) and one ghost thrown back through the optical centre. Kept at alphas you would struggle
   to measure — a flare you can point at is a filter, a flare you cannot is a lens. */
.wx-page-sky__flare {
    background:
        radial-gradient(ellipse calc(var(--wx-orb-r) * 11) calc(var(--wx-orb-r) * 0.16)
            at var(--wx-orb-x) var(--wx-orb-y),
            rgba(var(--wx-orb-glow), 0.20) 0%,
            rgba(var(--wx-orb-glow), 0.06) 45%,
            rgba(var(--wx-orb-glow), 0) 100%),
        radial-gradient(circle calc(var(--wx-orb-r) * 1.5)
            at calc(var(--wx-orb-x) - 22%) calc(var(--wx-orb-y) + 30%),
            rgba(var(--wx-orb-glow), 0.05) 0%,
            rgba(var(--wx-orb-glow), 0) 100%),
        radial-gradient(circle calc(var(--wx-orb-r) * 0.7)
            at calc(var(--wx-orb-x) - 34%) calc(var(--wx-orb-y) + 46%),
            rgba(186, 230, 253, 0.045) 0%,
            rgba(186, 230, 253, 0) 100%);
    mix-blend-mode: screen;
    opacity: calc(var(--wx-orb-o) * var(--wx-flare));
    transition: opacity 1.8s ease;
}

/* FOG AT EYE LEVEL. Overcast is a lid overhead; fog is the cloud standing in the room with you,
   and drawing it as more cloud in the ceiling was why the fog sky read as a slightly paler
   overcast. These are banks: wide, soft, thickest along the bottom of the frame where the ground
   is, drifting at three speeds so the near one overtakes the far ones.

   THE TILE HAS TO CONTAIN THE BANK. A gradient that has not reached full transparency by the edge
   of its own background tile is cut off there, and repeat-x turns that cut into a soft RECTANGLE
   marching across the page — which is exactly what the first version drew: an ellipse 92% tall
   inside a 300px tile, sliced flat top and bottom. Every ellipse below therefore has its centre
   and radii chosen so the whole shape fits inside its tile with room to spare in BOTH axes, and
   reaches zero alpha at 100% rather than at 72%.
   Three banks at three tile widths, all travelling exactly one tile per cycle: same duration,
   different distances, so they move at three different speeds and the group never resynchronises
   into a single visible wall of fog. */
.wx-page-sky__fog {
    background-image:
        radial-gradient(ellipse 24% 40% at 30% 50%,
            rgba(var(--wx-fog-rgb), 0.55) 0%, rgba(var(--wx-fog-rgb), 0) 100%),
        radial-gradient(ellipse 20% 38% at 68% 50%,
            rgba(var(--wx-fog-rgb), 0.42) 0%, rgba(var(--wx-fog-rgb), 0) 100%),
        radial-gradient(ellipse 28% 42% at 46% 50%,
            rgba(var(--wx-fog-rgb), 0.34) 0%, rgba(var(--wx-fog-rgb), 0) 100%),
        linear-gradient(0deg,
            rgba(var(--wx-fog-rgb), 0.22) 0%, rgba(var(--wx-fog-rgb), 0) 46%);
    background-size: 1180px 340px, 860px 260px, 1520px 300px, 100% 100%;
    background-repeat: repeat-x, repeat-x, repeat-x, no-repeat;
    background-position: 0 78%, 0 91%, 0 66%, 0 0;
    opacity: var(--wx-fog);
    animation: wxFogNear 150s linear infinite, wxFogFar 230s linear infinite;
    transition: opacity 1.8s ease;
}

@keyframes wxFogNear {
    from { background-position: 0 78%, 0 91%, 0 66%, 0 0; }
    to   { background-position: -1180px 78%, -860px 91%, -1520px 66%, 0 0; }
}

@keyframes wxFogFar {
    0%, 100% { opacity: var(--wx-fog); }
    50%      { opacity: calc(var(--wx-fog) * 0.72); }
}

/* Light SCATTERING, in front of the cloud rather than behind it.
   The orb sits under the cloud decks, so cloud already occludes it — that part was free. What
   was missing is the haze: near a light source the air itself glows, in front of everything,
   which is why you cannot look near the sun and still see cloud detail there. Screened over the
   top of the decks, so it washes them out exactly where the light is. */
.wx-page-sky__scatter {
    background: radial-gradient(circle at var(--wx-orb-x) var(--wx-orb-y),
        rgba(var(--wx-orb-glow), 0.15) 0,
        rgba(var(--wx-orb-glow), 0.07) calc(var(--wx-orb-r) * 3),
        rgba(var(--wx-orb-glow), 0.022) calc(var(--wx-orb-r) * 7),
        rgba(var(--wx-orb-glow), 0) calc(var(--wx-orb-r) * 12));
    mix-blend-mode: screen;
    opacity: var(--wx-orb-o);
    transition: opacity 1.8s ease, background 1.8s ease;
}

/* The sun and the moon are the same element, moved, resized and recoloured. A flat wash was not
   convincing as either — there was no BODY, just a bright corner. This is a disc with a corona:
   a near-solid core out to --wx-orb-r, then the corona falling away over eleven radii to the
   same colour at zero alpha, so there is no edge anywhere. */
/* The disc is small and the corona does the reaching. An earlier pass had it the other way round
   — a large, nearly-solid core with a bright shoulder — and the whole top corner of the page blew
   out into a featureless white blob. The sun is a POINT that lights everything around it; almost
   all of the apparent size of the sun in a photograph is glare, not disc. So: solid to r, then off
   a cliff, then a long faint reach out to eleven radii. */
.wx-page-sky__light {
    opacity: var(--wx-orb-o);
    background: radial-gradient(circle at var(--wx-orb-x) var(--wx-orb-y),
        rgba(var(--wx-orb-core), var(--wx-orb-core-a)) 0,
        rgba(var(--wx-orb-core), calc(var(--wx-orb-core-a) * 0.96)) calc(var(--wx-orb-r) * 0.72),
        rgba(var(--wx-orb-core), calc(var(--wx-orb-core-a) * 0.5)) var(--wx-orb-r),
        rgba(var(--wx-orb-glow), 0.26) calc(var(--wx-orb-r) * 1.7),
        rgba(var(--wx-orb-glow), 0.10) calc(var(--wx-orb-r) * 3.6),
        rgba(var(--wx-orb-glow), 0.035) calc(var(--wx-orb-r) * 6.5),
        rgba(var(--wx-orb-glow), 0) calc(var(--wx-orb-r) * 11));
    transition: opacity 1.8s ease, background 1.8s ease;
    animation: wxOrbBreathe 14s ease-in-out infinite;
}

/* The sun is never perfectly still — a slow, almost imperceptible swell keeps it from reading
   as a decal stuck on the background. */
@keyframes wxOrbBreathe {
    0%, 100% { transform: scale(1); }
    50%      { transform: scale(1.035); }
}

/* THE MOON'S PHASE. A second disc of the same radius, slid across the first: fully overlapping is
   a new moon, entirely clear of it is a full one, and everything between is the crescent it
   actually is tonight (the fraction comes from the synodic month, computed in the JS).
   It is a MASK and not a dark disc painted on top, because a painted shadow needs a colour and
   the only correct colour is whatever the sky is at that exact spot — which is a moving gradient.
   Masked, the unlit limb is simply not drawn and the sky behind shows through it.

   THE BODY AND THE HALO HAVE TO BE SEPARATE ELEMENTS. Masking them as one — which is how this
   started — takes a disc-sized bite out of the halo as well, and since the halo is at its
   brightest right against the disc, the bite reads as a black hole punched in the sky next to the
   moon: far more wrong than a moon that is always full. So the light layer keeps the halo and
   nothing else (zero alpha inside the disc, so it cannot brighten the unlit limb), and this layer
   carries the body and wears the terminator. Both run the same breathe animation, so they scale
   together and the terminator never drifts off the edge it belongs to. */
.wx-page-sky__disc {
    opacity: 0;
}

.wx-page-sky[data-sky="moon"] .wx-page-sky__disc,
.wx-page-sky[data-sky="partly-night"] .wx-page-sky__disc {
    opacity: var(--wx-orb-o);
    background: radial-gradient(circle at var(--wx-orb-x) var(--wx-orb-y),
        rgba(var(--wx-orb-core), var(--wx-orb-core-a)) 0,
        rgba(var(--wx-orb-core), calc(var(--wx-orb-core-a) * 0.96)) calc(var(--wx-orb-r) * 0.92),
        rgba(var(--wx-orb-core), 0) calc(var(--wx-orb-r) * 1.04));
    -webkit-mask-image: radial-gradient(circle var(--wx-orb-r)
        at calc(var(--wx-orb-x) + var(--wx-moon-shadow, -100vmax)) var(--wx-orb-y),
        rgba(0, 0, 0, 0) 0%,
        rgba(0, 0, 0, 0) 84%,
        rgba(0, 0, 0, 1) 100%);
    mask-image: radial-gradient(circle var(--wx-orb-r)
        at calc(var(--wx-orb-x) + var(--wx-moon-shadow, -100vmax)) var(--wx-orb-y),
        rgba(0, 0, 0, 0) 0%,
        rgba(0, 0, 0, 0) 84%,
        rgba(0, 0, 0, 1) 100%);
    animation: wxOrbBreathe 14s ease-in-out infinite;
    transition: opacity 1.8s ease;
}

/* Halo only — see the note above. Two details in the inner stops are doing real work:
   NO GAP. The halo has to be at full strength by the time the body's own edge has faded out. A
   version that only started rising a fifth of a radius further out left a band where neither
   layer painted anything, and that band read as a hard dark RING drawn around the moon.
   EARTHSHINE. The centre is not zero. Sunlight bounced off the Earth genuinely does light the
   unlit limb of a crescent — it is the "old moon in the new moon's arms" — and here it doubles as
   the fix for the other half of the ring problem: without it the shadowed limb came out darker
   than the sky around it, which is the one thing it can never be. */
.wx-page-sky[data-sky="moon"] .wx-page-sky__light,
.wx-page-sky[data-sky="partly-night"] .wx-page-sky__light {
    background: radial-gradient(circle at var(--wx-orb-x) var(--wx-orb-y),
        rgba(var(--wx-orb-glow), 0.09) 0,
        rgba(var(--wx-orb-glow), 0.12) calc(var(--wx-orb-r) * 0.7),
        rgba(var(--wx-orb-glow), 0.30) calc(var(--wx-orb-r) * 1.05),
        rgba(var(--wx-orb-glow), 0.12) calc(var(--wx-orb-r) * 3),
        rgba(var(--wx-orb-glow), 0.04) calc(var(--wx-orb-r) * 6),
        rgba(var(--wx-orb-glow), 0) calc(var(--wx-orb-r) * 11));
}

/* Two decks. The far one is small, dim and slow; the near one is large, brighter
   and moves half again as fast, in the same direction. That speed difference is
   the whole illusion — parallax is what separates "sky" from "a texture sliding
   past". They also run on different cycle lengths so the pair never visibly
   repeats, which a single tiled layer always eventually does. */
/* GUSTING. The mask drift is perfectly linear — constant velocity, forever, which no wind does.
   A slow translate superimposed on top of it makes the decks surge and ease without ever
   reversing, and because it is a transform it costs the compositor nothing. The two decks use
   different periods so they gust independently, the way separate layers of air actually do. */
.wx-page-sky__clouds {
    /* `background`, not `background-color`: the deck's tint moved into a gradient when the decks
       learned to thin out toward the horizon, and a transition still naming background-color
       would have left every change of weather snapping instead of crossing over. */
    transition: background 1.8s ease;
    will-change: mask-position, transform;
}

@keyframes wxGustFar {
    0%, 100% { transform: translate3d(0, 0, 0); }
    35%      { transform: translate3d(-2.2vw, 0, 0); }
    70%      { transform: translate3d(0.9vw, 0, 0); }
}

@keyframes wxGustNear {
    0%, 100% { transform: translate3d(0, 0, 0); }
    45%      { transform: translate3d(-3.4vw, 0, 0); }
    75%      { transform: translate3d(1.3vw, 0, 0); }
}

/* Clouds are FRACTAL NOISE, not shapes.
   Three or four soft ellipses tiling across a screen can only ever read as three or four soft
   ellipses — you can count them, the tile repeat is visible, and on a flat overcast wash a low-
   alpha ellipse just reads as a drifting smudge. Fractal noise at a low base frequency produces
   genuine cloud structure at every scale at once, with no countable parts.
   The noise drives a MASK, not the colour, so each condition still tints its own sky through
   --wx-cloud-rgb, and the alpha is multiplied by --wx-cover so "how much cloud" and "what colour
   the cloud is" are finally two separate questions.

   THE SEAM. These masks tile, and the SVG feTurbulence they started as does NOT wrap: its
   `stitchTiles` only smooths the seams of the tiles the FILTER is internally computed in, not the
   edges of the image you then repeat, so a hard grid line appeared every tile width and height —
   plainly visible on overcast, which is the one sky with nothing else in it to look at. (Tested
   four ways here: native tile size, an explicit userSpaceOnUse filter region, and a baseFrequency
   giving an exact integer number of periods across the tile. All four seam.) The JS bakes a real
   torus-wrapped noise tile and swaps it in through --wx-mask-*; the SVG above is only what is on
   screen for the first frame. */
.wx-page-sky__clouds {
    background-color: rgba(var(--wx-cloud-rgb), calc(var(--wx-cloud-a) * var(--wx-cover)));
    -webkit-mask-repeat: repeat;
    mask-repeat: repeat;
    /* THE DECKS HAVE TO BE WIDER THAN THE SKY. The gust translates them up to 3.4vw left and 1.3vw
       right, but `.wx-page-sky > *` gives them inset: 0 — exactly the sky's width — so every gust
       uncovered a bare lane of wash at whichever edge it drifted away from. Measured on a 2545px
       window mid-gust: the far deck's right edge sat at 2531 against a sky ending at 2545, and the
       near deck started 31px in from the left. It reads as a pale vertical stripe down the side of
       the page that slowly breathes in and out, which is exactly what it is.
       Overhang each side by more than the largest excursion that way and no gust can reach an edge.
       The masks tile and repeat, so the extra width costs a few more tiles and nothing else, and
       the container clips it. */
    left: -1.8vw;
    right: -4vw;
}

/* WHICH DECK CARRIES THE SKY. The high deck is smooth and structural; the low one is billowed
   into lumps. One alpha driving both meant every condition got the same MIX of the two, so
   overcast — which is a flat lid with a slow undulation in it — came out as the same field of
   cotton-wool lumps as a broken cumulus afternoon. These let a condition say which it is. */
.wx-page-sky__clouds--far,
.wx-page-sky__clouds--evolve {
    --wx-deck-a: calc(var(--wx-cloud-a) * var(--wx-cover) * var(--wx-k-far, 1));
}

.wx-page-sky__clouds--near {
    --wx-deck-a: calc(var(--wx-cloud-a) * var(--wx-cover) * var(--wx-k-near, 1));
}

/* AERIAL PERSPECTIVE. A flat tint through the mask gives a deck of uniform density from the top
   of the frame to the bottom, and nothing in the real world is uniform over that distance — the
   cloud near the horizon is being looked at through miles of haze and washes out into it. Running
   the tint down a gradient instead of holding it flat costs nothing and buys the one depth cue
   the decks did not have. It also happens to put the least cloud exactly where the calendar is. */
.wx-page-sky__clouds {
    --wx-deck-rgb: var(--wx-cloud-rgb);
    background-image: linear-gradient(180deg,
        rgba(var(--wx-deck-rgb), var(--wx-deck-a)) 0%,
        rgba(var(--wx-deck-rgb), calc(var(--wx-deck-a) * 0.62)) 62%,
        rgba(var(--wx-deck-rgb), calc(var(--wx-deck-a) * 0.22)) 100%);
    background-color: transparent;
}

.wx-page-sky__clouds--far {
    /* Low frequency, wide: the high deck, big slow structures. */
    -webkit-mask-image: var(--wx-mask-far);
    mask-image: var(--wx-mask-far);
    -webkit-mask-size: var(--wx-tile-far-w) var(--wx-tile-far-h);
    mask-size: var(--wx-tile-far-w) var(--wx-tile-far-h);
    /* Drift and cross-fade are set together further down, where its partner deck is defined. */
}

.wx-page-sky__clouds--near {
    /* Higher frequency, tighter, and a different seed so the two decks never rhyme. */
    -webkit-mask-image: var(--wx-mask-near);
    mask-image: var(--wx-mask-near);
    -webkit-mask-size: var(--wx-tile-near-w) var(--wx-tile-near-h);
    mask-size: var(--wx-tile-near-w) var(--wx-tile-near-h);
    /* calc keeps the two decks locked to one wind reading while staying different speeds. */
    animation:
        wxDriftNear calc(var(--wx-drift) * 0.62) linear infinite,
        wxGustNear calc(var(--wx-drift) * 1.7) ease-in-out infinite;
}

/* EVOLUTION. The same high deck on a different seed, cross-fading with the first over a cycle
   long enough that you never catch it happening. This is what stops the sky repeating: a fixed
   texture translated sideways brings the identical shapes back every cycle no matter how slow it
   is, and once you notice that, nothing else about the scene reads as real. Two fields trading
   places means shapes genuinely form and dissolve.
   The two cycles are deliberately COPRIME with the drift, so the combination of "which shapes"
   and "where they are" does not resynchronise on any watchable timescale. */
.wx-page-sky__clouds--evolve {
    -webkit-mask-image: var(--wx-mask-evolve);
    mask-image: var(--wx-mask-evolve);
    -webkit-mask-size: var(--wx-tile-far-w) var(--wx-tile-far-h);
    mask-size: var(--wx-tile-far-w) var(--wx-tile-far-h);
    opacity: 0;
    animation:
        wxDriftFar var(--wx-drift) linear infinite,
        wxFormB calc(var(--wx-drift) * 1.37) ease-in-out infinite,
        wxGustFar calc(var(--wx-drift) * 2.3) ease-in-out infinite;
}

/* Its partner fades the other way over the same period, and gusts identically — they are two
   renderings of one deck, so they have to move as one. */
.wx-page-sky__clouds--far {
    animation:
        wxDriftFar var(--wx-drift) linear infinite,
        wxFormA calc(var(--wx-drift) * 1.37) ease-in-out infinite,
        wxGustFar calc(var(--wx-drift) * 2.3) ease-in-out infinite;
}

@keyframes wxFormA {
    0%, 100% { opacity: 0.62; }
    50%      { opacity: 0.10; }
}

@keyframes wxFormB {
    0%, 100% { opacity: 0.08; }
    50%      { opacity: 0.58; }
}

/* VOLUME. The near deck's own shape, shifted up-left and lightened, screened over the top of it.
   A single flat colour through a mask reads as cut paper however good the mask is; one offset
   highlight along the same edge is enough to make it read as something with a top and a
   underside. Offsetting the mask ORIGIN keeps it in exact lockstep with the deck below — it is
   the same animation distance, just started from a different place. */
.wx-page-sky__clouds--lit {
    -webkit-mask-image: var(--wx-mask-near);
    mask-image: var(--wx-mask-near);
    -webkit-mask-size: var(--wx-tile-near-w) var(--wx-tile-near-h);
    mask-size: var(--wx-tile-near-w) var(--wx-tile-near-h);
    /* Rides the same gradient machinery as the decks below it, so the highlight fades toward the
       horizon with the cloud it belongs to instead of staying at full strength all the way down. */
    --wx-deck-rgb: var(--wx-lit-rgb, 255, 255, 255);
    --wx-deck-a: calc(var(--wx-lit-a, 0.05) * var(--wx-cover));
    mix-blend-mode: screen;
    /* Identical gust to the near deck. A highlight that gusts on its own period detaches from
       the cloud it is lighting, which is worse than having no highlight at all. */
    animation:
        wxDriftLit calc(var(--wx-drift) * 0.62) linear infinite,
        wxGustNear calc(var(--wx-drift) * 1.7) ease-in-out infinite;
}

@keyframes wxDriftLit {
    from {
        -webkit-mask-position: -7px -11px;
        mask-position: -7px -11px;
    }
    to {
        -webkit-mask-position: calc(-7px - var(--wx-tile-near-w)) -11px;
        mask-position: calc(-7px - var(--wx-tile-near-w)) -11px;
    }
}

/* Without mask support the tint would paint as a flat sheet of colour over the whole page, which
   is far worse than no clouds at all. Nothing rather than that. */
@supports not ((mask-image: none) or (-webkit-mask-image: none)) {
    .wx-page-sky__clouds { background-color: transparent; }
}

.wx-page-sky__veil {
    background: linear-gradient(180deg,
        rgba(var(--wx-veil-rgb), var(--wx-veil-a)) 0%,
        rgba(var(--wx-veil-rgb), calc(var(--wx-veil-a) * 0.4)) 32%,
        rgba(var(--wx-veil-rgb), 0) 66%);
    transition: background 1.8s ease;
}

.wx-page-sky__canvas {
    width: 100%;
    height: 100%;
    display: block;
}

/* The glass, plus the two things that turn a stack of divs into one photographed image.
   VIGNETTE: corners fall off, so the eye is pulled to the middle where the work is — the oldest
   trick in cinematography and the cheapest.
   GRAIN: a single layer of noise laid over EVERYTHING. This is the part that actually binds the
   composite: separate CSS layers each look flat and synthetic on their own, and one shared grain
   across all of them reads as a single exposure. Kept far below the threshold where it would
   look like texture rather than film.
   Light on purpose otherwise: the panels are already rgba(15,23,42,.78) over a blur, so text
   contrast is theirs to hold. Dimming here as well was double-dimming, and the weather
   disappeared entirely. The bottom of the readability wash used to be 0.44 and the vignette 0.34,
   which together flattened the lower half of every sky back to the same near-black — the horizon
   lift and the ground glow were both being drawn and then wiped off by the layer above them. The
   text that actually sits on bare sky is all in the top strip, where this wash is at its lightest;
   the calendar down at the bottom has a panel of its own. */
.wx-page-sky::after {
    content: '';
    position: absolute;
    inset: 0;
    background-image:
        /* grain */
        url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E"),
        /* vignette */
        radial-gradient(ellipse 124% 94% at 50% 44%, transparent 42%, rgba(1, 7, 20, 0.28) 100%),
        /* readability wash, heavier toward the bottom where the long calendar runs */
        linear-gradient(180deg, rgba(2, 6, 23, 0.06) 0%, rgba(2, 6, 23, 0.17) 55%, rgba(2, 6, 23, 0.27) 100%);
    background-size: 140px 140px, cover, cover;
    background-repeat: repeat, no-repeat, no-repeat;
    opacity: 1;
}

/* The grain is the only layer that needs to blend rather than stack, and it is worth isolating
   so it does not tint the wash underneath it. */
@supports (mix-blend-mode: overlay) {
    .wx-page-sky::after { mix-blend-mode: normal; }
    .wx-page-sky__grain {
        position: absolute;
        inset: 0;
        pointer-events: none;
        opacity: 0.05;
        mix-blend-mode: overlay;
        background-image:
            url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
        background-size: 140px 140px;
    }
    /* With a dedicated grain element, the pseudo-element drops its copy. */
    .wx-page-sky::after {
        background-image:
            radial-gradient(ellipse 124% 94% at 50% 44%, transparent 42%, rgba(1, 7, 20, 0.28) 100%),
            linear-gradient(180deg, rgba(2, 6, 23, 0.06) 0%, rgba(2, 6, 23, 0.17) 55%, rgba(2, 6, 23, 0.27) 100%);
        background-size: cover, cover;
        background-repeat: no-repeat, no-repeat;
    }
}

/* The clouds are a mask now, so it is the MASK that travels. Each deck moves by exactly ONE TILE
   WIDTH and zero vertically: with stitchTiles that loops with no seam and no jump. Any other
   distance — including a few px of vertical "drift" — pops back at the end of the cycle, which
   on a 2-minute loop is a single visible twitch and reads as a bug. The sense of depth comes
   from the two decks running at different speeds, not from moving them diagonally. */
@keyframes wxDriftFar {
    from {
        -webkit-mask-position: 0 0;
        mask-position: 0 0;
    }
    to {
        -webkit-mask-position: calc(var(--wx-tile-far-w) * -1) 0;
        mask-position: calc(var(--wx-tile-far-w) * -1) 0;
    }
}

@keyframes wxDriftNear {
    from {
        -webkit-mask-position: 0 0;
        mask-position: 0 0;
    }
    to {
        -webkit-mask-position: calc(var(--wx-tile-near-w) * -1) 0;
        mask-position: calc(var(--wx-tile-near-w) * -1) 0;
    }
}

/* ── Conditions ─────────────────────────────────────────────────────────── */
/* Clear day. The sun is a real body, high and to the right, small and fierce, with a corona that
   reaches most of the way across the page, shafts thrown through the haze, and a veiling streak
   off the glass. What it does NOT have is cloud: --wx-cover is 0, so all four decks paint nothing.
   A page that says "Clear" and then drifts cloud over itself is telling the reader that the widget
   and the background disagree about the weather, and the background is the more convincing liar.
   The far deck is not merely faint here, it is absent — see the cover note at the top. */
.wx-page-sky[data-sky="sun"] {
    --wx-wash-a: #0d2b57; --wx-wash-b: #1c4a80; --wx-wash-c: #3f76a8;
    --wx-cover: 0;
    --wx-cloud-rgb: 214, 233, 255; --wx-cloud-a: 0.08;
    --wx-orb-core: 255, 249, 226; --wx-orb-glow: 253, 205, 96;
    --wx-orb-r: 1.75vmax; --wx-orb-x: 82%; --wx-orb-y: 10%;
    --wx-orb-core-a: 0.95; --wx-orb-o: 1;
    /* Hard sun, so the tops catch it warmly and strongly. */
    --wx-lit-rgb: 255, 236, 186; --wx-lit-a: 0.075;
    /* Clear air is exactly where shafts and flare belong — nothing is in the way of the light. */
    --wx-rays: 1; --wx-ray-a: 0.035; --wx-flare: 1;
    /* Distance haze standing over the water, and the sky lifting toward the horizon. */
    --wx-glow-rgb: 186, 214, 245; --wx-glow-a: 0.13; --wx-glow-h: 30%;
    --wx-horizon-rgb: 190, 220, 250; --wx-horizon-a: 0.10;
}

/* Clear night. The moon is smaller, colder and tighter — a hard little disc with far less corona,
   which is what actually distinguishes it from the sun. Cover 0 for the same reason as above,
   and it matters twice over here: the stars are drawn on the canvas UNDER the decks, so a haze of
   phantom cloud on a clear night was quietly wiping out half the sky's best feature. */
.wx-page-sky[data-sky="moon"] {
    --wx-wash-a: #02060f; --wx-wash-b: #0a1330; --wx-wash-c: #1a2a55;
    --wx-cover: 0;
    --wx-cloud-rgb: 148, 163, 184; --wx-cloud-a: 0.06;
    --wx-orb-core: 240, 244, 250; --wx-orb-glow: 176, 205, 255;
    --wx-orb-r: 1.7vmax; --wx-orb-x: 84%; --wx-orb-y: 12%;
    /* Scaled by how much of the moon is lit: a crescent does not light a sky the way a full moon
       does, and drawing the same corona around a sliver was the tell that the disc was a decal. */
    --wx-orb-core-a: 0.88; --wx-orb-o: calc(0.92 * var(--wx-moon-lum, 1));
    /* Moonlight is cold and there is far less of it. */
    --wx-lit-rgb: 203, 222, 255; --wx-lit-a: 0.03;
    /* No shafts from a moon — there is not enough light to scatter — but the glass still flares. */
    --wx-rays: 0; --wx-flare: 0.55;
    /* The port at night: sodium and mercury off the yards, thrown up at the sky. */
    --wx-glow-rgb: 255, 176, 92; --wx-glow-a: 0.11; --wx-glow-h: 22%;
    --wx-horizon-rgb: 255, 186, 112; --wx-horizon-a: 0.05;
}

/* Sun behind broken cloud: still there, softened and dimmed, and now with real gaps in the deck
   for it to come through. This is the sky the ray shafts were invented for. */
.wx-page-sky[data-sky="partly-day"] {
    --wx-wash-a: #123156; --wx-wash-b: #224a77; --wx-wash-c: #456f9a;
    --wx-cover: 0.62;
    --wx-cloud-rgb: 216, 228, 243; --wx-cloud-a: 0.115;
    --wx-orb-core: 255, 248, 222; --wx-orb-glow: 252, 197, 78;
    --wx-orb-r: 2.1vmax; --wx-orb-x: 76%; --wx-orb-y: 11%;
    --wx-orb-core-a: 0.62; --wx-orb-o: 0.8;
    /* Broken cloud is where a lit top edge matters most — it is the whole shape of a cumulus. */
    --wx-lit-rgb: 255, 240, 205; --wx-lit-a: 0.095;
    /* Broken cloud is also where shafts are strongest: the gaps are what make them visible. */
    --wx-rays: 1; --wx-ray-a: 0.055; --wx-flare: 0.7;
    /* Broken cumulus: the lumpy near deck leads, and its tile runs bigger so the lumps are
       cumulus-sized rather than a fine cottage-cheese texture. Resizing the near deck used to be
       a trap — it would desynchronise from the lit highlight riding the same mask — but both now
       read the same --wx-tile-near-* pair, so they cannot come apart. */
    --wx-k-near: 1.15; --wx-k-far: 0.9;
    --wx-tile-near-w: 1560px; --wx-tile-near-h: 1080px;
    --wx-glow-rgb: 178, 206, 238; --wx-glow-a: 0.10; --wx-glow-h: 26%;
    --wx-horizon-rgb: 182, 210, 240; --wx-horizon-a: 0.08;
}

.wx-page-sky[data-sky="partly-night"] {
    --wx-wash-a: #03081a; --wx-wash-b: #0d1832; --wx-wash-c: #1e3057;
    --wx-cover: 0.6;
    --wx-cloud-rgb: 158, 174, 196; --wx-cloud-a: 0.085;
    --wx-orb-core: 232, 238, 248; --wx-orb-glow: 191, 219, 254;
    --wx-orb-r: 1.9vmax; --wx-orb-x: 80%; --wx-orb-y: 13%;
    --wx-orb-core-a: 0.55; --wx-orb-o: calc(0.66 * var(--wx-moon-lum, 1));
    --wx-rays: 0; --wx-flare: 0.4;
    --wx-k-near: 1.1; --wx-k-far: 0.9;
    --wx-tile-near-w: 1560px; --wx-tile-near-h: 1080px;
    --wx-glow-rgb: 255, 172, 96; --wx-glow-a: 0.10; --wx-glow-h: 20%;
    --wx-horizon-rgb: 255, 186, 112; --wx-horizon-a: 0.045;
}

/* WMO 3 is overcast, and it is the only code that maps to this kind — partly cloudy is its own
   partly-day / partly-night. So this is the lid: no light source at all, and the cloud closes
   over the top of the page. */
/* From here down the sky is closed in, so --wx-orb-o stays 0: you cannot see the sun through
   overcast, and drawing one anyway is the single fastest way to make a weather scene look fake. */

/* Overcast is a LID, and it was the weakest sky here — a couple of pale shapes drifting over a
   flat wash, which is the one condition that reading has no shapes in it at all. It is now the
   heaviest cloud in the set, moving slowest (an overcast deck barely appears to move) with a
   strong veil pulling the ceiling down over the top of the page. */
.wx-page-sky[data-sky="cloud"] {
    --wx-wash-a: #171d2c; --wx-wash-b: #2c3548; --wx-wash-c: #525d72;
    --wx-cover: 1;
    --wx-cloud-rgb: 214, 222, 233; --wx-cloud-a: 0.19;
    --wx-veil-rgb: 176, 189, 206; --wx-veil-a: 0.16;
    --wx-drift: 165s;
    /* A closed sky is lit from underneath by whatever is on the ground, and over a working port
       that is a lot. This is the pale grey lift along the bottom of every overcast day. */
    --wx-glow-rgb: 203, 213, 225; --wx-glow-a: 0.10; --wx-glow-h: 24%;
    --wx-horizon-rgb: 203, 213, 225; --wx-horizon-a: 0.09;
}

/* Under a closed sky the lit edge nearly vanishes — overcast is lit flatly from above, with no
   single light source to catch a top edge. Leaving the highlight at full strength was the one
   thing making overcast look like cumulus.
   (An earlier version also resized the near deck here. That silently desynchronised it from the
   highlight layer, which rides the same mask and has to travel the same distance — the volume
   effect only works while the two are in exact lockstep, and one condition-specific override was
   enough to break it. The evolution layer gives overcast its variation instead.) */
.wx-page-sky[data-sky="cloud"] {
    --wx-lit-a: 0.018;
    /* And the lumps go with it. Overcast is the SMOOTH deck: a lid with a slow undulation, not a
       field of cotton balls — which is what one shared alpha across both decks was producing. */
    --wx-k-near: 0.5;
    --wx-k-far: 1.15;
}

/* Fog is not a thinner overcast, which is how it used to be drawn — it is the cloud standing on
   the ground with you in it. The ceiling above thins right out (you cannot see it), the banks come
   in on their own layer at eye level, and everything loses contrast the way it does when you are
   looking through half a mile of suspended water. */
.wx-page-sky[data-sky="fog"] {
    --wx-wash-a: #1d2430; --wx-wash-b: #303845; --wx-wash-c: #59616e;
    --wx-cover: 0.4;
    --wx-cloud-rgb: 226, 232, 240; --wx-cloud-a: 0.09;
    --wx-veil-rgb: 226, 232, 240; --wx-veil-a: 0.16;
    --wx-fog: 0.5; --wx-fog-rgb: 214, 223, 235;
    --wx-glow-rgb: 226, 232, 240; --wx-glow-a: 0.13; --wx-glow-h: 34%;
    --wx-horizon-rgb: 226, 232, 240; --wx-horizon-a: 0.10;
    --wx-drift: 200s;
}

/* Drizzle falls out of a low, ragged, unremarkable sky — the deck is complete but thin, and there
   is no drama anywhere in it. That is the condition, and it should look like it. */
.wx-page-sky[data-sky="drizzle"] {
    --wx-wash-a: #101728; --wx-wash-b: #1e2a3d; --wx-wash-c: #3d4b62;
    --wx-cover: 0.85;
    --wx-cloud-rgb: 198, 214, 232; --wx-cloud-a: 0.10;
    --wx-veil-rgb: 148, 163, 184; --wx-veil-a: 0.06;
    --wx-fog: 0.18; --wx-fog-rgb: 186, 200, 218;
    --wx-glow-rgb: 176, 195, 214; --wx-glow-a: 0.08; --wx-glow-h: 22%;
    --wx-drift: 120s;
}

/* Rain: a full deck, and the ground lit by its own wet reflection — the sheen off a soaked yard
   under the lights is most of what a rainy night at the port looks like. */
.wx-page-sky[data-sky="rain"] {
    --wx-wash-a: #080e1a; --wx-wash-b: #152032; --wx-wash-c: #2e3f57;
    --wx-cover: 0.95;
    --wx-cloud-rgb: 190, 213, 236; --wx-cloud-a: 0.13;
    --wx-veil-rgb: 125, 211, 252; --wx-veil-a: 0.07;
    --wx-fog: 0.14; --wx-fog-rgb: 176, 199, 222;
    --wx-glow-rgb: 165, 200, 232; --wx-glow-a: 0.10; --wx-glow-h: 20%;
    --wx-lit-rgb: 203, 222, 245; --wx-lit-a: 0.03;
}

.wx-page-sky[data-sky="sleet"] {
    --wx-wash-a: #0a1122; --wx-wash-b: #1a2537; --wx-wash-c: #374861;
    --wx-cover: 1;
    --wx-cloud-rgb: 216, 226, 248; --wx-cloud-a: 0.12;
    --wx-veil-rgb: 199, 210, 254; --wx-veil-a: 0.07;
    --wx-glow-rgb: 199, 210, 254; --wx-glow-a: 0.09; --wx-glow-h: 20%;
    --wx-lit-rgb: 214, 226, 255; --wx-lit-a: 0.028;
}

/* Snow is the one condition that reads BRIGHTER, not darker — a snowy sky is lit from the ground
   up, and the ground is the brightest thing in the frame. The glow here is doing that job, and it
   is why a snow scene feels lit from below without a single extra layer. */
.wx-page-sky[data-sky="snow"] {
    --wx-wash-a: #1b2439; --wx-wash-b: #2f3b53; --wx-wash-c: #5c6880;
    --wx-cover: 0.9;
    --wx-cloud-rgb: 241, 245, 249; --wx-cloud-a: 0.13;
    --wx-veil-rgb: 241, 245, 249; --wx-veil-a: 0.10;
    --wx-fog: 0.12; --wx-fog-rgb: 241, 245, 249;
    --wx-glow-rgb: 236, 244, 255; --wx-glow-a: 0.16; --wx-glow-h: 26%;
    --wx-horizon-rgb: 236, 244, 255; --wx-horizon-a: 0.11;
    --wx-lit-rgb: 255, 255, 255; --wx-lit-a: 0.045;
    --wx-drift: 150s;
}

/* Storm. The heaviest lid in the set, moving fastest, with the cloud tinted the bruised indigo a
   thunderhead actually goes — and the lightning below lights THIS deck, which is why the tint has
   to be a colour that can be driven bright. */
.wx-page-sky[data-sky="storm"] {
    --wx-wash-a: #05080f; --wx-wash-b: #131a2c; --wx-wash-c: #2c334e;
    --wx-cover: 1;
    --wx-cloud-rgb: 150, 160, 220; --wx-cloud-a: 0.14;
    --wx-veil-rgb: 99, 102, 241; --wx-veil-a: 0.09;
    --wx-glow-rgb: 148, 163, 220; --wx-glow-a: 0.07; --wx-glow-h: 18%;
    --wx-lit-rgb: 205, 214, 255; --wx-lit-a: 0.03;
    --wx-drift: 34s;
}

/* LIGHTNING lights the sky, not a rectangle in front of it. The canvas draws the flash itself
   (see the strike envelope in the JS); this is the part that matters — the strike is BEHIND the
   cloud, so what you actually see is the deck lighting up from inside and the whole scene lifting
   for a fifth of a second. --wx-flash is a registered <number> so it can be driven per frame.
   Only the storm sky pays for these filters; every other condition leaves --wx-flash at 0 and the
   compositor skips them. */
.wx-page-sky[data-sky="storm"] .wx-page-sky__clouds {
    filter: brightness(calc(1 + var(--wx-flash) * 3.4));
}

.wx-page-sky[data-sky="storm"] .wx-page-sky__wash {
    filter: brightness(calc(1 + var(--wx-flash) * 0.85));
}

/* Fog banks are a fog-only layer. Left running everywhere they would be two more infinite
   animations composited behind every other sky for nothing. */
.wx-page-sky:not([data-sky="fog"]):not([data-sky="drizzle"]):not([data-sky="rain"]):not([data-sky="snow"]) .wx-page-sky__fog {
    display: none;
}

/* ── Time of day ────────────────────────────────────────────────────────────
   Applied on top of the condition, so "raining at 3 AM" is not the same page as
   "raining at noon". Night pulls every wash toward black; twilight warms the
   horizon the way a real one does. These rules come AFTER the conditions on
   purpose: equal specificity, so the later one wins and the hour gets the last
   word on where the sun sits and what colour it is. */
/* Eased off from 0.58: the wash palettes now start dark at the zenith by themselves, so the old
   night multiplier on top of them crushed the whole frame to one flat black. */
.wx-page-sky[data-phase="night"] .wx-page-sky__wash { filter: brightness(0.8) saturate(0.9); }
.wx-page-sky[data-phase="night"] .wx-page-sky__clouds { opacity: 0.55; }
/* After dark the ground light is the only light there is, so it gets warmer and reaches further —
   this is the sodium dome over a working port, and it is what a night shift looks up at. */
.wx-page-sky[data-phase="night"] {
    --wx-glow-rgb: 255, 168, 88;
    --wx-glow-a: 0.18;
    --wx-glow-h: 28%;
}

/* TWILIGHT is a condition of its own, not a dimmer switch. The sun does not fade out where it was
   at noon — it goes down, reddens hard as its light travels through the whole atmosphere, and
   throws the band of colour that everybody photographs. So it is moved to the horizon, resized,
   recoloured, and the glow underneath it turns to fire. The shafts go up with it: rays are at
   their most visible when the light is coming in almost flat. */
.wx-page-sky[data-phase="twilight"] .wx-page-sky__wash { filter: brightness(0.86) saturate(1.2); }
.wx-page-sky[data-phase="twilight"] {
    /* Low and to the right rather than ON the horizon. The horizon is the one part of this page
       nobody can see: the app column is 1500px of opaque panels down the middle, so a sun placed
       at 76% height and 68% across — where a setting sun physically belongs — was drawn entirely
       behind the calendar. The visible sky on this layout is the top strip and the two side
       margins, and that is where the light has to be for anyone to get any of it. The warm band
       along the bottom still does the rest of the work, in the margins where it shows. */
    --wx-orb-y: 34%;
    --wx-orb-x: 88%;
    --wx-orb-r: 2.6vmax;
    --wx-orb-core: 255, 226, 176;
    --wx-orb-glow: 247, 118, 52;
    --wx-lit-rgb: 255, 190, 130;
    --wx-glow-rgb: 249, 132, 62;
    --wx-glow-a: 0.22;
    --wx-glow-h: 36%;
    --wx-horizon-rgb: 253, 186, 116;
    --wx-horizon-a: 0.12;
    /* Eased back from 0.075. Shafts this close to the horizon are long and cross the whole page,
       so the same alpha that reads as haze at noon reads as a sunburst decal at sunset. */
    --wx-ray-a: 0.042;
}

/* The band itself: fire along the water, cooling up through rose into the blue that is still up
   there. Three stops rather than two — a two-stop sunset is a colour wash, and the transition
   from warm to cool is the entire reason a sunset is worth looking at. */
.wx-page-sky[data-phase="twilight"]::before {
    content: '';
    position: absolute;
    inset: auto 0 0 0;
    height: 62%;
    z-index: 1;
    background: linear-gradient(0deg,
        rgba(251, 146, 60, 0.20) 0%,
        rgba(244, 114, 182, 0.11) 30%,
        rgba(129, 140, 248, 0.05) 62%,
        transparent 100%);
}

/* Reduced motion: the sky still tells you what it is doing, it just stops moving. The two
   cross-fading decks have to be pinned to a readable blend when their animation stops — the
   evolve layer's resting opacity is 0, so without this it freezes invisible and the sky loses
   half its structure the moment motion is switched off. */
.wx-page-sky.is-still .wx-page-sky__clouds,
.wx-page-sky.is-still .wx-page-sky__rays,
.wx-page-sky.is-still .wx-page-sky__fog,
.wx-page-sky.is-still .wx-page-sky__light { animation: none; transform: none; }
.wx-page-sky.is-still .wx-page-sky__clouds--far { opacity: 0.5; }
.wx-page-sky.is-still .wx-page-sky__clouds--evolve { opacity: 0.42; }
/* The fog layer's opacity is normally held by an animation; with the animation gone it falls back
   to the declared value, which is what we want — but state it so the intent survives a refactor. */
.wx-page-sky.is-still .wx-page-sky__fog { opacity: var(--wx-fog); }

@media (prefers-reduced-motion: reduce) {
    .wx-page-sky__clouds,
    .wx-page-sky__rays,
    .wx-page-sky__fog,
    .wx-page-sky__light { animation: none; transform: none; }
    .wx-page-sky__clouds--far { opacity: 0.5; }
    .wx-page-sky__clouds--evolve { opacity: 0.42; }
    .wx-page-sky__fog { opacity: var(--wx-fog); }
}

/* Small screens carry the phone's own battery, and the sky is the least of what
   the page is for. Keep the colour and the grain, drop the moving parts and the
   layers that only pay off on a wide canvas. The rays and the flare stay: they are
   two static gradients, they cost a phone nothing, and they are most of what makes
   a clear sky look like a photograph rather than a fill colour. */
@media (max-width: 720px) {
    .wx-page-sky__clouds { animation: none; transform: none; }
    .wx-page-sky__clouds--far { opacity: 0.5; }
    .wx-page-sky__clouds--evolve { opacity: 0.42; }
    .wx-page-sky__clouds--near,
    .wx-page-sky__clouds--lit { display: none; }
    .wx-page-sky__rays { animation: none; }
    .wx-page-sky__fog { animation: none; opacity: var(--wx-fog); }
}

.hours-header {
    display: flex;
    justify-content: space-between;
    gap: 16px;
    align-items: flex-start;
    margin-bottom: 14px;
}

.hours-header h1 {
    margin: 6px 0 4px;
    font-size: 32px;
}

.hours-header p {
    margin: 0;
    color: #a5b4fc;
    font-weight: 600;
}

.back-link {
    color: #93c5fd;
    text-decoration: none;
    font-weight: 600;
}

.back-link:hover {
    color: #dbeafe;
}

.hours-header-right {
    display: flex;
    gap: 12px;
    /* Top-aligned. Centring boxes of four different heights left every one of them sitting at a
       different vertical position, which is most of why the row looked unplanned. */
    align-items: flex-start;
    flex-wrap: wrap;
    justify-content: flex-end;
}

/* The read-only instruments, as one block: clock and Port & Bay side by side, badge-ins under
   them across the full width. Two tidy rectangles in the header — the sky, then this — instead
   of five loose boxes. */
.header-instruments {
    display: grid;
    grid-template-columns: auto auto;
    gap: 8px;
    /* Stretch, so the clock and Port & Bay share a row edge instead of ending 21px apart. */
    align-items: stretch;
}

.header-instruments .badge-ins-wrap {
    grid-column: 1 / -1;
}

/* Given a taller cell, centre the readings in it rather than letting them ride the top. */
.header-instruments .server-clock,
.header-instruments .marine-alerts-widget {
    justify-content: center;
}

/* The one action up here. Centred against the cluster rather than pinned to the top corner,
   where a 33px button beside two 165px blocks just looked dropped. */
.hours-header-right .action-btn.ghost {
    align-self: center;
}

/* ── One frame for the instruments ───────────────────────────────────────
   These had grown four different border colours, three radii, two background treatments and a
   stray drop shadow, so the cluster read as unrelated parts. The frame is shared now and the
   accent lives on the icon and the value, where it identifies the reading rather than outlining
   the box. The weather keeps its own themed border on purpose: it is the one live element up
   here, and spending the colour budget on it is the point. */
.server-clock,
.marine-alerts-widget,
.badge-ins-widget,
.badge-ins-combined {
    border: 1px solid rgba(148, 163, 184, 0.22);
    background: rgba(15, 23, 42, 0.72);
    border-radius: 12px;
    box-shadow: none;
}

.weather-widget { border-radius: 12px; }

.hours-header-right .action-btn.ghost { border-radius: 12px; }


.view-toggle {
    display: flex;
    background: rgba(15, 23, 42, 0.8);
    border: 1px solid rgba(148, 163, 184, 0.25);
    border-radius: 999px;
    padding: 4px;
}

.view-btn {
    border: none;
    border-radius: 999px;
    padding: 8px 16px;
    font-weight: 700;
    cursor: pointer;
    background: transparent;
    color: #cbd5e1;
}

.view-btn.active {
    background: linear-gradient(135deg, #7c3aed, #ec4899);
    color: #fff;
}

.admin-bar {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 14px;
    padding: 10px 12px;
    /* Matches the header frames' radius. The magenta stays: this bar is a mode indicator — you
       are looking at someone else's hours — and that is exactly when colour earns its place. */
    border-radius: 12px;
    border: 1px solid rgba(236, 72, 153, 0.3);
    background: rgba(131, 24, 67, 0.24);
}

/* A worker switch refetches the month, the contract year and five side loads, so it is never
   instant. Pulse the bar while it is in flight rather than leaving the previous man's numbers
   sitting on screen looking current. */
.admin-bar--busy {
    animation: admin-bar-busy 1.1s ease-in-out infinite;
}

@keyframes admin-bar-busy {
    0%, 100% { border-color: rgba(236, 72, 153, 0.3); }
    50% { border-color: rgba(236, 72, 153, 0.85); }
}

@media (prefers-reduced-motion: reduce) {
    .admin-bar--busy { animation: none; border-color: rgba(236, 72, 153, 0.85); }
}

.admin-bar label {
    font-weight: 700;
    color: #fbcfe8;
}

.admin-bar input,
.admin-bar select {
    max-width: 340px;
}

select,
input,
textarea {
    width: 100%;
    border-radius: 10px;
    border: 1px solid rgba(148, 163, 184, 0.35);
    background: rgba(15, 23, 42, 0.85);
    color: #e2e8f0;
    padding: 10px 12px;
    font-size: 14px;
}

textarea {
    resize: vertical;
}

.hours-main {
    display: grid;
    grid-template-columns: minmax(0, 2.1fr) minmax(320px, 1fr);
    gap: 16px;
    /* Start, not stretch, so the side panel is free to stick rather than being pulled to the
       full height of a twelve-month calendar. */
    align-items: start;
}

/* The year grid runs far taller than the day panel, which left a long dead column beside it and
   put the entry form off-screen exactly when you scrolled to the day you wanted to log. It
   follows now. Capped and scrollable so a long day never traps its own Save button. */
@media (min-width: 1101px) {
    .editor-panel {
        position: sticky;
        top: 16px;
        max-height: calc(100vh - 32px);
        overflow-y: auto;
        scrollbar-width: thin;
        scrollbar-color: rgba(148, 163, 184, 0.35) transparent;
    }
}

.hours-content {
    display: grid;
    gap: 16px;
}

.panel {
    background: rgba(15, 23, 42, 0.78);
    border: 1px solid rgba(148, 163, 184, 0.22);
    border-radius: 14px;
    padding: 14px;
    backdrop-filter: blur(5px);
}

.panel-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 12px;
}

.panel-header h2 {
    margin: 0;
    font-size: 20px;
}

.nav-btn,
.action-btn {
    border: none;
    border-radius: 10px;
    cursor: pointer;
    padding: 9px 12px;
    font-weight: 700;
}

.nav-btn {
    background: rgba(30, 41, 59, 0.95);
    color: #cbd5e1;
}

.action-btn.primary {
    background: linear-gradient(135deg, #2563eb, #7c3aed);
    color: #fff;
}

.action-btn.ghost {
    background: rgba(30, 41, 59, 0.88);
    color: #cbd5e1;
}

.action-btn.full-width {
    width: 100%;
}

/* Three across, so six cards make two full rows. auto-fit was packing five into row one and
   orphaning the sixth beside a half-empty gap, which is the single untidiest thing on the page.
   Hours / days / average, then gross / tax / net — the split is also the meaning. */
.summary-cards {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 10px;
    margin-bottom: 12px;
}

@media (max-width: 900px) {
    .summary-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 520px) {
    .summary-cards { grid-template-columns: 1fr; }
}

.money-scope-toggle {
    display: inline-flex;
    gap: 2px;
    margin-bottom: 12px;
    padding: 3px;
    border-radius: 999px;
    background: rgba(15, 23, 42, 0.8);
    border: 1px solid rgba(148, 163, 184, 0.22);
}

.money-scope-btn {
    border: none;
    border-radius: 999px;
    padding: 6px 14px;
    font-size: 13px;
    font-weight: 700;
    cursor: pointer;
    background: transparent;
    color: #94a3b8;
    transition: color 120ms ease, background-color 120ms ease;
}

.money-scope-btn:hover { color: #e2e8f0; }

.money-scope-btn.is-active {
    background: linear-gradient(135deg, #2563eb, #7c3aed);
    color: #fff;
}

/* ── The Run ─────────────────────────────────────────────────────────────
   The only element on the page that answers "am I going to make my hours".
   One axis of WORKING days across the contract year: a white hairline for today,
   a crane-yellow fill for how far the banked hours carry you at even pace. The
   gap between them is the verdict, and it is the only place colour is spent. */
/* ── The Rule ──────────────────────────────────────────────────────────────────────────────
   The contract year drawn as a machinist's scale rather than a progress bar. Brass is the hours
   banked, the scribe line is where the calendar says you should be, and the tag riding that line
   carries the one number the card exists to give you — so the verdict sits on the mark it is
   measured from instead of floating in a corner. Gradations are spaced by WORKING days, like the
   scribe above them, so a month with more shifts in it takes up more of the rule.
   Numerals are set in the mono face throughout: this is a gauge being read, not a headline. */
.pace {
    --rule-plate: #0a0f1a;
    --rule-brass: #c9a227;
    --rule-brass-lit: #e8c766;
    --rule-brass-deep: #7a5f16;
    --rule-verdict: #94a3b8;
    --rule-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", monospace;
    position: relative;
    margin-bottom: 16px;
    padding: 15px 18px 13px;
    border-radius: 12px;
    border: 1px solid rgba(148, 163, 184, 0.18);
    background:
        linear-gradient(180deg, rgba(148, 163, 184, 0.05), transparent 42%),
        var(--rule-plate);
}

/* Ahead of pace and past the target are two different facts — one is about the calendar, the other
   about the number — but they are the same direction of good news, so they read as one scale
   rather than two unrelated states. Made escalates by FILLING the plate rather than by changing
   hue; painting it brass put the verdict in the same colour as the rule it sits on, which is where
   a good answer goes to hide. */
.pace--ahead { --rule-verdict: #4ade80; }
.pace--behind { --rule-verdict: #fb7185; }
.pace--made { --rule-verdict: #4ade80; }

.pace--made .pace-scribe-tag {
    background: #4ade80;
    border-color: #4ade80;
    color: #052e16;
}

.pace-eyebrow {
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: #56657c;
}

.pace-figure {
    display: flex;
    align-items: baseline;
    gap: 6px;
    margin: 11px 0 36px;
    font-family: var(--rule-mono);
}

.pace-banked {
    font-size: 38px;
    font-weight: 700;
    line-height: 0.85;
    letter-spacing: -0.045em;
    font-variant-numeric: tabular-nums;
    color: #f8fafc;
}

.pace-slash {
    font-size: 20px;
    font-weight: 400;
    color: #3d4a5f;
}

/* The target is the one thing here you can change, so it is the one thing underlined. */
.pace-target {
    font-family: inherit;
    font-size: 20px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    letter-spacing: -0.02em;
    color: var(--rule-brass-lit);
    background: none;
    border: 0;
    border-bottom: 1px dashed rgba(201, 162, 39, 0.5);
    padding: 0 1px 2px;
    cursor: pointer;
}

.pace-target:hover { color: #ffd97a; border-bottom-color: #ffd97a; }

.pace-target:focus-visible {
    outline: 2px solid var(--rule-brass-lit);
    outline-offset: 3px;
    border-radius: 2px;
}

.pace-unit {
    font-size: 13px;
    color: #56657c;
}

/* Splits the headline figure back into its two parts. Without it the hero reads 696 while the
   card below reads 263, with nothing on screen connecting them. */
.pace-split {
    margin-left: auto;
    align-self: flex-end;
    font-size: 11px;
    color: #64748b;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    cursor: help;
}

.pace-split span { color: #3d4a5f; padding: 0 2px; }

/* The two figures are the legend for the two shades on the rule — each number carries the colour of
   the span it measures, so nobody has to be told which brass is which. */
.pace-split b { font-weight: 700; }
.pace-split .is-tracked { color: var(--rule-brass-lit); }
.pace-split .is-carried { color: #a08a4a; }

.pace-rule { position: relative; }

/* ── The year, month by month ───────────────────────────────────────────────────────────────
   ONE instrument with ONE meaning on its y-axis: the dashed line is the pace, and a column is
   that month's hours against what that month owed. Over the line is a month you beat it, under
   is one you did not — so which month you worked hardest is a height comparison, the thing eyes
   are actually good at. The cumulative climb this replaced could not answer that at all: reading
   slope is far harder than reading height. Widths are each month's share of the working days, so
   a short month is a narrow column and the heights stay a fair comparison. */
.pace-year {
    position: relative;
    height: 58px;
    border-bottom: 1px solid rgba(148, 163, 184, 0.22);
}

/* Keeping up. The one reference the columns are read against. */
/* The reference height: a month that exactly kept up with its share of the target. Every column is
   drawn against it, and until it was labelled the tooltips referred to "the line" on a chart that
   never said which line or what it meant. It sits above the columns so the tag stays readable when
   a tall month runs behind it. */
.pace-pace-line {
    position: absolute;
    left: 0;
    right: 0;
    height: 0;
    border-top: 1px dashed rgba(232, 199, 102, 0.55);
    z-index: 2;
}

.pace-pace-line::after {
    content: 'PACE';
    position: absolute;
    right: 0;
    bottom: 2px;
    font-size: 8px;
    font-weight: 700;
    letter-spacing: 0.14em;
    color: rgba(232, 199, 102, 0.75);
    /* Sits over whatever column happens to be at the right-hand end of the year. */
    text-shadow: 0 0 4px #0d1220, 0 0 4px #0d1220, 0 0 2px #0d1220;
    pointer-events: none;
}

.pace-cell {
    position: absolute;
    top: 0;
    bottom: 0;
    display: flex;
    align-items: flex-end;
    padding-right: 3px;
    box-shadow: inset 1px 0 0 rgba(148, 163, 184, 0.13);
}

/* A month that has not started yet is drawn as its empty slot rather than as a zero — nothing was
   owed and nothing was missed. */
.pace-cell--future { box-shadow: inset 1px 0 0 rgba(148, 163, 184, 0.08); }

.pace-cell-bar {
    display: block;
    width: 100%;
    min-height: 1px;
    border-radius: 1px 1px 0 0;
    background: linear-gradient(180deg, var(--rule-brass-lit), var(--rule-brass) 60%, var(--rule-brass-deep));
}

/* THE MONTH YOU ARE IN.
   This was a full-height emerald ring around the cell, and on the 2nd of a month that made the
   emptiest column on the chart the brightest thing on it: August owed a whole month and had one
   day in it, so a bright box round a bar of almost no height read as a catastrophic month rather
   than a new one. The height is honest — the column fills as the month goes — but the ring was
   putting emphasis exactly where there was nothing to see.
   So the running month is a quiet wash over its column, and the emerald + pulsing dot move to the
   month's LABEL, which is what the calendar highlights too: the number, not the whole square. */
.pace-cell--now {
    background: linear-gradient(180deg, rgba(16, 185, 129, 0.09), rgba(16, 185, 129, 0.015));
    box-shadow: inset 1px 0 0 rgba(16, 185, 129, 0.5);
}

.pace-marks {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    height: 58px;
    pointer-events: none;
}

/* The gradations themselves are drawn inside the graph; this layer only carries their labels. */
.pace-mark { position: absolute; top: 0; height: 0; width: 0; }

.pace-mark-label {
    position: absolute;
    top: 63px;
    left: 3px;
    font-size: 9px;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: #46536a;
    white-space: nowrap;
}

/* Same emerald and same pulse as .heat-cell.is-today on the calendar below — a page with two
   different ways of saying "now" is a page you have to learn twice. */
.pace-mark--now .pace-mark-label {
    color: #10b981;
    padding-left: 9px;
}

.pace-mark--now .pace-mark-label::before {
    content: '';
    position: absolute;
    left: 0;
    top: 50%;
    width: 5px;
    height: 5px;
    margin-top: -2px;
    border-radius: 50%;
    background: #10b981;
    box-shadow: 0 0 6px rgba(16, 185, 129, 0.9);
    animation: hoursTodayPulse 1.6s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
    .pace-mark--now .pace-mark-label::before { animation: none; }
}

.pace-scribe {
    position: absolute;
    top: -7px;
    /* Runs the full height of the climb, so today is one line through the whole reading. */
    height: 67px;
    width: 1px;
    background: #f8fafc;
    box-shadow: 0 0 0 1px rgba(6, 10, 18, 0.85);
}

.pace-scribe::after {
    content: '';
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    border-left: 3px solid transparent;
    border-right: 3px solid transparent;
    border-top: 4px solid #f8fafc;
}

.pace-scribe-tag {
    position: absolute;
    bottom: calc(100% + 5px);
    left: 5px;
    white-space: nowrap;
    font-family: var(--rule-mono);
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--rule-verdict);
    background: var(--rule-plate);
    border: 1px solid currentColor;
    border-radius: 3px;
    padding: 3px 7px;
}

/* Past the middle of the rule the tag would run off the end, so it hangs the other way. */
.pace-scribe--flip .pace-scribe-tag { left: auto; right: 5px; }

.pace-foot {
    display: flex;
    flex-wrap: wrap;
    gap: 8px 26px;
    margin-top: 82px;
    font-family: var(--rule-mono);
}

.pace-stat {
    display: flex;
    align-items: baseline;
    gap: 6px;
}

.pace-stat b {
    font-size: 15px;
    font-weight: 700;
    letter-spacing: -0.02em;
    color: #e2e8f0;
    font-variant-numeric: tabular-nums;
}

.pace-stat i {
    font-style: normal;
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: #56657c;
}

/* Twelve gradation labels will not fit on a phone; drop to a quarterly read, keeping September —
   the month the year runs out is the one a man checking this actually measures against. The
   gradations themselves all stay, so the scale keeps its resolution. */
@media (max-width: 660px) {
    .pace-banked { font-size: 32px; }
    .pace-target { font-size: 18px; }
    .pace-split { margin-left: 0; flex-basis: 100%; white-space: normal; }
    /* The split wraps to its own line here, and the verdict tag hangs above the rule — without the
       extra room the tag sits on top of the target's underline. */
    .pace-figure { flex-wrap: wrap; margin-bottom: 40px; }
    /* The month you are in is never one of the ones dropped — it is the only label on the scale
       that says where you are standing, and on a phone it was hidden eight months in twelve. */
    .pace-mark:not(:nth-child(3n+1)):not(:last-child):not(.pace-mark--now) .pace-mark-label { display: none; }
    .pace-foot { gap: 8px 18px; }
}

@media (prefers-reduced-motion: reduce) {
    /* revealPaceGraphOnce checks the same preference before it sets an inline transition; this is
       the belt to that pair of braces. */
    .pace-year { transition: none !important; }
}

@media (max-width: 640px) {
    .pace { padding: 14px 14px 12px; }
    /* Four spans do not fit on one line at 390px — wrap rather than clip the last one. */
    .money-scope-toggle { display: flex; flex-wrap: wrap; }
    .money-scope-btn { padding: 6px 11px; }
}

.summary-card {
    border-radius: 10px;
    border: 1px solid rgba(148, 163, 184, 0.22);
    background: rgba(15, 23, 42, 0.65);
    padding: 10px;
}

.summary-card .label {
    color: #a5b4fc;
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    margin-bottom: 4px;
}

.hours-target-switch {
    margin-left: 8px;
    border: 1px solid rgba(148, 163, 184, 0.4);
    background: rgba(30, 41, 59, 0.85);
    color: #e2e8f0;
    font-size: 11px;
    font-weight: 700;
    border-radius: 999px;
    padding: 2px 8px;
    cursor: pointer;
    vertical-align: middle;
}

.hours-target-switch:hover {
    border-color: rgba(129, 140, 248, 0.7);
    color: #ffffff;
}

.summary-card .value {
    font-size: 24px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}

/* Splits a headline figure back into its parts, so a total that includes hours the worker typed
   in never looks unrelated to the tracked number it was built from. */
.summary-sub {
    margin-top: 3px;
    font-size: 11px;
    font-weight: 600;
    color: #64748b;
    font-variant-numeric: tabular-nums;
}

/* Hours are the subject of the page; the rest of the row supports them. */
.summary-card--hours {
    border-color: rgba(251, 191, 36, 0.35);
    background: rgba(251, 191, 36, 0.07);
}

.summary-card--hours .label { color: #fbbf24; }

/* Carried-in hours. Visible, not folded away: it is the one setting that moves the headline
   total, and it was unfindable while it sat inside the collapsed wage panel. */
.carried-hours {
    margin-top: 18px;
    border-top: 1px solid rgba(148, 163, 184, 0.18);
    padding-top: 14px;
    display: grid;
    gap: 8px;
}

.carried-hours > label {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 14px;
    font-weight: 700;
    color: #cbd5e1;
}

.carried-hours > label i {
    color: #38bdf8;
}

.carried-hours-row {
    display: flex;
    gap: 8px;
    align-items: stretch;
}

.carried-hours-row input {
    flex: 1 1 auto;
    min-width: 0;
}

.carried-hours-row .action-btn {
    flex: 0 0 auto;
    white-space: nowrap;
}

.carried-hours-row .action-btn.is-saved {
    background: #10b981;
    border-color: #10b981;
    color: #04211a;
}

.carried-hours-hint {
    margin: 0;
    font-size: 12px;
    line-height: 1.45;
    color: #94a3b8;
}

/* The second way in to the same number. Quieter than the primary label so the block reads as one
   setting with two entry points, not two competing settings. */
.carried-hours-alt {
    margin-top: 4px;
    padding-top: 12px;
    border-top: 1px dashed rgba(148, 163, 184, 0.18);
    font-size: 13px;
    font-weight: 600;
    color: #94a3b8;
}

.carried-hours-alt i {
    color: #64748b;
}

.wage-details {
    margin-top: 18px;
    border-top: 1px solid rgba(148, 163, 184, 0.18);
    padding-top: 12px;
}

.wage-details > summary {
    display: flex;
    align-items: center;
    gap: 8px;
    cursor: pointer;
    list-style: none;
    font-size: 14px;
    font-weight: 700;
    color: #cbd5e1;
    padding: 4px 0;
}

.wage-details > summary::-webkit-details-marker { display: none; }

.wage-details > summary::after {
    content: "▾";
    margin-left: auto;
    color: #64748b;
    transition: transform 140ms ease;
}

.wage-details[open] > summary::after { transform: rotate(180deg); }

.wage-summary-rate {
    font-weight: 600;
    font-size: 12px;
    color: #64748b;
    font-variant-numeric: tabular-nums;
}

@media (prefers-reduced-motion: reduce) {
    .wage-details > summary::after { transition: none; }
}

.summary-card.money .value {
    color: #86efac;
}

.summary-card.tax .value {
    color: #fdba74;
}

.heatmap-wrap {
    overflow-x: auto;
}

.heatmap-legend {
    display: flex;
    align-items: center;
    gap: 8px 18px;
    flex-wrap: wrap;
    font-size: 12px;
    color: #94a3b8;
    margin-bottom: 10px;
}

.legend-key {
    display: flex;
    align-items: center;
    gap: 14px;
    flex-wrap: wrap;
}

.legend-item {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: #64748b;
}

.legend-swatch {
    position: relative;
    width: 13px;
    height: 13px;
    border-radius: 3px;
    flex: none;
}

/* A stepped scale, in the grid's own colours — the busy ramp is discrete, not a gradient. */
.legend-item--scale {
    gap: 4px;
    cursor: help;
}

.legend-step {
    width: 11px;
    height: 11px;
    border-radius: 2px;
    flex: none;
}

.legend-step--1 { background: var(--lvl-1); }
.legend-step--2 { background: var(--lvl-2); }
.legend-step--3 { background: var(--lvl-3); }
.legend-step--4 { background: var(--lvl-4); }
.legend-step--5 { background: var(--lvl-5); }

.legend-scale-cap {
    font-size: 10px;
    font-weight: 700;
    color: #64748b;
    font-variant-numeric: tabular-nums;
}

.legend-scale-name { margin-left: 3px; }
.legend-swatch--out {
    background: rgba(76, 29, 49, 0.9);
    background-image: repeating-linear-gradient(135deg, rgba(251, 113, 133, 0.85) 0 2px, transparent 2px 5px);
    box-shadow: inset 0 0 0 1px rgba(251, 113, 133, 0.8);
}
.legend-swatch--cut {
    background: rgba(23, 51, 79, 0.92);
    background-image: repeating-linear-gradient(135deg, rgba(96, 165, 250, 0.85) 0 2px, transparent 2px 5px);
    box-shadow: inset 0 0 0 1px rgba(96, 165, 250, 0.75);
}
/* Keeps the day's earned colour and flags the corner, exactly like .heat-cell.is-partial. */
.legend-swatch--partial {
    background: var(--lvl-2);
    box-shadow: inset -6px 6px 0 -3px #fb7185;
}
/* Must match .heat-cell.is-today exactly — an emerald ring plus the corner dot. A legend that
   draws a different marker than the grid is worse than no legend. */
.legend-swatch--today {
    background: #1e293b;
    margin: 0 3px;
    box-shadow: 0 0 0 2px #10b981, 0 0 8px rgba(16, 185, 129, 0.5);
}

.legend-swatch--today::after {
    content: '';
    position: absolute;
    top: 1px;
    right: 1px;
    width: 4px;
    height: 4px;
    border-radius: 50%;
    background: #10b981;
    box-shadow: 0 0 4px rgba(16, 185, 129, 0.9);
}

/* ── Called out ──────────────────────────────────────────────────────────
   The grid used to colour a day by how many orders it held, so a day you were
   ordered and called out looked identical to a day you worked it. A struck cell
   reads as an absence rather than a smaller quantity, and the hatch survives
   both a 13px year cell and colour-blind vision, which a hue swap alone does not. */
.heat-cell.is-out {
    background-color: rgba(76, 29, 49, 0.92) !important;
    background-image: repeating-linear-gradient(135deg, rgba(251, 113, 133, 0.85) 0 2px, transparent 2px 5px) !important;
    box-shadow: inset 0 0 0 1px rgba(251, 113, 133, 0.8);
    color: #fecdd3;
}

/* Cut from a hall job. Struck like a call-out, because no hours came of it — but in the blue
   the main page already uses for a hall cut, never the red of an actual call-out, because this
   one is not held against you. */
.heat-cell.is-cut {
    background-color: rgba(23, 51, 79, 0.94) !important;
    background-image: repeating-linear-gradient(135deg, rgba(96, 165, 250, 0.8) 0 2px, transparent 2px 5px) !important;
    box-shadow: inset 0 0 0 1px rgba(96, 165, 250, 0.75);
    color: #bfdbfe;
}

/* Worked some of it — keep the earned colour, flag the corner. */
.heat-cell.is-partial {
    position: relative;
    box-shadow: inset -8px 8px 0 -4px #fb7185;
}

.mini-cell.is-partial {
    box-shadow: inset -6px 6px 0 -3px #fb7185;
}

.heatmap-grid {
    min-height: 190px;
}

.heatmap-table {
    border-collapse: separate;
    border-spacing: 6px;
}

.weekday-label {
    width: 36px;
    color: #cbd5e1;
    font-weight: 700;
    text-align: right;
    padding-right: 8px;
}

.heat-cell {
    width: 26px;
    height: 26px;
    border-radius: 5px;
    border: 1px solid rgba(148, 163, 184, 0.18);
    cursor: pointer;
    transition: transform 0.15s ease;
    background: #111827;
    color: #e2e8f0;
    font-size: 11px;
    font-weight: 700;
    line-height: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.heat-cell:hover {
    transform: scale(1.16);
}

.heat-cell.out-month {
    opacity: 0.26;
}

/* One source of truth for the busy scale. The legend used to hardcode its own gradient and had
   drifted off the real ramp entirely — it faded out at #7c3aed while the grid runs all the way
   to #f0abfc. Both read these now, so a legend that lies about the grid is not possible. */
:root {
    --lvl-0: #111827;
    --lvl-1: #4c1d95;
    --lvl-2: #7e22ce;
    --lvl-3: #c026d3;
    --lvl-4: #ec4899;
    --lvl-5: #f0abfc;
}

.heat-cell.level-0 { background: var(--lvl-0); }
.heat-cell.level-1 { background: var(--lvl-1); }
.heat-cell.level-2 { background: var(--lvl-2); }
.heat-cell.level-3 { background: var(--lvl-3); }
.heat-cell.level-4 { background: var(--lvl-4); }
.heat-cell.level-5 { background: var(--lvl-5); }

.heat-cell.active-date {
    outline: 2px solid #f8fafc;
}

.heat-cell.is-today {
    box-shadow: 0 0 0 2px #10b981, 0 0 12px rgba(16, 185, 129, 0.55);
    position: relative;
}
.heat-cell.has-impact {
    position: relative;
}
.heat-cell.has-impact::before {
    content: '';
    position: absolute;
    bottom: 4px;
    left: 4px;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: #facc15;
    box-shadow: 0 0 7px rgba(250, 204, 21, 0.95);
}
.heat-cell.impact-danger::before {
    background: #ef4444;
    box-shadow: 0 0 8px rgba(239, 68, 68, 0.95);
}
.heat-cell.is-today::after {
    content: '';
    position: absolute;
    top: 4px;
    right: 4px;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: #10b981;
    box-shadow: 0 0 6px rgba(16, 185, 129, 0.9);
    animation: hoursTodayPulse 1.6s ease-in-out infinite;
}
.heat-cell.is-today.active-date {
    outline-color: #10b981;
}

.scale-btn {
    font-size: 1.2rem;
    font-weight: 700;
    min-width: 34px;
    line-height: 1;
    margin-left: 4px;
}

.year-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 14px;
    padding: 4px 2px;
}
.mini-month {
    background: rgba(15, 23, 42, 0.55);
    border: 1px solid rgba(148, 163, 184, 0.15);
    border-radius: 10px;
    padding: 10px 10px 8px;
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.mini-month--current {
    border-color: rgba(16, 185, 129, 0.45);
    box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.2);
}
.mini-month-header {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 6px;
}
.mini-month-name {
    font-size: 0.9rem;
    font-weight: 700;
    color: #f1f5f9;
    letter-spacing: 0.4px;
    text-transform: uppercase;
}
.mini-month-year {
    font-size: 0.65rem;
    color: #64748b;
    letter-spacing: 0.4px;
}
.mini-month-weekdays {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 3px;
    font-size: 0.58rem;
    color: #64748b;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    text-align: center;
}
.mini-month-grid {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 3px;
}
.heat-cell.mini-cell {
    width: auto;
    height: 22px;
    aspect-ratio: 1 / 1;
    font-size: 9px;
    border-radius: 4px;
    padding: 0;
    border-width: 1px;
}
.heat-cell.mini-cell:hover {
    transform: scale(1.12);
}
.heat-cell.mini-cell.is-today::after {
    top: 2px;
    right: 2px;
    width: 4px;
    height: 4px;
    box-shadow: 0 0 4px rgba(16, 185, 129, 0.9);
}
.heat-cell.mini-cell.has-impact::before {
    bottom: 2px;
    left: 2px;
    width: 4px;
    height: 4px;
    box-shadow: 0 0 4px rgba(250, 204, 21, 0.9);
}
.heat-cell.mini-cell.impact-danger::before {
    box-shadow: 0 0 4px rgba(239, 68, 68, 0.9);
}
.mini-cell--empty {
    aspect-ratio: 1 / 1;
    background: transparent;
    border: 1px dashed rgba(148, 163, 184, 0.05);
    border-radius: 4px;
}
@media (max-width: 640px) {
    .year-grid {
        grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
        gap: 10px;
    }
    .mini-month {
        padding: 8px 8px 6px;
    }
    .mini-month-name {
        font-size: 0.8rem;
    }
    .heat-cell.mini-cell {
        height: 20px;
        font-size: 8px;
    }
}
@keyframes hoursTodayPulse {
    0%, 100% { opacity: 1; transform: scale(1); }
    50% { opacity: 0.55; transform: scale(1.25); }
}

.server-clock {
    display: inline-flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 1px;
    padding: 7px 14px;
    background: linear-gradient(135deg, rgba(15, 23, 42, 0.92) 0%, rgba(30, 58, 111, 0.88) 100%);
    border: 1px solid rgba(52, 152, 219, 0.4);
    border-radius: 10px;
    color: #e2e8f0;
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", monospace;
    min-width: 150px;
    box-shadow: 0 2px 8px rgba(15, 23, 42, 0.35), inset 0 0 0 1px rgba(255, 255, 255, 0.03);
    position: relative;
}
.server-clock::before {
    content: '';
    position: absolute;
    top: 10px;
    left: 8px;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: #10b981;
    box-shadow: 0 0 6px rgba(16, 185, 129, 0.8);
    animation: hoursTodayPulse 1.6s ease-in-out infinite;
}
.server-clock--offline::before {
    background: #64748b;
    box-shadow: none;
    animation: none;
}
.server-clock-label {
    font-size: 0.62rem;
    text-transform: uppercase;
    letter-spacing: 0.7px;
    color: #94a3b8;
    font-family: inherit;
    padding-left: 14px;
}
.server-clock-label i {
    color: #3498db;
    margin-right: 4px;
}
.server-clock-value {
    display: inline-flex;
    align-items: baseline;
    gap: 8px;
    font-variant-numeric: tabular-nums;
}
#serverClockTime {
    font-size: 1.05rem;
    font-weight: 700;
    color: #f1f5f9;
    letter-spacing: 0.5px;
}
.server-clock-date {
    font-size: 0.7rem;
    color: #94a3b8;
}
.server-clock-tz {
    font-size: 0.6rem;
    color: #64748b;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}
.server-clock--offline {
    border-color: rgba(100, 116, 139, 0.4);
    opacity: 0.7;
}
@media (max-width: 640px) {
    .server-clock {
        min-width: 0;
        padding: 6px 10px 6px 20px;
    }
    #serverClockTime {
        font-size: 0.95rem;
    }
    .server-clock-date {
        display: none;
    }
}

.weather-widget {
    position: relative;
    display: inline-flex;
    flex-direction: column;
    align-items: stretch;
    gap: 6px;
    padding: 8px 12px;
    background: linear-gradient(135deg, rgba(15, 23, 42, 0.92) 0%, rgba(30, 64, 111, 0.88) 100%);
    border: 1px solid rgba(59, 130, 246, 0.4);
    border-radius: 10px;
    color: #e2e8f0;
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", monospace;
    min-width: 280px;
    box-shadow: 0 2px 8px rgba(15, 23, 42, 0.35), inset 0 0 0 1px rgba(255, 255, 255, 0.03);
    margin-left: 10px;
}
.weather-top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}
.weather-label {
    font-size: 0.62rem;
    text-transform: uppercase;
    letter-spacing: 0.7px;
    color: #94a3b8;
    font-family: inherit;
}
.weather-label i {
    color: #3498db;
    margin-right: 4px;
}
.weather-controls {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}
.weather-unit-btn {
    background: rgba(15, 23, 42, 0.8);
    border: 1px solid rgba(148, 163, 184, 0.25);
    color: #e2e8f0;
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", monospace;
    font-size: 0.62rem;
    font-weight: 700;
    letter-spacing: 0.4px;
    padding: 4px 9px;
    border-radius: 999px;
    cursor: pointer;
    transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
    min-width: 32px;
}
.weather-unit-btn:hover {
    background: rgba(59, 130, 246, 0.2);
    border-color: rgba(59, 130, 246, 0.5);
    color: #f1f5f9;
}
.weather-mode-toggle {
    display: inline-flex;
    background: rgba(15, 23, 42, 0.8);
    border: 1px solid rgba(148, 163, 184, 0.25);
    border-radius: 999px;
    padding: 2px;
    gap: 2px;
}
.weather-mode-toggle button {
    background: transparent;
    border: none;
    color: #94a3b8;
    font-family: inherit;
    font-size: 0.62rem;
    font-weight: 600;
    letter-spacing: 0.4px;
    padding: 3px 9px;
    border-radius: 999px;
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease;
}
.weather-mode-toggle button:hover {
    color: #e2e8f0;
}
.weather-mode-toggle button.is-active {
    background: rgba(59, 130, 246, 0.25);
    color: #f1f5f9;
    box-shadow: inset 0 0 0 1px rgba(59, 130, 246, 0.5);
}
.weather-main {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    font-variant-numeric: tabular-nums;
    flex-wrap: wrap;
}
.weather-icon {
    color: #94a3b8;
    font-size: 1.4rem;
}

.wx--sun {
    color: #fbbf24;
    filter: drop-shadow(0 0 6px rgba(251, 191, 36, 0.55));
}
.wx--moon {
    color: #e2e8f0;
    filter: drop-shadow(0 0 6px rgba(226, 232, 240, 0.45));
}
.wx--partly-day {
    color: #fcd34d;
    filter: drop-shadow(0 0 4px rgba(252, 211, 77, 0.4));
}
.wx--partly-night {
    color: #cbd5e1;
    filter: drop-shadow(0 0 4px rgba(203, 213, 225, 0.35));
}
.wx--cloud {
    color: #cbd5e1;
    filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.35));
}
.wx--fog {
    color: #94a3b8;
    filter: drop-shadow(0 0 3px rgba(148, 163, 184, 0.4));
}
.wx--rain {
    color: #60a5fa;
    filter: drop-shadow(0 0 5px rgba(96, 165, 250, 0.55));
}
.wx--sleet {
    color: #93c5fd;
    filter: drop-shadow(0 0 5px rgba(147, 197, 253, 0.55));
}
.wx--snow {
    color: #e0f2fe;
    filter: drop-shadow(0 0 6px rgba(224, 242, 254, 0.6));
}
.wx--storm {
    color: #facc15;
    filter: drop-shadow(0 0 6px rgba(250, 204, 21, 0.65));
}
.weather-temp {
    font-size: 1.3rem;
    font-weight: 700;
    color: #f1f5f9;
    letter-spacing: 0.5px;
}
.weather-desc {
    font-size: 0.65rem;
    color: #94a3b8;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    flex: 1 1 auto;
    min-width: 0;
}
.weather-strip {
    display: flex;
    gap: 6px;
    overflow-x: auto;
    overflow-y: hidden;
    padding: 4px 0 2px;
    scrollbar-width: thin;
    scrollbar-color: rgba(59, 130, 246, 0.4) transparent;
}
.weather-strip:empty {
    display: none;
}
.weather-strip::-webkit-scrollbar {
    height: 4px;
}
.weather-strip::-webkit-scrollbar-thumb {
    background: rgba(59, 130, 246, 0.4);
    border-radius: 4px;
}
.weather-strip-item {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    min-width: 38px;
    padding: 5px 6px;
    background: rgba(15, 23, 42, 0.55);
    border: 1px solid rgba(148, 163, 184, 0.15);
    border-radius: 7px;
    transition: background 0.15s ease, border-color 0.15s ease, transform 0.12s ease;
    color: inherit;
    font: inherit;
}
button.weather-strip-item {
    appearance: none;
    -webkit-appearance: none;
    cursor: pointer;
}
.weather-strip-item:hover {
    background: rgba(30, 64, 111, 0.6);
    border-color: rgba(59, 130, 246, 0.45);
}
button.weather-strip-item--day:hover {
    transform: translateY(-1px);
    box-shadow: 0 4px 12px rgba(14, 165, 233, 0.25);
}
button.weather-strip-item--day:focus-visible {
    outline: 2px solid rgba(59, 130, 246, 0.8);
    outline-offset: 2px;
}
.weather-strip-back {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 10px;
    margin-right: 4px;
    background: rgba(59, 130, 246, 0.18);
    border: 1px solid rgba(59, 130, 246, 0.45);
    color: #bfdbfe;
    border-radius: 7px;
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    cursor: pointer;
    transition: background 0.15s ease, border-color 0.15s ease, transform 0.12s ease;
    font-family: inherit;
}
.weather-strip-back:hover {
    background: rgba(59, 130, 246, 0.28);
    border-color: rgba(59, 130, 246, 0.7);
    transform: translateY(-1px);
}
.weather-strip-back:focus-visible {
    outline: 2px solid rgba(59, 130, 246, 0.8);
    outline-offset: 2px;
}
.weather-strip-back-label {
    white-space: nowrap;
}
/* ── Hourly weather: curve + shift bands ─────────────────────────────────
   Twenty-four tiles took over half the header to report a mild, cloudy day. The curve carries
   the same information in a quarter of the space, and the bands under it answer the question
   this page is actually for: does the weather land on a shift I might work. */
.weather-strip--chart {
    display: block;
    overflow: visible;
    padding-bottom: 2px;
}

.wx-chart-wrap {
    position: relative;
    width: 100%;
    cursor: crosshair;
}

/* The curve drops the twenty-four hour labels on purpose; this is where the exact reading is. */
.wx-hover-line {
    position: absolute;
    top: 2px;
    bottom: 14px;
    width: 1px;
    background: rgba(226, 232, 240, 0.55);
    pointer-events: none;
}

/* While scrubbing, the headline is showing a forecast hour rather than right now — say so
   quietly so the number is never mistaken for the current conditions. */
.weather-widget.is-scrubbing .weather-temp { color: #7dd3fc; }
.weather-widget.is-scrubbing .weather-desc { color: #7dd3fc; }

.wx-chart {
    display: block;
    width: 100%;
    height: 58px;
    overflow: visible;
}

.wx-area { fill: rgba(56, 189, 248, 0.14); stroke: none; }
.wx-line { fill: none; stroke: #38bdf8; stroke-width: 1.6; stroke-linecap: round; }
.wx-dot { fill: #38bdf8; }
.wx-extreme {
    fill: #cbd5e1;
    font-size: 8px;
    font-weight: 700;
    font-family: inherit;
}
.wx-tick {
    fill: #64748b;
    font-size: 7px;
    font-weight: 700;
    letter-spacing: 0.06em;
    font-family: inherit;
}
.wx-now { stroke: #10b981; stroke-width: 1; stroke-dasharray: 2 2; }
.wx-now-dot { fill: #10b981; }
/* The window is centred on now, so half the curve is already behind you. Knocking the past back
   is what makes the split readable at a glance — otherwise the two halves look equally live. */
.wx-past { fill: rgba(2, 6, 23, 0.34); }

/* Wet hours, located rather than listed. */
.wx-band { opacity: 0.85; }
.wx-band--rain { fill: #38bdf8; }
.wx-band--sleet { fill: #a5b4fc; }
.wx-band--snow { fill: #e0f2fe; }
.wx-band--storm { fill: #f472b6; }
.wx-band--fog { fill: #94a3b8; }

.wx-shifts {
    display: flex;
    gap: 6px;
    margin-top: 4px;
}

.wx-shift {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    flex: 1 1 0;
    min-width: 0;
    padding: 3px 7px;
    border-radius: 7px;
    border: 1px solid rgba(148, 163, 184, 0.22);
    background: rgba(15, 23, 42, 0.5);
    font-size: 0.62rem;
    white-space: nowrap;
}

.wx-shift-name {
    font-weight: 800;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: #94a3b8;
}

.wx-shift-temp { margin-left: auto; font-weight: 700; color: #e2e8f0; }
.wx-shift-lo { color: #64748b; font-weight: 600; }

/* Only a shift with weather worth knowing about gets colour. */
.wx-shift--sev2 { border-color: rgba(148, 163, 184, 0.45); }
.wx-shift--sev3 { border-color: rgba(56, 189, 248, 0.55); background: rgba(56, 189, 248, 0.1); }
.wx-shift--sev4 { border-color: rgba(165, 180, 252, 0.6); background: rgba(165, 180, 252, 0.12); }
.wx-shift--sev5 { border-color: rgba(244, 114, 182, 0.65); background: rgba(244, 114, 182, 0.14); }
.wx-shift--sev5 .wx-shift-name { color: #f9a8d4; }

@media (max-width: 640px) {
    .wx-shift-name { letter-spacing: 0.04em; }
    .wx-shift { padding: 3px 5px; gap: 3px; }
}

.weather-strip-item.is-today {
    background: rgba(59, 130, 246, 0.2);
    border-color: rgba(59, 130, 246, 0.55);
}
.weather-strip-item.is-now {
    background: rgba(16, 185, 129, 0.22);
    border-color: rgba(16, 185, 129, 0.6);
    box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.35), 0 0 10px rgba(16, 185, 129, 0.25);
}
.weather-strip-item.is-now .weather-strip-label {
    color: #a7f3d0;
}
.weather-strip-label {
    font-size: 0.58rem;
    color: #94a3b8;
    text-transform: uppercase;
    letter-spacing: 0.4px;
}
.weather-strip-icon {
    font-size: 0.85rem;
    color: #94a3b8;
}
.weather-strip-temp {
    font-size: 0.72rem;
    font-weight: 700;
    color: #f1f5f9;
    font-variant-numeric: tabular-nums;
    display: inline-flex;
    gap: 3px;
    align-items: baseline;
}
.weather-strip-lo {
    font-size: 0.62rem;
    font-weight: 500;
    color: #64748b;
}
.weather-widget--loading .weather-icon {
    opacity: 0.5;
}
/* ── Live sky ────────────────────────────────────────────────────────────
   The widget is a window onto the weather at Dundalk, not a readout of it. The background and
   an animated atmosphere layer follow the current condition, and scrubbing the curve walks them
   through the day. Built from two pseudo-elements — no extra DOM, no per-frame JavaScript, and
   nothing but tiled gradients moved by background-position, so it stays cheap on a phone.
   Every tile step below is an exact multiple of its background-size, which is what makes the
   loops seamless instead of visibly jumping. */
/* Unclipped, so the change chip that hangs above the widget's top edge is not sliced off by the
   widget's own overflow:hidden (which the animated sky needs). */
.weather-slot {
    position: relative;
    display: flex;
}

/* ONE CARD SIZE FOR EVERY RANGE.
   The widget is an inline-flex box with a min-width and no max, so its width was whatever its
   widest child wanted — and the forecast strip lays out one tile per period at ~52px each. Picking
   14d therefore asked for 827px instead of 298px: the card nearly tripled, .hours-header-right ran
   out of room and wrapped, the header went 165px -> 311px, the page title was squeezed until
   "Tier 1" broke onto its own line, and everything below the header dropped 148px. Even 7d moved
   it 125px.
   Width is now FIXED, so 24h / 7d / 14d all occupy the same box and switching between them moves
   nothing on the page. The 14-day strip overflows it and scrolls — overflow-x and a styled 4px
   scrollbar were already on .weather-strip, waiting for something to actually constrain the width —
   so it reads seven days at a time and you scroll for the second week. min-width:0 lets the strip
   shrink below its content instead of pushing the card open again from the inside.
   The width is the one the 24h view was DESIGNED at. Sizing it to fit seven day-tiles instead
   (424px) held the layout still but stretched the default view to do it: the hourly chart is drawn
   with preserveAspectRatio="none", so a wider box scaled the curve and its hour labels
   horizontally, and the shift chips spread until each was mostly gap. The forecast tiles are the
   flexible part — they are seven small boxes, and they fit here — so they yield instead. */
.weather-widget {
    width: 300px;
    max-width: 100%;
}

.weather-strip {
    min-width: 0;
    /* ...and the same argument vertically. Fixing the WIDTH stopped the header collapsing, but the
       card's height still followed its contents: the 24h temperature curve is 87px tall against
       59px of day tiles, so the card was 165px on 24h and 136px on 7d. At 165 it is the tallest
       thing in the header and sets the header's height; at 136 the instruments column next to it
       (163px) takes over. So every switch between 24h and a forecast range nudged the whole page
       by that 2px difference.
       Pinning the strip to the chart's height makes the card 165px in all three ranges, so the
       header is 165px whichever is selected — and it no longer depends on the instruments column
       happening to be a similar height, which is what made the shift so small and so odd-looking. */
    min-height: 88px;
}

/* The day tiles stretch to fill that height, so their content centres in the box rather than
   hanging from the top of it.
   They also SHARE the strip rather than each claiming a fixed width: seven of them divide it
   evenly and fill the row exactly, while fourteen hit the min-width floor and overflow into the
   scroll. One rule covers both, with no per-range class to keep in step. */
.weather-strip-item {
    justify-content: center;
    flex: 1 1 auto;
    min-width: 34px;
    padding: 5px 3px;
}

/* Hi over lo instead of side by side. Packed onto one line the pair needed ~38px of the tile all
   by itself, which is what made a seven-day row too wide for the card and forced the whole widget
   open. Stacked, the tile fits the width the 24h view wants — and the strip is 88px tall now, so
   the second line costs nothing. */
.weather-strip-temp {
    flex-direction: column;
    align-items: center;
    gap: 0;
}

/* The widget used to run its OWN sky — its own gradient per condition, its own tiled
   precipitation, its own particle canvas — inside a 280px box with overflow:hidden. Two skies on
   one page never line up, and a cloud that stops dead at the widget's border is the tell. So the
   box is now GLASS: the one page-wide sky passes straight through it, unbroken, and the widget
   only supplies the readable surface the numbers sit on. That deleted ~220 lines of parallel
   weather CSS and a whole rAF loop with it. */
.weather-widget {
    overflow: hidden;
    transition: border-color 700ms ease, background-color 900ms ease;
    background-image: none;
    background-color: rgba(2, 6, 23, 0.30);
    -webkit-backdrop-filter: blur(2px);
    backdrop-filter: blur(2px);
}

/* After dark the glass darkens with the sky rather than staying a bright rectangle in a night
   scene — the same instinct the old --sky-dim had, applied to the pane instead of a fake sky. */
.weather-widget[data-phase="twilight"] { background-color: rgba(2, 6, 23, 0.36); }
.weather-widget[data-phase="night"] { background-color: rgba(2, 6, 23, 0.46); }

.weather-widget > * {
    position: relative;
    z-index: 1;
}

.weather-widget--offline {
    border-color: rgba(100, 116, 139, 0.4);
    opacity: 0.7;
}
.weather-widget--offline .weather-icon {
    color: #64748b;
}

/* Marine alerts widget */
.marine-alerts-widget {
    position: relative;
    appearance: none;
    -webkit-appearance: none;
    background: rgba(15, 23, 42, 0.72);
    border: 1px solid rgba(56, 189, 248, 0.25);
    border-radius: 14px;
    padding: 8px 14px;
    min-width: 168px;
    color: #e2e8f0;
    text-align: left;
    display: flex;
    flex-direction: column;
    gap: 4px;
    cursor: pointer;
    transition: transform 120ms ease, border-color 160ms ease, box-shadow 160ms ease;
    font: inherit;
}
.marine-alerts-widget:hover {
    transform: translateY(-1px);
    box-shadow: 0 6px 18px rgba(14, 165, 233, 0.25);
}
.marine-alerts-widget:focus-visible {
    outline: 2px solid rgba(56, 189, 248, 0.8);
    outline-offset: 2px;
}
.marine-alerts-top {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: #7dd3fc;
}
.marine-alerts-top i {
    font-size: 0.95rem;
}
.marine-alerts-badge {
    margin-left: auto;
    min-width: 22px;
    padding: 2px 7px;
    border-radius: 999px;
    background: rgba(56, 189, 248, 0.18);
    color: #bae6fd;
    font-size: 0.7rem;
    font-weight: 700;
    text-align: center;
}
.marine-alerts-status {
    display: flex;
    align-items: baseline;
    gap: 6px;
}
.marine-alerts-overall {
    font-size: 1.05rem;
    font-weight: 700;
    color: #f8fafc;
}
.marine-alerts-meta {
    font-size: 0.7rem;
    color: #94a3b8;
}
/* "All clear" is the normal state, and normal states should not outline themselves in colour —
   the green anchor and badge below already say it. Advisory, warning and danger keep their
   frames, so a coloured border up here means something is actually wrong. */
.marine-alerts-widget--clear {
    border-color: rgba(148, 163, 184, 0.22);
}
.marine-alerts-widget--clear .marine-alerts-badge {
    background: rgba(34, 197, 94, 0.18);
    color: #86efac;
}
.marine-alerts-widget--clear .marine-alerts-top,
.marine-alerts-widget--clear .marine-alerts-top i {
    color: #86efac;
}
.marine-alerts-widget--advisory {
    border-color: rgba(250, 204, 21, 0.45);
    box-shadow: 0 0 0 1px rgba(250, 204, 21, 0.08) inset;
}
.marine-alerts-widget--advisory .marine-alerts-badge {
    background: rgba(250, 204, 21, 0.22);
    color: #fde68a;
}
.marine-alerts-widget--advisory .marine-alerts-top,
.marine-alerts-widget--advisory .marine-alerts-top i {
    color: #fde68a;
}
.marine-alerts-widget--warning {
    border-color: rgba(249, 115, 22, 0.55);
    box-shadow: 0 0 0 1px rgba(249, 115, 22, 0.10) inset;
}
.marine-alerts-widget--warning .marine-alerts-badge {
    background: rgba(249, 115, 22, 0.25);
    color: #fed7aa;
}
.marine-alerts-widget--warning .marine-alerts-top,
.marine-alerts-widget--warning .marine-alerts-top i {
    color: #fed7aa;
}
.marine-alerts-widget--danger {
    border-color: rgba(239, 68, 68, 0.65);
    box-shadow: 0 0 0 1px rgba(239, 68, 68, 0.14) inset;
    animation: marineDangerPulse 1.8s ease-in-out infinite;
}
.marine-alerts-widget--danger .marine-alerts-badge {
    background: rgba(239, 68, 68, 0.3);
    color: #fecaca;
}
.marine-alerts-widget--danger .marine-alerts-top,
.marine-alerts-widget--danger .marine-alerts-top i {
    color: #fecaca;
}
@keyframes marineDangerPulse {
    0%, 100% { box-shadow: 0 0 0 1px rgba(239, 68, 68, 0.14) inset, 0 0 0 rgba(239, 68, 68, 0); }
    50%      { box-shadow: 0 0 0 1px rgba(239, 68, 68, 0.14) inset, 0 0 14px rgba(239, 68, 68, 0.35); }
}
.marine-alerts-widget--loading {
    opacity: 0.75;
}
.marine-alerts-widget--offline {
    opacity: 0.6;
    border-color: rgba(100, 116, 139, 0.4);
}

/* Marine alerts modal */
.marine-alerts-overlay {
    position: fixed;
    inset: 0;
    background: rgba(2, 6, 23, 0.65);
    backdrop-filter: blur(3px);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1500;
    padding: 20px;
}
.marine-alerts-overlay.hidden {
    display: none;
}
.marine-alerts-modal {
    background: #0b1220;
    border: 1px solid rgba(56, 189, 248, 0.2);
    border-radius: 16px;
    width: min(720px, 100%);
    max-height: 90vh;
    display: flex;
    flex-direction: column;
    box-shadow: 0 25px 60px rgba(0, 0, 0, 0.55);
    overflow: hidden;
    color: #e2e8f0;
}
.marine-alerts-modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 14px 18px;
    border-bottom: 1px solid rgba(148, 163, 184, 0.15);
}
.marine-alerts-modal-header h3 {
    margin: 0;
    font-size: 1.05rem;
    font-weight: 700;
    color: #f8fafc;
}
.marine-alerts-modal-header h3 i {
    margin-right: 8px;
    color: #7dd3fc;
}
.marine-alerts-modal-close {
    appearance: none;
    background: none;
    border: none;
    color: #cbd5f5;
    font-size: 1.6rem;
    line-height: 1;
    cursor: pointer;
    padding: 4px 10px;
    border-radius: 8px;
}
.marine-alerts-modal-close:hover {
    background: rgba(148, 163, 184, 0.12);
}
.marine-alerts-modal-summary {
    padding: 12px 18px 0;
}
.marine-summary-pill {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    padding: 8px 14px;
    border-radius: 999px;
    background: rgba(56, 189, 248, 0.12);
    border: 1px solid rgba(56, 189, 248, 0.25);
    font-size: 0.85rem;
    color: #e2e8f0;
}
.marine-summary-pill strong {
    font-weight: 700;
}
.marine-summary-pill--clear { background: rgba(34, 197, 94, 0.14); border-color: rgba(34, 197, 94, 0.4); color: #bbf7d0; }
.marine-summary-pill--advisory { background: rgba(250, 204, 21, 0.16); border-color: rgba(250, 204, 21, 0.5); color: #fde68a; }
.marine-summary-pill--warning { background: rgba(249, 115, 22, 0.18); border-color: rgba(249, 115, 22, 0.55); color: #fed7aa; }
.marine-summary-pill--danger { background: rgba(239, 68, 68, 0.22); border-color: rgba(239, 68, 68, 0.65); color: #fecaca; }

.marine-alerts-modal-body {
    padding: 12px 18px 18px;
    overflow-y: auto;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
}
@media (max-width: 640px) {
    .marine-alerts-modal-body {
        grid-template-columns: 1fr;
    }
}
.marine-zone-card {
    background: rgba(15, 23, 42, 0.75);
    border: 1px solid rgba(148, 163, 184, 0.15);
    border-radius: 12px;
    padding: 12px 14px;
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.marine-zone-card--advisory { border-color: rgba(250, 204, 21, 0.45); }
.marine-zone-card--warning  { border-color: rgba(249, 115, 22, 0.55); }
.marine-zone-card--danger   { border-color: rgba(239, 68, 68, 0.65); background: rgba(69, 10, 10, 0.25); }

.marine-zone-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}
.marine-zone-name {
    display: flex;
    flex-direction: column;
}
.marine-zone-name strong {
    font-size: 0.95rem;
    color: #f8fafc;
}
.marine-zone-kind {
    font-size: 0.65rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: #94a3b8;
    margin-top: 2px;
}
.marine-zone-severity {
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    padding: 3px 10px;
    border-radius: 999px;
    background: rgba(34, 197, 94, 0.15);
    color: #86efac;
}
.marine-zone-severity--advisory { background: rgba(250, 204, 21, 0.18); color: #fde68a; }
.marine-zone-severity--warning  { background: rgba(249, 115, 22, 0.22); color: #fed7aa; }
.marine-zone-severity--danger   { background: rgba(239, 68, 68, 0.26); color: #fecaca; }

.marine-zone-conditions {
    display: flex;
    flex-wrap: wrap;
    gap: 6px 12px;
    font-size: 0.78rem;
    color: #cbd5f5;
}
.marine-zone-conditions i {
    margin-right: 4px;
    color: #7dd3fc;
    width: 12px;
    text-align: center;
}
.marine-zone-nodata {
    font-size: 0.75rem;
    color: #64748b;
    font-style: italic;
}
.marine-alert-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.marine-alert-item {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    padding: 6px 10px;
    border-radius: 8px;
    background: rgba(30, 41, 59, 0.55);
    font-size: 0.78rem;
    color: #e2e8f0;
}
.marine-alert-item i {
    margin-top: 2px;
    width: 14px;
    text-align: center;
}
.marine-alert-item--advisory { border-left: 3px solid #facc15; }
.marine-alert-item--warning  { border-left: 3px solid #f97316; }
.marine-alert-item--danger   { border-left: 3px solid #ef4444; background: rgba(69, 10, 10, 0.35); }

.marine-alerts-modal-footer {
    padding: 10px 18px;
    border-top: 1px solid rgba(148, 163, 184, 0.12);
    font-size: 0.72rem;
    color: #94a3b8;
    text-align: right;
}

@media (max-width: 820px) {
    .marine-alerts-widget {
        min-width: 0;
        width: 100%;
    }
}
@media (max-width: 820px) {
    .weather-widget {
        min-width: 0;
        width: 100%;
        margin-left: 0;
    }
}
@media (max-width: 640px) {
    .weather-widget {
        padding: 7px 10px;
    }
    .weather-temp {
        font-size: 1.1rem;
    }
    .weather-icon {
        font-size: 1.2rem;
    }
    .weather-strip-item {
        min-width: 34px;
        padding: 4px 5px;
    }
}

.entry-pill {
    border-radius: 999px;
    padding: 4px 10px;
    font-size: 12px;
    font-weight: 700;
}

.entry-pill.manual {
    background: rgba(14, 165, 233, 0.18);
    color: #bae6fd;
}

.entry-pill.order {
    background: rgba(34, 197, 94, 0.18);
    color: #bbf7d0;
}

.editor-panel {
    height: fit-content;
    position: sticky;
    top: 10px;
}

.wage-panel {
    border: 1px solid rgba(148, 163, 184, 0.24);
    border-radius: 10px;
    padding: 12px;
    margin-bottom: 14px;
    background: rgba(30, 41, 59, 0.45);
    display: grid;
    gap: 8px;
}

.wage-panel h2 {
    margin: 0 0 4px;
    font-size: 17px;
}

.resolved-rate {
    font-size: 13px;
    font-weight: 700;
    color: #bfdbfe;
    padding: 6px 8px;
    border-radius: 8px;
    background: rgba(30, 58, 138, 0.25);
    border: 1px solid rgba(96, 165, 250, 0.3);
}

/* Five labelled figures instead of one pipe-delimited run-on — this is the line you read on
   every day you click. */
.day-stat {
    display: inline-flex;
    flex-direction: column;
    gap: 1px;
    padding: 4px 10px 4px 0;
    margin-right: 10px;
    border-right: 1px solid rgba(148, 163, 184, 0.16);
}

.day-stat:last-child { border-right: none; margin-right: 0; padding-right: 0; }

.day-stat-label {
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: #64748b;
}

.day-stat-value {
    font-size: 14px;
    font-weight: 800;
    color: #e2e8f0;
    font-variant-numeric: tabular-nums;
}

.day-stat--hours .day-stat-value { color: #fbbf24; }
.day-stat--net .day-stat-value { color: #86efac; }

.day-totals {
    display: flex;
    flex-wrap: wrap;
    align-items: stretch;
    color: #cbd5e1;
    margin-bottom: 10px;
    font-weight: 600;
}

.day-order-suggestions {
    display: grid;
    gap: 8px;
    margin-bottom: 12px;
}

.mini-order-card {
    border-radius: 10px;
    border: 1px solid rgba(148, 163, 184, 0.24);
    padding: 10px;
    background: rgba(15, 23, 42, 0.56);
}

.mini-order-card .title {
    font-weight: 700;
}

.mini-order-card .meta {
    color: #94a3b8;
    font-size: 12px;
    margin-top: 4px;
}

.mini-order-card .actions {
    margin-top: 8px;
}

.day-entries {
    display: grid;
    gap: 8px;
    margin-bottom: 12px;
}

.day-impact-note {
    border-radius: 10px;
    border: 1px solid rgba(251, 191, 36, 0.35);
    background: rgba(120, 53, 15, 0.22);
    color: #fde68a;
    padding: 9px 10px;
    margin-bottom: 8px;
    font-size: 12px;
    font-weight: 700;
}
.day-impact-note--danger {
    border-color: rgba(239, 68, 68, 0.45);
    background: rgba(127, 29, 29, 0.28);
    color: #fecaca;
}

.day-entry {
    border-radius: 10px;
    border: 1px solid rgba(148, 163, 184, 0.24);
    padding: 10px;
    background: rgba(2, 6, 23, 0.6);
}

.day-entry .meta {
    color: #94a3b8;
    font-size: 12px;
    margin-top: 4px;
}

.entry-row-actions {
    margin-top: 8px;
    display: flex;
    gap: 8px;
}

.mini-btn {
    border: none;
    border-radius: 8px;
    cursor: pointer;
    padding: 6px 8px;
    font-size: 12px;
    font-weight: 700;
    background: rgba(51, 65, 85, 0.95);
    color: #e2e8f0;
}

.mini-btn.delete {
    background: rgba(127, 29, 29, 0.9);
    color: #fecaca;
}

.entry-form {
    display: grid;
    gap: 8px;
}

.entry-form h3 {
    margin: 0 0 6px;
}

.checkbox-row {
    display: flex;
    align-items: center;
    gap: 8px;
    color: #e2e8f0;
    font-weight: 600;
}

.checkbox-row input[type="checkbox"] {
    width: auto;
    accent-color: #8b5cf6;
}

.form-actions {
    display: flex;
    gap: 8px;
    margin-top: 4px;
}

.hidden {
    display: none !important;
}

@media (max-width: 1100px) {
    .hours-main {
        grid-template-columns: 1fr;
    }
    .editor-panel {
        position: static;
    }
}

@media (max-width: 760px) {
    .hours-header {
        flex-direction: column;
    }
    .hours-header-right {
        width: 100%;
        justify-content: space-between;
    }
}

.demo-login-notice {
    position: fixed;
    left: 50%;
    bottom: 22px;
    transform: translateX(-50%);
    z-index: 9999;
    max-width: min(520px, calc(100vw - 24px));
    display: grid;
    grid-template-columns: auto 1fr auto;
    grid-template-rows: auto auto;
    align-items: center;
    gap: 4px 14px;
    padding: 14px 18px;
    border: 1px solid rgba(139, 92, 246, 0.55);
    border-radius: 16px;
    background: linear-gradient(135deg, rgba(30, 41, 59, 0.97), rgba(49, 27, 96, 0.97));
    color: #f1f5f9;
    text-align: left;
    cursor: pointer;
    box-shadow: 0 18px 45px rgba(15, 23, 42, 0.55), 0 0 0 1px rgba(139, 92, 246, 0.22);
    backdrop-filter: blur(6px);
    animation: demoNoticePulse 3.2s ease-in-out infinite;
    transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
}

.demo-login-notice:hover,
.demo-login-notice:focus-visible {
    transform: translateX(-50%) translateY(-2px);
    border-color: rgba(167, 139, 250, 0.95);
    box-shadow: 0 22px 55px rgba(15, 23, 42, 0.6), 0 0 0 1px rgba(167, 139, 250, 0.45);
    outline: none;
}

.demo-login-notice__badge {
    grid-row: 1 / span 2;
    align-self: center;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 10px;
    background: rgba(139, 92, 246, 0.22);
    border: 1px solid rgba(167, 139, 250, 0.6);
    color: #ddd6fe;
    border-radius: 999px;
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    white-space: nowrap;
}

.demo-login-notice__title {
    grid-column: 2;
    grid-row: 1;
    font-size: 0.98rem;
    font-weight: 700;
    color: #f8fafc;
}

.demo-login-notice__sub {
    grid-column: 2;
    grid-row: 2;
    font-size: 0.82rem;
    color: #cbd5e1;
    line-height: 1.35;
}

.demo-login-notice__sub u {
    color: #c4b5fd;
    text-decoration-thickness: 2px;
    text-underline-offset: 2px;
}

.demo-login-notice__cta {
    grid-column: 3;
    grid-row: 1 / span 2;
    align-self: center;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 9px 14px;
    background: linear-gradient(135deg, #8b5cf6, #6366f1);
    color: #fff;
    border-radius: 10px;
    font-size: 0.85rem;
    font-weight: 700;
    white-space: nowrap;
    box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4);
}

@keyframes demoNoticePulse {
    0%, 100% { box-shadow: 0 18px 45px rgba(15, 23, 42, 0.55), 0 0 0 1px rgba(139, 92, 246, 0.22); }
    50% { box-shadow: 0 20px 50px rgba(15, 23, 42, 0.6), 0 0 0 4px rgba(139, 92, 246, 0.22); }
}

@media (max-width: 540px) {
    .demo-login-notice {
        left: 12px;
        right: 12px;
        bottom: 12px;
        transform: none;
        max-width: none;
        grid-template-columns: auto 1fr;
        padding: 12px 14px;
    }
    .demo-login-notice:hover,
    .demo-login-notice:focus-visible {
        transform: translateY(-2px);
    }
    .demo-login-notice__cta {
        grid-column: 1 / -1;
        grid-row: 3;
        justify-content: center;
        margin-top: 6px;
    }
}

.change-chip {
    position: absolute;
    top: -12px;
    right: 10px;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 5px 11px;
    background: rgba(30, 41, 59, 0.97);
    color: #f1f5f9;
    border: 1px solid rgba(148, 163, 184, 0.4);
    border-radius: 999px;
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    line-height: 1;
    white-space: nowrap;
    opacity: 0;
    transform: translateY(-4px) scale(0.9);
    pointer-events: none;
    transition: opacity 0.35s ease, transform 0.35s ease;
    box-shadow: 0 6px 16px rgba(2, 6, 23, 0.5);
    z-index: 5;
}

.change-chip.is-visible {
    opacity: 1;
    transform: translateY(0) scale(1);
    animation: changeChipPulse 2.2s ease-in-out infinite;
}

.change-chip i {
    font-size: 0.78rem;
}

.change-chip--info {
    background: rgba(30, 41, 59, 0.97);
    border-color: rgba(148, 163, 184, 0.45);
}

.change-chip--change {
    background: linear-gradient(135deg, #5b21b6, #8b5cf6);
    border-color: rgba(167, 139, 250, 0.7);
    color: #f5f3ff;
}

.change-chip--warn {
    background: linear-gradient(135deg, #b45309, #f59e0b);
    border-color: rgba(251, 191, 36, 0.75);
    color: #fff7ed;
}

.change-chip--cool {
    background: linear-gradient(135deg, #1e40af, #3b82f6);
    border-color: rgba(96, 165, 250, 0.75);
    color: #eff6ff;
}

.change-chip--success {
    background: linear-gradient(135deg, #047857, #10b981);
    border-color: rgba(52, 211, 153, 0.8);
    color: #ecfdf5;
}

.change-chip--danger {
    background: linear-gradient(135deg, #991b1b, #ef4444);
    border-color: rgba(248, 113, 113, 0.85);
    color: #fff1f2;
}

@keyframes changeChipPulse {
    0%, 100% { box-shadow: 0 6px 16px rgba(2, 6, 23, 0.5); }
    50%      { box-shadow: 0 6px 22px rgba(99, 102, 241, 0.45); }
}

/* Badge-ins widget */
.badge-ins-wrap {
    display: flex;
    align-items: stretch;
    gap: 6px;
}
.badge-ins-widget {
    position: relative;
    appearance: none;
    -webkit-appearance: none;
    background: rgba(15, 23, 42, 0.72);
    border: 1px solid rgba(168, 85, 247, 0.3);
    border-radius: 14px;
    padding: 8px 14px;
    min-width: 120px;
    color: #e2e8f0;
    text-align: left;
    display: flex;
    flex-direction: column;
    gap: 2px;
    cursor: pointer;
    transition: transform 120ms ease, border-color 160ms ease, box-shadow 160ms ease;
    font: inherit;
}
.badge-ins-widget:hover {
    transform: translateY(-1px);
    box-shadow: 0 6px 18px rgba(168, 85, 247, 0.25);
}
.badge-ins-widget:focus-visible {
    outline: 2px solid rgba(192, 132, 252, 0.8);
    outline-offset: 2px;
}
/* Having badge-ins is information, not an alert, and it is true most of the time — so it tints
   rather than outlines. The count and the icon carry it. */
.badge-ins-widget.has-badges {
    border-color: rgba(192, 132, 252, 0.3);
    background: rgba(88, 28, 135, 0.18);
}
.badge-ins-top {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: #d8b4fe;
}
.badge-ins-top i {
    font-size: 0.95rem;
}
.badge-ins-count {
    font-size: 1.3rem;
    font-weight: 700;
    color: #f8fafc;
    line-height: 1.1;
}
.badge-ins-sub {
    font-size: 0.7rem;
    color: #a78bfa;
}
.badge-ins-combined {
    align-self: stretch;
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding: 6px 10px;
    border-radius: 12px;
    background: rgba(15, 23, 42, 0.55);
    border: 1px dashed rgba(148, 163, 184, 0.35);
    color: #e2e8f0;
    min-width: 96px;
}
.badge-ins-combined-label {
    font-size: 0.62rem;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: #94a3b8;
}
.badge-ins-combined-value {
    font-size: 0.82rem;
    font-weight: 700;
    color: #f1f5f9;
    white-space: nowrap;
}

/* Badge-ins modal */
.badge-ins-overlay {
    position: fixed;
    inset: 0;
    background: rgba(2, 6, 23, 0.65);
    backdrop-filter: blur(3px);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1500;
    padding: 20px;
}
.badge-ins-overlay.hidden {
    display: none;
}
.badge-ins-modal {
    background: #0b1220;
    border: 1px solid rgba(168, 85, 247, 0.25);
    border-radius: 16px;
    width: min(520px, 100%);
    max-height: 90vh;
    display: flex;
    flex-direction: column;
    box-shadow: 0 25px 60px rgba(0, 0, 0, 0.55);
    overflow: hidden;
    color: #e2e8f0;
}
.badge-ins-modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 14px 18px;
    border-bottom: 1px solid rgba(148, 163, 184, 0.15);
}
.badge-ins-modal-header h3 {
    margin: 0;
    font-size: 1.05rem;
    font-weight: 700;
    color: #f8fafc;
}
.badge-ins-modal-header h3 i {
    margin-right: 8px;
    color: #c084fc;
}
.badge-ins-modal-close {
    appearance: none;
    background: none;
    border: none;
    color: #cbd5f5;
    font-size: 1.6rem;
    line-height: 1;
    cursor: pointer;
    padding: 4px 10px;
    border-radius: 8px;
}
.badge-ins-modal-close:hover {
    background: rgba(148, 163, 184, 0.12);
}
.badge-ins-modal-body {
    padding: 16px 18px;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 12px;
}
.badge-ins-add-row {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}
.badge-ins-add-row label {
    font-size: 0.8rem;
    color: #cbd5f5;
}
.badge-ins-add-row input[type="date"] {
    background: rgba(15, 23, 42, 0.7);
    border: 1px solid rgba(148, 163, 184, 0.35);
    color: #e2e8f0;
    border-radius: 8px;
    padding: 6px 10px;
    font: inherit;
}
.badge-ins-add-row .action-btn[disabled] {
    opacity: 0.5;
    cursor: not-allowed;
}
.badge-ins-message {
    min-height: 1em;
    font-size: 0.8rem;
    color: #cbd5f5;
}
.badge-ins-message.is-warning {
    color: #fca5a5;
}
.badge-ins-list {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 6px;
    max-height: 320px;
    overflow-y: auto;
}
.badge-ins-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 8px 12px;
    background: rgba(15, 23, 42, 0.6);
    border: 1px solid rgba(148, 163, 184, 0.15);
    border-radius: 10px;
}
.badge-ins-item-main {
    display: flex;
    align-items: center;
    gap: 10px;
    font-size: 0.88rem;
}
.badge-ins-item-main i {
    color: #c084fc;
}
.badge-ins-item-hours {
    color: #94a3b8;
    font-size: 0.78rem;
}
.badge-ins-item-remove {
    appearance: none;
    background: none;
    border: none;
    color: #94a3b8;
    font-size: 1.2rem;
    line-height: 1;
    cursor: pointer;
    padding: 2px 8px;
    border-radius: 6px;
}
.badge-ins-item-remove:hover {
    background: rgba(239, 68, 68, 0.18);
    color: #fecaca;
}
.badge-ins-empty {
    padding: 12px;
    text-align: center;
    color: #94a3b8;
    background: rgba(15, 23, 42, 0.4);
    border: 1px dashed rgba(148, 163, 184, 0.25);
    border-radius: 10px;
    font-size: 0.85rem;
}
.badge-ins-modal-footer {
    padding: 12px 18px;
    border-top: 1px solid rgba(148, 163, 184, 0.15);
    font-size: 0.78rem;
    color: #94a3b8;
    background: rgba(15, 23, 42, 0.45);
}

@media (max-width: 760px) {
    .badge-ins-wrap {
        flex-direction: row;
        flex-wrap: wrap;
    }
    .badge-ins-widget,
    .badge-ins-combined {
        flex: 1 1 auto;
        min-width: 0;
    }
}
