/* ============================================================
   Aweigh — Marine Forecast Assistant design system
   Tokens, base, components.
   ============================================================ */

@import url('https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,500;8..60,600;8..60,700&family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap');

/* Day mode — navy ink on cream paper, derived from the Aweigh logo.
 *
 * --ground and --ink were --hull and --foam. They were renamed because the
 * theme inverted: "hull" meaning cream and "foam" meaning navy is nonsense.
 * The rest keep their names — --seaglass and --signal aren't only Go/No-Go,
 * they also colour the high and low tide swatches, so renaming them --go and
 * --nogo would have been worse than leaving them.
 *
 * Every foreground here clears WCAG AA (4.5:1) against both --ground and
 * --tide; ink is 10.7:1. The semantics are much darker than their dark-theme
 * values, which were tuned to glow against near-black and washed out to
 * nothing on paper.
 */
:root {
    --ground: #EDE9DD;    /* page — the logo's paper */
    --tide: #F7F4EB;      /* cards and panels, lifting toward white */
    --tide-2: #FCFAF5;    /* panel gradient end */
    --ink: #233054;       /* the logo's navy */
    --ink-2: #5C6683;     /* muted slate, 4.7:1 — still body-text legible */
    --brass: #7A5C12;     /* deepened bronze; gold on cream is unreadable */
    --brass-2: #5C4409;
    --signal: #B03227;    /* NO-GO, low tide */
    --signal-2: #8A2318;  /* gradient end, exactly as --brass-2 is for --brass */
    --seaglass: #155741;  /* GO, high tide */
    --buoy: #8A4B0B;      /* warnings */

    /* Hairline is ink-based now. Brass at 28% vanished on cream. */
    --rule: 1px solid rgba(35, 48, 84, .22);

    /* RGB triplets for the translucent layers. Components write
     * rgba(var(--panel-rgb), .55) instead of a literal, so a theme switch
     * re-tints every wash, hairline and glow by overriding three values.
     *
     * This replaces the hardcoded-hex exception the old CLAUDE.md documented,
     * where ~14 selectors had to be re-declared inside the night-mode block
     * purely to restate their backgrounds. Those duplicates are gone.
     *
     *   --panel-rgb  translucent panel fill. LIFTS toward white on paper;
     *                sinks toward black in Red mode. Same alpha, opposite
     *                direction — which is exactly why it has to be a token.
     *   --edge-rgb   hairlines, dividers, faint washes, the chart grid.
     *   --accent-rgb glows and accent borders.
     */
    --panel-rgb: 255, 255, 255;
    --edge-rgb: 35, 48, 84;
    --accent-rgb: 122, 92, 18;

    /* Shadow tuned for paper. The dark theme could throw near-black at 35%
       because it landed on near-black; on cream that reads as soot. */
    --shadow-card: 0 6px 18px rgba(35, 48, 84, .10);
    --r-sm: 8px;
    --r-md: 14px;
    --r-lg: 22px;

    --ff-display: "Source Serif 4", "Cormorant Garamond", Georgia, serif;
    --ff-body:    "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
    --ff-mono:    "IBM Plex Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;
}

* { box-sizing: border-box }

html, body {
    margin: 0;
    background: var(--ground);
    color: var(--ink);
    font-family: var(--ff-body);
    font-feature-settings: "tnum", "ss01";
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
}

a { color: var(--brass); text-decoration: none }
a:hover { color: var(--ink) }

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

.aweigh {
    max-width: 1100px;
    margin: 0 auto;
    /* Ride higher on ordinary viewports (12px), but drop below the iPhone
       status bar / Dynamic Island in standalone mode via the safe-area inset.
       Needs viewport-fit=cover on the meta tag or the insets resolve to 0.
       Side insets guard landscape notches. */
    padding: max(12px, env(safe-area-inset-top))
             max(18px, env(safe-area-inset-right))
             80px
             max(18px, env(safe-area-inset-left));
    position: relative;
    /* Own stacking context, so the z-indexes used inside components (day band
       markers, the NOW rule, expanded rows) stay contained and can't interleave
       with anything outside. Originally added for the compass-rose backdrop,
       which is gone; the isolation is still worth keeping. Modal dialogs live
       in the top layer, so this doesn't trap them. */
    z-index: 0;
}

.aweigh__brand {
    display: flex;
    /* Was baseline, which suited two lines of type. The brand is now an image
       with no baseline to align to, so the action buttons centre against it. */
    align-items: center;
    gap: 14px;
    margin-bottom: 22px;
    position: relative;
}

/* Brand lockup. The artwork is a squarish wordmark-plus-anchor (~1.27:1), not
   a wide header lockup, so it cannot simply occupy the height the old two
   lines of type did — shrink it to that height and the script becomes
   illegible. It gets a real brand block instead. */
.aweigh__logo {
    margin: 0;
    line-height: 0;   /* the image is the entire heading; kill descender space */
}
.aweigh__logo-img {
    display: block;
    width: 148px;
    height: auto;
}
@media (max-width: 720px) {
    .aweigh__logo-img { width: 112px }
}

/* Day is the default so a missing data-aweigh attribute (JS blocked, or the
   boot script failing) still shows a usable mark rather than none. */
.aweigh__logo-img--red { display: none }
[data-aweigh="red"] .aweigh__logo-img--red { display: block }
[data-aweigh="red"] .aweigh__logo-img--day { display: none }



.aweigh__actions {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    gap: 8px;
}

.theme-toggle {
    appearance: none;
    background: rgba(var(--panel-rgb), .55);
    border: 1px solid rgba(var(--edge-rgb), .12);
    color: var(--ink-2);
    width: 40px; height: 40px;
    border-radius: 50%;
    cursor: pointer;
    display: inline-flex;
    align-items: center; justify-content: center;
    transition: color .15s ease, border-color .15s ease, transform .12s ease;
    flex: 0 0 auto;
}
/* The `display` above outranks the UA stylesheet's `[hidden] { display: none }`,
   so without this the attribute is inert. #a2hs-trigger ships `hidden` and is
   only revealed for iOS Safari — every other browser was rendering it anyway,
   as a dead button: wireA2HS() bails before binding the click handler, so it
   did nothing when tapped. It also cost 48px in a header row that cannot wrap,
   pushing "Sign in" off the right edge under ~346px of viewport. */
.theme-toggle[hidden] { display: none }
.theme-toggle:hover { color: var(--ink); border-color: var(--brass) }
.theme-toggle:active { transform: scale(.94) }

/* Signed-in avatar: the account button fills with the user's initials instead
   of the generic person glyph. Same 40px circle, so the header row is
   unchanged; only the read differs.

   brass-on-tide works in BOTH themes because the two tokens invert together —
   dark gold behind near-white in day mode (~5.6:1), light salmon behind
   near-black in Red mode (~7:1). Both clear AA, and neither is hardcoded. */
.admin-trigger--me {
    background: var(--brass);
    border-color: var(--brass);
}
.admin-trigger--me:hover { border-color: var(--brass); filter: brightness(1.08) }
.admin-trigger__initials {
    font: 700 12px/1 var(--ff-mono);
    letter-spacing: .04em;
    color: var(--tide);
    /* The glyphs are uppercase already; this stops a stray lowercase fallback
       from a name we could not case-fold. */
    text-transform: uppercase;
}
.theme-icon { width: 18px; height: 18px }
[data-aweigh="day"] .theme-icon--sun  { display: none }
[data-aweigh="red"] .theme-icon--moon { display: none }


/* ---------- Chart paper texture on dark cards ---------- */

.card,
.aweigh-form,
.tide-card,
.day-band,
.results {
    background-image:
        linear-gradient(rgba(var(--edge-rgb), .04) 1px, transparent 1px),
        linear-gradient(90deg, rgba(var(--edge-rgb), .04) 1px, transparent 1px);
    background-size: 40px 40px, 40px 40px;
    background-position: 0 0;
}

/* ---------- Form (Aweigh header) ---------- */

.aweigh-form {
    background: linear-gradient(180deg, var(--tide), var(--tide-2));
    border: var(--rule);
    border-radius: var(--r-lg);
    padding: 20px;
    display: grid;
    grid-template-columns: 1.4fr 1fr 1fr auto;
    gap: 14px;
    align-items: end;
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, .05), var(--shadow-card);
}
@media (max-width: 720px) {
    .aweigh-form {
        grid-template-columns: 1fr 1fr;
        padding: 14px;   /* tighter than the 20px desktop padding */
        gap: 12px;
    }
    .aweigh-form__submit { grid-column: 1 / -1; }
    /* iOS Safari's <input type="date"> has a min intrinsic width that can
       punch past the parent on narrow viewports. min-width:0 lets it shrink
       to its container; max-width:100% caps it as a belt-and-braces. */
    .aweigh-form select,
    .aweigh-form input[type=text],
    .aweigh-form input[type=email],
    .aweigh-form input[type=password],
    .aweigh-form input[type=date] {
        min-width: 0;
        max-width: 100%;
    }
    /* But min-width / max-width are ignored by iOS Safari while the date
       input is rendered as a native button-style control. appearance:none
       strips that rendering so the input becomes a plain text-input-ish
       field that obeys the width:100% set higher up. Calendar icon goes
       away but the native picker still opens on tap. */
    .aweigh-form input[type=date] {
        -webkit-appearance: none;
        appearance: none;
        background-clip: padding-box;
    }
}

.aweigh-form label {
    display: block;
    font: 600 10px/1 var(--ff-body);
    letter-spacing: .22em;
    text-transform: uppercase;
    color: var(--brass);
    margin-bottom: 6px;
}

.aweigh-form select,
.aweigh-form input[type=text],
.aweigh-form input[type=date] {
    width: 100%;
    background: rgba(var(--panel-rgb), .55);
    color: var(--ink);
    border: 1px solid rgba(var(--edge-rgb), .12);
    border-radius: var(--r-md);
    padding: 11px 12px;
    font: 500 14px/1 var(--ff-body);
    font-feature-settings: "tnum";
    min-height: 44px;
    transition: border-color .15s ease, background .15s ease;
}
.aweigh-form select:focus,
.aweigh-form input:focus {
    outline: none;
    border-color: var(--brass);
    background: rgba(var(--panel-rgb), .8);
}
.aweigh-form input[type=date] {
    color-scheme: dark;
    font-family: var(--ff-mono);
}

/* ---------------------------------------------------------------------------
   Collapsed location + date pickers.

   Sits under the Day Scroller inside the same <form>, so the pills keep posting
   the collapsed select. Styled as a quiet row rather than a button: it is a way
   back to the controls, not something the page is asking anyone to press.
   --------------------------------------------------------------------------- */
/* **Full width of the card, and the fields keep their row when open.**

   `.aweigh-form` is a four-column grid (`1.4fr 1fr 1fr auto`) built for the
   fields it used to hold directly — location, custom, date, submit. Wrapping
   them in <details> made the <details> itself the grid item, so it took column
   one and rendered at roughly a third of the card, and the fields inside it lost
   the grid entirely and stacked in a tall column.

   Two fixes, one cause. Span the whole row, then re-apply the same track sizes
   to the open block so the fields land where they always did. The tracks are
   duplicated rather than inherited because a grid does not pass its template to
   a nested one — and they must stay in step with .aweigh-form above. */
.pickers {
    margin-top: 12px;
    grid-column: 1 / -1;
}
.pickers__body {
    display: grid;
    grid-template-columns: 1.4fr 1fr 1fr auto;
    gap: 14px;
    align-items: end;
    margin-top: 12px;
}

@media (max-width: 720px) {
    .pickers__body { grid-template-columns: 1fr 1fr }
    .pickers__body > .aweigh-form__submit { grid-column: 1 / -1 }
}
.pickers__summary {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 9px 12px;
    border: var(--rule);
    border-radius: var(--r-md);
    background: rgba(var(--panel-rgb), .45);
    color: var(--ink-2);
    font: 500 13px/1 var(--ff-body);
    cursor: pointer;
    list-style: none;
    user-select: none;
    transition: color .15s ease, border-color .15s ease, background .15s ease;
}
.pickers__summary::-webkit-details-marker { display: none }   /* Safari marker */
.pickers__summary:hover { color: var(--ink); background: rgba(var(--panel-rgb), .7) }
.pickers__summary:focus-visible { outline: 2px solid var(--brass); outline-offset: 2px }

.pickers__icons { display: inline-flex; align-items: center; gap: 5px; flex: 0 0 auto; line-height: 0 }
.pickers__icons svg { width: 17px; height: 17px; display: block }
.pickers__label { flex: 1 1 auto }
.pickers__chev { flex: 0 0 auto; line-height: 0; opacity: .7 }
.pickers__chev svg {
    width: 15px; height: 15px; display: block;
    transition: transform .18s ease;
}
.pickers[open] .pickers__chev svg { transform: rotate(180deg) }

/* Open state: the summary becomes a header for the fields under it, so it drops
   its box and the fields get the breathing room the form always had. */
.pickers[open] .pickers__summary {
    background: transparent;
    border-color: transparent;
    padding-left: 0;
    padding-right: 0;
    color: var(--ink);
}

/* On a phone this row is the ONLY way back to the location and date controls, so
   it has to be comfortably tappable. At the desktop padding it computes to about
   35px tall, under the 44px both Apple and Google give as the minimum. This is
   also the width where the Day Scroller appears directly above it, so the two
   need enough separation not to be hit by accident.

   min-height rather than more padding, so the row grows without the icons and
   label drifting apart from each other. */
@media (max-width: 720px) {
    .pickers { margin-top: 14px }
    .pickers__summary {
        min-height: 44px;
        padding: 10px 14px;
        font-size: 14px;
    }
    .pickers__icons svg { width: 19px; height: 19px }
}

@media (prefers-reduced-motion: reduce) {
    .pickers__chev svg { transition: none }
}

.aweigh-form__custom { transition: opacity .2s ease, transform .2s ease }
.aweigh-form__custom[hidden] { display: none }

/* ---------- "Use my location" ----------
   Sits on the label row so it costs no vertical space on a phone, where the
   form is already four stacked fields above the fold. */

.aweigh-form__loc-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
}
.aweigh-form__loc-head label { margin-bottom: 6px }

/* On a phone the form is two columns, and "LOCATION" plus "USE MY LOCATION"
   do not both fit across half of a 375 px screen — the button wraps under the
   label and the field loses its heading. Give the location field the full row:
   it is the most important control on the page and deserves the width anyway. */
@media (max-width: 720px) {
    .aweigh-form__loc { grid-column: 1 / -1 }
}

.geo-btn {
    appearance: none;
    display: inline-flex;
    align-items: center;
    gap: 5px;
    background: none;
    border: 0;
    padding: 0 0 6px;
    cursor: pointer;
    color: var(--brass);
    font: 600 10px/1 var(--ff-body);
    letter-spacing: .12em;
    text-transform: uppercase;
    transition: color .15s ease;
}
.geo-btn[hidden] { display: none }
.geo-btn:hover { color: var(--ink) }
.geo-btn svg { width: 14px; height: 14px; flex: none }

/* Pulse rather than spin: the pin is the thing being resolved, and a spinner
   next to a stationary pin reads as two separate controls. */
.geo-btn--busy { opacity: .65; cursor: progress }
.geo-btn--busy svg { animation: geo-ping 1.1s ease-in-out infinite }
@keyframes geo-ping {
    0%, 100% { opacity: 1;  transform: scale(1) }
    50%      { opacity: .45; transform: scale(.86) }
}
@media (prefers-reduced-motion: reduce) {
    .geo-btn--busy svg { animation: none }
}

.geo-note {
    margin: 7px 0 0;
    font: 500 11.5px/1.35 var(--ff-body);
    color: var(--ink-2);
}
.geo-note[hidden] { display: none }
.geo-note--err { color: var(--signal) }
.geo-note__dist {
    font-family: var(--ff-mono);
    font-size: 10.5px;
    color: var(--brass);
    white-space: nowrap;
}
.geo-note__dist::before { content: "· " }

.aweigh-form__submit {
    appearance: none;
    border: 1px solid var(--brass);
    background: linear-gradient(180deg, var(--brass), var(--brass-2));
    color: var(--ground);
    font: 700 13px/1 var(--ff-body);
    letter-spacing: .14em;
    text-transform: uppercase;
    padding: 13px 22px;
    border-radius: var(--r-md);
    min-height: 44px;
    cursor: pointer;
    box-shadow: 0 4px 14px rgba(var(--accent-rgb), .25), inset 0 1px 0 rgba(255, 255, 255, .25);
    transition: transform .08s ease, box-shadow .15s ease;
}
.aweigh-form__submit:hover { box-shadow: 0 6px 18px rgba(var(--accent-rgb), .35), inset 0 1px 0 rgba(255, 255, 255, .35) }
.aweigh-form__submit:active { transform: translateY(1px) }

/* ---------- Day Scroller — every width, not just portrait phones ----------

   It was `display: none` above 720px, so it vanished on a landscape phone as
   well as on every laptop. That threshold was picked when it was thought of as
   "the mobile date picker", and it is the wrong frame: one tap to change day is
   the most useful control on the page whatever is holding the screen, and a
   landscape iPhone is ~844px wide, which put it on the hidden side of the line
   for no reason a user could tell.

   Same element and same markup at every width. The only thing that changes is
   how the pills distribute: they scroll on a narrow screen and spread to fill on
   a wide one, with overflow-x kept in both cases so a very long list or a very
   large font can still scroll rather than overflow the card.
   -------------------------------------------------------------------------- */

.day-scroller {
    display: flex;
    grid-column: 1 / -1;
    gap: 8px;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scrollbar-width: thin;
    scrollbar-color: var(--brass-2) transparent;
    padding: 2px 10px 8px;
    margin: -2px -10px 4px;
    -webkit-overflow-scrolling: touch;
}

/* Wide screens: let the pills share the row instead of queueing off the edge.
   flex-grow with a min-width means they fill the card when they fit and fall
   back to scrolling when they do not, so nothing has to know the pill count. */
@media (min-width: 721px) {
    .day-scroller { scroll-snap-type: none }
    /* The 68px floor is a thumb target, and a mouse does not need one. Measured:
       fourteen pills at 68 want 1088px against a 1042px card, so keeping it
       produced a scrollbar and a clipped pill on a screen with room to spare.
       At 50 they need 832px and flex-grow spreads them across whatever the card
       actually is. Narrower laptops still scroll, which is the honest fallback —
       the pills never shrink below legibility, the row just runs off. */
    /* Scoped through the parent for SPECIFICITY, not for readability: the base
       .day-scroller__pill rule is declared further down this file, so at equal
       specificity it wins on order and this block silently did nothing. */
    .day-scroller .day-scroller__pill { flex: 1 1 auto; min-width: 50px }
}

@media (max-width: 720px) {
    /* Native date input becomes secondary on mobile (kept available for off-scroller dates) */
    .aweigh-form__date-field {
        grid-column: 1 / -1;
        opacity: .72;
    }
    .aweigh-form__date-field label {
        font-size: 9px;
    }
}

.day-scroller__pill {
    flex: 0 0 auto;
    scroll-snap-align: center;
    min-width: 68px;
    padding: 9px 6px;
    border: 1px solid rgba(var(--edge-rgb), .12);
    background: rgba(var(--panel-rgb), .55);
    color: var(--ink-2);
    border-radius: var(--r-md);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    cursor: pointer;
    transition: background .15s ease, border-color .15s ease, color .15s ease;
    appearance: none;
    font-family: inherit;
}
.day-scroller__pill:hover {
    border-color: var(--brass);
    color: var(--ink);
}
.day-scroller__pill--active {
    background: var(--brass);
    border-color: var(--brass);
    color: var(--ground);
    box-shadow: 0 0 0 3px rgba(var(--accent-rgb), .15);
}
.day-scroller__day {
    font: 600 9px/1 var(--ff-body);
    letter-spacing: .14em;
    text-transform: uppercase;
}
.day-scroller__num {
    font: 700 20px/1 var(--ff-display);
    font-feature-settings: "tnum";
}

/* ---------- Results header (Go/No-Go + Beaufort + units + profile) ---------- */

.results {
    margin-top: 22px;
    background: linear-gradient(180deg, var(--tide), var(--tide-2));
    border: var(--rule);
    border-radius: var(--r-lg);
    padding: 22px;
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, .05), var(--shadow-card);
}

.results__head {
    display: flex;
    align-items: center;
    gap: 14px;
    flex-wrap: wrap;
    margin-bottom: 16px;
}
.results__title {
    margin: 0;
    font: 600 22px/1.15 var(--ff-display);
    flex: 1 1 auto;
    min-width: 200px;
}
.results__title .eyebrow {
    display: block;
    font: 600 10px/1 var(--ff-body);
    letter-spacing: .26em;
    text-transform: uppercase;
    color: var(--brass);
    margin-bottom: 4px;
}

.results__controls {
    display: flex;
    gap: 10px;
    align-items: center;
    flex-wrap: wrap;
    margin-left: auto;
}

.units-toggle,
.profile-picker {
    display: inline-flex;
    background: rgba(var(--panel-rgb), .6);
    border: 1px solid rgba(var(--edge-rgb), .12);
    border-radius: 999px;
    padding: 3px;
    font: 600 11px/1 var(--ff-mono);
    letter-spacing: .08em;
}
.units-toggle button,
.profile-picker button {
    appearance: none;
    background: transparent;
    border: none;
    color: var(--ink-2);
    padding: 7px 12px;
    border-radius: 999px;
    cursor: pointer;
    min-height: 32px;
    text-transform: uppercase;
}
.units-toggle button[aria-pressed=true],
.profile-picker button[aria-pressed=true] {
    background: var(--brass);
    color: var(--ground);
}

/* ---------- Go/No-Go stamp ---------- */

.gng {
    font: 800 14px/1 var(--ff-display);
    letter-spacing: .18em;
    text-transform: uppercase;
    border: 2px solid currentColor;
    padding: 7px 12px;
    border-radius: 6px;
    transform: rotate(-6deg);
    display: inline-block;
    opacity: .94;
    align-self: center;
    cursor: pointer;   /* whole stamp opens the "why?" explainer (see aweigh.js) */
    -webkit-tap-highlight-color: transparent;
    transition: opacity .15s ease, transform .08s ease;
}
/* **The glow is OUTSIDE the stamp, never inside it.** The stamp is an outlined
   rubber mark on cream, which is the look — but it left the biggest decision on
   the page carrying no more visual weight than the body text around it. Filling
   it would fix that and lose the stamp; a halo fixes it and keeps the stamp.

   Two shadows at different radii rather than one, because a single blur has a
   visible edge where it stops. Layering a tight one inside a wide faint one
   gives a falloff that reads as a gradient. **The four percentages are the
   dial**, and they were set by looking rather than by theory: 30/14 read too
   politely on cream, 55/28 overshot, 42/21 is the point between them Roland
   settled on. Red mode is tuned separately below, never scaled from these — a
   halo on near-black is a different problem, and it landed at 25/11.

   **currentColor is doing the real work here.** The stamp is --seaglass on GO,
   --signal on NO-GO and --buoy on GO*, and every one of those is redefined again
   in Red mode. Deriving the halo from the text colour means one rule covers six
   states and cannot drift from them — a hardcoded rgba() would be six values to
   maintain and five chances to get one wrong. */
.gng {
    box-shadow:
        0 0 11px 2px color-mix(in srgb, currentColor 42%, transparent),
        0 0 29px 10px color-mix(in srgb, currentColor 21%, transparent);
}
.gng:hover { opacity: 1 }
.gng:active { transform: rotate(-6deg) scale(.96) }
.gng:focus-visible { outline: 2px solid var(--brass); outline-offset: 3px }
.gng--go { color: var(--seaglass) }
.gng--no { color: var(--signal) }

/* Stamp + "why?" info button travel together; only the stamp is rotated. */
.gng-wrap {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    align-self: center;
}
.gng-info {
    appearance: none;
    background: transparent;
    border: none;
    color: var(--ink-2);
    width: 26px; height: 26px;
    border-radius: 50%;
    cursor: pointer;
    display: inline-flex;
    align-items: center; justify-content: center;
    padding: 0;
    transition: color .15s ease, background .15s ease;
}
.gng-info svg { width: 18px; height: 18px }
.gng-info:hover { color: var(--brass); background: rgba(var(--panel-rgb), .5) }

/* Explainer dialog — inherits .qr-flyout chrome (centered), just the body is bespoke. */
/* The explainer borrows the QR flyout's chrome, but the flyout's 380px cap was
   sized around a square QR image. This holds prose, and at 16px that width
   forced awkward wraps — a trailing "PM)." landing alone on its own line.
   Still narrower than a comfortable reading measure, which is right for a
   centred dialog. */
/* Specificity 0,2,0 deliberately: .qr-flyout's own max-width is declared later
   in this file, so a single-class selector here would lose on source order. */
.qr-flyout.gng-explain { max-width: 460px }

.gng-explain__body {
    color: var(--ink);
    font-size: 16px;
    line-height: 1.6;
    margin-top: 4px;
}
.gng-explain__verdict {
    font: 800 19px/1 var(--ff-display);
    letter-spacing: .12em;
    text-transform: uppercase;
    display: inline-block;
    margin-bottom: 8px;
}
.gng-explain__verdict--go { color: var(--seaglass) }
.gng-explain__verdict--no { color: var(--signal) }
.gng-explain__body strong { color: var(--ink) }
.gng-explain__body .muted { color: var(--ink-2); font-size: 14px }

/* The keyboard route into the per-hour explainer, inside the detail panel.
   Quiet by design: the pill above is the primary affordance and this is the
   accessible twin, not a second call to action. */
.bridge__why {
    appearance: none;
    background: none;
    border: 0;
    padding: 4px 0 8px;
    margin: 0;
    color: var(--brass);
    font: 600 11px/1 var(--ff-mono);
    letter-spacing: .08em;
    text-transform: uppercase;
    text-decoration: underline;
    text-underline-offset: 3px;
    cursor: pointer;
}
.bridge__why:hover { color: var(--ink) }
.bridge__why[hidden] { display: none }

/* ---------- Per-hour clause checklist ----------
   One row per limit: mark, what it is, the reading, the limit. A grid rather
   than a table so the marks and the numbers line up in a column the eye can run
   down — which is the whole point of showing the passes alongside the failure. */
.hourx {
    list-style: none;
    margin: 14px 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 1px;
}
.hourx__row {
    display: grid;
    grid-template-columns: 16px 1fr auto auto;
    gap: 10px;
    align-items: baseline;
    padding: 7px 0;
    border-bottom: 1px solid rgba(var(--edge-rgb), .07);
    font: 500 13.5px/1.3 var(--ff-body);
}
.hourx__row:last-child { border-bottom: 0 }
.hourx__mark { font-weight: 700; text-align: center }
.hourx__label { color: var(--ink-2) }
/* Tabular figures so the readings stack in a straight column. */
.hourx__value { color: var(--ink); font-weight: 600; font-variant-numeric: tabular-nums }
.hourx__limit {
    color: var(--ink-2);
    font: 500 12px/1.3 var(--ff-mono);
    font-variant-numeric: tabular-nums;
}
.hourx__row--ok     .hourx__mark { color: var(--seaglass) }
/* The failing clause is the answer to "why", so it is the one thing in the
   dialog that carries weight — label and reading both, not just the mark. */
.hourx__row--bad    .hourx__mark,
.hourx__row--bad    .hourx__label,
.hourx__row--bad    .hourx__value { color: var(--signal); font-weight: 700 }
/* No reading is neither a pass nor a failure, and must not be dressed as either. */
.hourx__row--absent .hourx__mark,
.hourx__row--absent .hourx__value { color: var(--ink-2); font-weight: 500; font-style: italic }

/* Active-profile thresholds footer — the limits this verdict was judged against. */
.gng-explain__profile {
    margin: 14px 0 0;
    padding-top: 12px;
    border-top: 1px solid rgba(var(--edge-rgb), .12);
    font: 500 13px/1.6 var(--ff-mono);
    color: var(--ink-2);
}
.gng-explain__profile strong {
    color: var(--brass);
    text-transform: uppercase;
    letter-spacing: .06em;
}
/* Sits inline at the end of the limits it edits, so the link is next to the
   thing it changes rather than announcing itself. Underlined because it is the
   only interactive text in the dialog and colour alone would not say so. */
.gng-explain__edit {
    color: var(--brass);
    font-weight: 600;
    text-decoration: underline;
    text-underline-offset: 2px;
    white-space: nowrap;
}
.gng-explain__edit:hover,
.gng-explain__edit:focus-visible { color: var(--ink) }

/* Data-age line under the location. Quiet by default — it should be findable
   when someone wonders how current the reading is, not competing with the
   forecast itself. Goes amber once the reading is older than the server-side
   cache window, which means something is wrong upstream rather than merely
   cached. */
.results__asof {
    display: block;
    margin-top: 4px;
    font: 500 11px/1.4 var(--ff-mono);
    letter-spacing: .04em;
    color: var(--ink-2);
    opacity: .8;
}
.results__asof[data-stale] {
    color: var(--buoy);
    opacity: 1;
}
/* Every live source failed and the page is showing the day's last reading.
   --signal, not --buoy: the stamp turning buoy means "older than you might
   think"; this means "the number you would have got is not available". */
.results__stale {
    margin: 6px 0 0;
    max-width: 52ch;
    font: 500 13px/1.45 var(--ff-body);
    color: var(--signal);
}

/* A reading from a PREVIOUS DAY is a different order of wrong from one that is
   twenty minutes old, and it must not look like the same amber hint. The
   ordinary stale state means "upstream is lagging"; this one means the page
   itself is a leftover — the verdict above it describes yesterday. It reads as
   an alert because a day-old go/no-go call on a safety tool is one. */
.results__asof[data-stale-day] {
    color: var(--signal);
    opacity: 1;
    font-weight: 700;
    padding: 3px 8px;
    margin-top: 6px;
    border-radius: var(--r-sm);
    border: 1px solid var(--signal);
    background: rgba(var(--edge-rgb), .06);
}

/* ---------- Beaufort badge ---------- */

.badge {
    font: 600 11px/1 var(--ff-mono);
    letter-spacing: .06em;
    padding: 7px 11px;
    border-radius: 999px;
    border: 1px solid rgba(var(--accent-rgb), .5);
    color: var(--brass);
    white-space: nowrap;
}
.badge--beaufort { background: rgba(var(--accent-rgb), .08) }

/* ---------- Day Band ---------- */

.day-band {
    position: relative;
    margin-top: 4px;
    background: var(--ground);
    border: var(--rule);
    border-radius: var(--r-md);
    padding: 18px 16px 14px;
    overflow: hidden;

    /* The sky ribbon paints its OWN gradient (.day-band__sky, 85% opacity) and
       that gradient is dark, in both themes, because it depicts night. So the
       band is a dark island on a light page, and everything inside it has to
       read light-on-dark no matter what the page theme is.
       Redefining the tokens here rescopes the whole subtree — markers, hour
       ticks and the lunar chip all inherit these instead of the page values.
       Without it, the light flip put navy text on a navy sky and the sunrise
       and tide times became unreadable. */
    --ink: #F4F1EA;
    --ink-2: #CFC9BC;
    --edge-rgb: 244, 241, 234;
}

.day-band__sky {
    position: absolute;
    inset: 0;
    z-index: 0;
    opacity: .85;
}

.day-band__inner {
    position: relative;
    z-index: 1;
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 14px;
    align-items: center;
}

.day-band__track {
    position: relative;
    height: 78px;
}

.day-band__hours {
    position: absolute;
    inset: auto 0 0 0;
    height: 14px;
    display: flex;
    justify-content: space-between;
    font: 500 9px/1 var(--ff-mono);
    color: var(--ink-2);
    opacity: .55;
}
.day-band__hours span { transform: translateX(-50%) }
.day-band__hours span:first-child { transform: none }
.day-band__hours span:last-child { transform: translateX(-100%) }

.day-band__axis {
    position: absolute;
    left: 0; right: 0; bottom: 18px;
    height: 1px;
    background: rgba(var(--accent-rgb), .35);
}

.day-band__arc {
    position: absolute;
    left: 0; right: 0; top: 4px; bottom: 18px;
    pointer-events: none;
}
.day-band__arc svg { width: 100%; height: 100%; overflow: visible }

.day-band__marker {
    position: absolute;
    bottom: 18px;
    transform: translate(-50%, 50%);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    color: var(--ink);
    pointer-events: auto;
}
.day-band__marker .ico {
    width: 22px; height: 22px;
    display: flex; align-items: center; justify-content: center;
    background: var(--ground);
    border: 1px solid currentColor;
    border-radius: 50%;
    color: var(--brass);
    flex: 0 0 auto;
}
.day-band__marker .ico svg { width: 14px; height: 14px }
.day-band__marker .lbl {
    font: 600 10px/1 var(--ff-mono);
    color: var(--ink-2);
    white-space: nowrap;
    transform: translateY(8px);
}
.day-band__marker--high .ico { color: var(--seaglass) }
.day-band__marker--low  .ico { color: var(--signal) }
.day-band__marker--sunrise .ico,
.day-band__marker--sunset .ico,
.day-band__marker--noon .ico { color: var(--buoy) }

.day-band__now {
    position: absolute;
    top: 0; bottom: 14px;
    width: 1px;
    background: var(--ink);
    z-index: 3;
}
.day-band__now::before {
    content: 'NOW';
    position: absolute;
    top: -4px;
    left: 4px;
    font: 700 9px/1 var(--ff-mono);
    letter-spacing: .12em;
    color: var(--ink);
}
.day-band__now::after {
    content: '';
    position: absolute;
    left: 50%; bottom: -3px;
    width: 7px; height: 7px;
    border-radius: 50%;
    background: var(--ink);
    transform: translateX(-50%);
}
@media (prefers-reduced-motion: no-preference) {
    .day-band__now::after { animation: aweighPulse 2.4s ease-in-out infinite }
}
@keyframes aweighPulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(var(--edge-rgb), .45) }
    50%      { box-shadow: 0 0 0 6px rgba(var(--edge-rgb), 0) }
}

.day-band__moon {
    display: flex; flex-direction: column; align-items: center;
    gap: 4px;
    padding: 8px 14px;
    border-left: 1px solid rgba(var(--edge-rgb), .12);
    color: var(--ink-2);
    min-width: 90px;
}
.day-band__moon .glyph { font-size: 30px; line-height: 1; filter: drop-shadow(0 0 6px rgba(var(--edge-rgb), .15)) }
.day-band__moon .name  { font: 500 10px/1.2 var(--ff-mono); text-align: center; max-width: 90px }
.day-band__moon .illum { font: 600 10px/1 var(--ff-mono); color: var(--brass) }

@media (max-width: 520px) {
    .day-band__inner { grid-template-columns: 1fr; }
    .day-band__moon  { flex-direction: row; align-items: center; gap: 10px;
                       border-left: none; border-top: 1px solid rgba(var(--edge-rgb), .12);
                       padding: 10px 0 0; min-width: 0; }
    .day-band__moon .glyph { font-size: 22px }

    /* At narrow widths, absolute-positioned marker labels overlap. Drop the
       text labels on tide H/L markers (their colored icons + position on the
       ribbon already convey the timing); shrink the sun/noon/sunset labels;
       and give the track a touch more vertical room for the staggered look. */
    .day-band__track { height: 88px }
    .day-band__marker .lbl { font-size: 8px; letter-spacing: 0; transform: translateY(10px) }
    .day-band__marker--high .lbl,
    .day-band__marker--low .lbl { display: none }
}

/* ---------- Instrument Cluster ---------- */

.cluster {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 14px;
    margin-top: 20px;
}

/* The cluster is a variable number of dials: three always (wind, gust, temp),
   plus visibility when it is restricted — which is a handful of days a year —
   plus water temperature wherever a reading exists for the harbor. So three,
   four or five.

   The count comes from PHP as a modifier rather than from auto-fit, because
   auto-fit derives its track count from container width and would rewrap at
   sizes nobody tested. Mobile overrides the columns entirely below, so this only
   governs the wide layout.

   Was `.cluster--three`, a single boolean modifier, when visibility was the only
   optional dial. A second optional dial makes the count the thing worth naming. */
.cluster--3 { grid-template-columns: repeat(3, 1fr) }
.cluster--4 { grid-template-columns: repeat(4, 1fr) }
.cluster--5 { grid-template-columns: repeat(5, 1fr) }
@media (max-width: 720px) {
    .cluster {
        grid-template-columns: none;
        grid-auto-flow: column;
        grid-auto-columns: 78%;
        overflow-x: auto;
        scroll-snap-type: x mandatory;
        scroll-padding: 10px;
        padding: 0 0 8px;
        margin-left: -2px;
        margin-right: -2px;
        scrollbar-width: thin;
    }
    .dial { scroll-snap-align: center }
}

.dial {
    margin: 0;
    background: linear-gradient(180deg, var(--tide), var(--tide-2));
    border: var(--rule);
    border-radius: var(--r-lg);
    padding: 18px 14px 16px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, .05), var(--shadow-card);
}

.dial svg {
    width: 100%;
    max-width: 170px;
    height: auto;
    display: block;
}

.dial__track {
    stroke: rgba(var(--edge-rgb), .08);
    fill: none;
    stroke-width: 8;
    stroke-linecap: round;
}
.dial__arc {
    stroke: var(--brass);
    fill: none;
    stroke-width: 8;
    stroke-linecap: round;
    transition: stroke .35s ease;
    filter: drop-shadow(0 0 6px rgba(var(--accent-rgb), .25));
}
.dial__needle line {
    stroke: var(--ink);
    stroke-width: 2;
    stroke-linecap: round;
}
.dial__needle circle {
    fill: var(--brass);
    stroke: var(--ground);
    stroke-width: 1.5;
}

@media (prefers-reduced-motion: no-preference) {
    .dial__needle { transition: transform .6s cubic-bezier(.34, 1.56, .64, 1) }
}

.dial figcaption {
    text-align: center;
    margin-top: -36px;        /* tuck readout up into the dial */
    position: relative;
    z-index: 2;
}
.dial__readout {
    display: inline-flex;
    align-items: baseline;
    gap: 4px;
}
.dial__num {
    font: 700 30px/1 var(--ff-display);
    color: var(--ink);
    font-feature-settings: "tnum";
}
.dial__unit {
    font: 500 11px/1 var(--ff-mono);
    color: var(--ink-2);
}
.dial__label {
    display: block;
    margin-top: 26px;
    font: 600 10px/1 var(--ff-body);
    letter-spacing: .22em;
    text-transform: uppercase;
    color: var(--brass);
}
.dial__when {
    display: block;
    margin-top: 5px;
    font: 500 11px/1 var(--ff-mono);
    color: var(--ink-2);
}

/* ---------- Hourly forecast ---------- */

/* The hourly list is NOT its own card — it sits flush inside the Forecast
   (.results) card so it sits directly under the Forecast heading and
   the hour rows get the full content width. No background/border/padding;
   only the per-row chrome remains. */
.bridge {
    margin-top: 20px;
}

.card__head {
    display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
    margin-bottom: 12px;
}
.card__eyebrow {
    font: 600 11px/1 var(--ff-body);
    letter-spacing: .22em;
    text-transform: uppercase;
    color: var(--brass);
}
.card__title { margin: 0; font: 600 18px/1.2 var(--ff-display) }
.card__head .badge { margin-left: auto }

.bridge__list {
    list-style: none; padding: 0; margin: 0;
    display: grid; grid-template-columns: 1fr; gap: 6px;
}

.bridge__row {
    border-radius: var(--r-md);
    background: rgba(var(--panel-rgb), .45);
    border: 1px solid rgba(var(--edge-rgb), .05);
    transition: background .15s ease, border-color .15s ease, box-shadow .15s ease;
    overflow: hidden;   /* keep the detail panel inside the rounded corners */
}

/* An expanded row gets a defined edge, so the open card reads as one object
   rather than as detail floating under a list.

   Opacity rather than a colour, because --edge-rgb inverts between themes:
   navy on paper in day mode (raising it darkens) and salmon on near-black in
   Red mode (raising it brightens). Both directions read as "more defined",
   which is the actual intent.

   Deliberately border + shadow only, leaving `background` alone: the current
   hour already claims the background with its own tint, and this rule sits
   after [data-now] in the cascade — touching background here would silently
   strip the "now" marker from whichever row happens to be open. */
.bridge__row[data-open] {
    border-color: rgba(var(--edge-rgb), .3);
    box-shadow: 0 2px 10px rgba(var(--edge-rgb), .07);
}
/* The summary/detail divider lifts to match, otherwise a stronger outer border
   makes the hairline inside it look like a rendering artefact. */
.bridge__row[data-open] .bridge__detail {
    border-top-color: rgba(var(--edge-rgb), .18);
}
.bridge__row[data-now] {
    background: rgba(var(--edge-rgb), .04);
    outline: 1px solid var(--ink);
    box-shadow: 0 0 0 3px rgba(var(--edge-rgb), .08);
}

/* The summary is the accessible click target — a real <button> so Enter/Space
   and focus come for free. The grid that used to live on the row lives here. */
.bridge__summary {
    display: grid;
    /* time · conditions · temp · wind · precip · seas · verdict · chevron */
    grid-template-columns: 66px 1fr 66px 100px 64px 58px 50px 18px;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 11px 14px;
    margin: 0;
    background: none;
    border: 0;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}
.bridge__summary:hover { background: rgba(var(--edge-rgb), .03) }
.bridge__summary:focus-visible { outline: 2px solid var(--brass); outline-offset: -2px }

.bridge__chevron {
    width: 14px; height: 14px;
    color: var(--ink-2);
    justify-self: center;
    transition: transform .18s ease;
}
.bridge__row[data-open] .bridge__chevron { transform: rotate(180deg) }

/* Expandable detail: gusts, cardinal wind direction, tide state + height. */
.bridge__detail { padding: 4px 14px 12px; border-top: 1px dashed rgba(var(--edge-rgb), .1) }
.bridge__detail[hidden] { display: none }
.bridge__detail-grid {
    margin: 0;
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 0 18px;
}
.bridge__detail-item {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 10px;
    padding: 7px 0;
    border-bottom: 1px solid rgba(var(--edge-rgb), .05);
}
.bridge__detail-item dt {
    font: 600 10px/1 var(--ff-mono);
    text-transform: uppercase;
    letter-spacing: .08em;
    color: var(--ink-2);
}
.bridge__detail-item dd { margin: 0; font: 600 14px/1 var(--ff-body); color: var(--ink) }
.bridge__detail-item dd small { color: var(--ink-2); font-weight: 500; margin-left: 2px }

.bridge__tide--rising  { color: var(--seaglass) }
.bridge__tide--falling { color: var(--brass) }
.bridge__tide--slack   { color: var(--ink-2) }

.bridge__time { font: 600 13px/1 var(--ff-mono); color: var(--ink) }
.bridge__cond { display: flex; align-items: center; gap: 10px; color: var(--ink-2); min-width: 0 }

/* Sea state. --tide is the water token, which is what distinguishes this
   from the precipitation drop beside it — both are water, one falls and one
   is already there. The --none variant is an empty placeholder that keeps the
   grid columns aligned on hours the wave model did not cover; it renders
   nothing rather than a dash, because a dash reads as a measured zero. */
.bridge__sea {
    display: flex; align-items: center; gap: 4px;
    color: var(--ink-2); font-size: 12px; min-width: 0;
}
.bridge__sea .wave { width: 13px; height: 13px; flex: 0 0 auto; opacity: .75 }
.bridge__sea-num small { font-size: .78em; opacity: .7; margin-left: 1px }
.bridge__sea--none { visibility: hidden }
.bridge__desc { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 13px }
/* Conditions inside the accordion. Hidden by default because the summary row
   already carries them at desktop widths; the phone breakpoint switches it on.
   Duplicating there is the point — under 520px the row version is display:none. */
.bridge__detail-item--cond { display: none }
.bridge__cond-detail { display: flex; align-items: center; gap: 8px }
.bridge__icon--sm { width: 32px; height: 32px }

.bridge__icon {
    width: 40px; height: 40px;
    flex: 0 0 auto;
    /* OWM icons are blue/cool on a transparent bg. A gentle warm desaturation
       blends them into Aweigh's brass-on-tide palette without losing readability. */
    filter: saturate(.9);
}
[data-aweigh="red"] .bridge__icon {
    /* Tint to monochrome red for night vision. CSS recipe: strip color, push
       to sepia (~30°), then rotate the hue back to red and boost saturation. */
    filter: grayscale(1) sepia(1) saturate(6) hue-rotate(-30deg) brightness(.65);
}

.bridge__temp {
    font: 600 16px/1 var(--ff-body);
    text-align: right;
    font-feature-settings: "tnum";
}
.bridge__temp small { color: var(--ink-2); font-weight: 500; margin-left: 2px }

.bridge__wind {
    display: flex; align-items: center; gap: 10px;
    justify-content: flex-end; color: var(--ink);
}
.bridge__card {
    font: 500 13px/1 var(--ff-mono);
    color: var(--ink-2);
    flex: 0 0 auto;
}
.bridge__mph { font: 500 13px/1 var(--ff-mono); color: var(--ink-2) }

.bridge__precip {
    display: flex;
    align-items: center;
    gap: 5px;
    justify-content: flex-end;
    font: 600 13px/1 var(--ff-mono);
    font-feature-settings: "tnum";
    color: var(--ink-2);
    white-space: nowrap;
}
.bridge__precip .drop { width: 11px; height: 11px; flex: 0 0 auto; opacity: .75 }
.bridge__precip small { font-size: 10px; margin-left: 1px; opacity: .7 }
.bridge__precip[data-intensity="low"]  { color: var(--ink-2); opacity: .55 }
.bridge__precip[data-intensity="med"]  { color: var(--brass) }
.bridge__precip[data-intensity="high"] { color: var(--signal); font-weight: 700 }

/* **Visibility only renders when restricted**, so unlike the other detail cells
   its presence is already the message and it is allowed to shout a little.
   Tokens only — Red mode inherits both, same as the alert banner. */
/* ---------- Sunrise / sunset markers in the hourly list ----------
   Deliberately unlike a row: no card, no border, no hover, nothing to tap. It
   is a divider that happens to carry a time, and it should read as the list
   breaking rather than as another hour in it.

   Its own class, never .bridge__row — see the note in bridge-watch.php. */
.bridge__sun {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 7px 14px;
    color: var(--brass);
    font: 600 11px/1 var(--ff-mono);
    letter-spacing: .1em;
    text-transform: uppercase;
}
.bridge__sun-time { font-variant-numeric: tabular-nums }
.bridge__sun-ico { display: inline-flex; flex: 0 0 auto }
.bridge__sun-ico svg { width: 15px; height: 15px }
/* The rule runs to the end of the row, so the marker reads as a line across the
   day rather than a label floating beside it. */
.bridge__sun-label { flex: 0 0 auto }
.bridge__sun::after {
    content: "";
    flex: 1 1 auto;
    height: 1px;
    background: rgba(var(--accent-rgb), .35);
}
@media (max-width: 520px) {
    .bridge__sun { padding: 6px 8px; gap: 7px; font-size: 10px }
}

/* ---------- Fog risk ----------
   The forward-looking half of visibility: dew point conditions that produce fog
   hours before the visibility model commits to it.

   **Severity is carried by the word and the weight, not by the hue.** --signal
   and --brass measured 1.01:1 against each other in Red mode — same lightness,
   hue only — which is the finding that made every NWS alert one colour in
   August. Fog matters most at night, which is exactly when that pair stops
   being two colours, so "Likely"/"Possible" and "FOG"/"FOG?" have to survive
   monochrome on their own. The colour is a nudge for the day-mode reader and
   nothing more. */
.bridge__fog--likely   { color: var(--signal); font-weight: 700 }
.bridge__fog--possible { color: var(--brass);  font-weight: 600 }
.bridge__fog small {
    display: inline-block;
    margin-left: 4px;
    font-size: .78em;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: .04em;
    opacity: .85;
}

/* The at-a-glance chip, inside .bridge__cond rather than in a column of its own
   — see the note in bridge-watch.php. flex: 0 0 auto so it never shrinks: the
   description beside it is the element with the ellipsis, and it is the one that
   should give way. */
.bridge__fogchip {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    flex: 0 0 auto;
    font: 700 10px/1 var(--ff-mono);
    letter-spacing: .06em;
    color: var(--signal);
}
.bridge__fogchip--possible { color: var(--brass); font-weight: 600 }
.bridge__fogchip svg { width: 13px; height: 13px; flex: 0 0 auto }

/* ---------- Welcome line ----------
   One slim row under the brand header saying who the app thinks you are. Three
   variants ship; **CSS picks exactly one** from attributes the synchronous head
   script stamped, so nothing is inserted after load and nothing moves.

   Default state is HIDDEN for the two signed-out variants. That direction is
   deliberate: if the head script is blocked or localStorage throws (private
   mode, blocked site data), a visitor sees no welcome line rather than both of
   them contradicting each other. */
.hello {
    display: none;
    align-items: center;
    gap: 10px;
    margin: 0 0 14px;
    padding: 10px 14px;
    background: rgba(var(--panel-rgb), .55);
    border: 1px solid rgba(var(--edge-rgb), .12);
    border-radius: var(--r-md);
    font: 500 13.5px/1.4 var(--ff-body);
    color: var(--ink-2);
}
.hello strong { color: var(--ink); font-weight: 600 }
.hello span { flex: 1 1 auto; min-width: 0 }

/* The greeting is server-rendered only when signed in, so it needs no
   known/unknown test — just the once-per-session one. */
.hello--greet { display: flex }
[data-greeted] .hello--greet { display: none }

/* Signed out: the flag decides which. Neither shows once dismissed. */
[data-known]:not([data-hello-off]) .hello--known { display: flex }
html:not([data-known]):not([data-hello-off]) .hello--new { display: flex }

.hello__cta {
    flex: 0 0 auto;
    color: var(--brass);
    font-weight: 600;
    text-decoration: underline;
    text-underline-offset: 3px;
    white-space: nowrap;
}
.hello__cta:hover { color: var(--ink) }
.hello__x {
    flex: 0 0 auto;
    appearance: none;
    background: none;
    border: 0;
    color: var(--ink-2);
    font-size: 20px;
    line-height: 1;
    padding: 0 2px;
    cursor: pointer;
}
.hello__x:hover { color: var(--ink) }
@media (max-width: 520px) {
    .hello { flex-wrap: wrap }
    .hello span { flex: 1 1 100% }
}

/* ---------- The day in a sentence ----------
   Sits between the verdict and the vessel picker. Like .fogline below it, this
   takes a full row inside .results__head — already a wrapping flex row — so it
   needs no change to the header's layout and simply is not there when the
   providers had nothing to say.

   Body font at 15px, not the display serif: it is prose to be read once, and
   the serif is reserved for the things that name the page (the location, the
   card titles) and for the verdict itself. */
.daysum {
    flex: 1 0 100%;
    margin: 2px 0 0;
    font: 400 15px/1.5 var(--ff-body);
    color: var(--ink-2);
    max-width: 62ch;
}
/* The high/low is the number people scan for, so it holds ink weight while the
   prose around it stays muted. */
.daysum__temps { color: var(--ink); font-weight: 600 }
/* Inline spans, spaced by a real space rather than a margin — they are
   sentences in a paragraph, and margins would break at a line wrap. */
/* The window sentence, its own line under the weather one. Ink rather than
   --ink-2: it is the actionable half of the summary, and it sits directly under
   prose that is deliberately quieter. */
.daysum--window { margin-top: 4px; color: var(--ink); font-weight: 600 }
.daysum--window[hidden] { display: none }

.daysum__temps + .daysum__sky::before,
.daysum__sky + .daysum__rain::before,
.daysum__temps + .daysum__rain::before { content: " " }

/* The day-level clause under the Go/No-Go stamp. flex-basis 100% drops it onto
   its own line inside .results__head, which is already a wrapping flex row — so
   it needs no change to the header's layout, and it simply is not there on the
   overwhelming majority of days. */
.fogline {
    flex: 1 0 100%;
    display: flex;
    align-items: center;
    gap: 7px;
    margin: 2px 0 0;
    font: 500 13px/1.35 var(--ff-body);
    color: var(--signal);
}
.fogline--possible { color: var(--brass) }
.fogline svg { width: 15px; height: 15px; flex: 0 0 auto }
.fogline strong { font-weight: 700; text-transform: capitalize }
.fogline span { color: var(--ink-2); font-weight: 500 }

.bridge__vis--restricted { color: var(--brass); font-weight: 600 }
.bridge__vis--fog        { color: var(--signal); font-weight: 700 }
.bridge__vis small {
    display: inline-block;
    margin-left: 4px;
    font-size: .78em;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: .04em;
    opacity: .85;
}
.dial--vis-restricted .dial__label { color: var(--brass) }
.dial--vis-fog        .dial__label { color: var(--signal); font-weight: 700 }
.bridge__precip[data-intensity="high"] .drop { opacity: 1 }

/* The pill is a tap target now — tapping it explains the hour rather than
   expanding the row. It stays a <span> because it sits inside a <button>; see
   the note in bridge-watch.php. The cursor and the hover lift are the only
   affordance it can carry, since it cannot take focus of its own. */
.row-gng { cursor: pointer }
.row-gng:hover { filter: brightness(1.08) }
@media (hover: none) { .row-gng:hover { filter: none } }

.row-gng {
    font: 700 9px/1 var(--ff-mono);
    letter-spacing: .14em;
    text-align: center;
    padding: 6px 8px;
    border-radius: 999px;
    text-transform: uppercase;
    justify-self: end;
    min-width: 38px;
    border: 1px solid currentColor;
    transition: background .15s ease, color .15s ease, border-color .15s ease;
}
.row-gng[data-verdict="go"] {
    color: var(--seaglass);
    background: rgba(127, 179, 164, .12);
    border-color: rgba(127, 179, 164, .35);
}
.row-gng[data-verdict="no"] {
    color: var(--signal);
    background: rgba(200, 69, 58, .12);
    border-color: rgba(200, 69, 58, .35);
}

@media (max-width: 520px) {
    /* The hourly list has no wrapper card, so rows span the full .results
       content width. Keep the row internals tight:
       - summary padding 11/14→8/6 (gains horizontal room)
       - wind column uses minmax(0, 1fr) instead of 1fr — without minmax(0,…)
         the column refuses to shrink below its content's intrinsic width and
         pushes the row past the card border. Overflow:hidden on the wind cell
         clips the (rare) case where even the minimum doesn't fit. */
    .bridge__summary {
        grid-template-columns: 50px 56px minmax(0, 1fr) 44px 34px 14px;
        gap: 6px;
        padding: 8px 6px;
        min-width: 0;
    }
    .bridge__detail { padding: 4px 8px 10px }
    .bridge__detail-grid { grid-template-columns: 1fr }
    .bridge__chevron { width: 12px; height: 12px }
    /* TWO LINES ON A PHONE, added 2026-08-18.
       The row used to be a single seven-column strip that ran out of width, so
       the weather icon and description were hidden and duplicated inside the
       accordion. That put the most glanceable thing on the page behind a tap:
       "what does 3 PM look like" is answered by the icon before any number.

       Now the row wraps. Line one is identity — hour, icon, description, and
       the verdict. Line two is the numbers. The icon comes back to the surface,
       and the second line is what makes room for sea state alongside temp,
       wind and rain. */
    .bridge__summary {
        /* Column four is auto, NOT the 14px the chevron used to occupy — the
           chevron is hidden at this width and the verdict pill and sea state
           now live there. Leaving it at 14px made those two cells overflow
           and overlap the precipitation beside them by 21px. */
        grid-template-columns: 58px 1fr auto auto;
        grid-template-areas:
            "time cond cond gng"
            "temp wind pop  sea";
        row-gap: 7px;
        column-gap: 8px;
        align-items: center;
        padding: 10px 12px;
    }
    .bridge__time   { grid-area: time }
    .bridge__cond   { grid-area: cond; display: flex; gap: 7px }
    .row-gng        { grid-area: gng; justify-self: end }
    .bridge__temp   { grid-area: temp }
    .bridge__wind   { grid-area: wind }
    .bridge__precip { grid-area: pop; justify-self: end }
    .bridge__sea    { grid-area: sea; justify-self: end }

    /* The chevron has no cell of its own — four columns are spoken for and the
       whole row is the button. The [open] state on the row already signals it,
       so the affordance is not lost with it. */
    .bridge__chevron { display: none }

    /* Smaller than the desktop 40px so a two-line row stays a two-line row,
       and the description gets one line with an ellipsis rather than wrapping
       the layout open. */
    .bridge__icon { width: 30px; height: 30px }
    .bridge__desc {
        white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
        font-size: 12px; min-width: 0;
    }
    .bridge__detail-item--cond { display: none }
    .bridge__time { font-size: 12px }
    .bridge__temp { font-size: 15px }
    .bridge__wind { gap: 5px; min-width: 0; overflow: hidden }
    .bridge__mph { white-space: nowrap; overflow: hidden; text-overflow: ellipsis }
    .bridge__card { font-size: 12px }
    .bridge__precip { font-size: 11px; gap: 3px; min-width: 0 }
    .bridge__precip .drop { width: 9px; height: 9px }
    /* Line two has room now, so the % sign comes back — it was dropped only
       because seven columns did not fit across a phone. */
    .bridge__precip small { display: inline }
    .row-gng { padding: 5px 6px; min-width: 26px; font-size: 8px; letter-spacing: .1em }
}

/* ---------- Tide Curve ---------- */

.tide-card {
    margin-top: 20px;
    background: linear-gradient(180deg, var(--tide), var(--tide-2));
    border: var(--rule);
    border-radius: var(--r-lg);
    padding: 18px;
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, .05), var(--shadow-card);
}
.tide-card__chart {
    height: 280px;
    position: relative;
    margin-top: 4px;
}
.tide-card__legend {
    list-style: none; padding: 0; margin: 12px 4px 0;
    display: flex; gap: 18px; flex-wrap: wrap;
    font: 500 11px/1 var(--ff-mono); color: var(--ink-2);
}
.dot {
    width: 9px; height: 9px; border-radius: 50%;
    display: inline-block; margin-right: 7px; vertical-align: middle;
    border: 1px solid var(--ground);
}
.dot--high { background: var(--seaglass) }
.dot--low  { background: var(--signal) }
.dot--now  { background: var(--ink) }
.dot--curve { background: var(--brass) }

/* ---------- Extreme tide quick list (under the curve) ---------- */

.tide-extremes {
    list-style: none; padding: 0; margin: 14px 0 0;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    gap: 8px;
}
.tide-extremes li {
    display: flex; align-items: center; gap: 10px;
    padding: 10px 12px;
    background: rgba(var(--panel-rgb), .45);
    border: 1px solid rgba(var(--edge-rgb), .05);
    border-radius: var(--r-md);
}
.tide-extremes .swatch {
    width: 8px; height: 32px; border-radius: 2px;
}
.tide-extremes li[data-type="High"] .swatch { background: var(--seaglass) }
.tide-extremes li[data-type="Low"]  .swatch { background: var(--signal) }
.tide-extremes .meta { display: flex; flex-direction: column; gap: 2px }
.tide-extremes .ttl { font: 600 11px/1 var(--ff-mono); letter-spacing: .12em;
                      text-transform: uppercase; color: var(--ink-2) }
.tide-extremes .val { font: 600 14px/1 var(--ff-body); font-feature-settings: "tnum" }
.tide-extremes .tm  { font: 500 11px/1 var(--ff-mono); color: var(--ink-2) }

/* ---------- Footer / footnotes ---------- */

/* Feedback CTA — same brass treatment as the "Get Forecast" submit button,
   but an inline-flex mailto link (icon + label) that sits on its own line. */
.aweigh__cta {
    margin-top: 30px;
    text-align: center;
}
.aweigh-feedback-cta {
    display: inline-flex;
    align-items: center;
    gap: 9px;
    appearance: none;
    border: 1px solid var(--brass);
    background: linear-gradient(180deg, var(--brass), var(--brass-2));
    color: var(--ground);
    font: 700 13px/1 var(--ff-body);
    letter-spacing: .14em;
    text-transform: uppercase;
    padding: 13px 24px;
    border-radius: var(--r-md);
    min-height: 44px;
    cursor: pointer;
    box-shadow: 0 4px 14px rgba(var(--accent-rgb), .25), inset 0 1px 0 rgba(255, 255, 255, .25);
    transition: transform .08s ease, box-shadow .15s ease;
}
/* Keep the dark hull text on hover — global `a:hover` flips links to foam,
   which would wash out against the brass fill. */
.aweigh-feedback-cta:hover {
    color: var(--ground);
    box-shadow: 0 6px 18px rgba(var(--accent-rgb), .35), inset 0 1px 0 rgba(255, 255, 255, .35);
}
.aweigh-feedback-cta:active { transform: translateY(1px) }
.aweigh-feedback-cta svg { width: 16px; height: 16px; flex: 0 0 auto }

.aweigh__footnote {
    margin-top: 20px;
    font: 500 11px/1.5 var(--ff-mono);
    color: var(--ink-2);
    opacity: .75;
    text-align: center;
}
.aweigh__footnote a { color: var(--brass-2) }

/* The build stamp sits below the attribution and is deliberately the quietest
   thing on the page — it exists so a bug report can name the build, not to be
   read. user-select stays on: being copy-pasteable IS the feature. */
.aweigh__build {
    display: inline-block;
    margin-top: 4px;
    opacity: .7;
    letter-spacing: .04em;
}

/* ---------- Admin area ---------- */

.aweigh--admin { padding-top: 18px }

.admin__bar {
    display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
    padding: 14px 18px;
    background: linear-gradient(180deg, var(--tide), var(--tide-2));
    border: var(--rule);
    border-radius: var(--r-lg);
    margin-bottom: 22px;
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, .05), var(--shadow-card);
}

.admin__brand { display: flex; flex-direction: column; gap: 2px; color: var(--ink) }
.admin__brand .eyebrow {
    font: 600 9px/1 var(--ff-body);
    letter-spacing: .26em;
    text-transform: uppercase;
    color: var(--brass);
}
.admin__brand .title { font: 600 18px/1 var(--ff-display) }

.admin__nav { display: flex; gap: 4px; flex-wrap: wrap; margin-left: 6px }
.admin__nav a {
    color: var(--ink-2);
    padding: 8px 12px;
    border-radius: 999px;
    font: 600 12px/1 var(--ff-body);
    letter-spacing: .04em;
    border: 1px solid transparent;
}
.admin__nav a:hover { color: var(--ink); background: rgba(var(--edge-rgb), .05) }
.admin__nav a[aria-current=page] {
    color: var(--ground);
    background: var(--brass);
    border-color: var(--brass);
}

.admin__session {
    margin-left: auto;
    display: flex; align-items: center; gap: 12px;
    font: 500 12px/1 var(--ff-mono);
}
.admin__session .who { color: var(--ink-2) }
/* ONE pill, every caller: admin's Log out, account's Log out, the consent
   page's "Skip to accept", and account's Forecast/Ports/Vessels row.
   It was `.admin__session .logout`, which is why the SAME markup rendered as a
   badge in /admin/ and as bare underlined text on account.php — that link sits
   in .acct__head, so the descendant selector never matched it. Scoping a
   component's look to one ancestor is how two copies of a control end up
   looking like two different controls. */
.logout,
.consent__jump-link,
.acct__nav-link {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    color: var(--brass);
    border: 1px solid rgba(var(--accent-rgb), .5);
    padding: 8px 14px;
    border-radius: 999px;
    font-size: 13px;
    font-weight: 600;
    letter-spacing: .1em;
    text-transform: uppercase;
    text-decoration: none;
    white-space: nowrap;
}
.logout:hover,
.consent__jump-link:hover,
.acct__nav-link:hover {
    background: rgba(var(--accent-rgb), .12);
    color: var(--ink);
}

/* "Stay signed in" — a checkbox row inside .admin__form, whose children are
   otherwise stacked label-over-input. Overriding to a horizontal row here
   rather than adding a variant to the form itself, since this is the only
   checkbox on any auth page. Tokens throughout, so Red mode needs no override. */
.auth__remember {
    display: flex;
    align-items: center;
    gap: 9px;
    color: var(--ink-2);
    font-size: 14px;
    cursor: pointer;
    /* Nudged up under the password field: the default form gap reads as a gap
       between sections, and this belongs to the field above it. */
    margin-top: -4px;
}
.auth__remember input[type="checkbox"] {
    width: 16px;
    height: 16px;
    margin: 0;
    flex: 0 0 auto;
    accent-color: var(--brass);
    cursor: pointer;
}
.auth__remember span { user-select: none }

/* ---------------------------------------------------------------- back to top
   The Kirkor standard control, ported from the marketing site so the two
   properties share one compass rose. Tokens throughout — the mark is drawn in
   currentColor, so Red mode needs no override and no second asset. */
.visually-hidden {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

.backtotop {
    position: fixed;
    /* Clear of an iOS home-screen install's gesture bar. The inset is 0 in a
       normal browser, so desktop sits at a flat 18px. */
    bottom: calc(18px + env(safe-area-inset-bottom));
    right: calc(18px + env(safe-area-inset-right));
    /* Under the QR flyout and the Go/No-Go explainer, which are <dialog> and
       render in the top layer regardless — but above the page's own cards. */
    z-index: 1900;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* 44px is the smallest comfortable touch target, and this is aimed at a
       thumb on a phone at the bottom of a long forecast. */
    width: 44px;
    height: 44px;
    border-radius: 50%;
    border: 1px solid rgba(var(--accent-rgb), .45);
    background: var(--tide);
    color: var(--brass);
    box-shadow: var(--shadow-card);
    /* visibility, not display: display is not transitionable, so the fade would
       not happen at all. visibility is deliberately NOT in the transition list
       — it is a discrete property, so transitioning it flips at the halfway
       mark, leaving the control unfocusable for ~90ms after it has visibly
       appeared, and pinning it at `hidden` outright wherever transitions are
       stalled (a background tab throttles them). Flipping it instantly and
       fading only opacity and transform looks identical with none of that. */
    opacity: 0;
    visibility: hidden;
    transform: translateY(6px);
    transition: opacity .18s ease, transform .18s ease;
}
.backtotop:hover {
    background: var(--tide-2);
    color: var(--ink);
    border-color: rgba(var(--accent-rgb), .7);
}
.backtotop.is-visible { opacity: 1; visibility: visible; transform: none }
.backtotop__icon { display: block }

@media (prefers-reduced-motion: reduce) { .backtotop { transition: none } }
@media print { .backtotop { display: none } }

/* The scroll destination on both layouts. No focus ring: it receives focus only
   programmatically, after the button has already taken the reader there. */
#top { outline: none }

/* consent.php — "Skip to accept", the one link at the top of the re-accept page.
   Its pill styling is the shared .logout/.consent__jump-link/.acct__nav-link rule. */
.consent__jump { margin: 14px 0 0 }

/* account.php — Forecast / Ports / Vessels, the row under the greeting.
   Two of the three are in-page jumps and one leaves; they are styled alike
   because they answer one question — "where am I going next?" — and splitting
   them visually would only raise the question of why. */
.acct__nav { display: flex; flex-wrap: wrap; gap: 8px; margin: 16px 0 0 }

/* The anchor lands here, so keep the card's own heading clear of the top edge
   rather than flush against it — and of an iOS notch, since the auth pages send
   viewport-fit=cover. Applied to the target, not globally: smooth scrolling is
   deliberately NOT enabled, because a long policy page animating past several
   screens is slower than the jump it replaced. */
#accept,
#ports,
#vessels {
    scroll-margin-top: calc(18px + env(safe-area-inset-top));
    /* No focus ring on a container that only receives focus programmatically —
       the checkbox and button keep theirs. */
    outline: none;
}

/* Signed-in devices list on account.php. */
.device-list { list-style: none; margin: 0 0 16px; padding: 0 }
.device-list__item {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 4px 12px;
    padding: 10px 0;
    border-bottom: 1px solid rgba(var(--accent-rgb), .18);
}
.device-list__item:last-child { border-bottom: 0 }
.device-list__name { color: var(--ink); font-weight: 600 }
.device-list__here {
    font-style: normal;
    font-size: 12px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .1em;
    color: var(--brass);
    margin-left: 8px;
}
.device-list__seen { color: var(--ink-2); font-size: 13px }

.admin__hero { margin-bottom: 22px }
.admin__title {
    margin: 0 0 6px;
    font: 600 24px/1.15 var(--ff-display);
    color: var(--ink);
}
/* Muted footnote under the Sign-in button. --ink-2 re-tints in Red mode. */
.admin__note {
    margin: 10px 0 0;
    text-align: center;
    font-size: 13px;
    color: var(--ink-2);
}
.admin__lede {
    margin: 0 0 14px;
    color: var(--ink-2);
    max-width: 60ch;
    line-height: 1.5;
}

.admin__grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 14px;
}
.admin__tile {
    display: flex; flex-direction: column; gap: 4px;
    padding: 20px;
    background: linear-gradient(180deg, var(--tide), var(--tide-2));
    border: var(--rule);
    border-radius: var(--r-lg);
    color: var(--ink);
    transition: transform .12s ease, box-shadow .15s ease, border-color .15s ease;
    text-decoration: none;
    box-shadow: var(--shadow-card);
}
.admin__tile:hover {
    transform: translateY(-2px);
    border-color: rgba(var(--accent-rgb), .55);
    color: var(--ink);
}
.admin__tile .tile__count {
    font: 700 32px/1 var(--ff-display);
    color: var(--brass);
    font-feature-settings: "tnum";
}
.admin__tile .tile__label {
    font: 600 11px/1 var(--ff-body);
    letter-spacing: .22em;
    text-transform: uppercase;
    color: var(--ink-2);
    margin-top: 8px;
}
.admin__tile .tile__hint { font-size: 13px; color: var(--ink-2); margin-top: 4px }

.admin__card {
    background: linear-gradient(180deg, var(--tide), var(--tide-2));
    border: var(--rule);
    border-radius: var(--r-lg);
    padding: 22px;
    box-shadow: var(--shadow-card);
}

.admin__form { display: grid; gap: 14px; max-width: 460px }
/* A card with no lede between its title and its form. `.admin__lede` was
   carrying the 14px of air, so dropping it left the first field crowding the
   heading. Adjacent-sibling only, so every card that still has a lede is
   untouched. */
.admin__title + .admin__form { margin-top: 14px }
.admin__form label {
    display: block;
    font: 600 10px/1 var(--ff-body);
    letter-spacing: .22em;
    text-transform: uppercase;
    color: var(--brass);
    margin-bottom: 6px;
}
.admin__form label small {
    color: var(--ink-2);
    font-weight: 500;
    letter-spacing: 0;
    text-transform: none;
    margin-left: 6px;
}
.admin__form input[type=text],
.admin__form input[type=email],
.admin__form input[type=password],
.admin__form input[type=number],
.admin__form input[type=date],
.admin__form select,
.admin__textarea {
    width: 100%;
    background: rgba(var(--panel-rgb), .55);
    color: var(--ink);
    border: 1px solid rgba(var(--edge-rgb), .12);
    border-radius: var(--r-md);
    padding: 11px 12px;
    font: 500 14px/1.3 var(--ff-body);
    font-feature-settings: "tnum";
    min-height: 44px;
    transition: border-color .15s ease, background .15s ease;
}
.admin__textarea { font-family: var(--ff-mono); min-height: 200px; line-height: 1.5 }
.admin__form input:focus,
.admin__form select:focus,
.admin__textarea:focus {
    outline: none;
    border-color: var(--brass);
    background: rgba(var(--panel-rgb), .8);
}

/* Password show/hide eye (button injected by the _layout.php footer script) */
.pw-wrap { position: relative }
.pw-wrap input[type=password],
.pw-wrap input[type=text] { padding-right: 46px }
.pw-eye {
    appearance: none;
    position: absolute;
    top: 50%; right: 5px;
    transform: translateY(-50%);
    width: 34px; height: 34px;
    display: inline-flex;
    align-items: center; justify-content: center;
    background: none;
    border: none;
    border-radius: 50%;
    color: var(--ink-2);
    cursor: pointer;
    transition: color .15s ease;
}
.pw-eye:hover { color: var(--brass) }
.pw-eye svg { width: 18px; height: 18px }
.pw-wrap[data-shown] .pw-eye__show { display: none }
.pw-wrap:not([data-shown]) .pw-eye__hide { display: none }

.admin__actions { display: flex; align-items: center; gap: 14px; margin-top: 6px }
.admin__link {
    color: var(--ink-2);
    font: 600 11px/1 var(--ff-body);
    letter-spacing: .12em;
    text-transform: uppercase;
}
.admin__link:hover { color: var(--brass) }

/* Profiles table */

.profiles__table { display: grid; gap: 8px; margin: 4px 0 16px }
.profiles__head,
.profiles__row {
    display: grid;
    grid-template-columns: 1.4fr 1fr 1fr 1fr auto;
    gap: 8px;
    align-items: center;
}
.profiles__head {
    font: 600 10px/1 var(--ff-body);
    letter-spacing: .18em;
    text-transform: uppercase;
    color: var(--brass);
    padding: 0 4px;
}
.profiles__head small {
    color: var(--ink-2);
    font-weight: 500;
    letter-spacing: 0;
    text-transform: none;
    margin-left: 4px;
}
.profiles__row input[type=text],
.profiles__row input[type=number] {
    width: 100%;
    background: rgba(var(--panel-rgb), .55);
    color: var(--ink);
    border: 1px solid rgba(var(--edge-rgb), .12);
    border-radius: var(--r-sm);
    padding: 9px 10px;
    font: 500 13px/1.2 var(--ff-body);
    font-feature-settings: "tnum";
    min-height: 38px;
}
.profiles__row input:focus { outline: none; border-color: var(--brass) }
.profiles__delete {
    display: inline-flex; align-items: center; gap: 6px;
    font: 600 10px/1 var(--ff-mono);
    color: var(--signal);
    letter-spacing: .08em;
    text-transform: uppercase;
    cursor: pointer;
    user-select: none;
}
.profiles__delete--ghost { color: var(--ink-2); opacity: .55; cursor: default }
.profiles__row--new input { border-style: dashed; opacity: .8 }
.profiles__row--new input:focus { opacity: 1; border-style: solid }

.admin__form .profiles__table,
.admin__form .admin__actions { max-width: none }

/* ---------- Account dashboard + auth extras ---------- */
.acct__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px }
.acct__section {
    margin: 26px 0 4px;
    padding-top: 18px;
    border-top: 1px solid rgba(var(--edge-rgb), .1);
    font: 600 15px/1.2 var(--ff-display);
    color: var(--ink);
}
.acct__hint  { margin: 0 0 12px; color: var(--ink-2); font-size: 12.5px }
.acct__empty { margin: 0 0 12px; color: var(--ink-2); font-size: 13px }
.acct__list  { display: flex; flex-direction: column; gap: 8px; margin-bottom: 14px }
.acct__row {
    display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
    padding: 10px 12px;
    background: rgba(var(--panel-rgb), .4);
    border: 1px solid rgba(var(--edge-rgb), .1);
    border-radius: var(--r-md);
}
.acct__row--profile { align-items: flex-end }
.acct__label { flex: 1 1 auto; display: flex; align-items: center; gap: 8px; color: var(--ink); font-weight: 500 }
.acct__badge {
    font: 600 9px/1 var(--ff-body); letter-spacing: .12em; text-transform: uppercase;
    color: var(--brass); border: 1px solid var(--brass); border-radius: 999px; padding: 3px 7px;
}
/* ---------------------------------------------------------------------------
   NWS alerts banner — sits directly above the Go/No-Go stamp.

   **Every alert is --signal, warning and advisory alike.** Advisories were
   --brass until 2026-08-21, on the argument that painting a Small Craft Advisory
   the same red as a Tornado Warning flattens a distinction the NWS is making.
   That argument does not survive being measured.

   --signal and --brass differ by **1.01:1** in contrast — the same lightness,
   separated only by hue. So the distinction was close to invisible at a glance
   anyway, while the cost was real: a brown reads as decorative chrome, not as a
   hazard, and Roland reported the banner getting lost on the page. A safety
   banner nobody's eye stops on has failed at the only job it has, and an
   advisory that goes unread is worse than one shown a shade too urgently.

   **Severity is still carried, just not by hue** — warnings render `open` while
   advisories render collapsed, the icon differs, and the event names itself.
   Three signals, none of which depend on telling two same-lightness browns apart
   on a phone in daylight.

   Tokens only, so Red mode inherits.
   --------------------------------------------------------------------------- */
.alerts { display: flex; flex-direction: column; gap: 8px; margin: 0 0 16px }
.alerts__item {
    background: rgba(var(--panel-rgb), .55);
    border: 1px solid currentColor;
    border-left-width: 4px;
    border-radius: var(--r-md);
}
.alerts__item--warning,
.alerts__item--advisory { color: var(--signal) }

/* Red mode turns the page near-black, where the stamp's halo reads far louder
   than it does on cream — and this theme exists to protect a helmsman's dark
   adaptation, so the one element allowed to glow is the one that must not shout.
   Same two-radius shape, roughly half the strength. */
[data-aweigh="red"] .gng {
    box-shadow:
        0 0 9px 1px color-mix(in srgb, currentColor 25%, transparent),
        0 0 22px 7px color-mix(in srgb, currentColor 11%, transparent);
}

/* The always-visible half. Collapsed, this is the whole alert — icon, event and
   how long it runs — so it carries the padding and stays legible on its own. */
.alerts__summary {
    display: flex; align-items: flex-start; gap: 10px;
    padding: 10px 14px;
    cursor: pointer;
    list-style: none;              /* Firefox marker */
}
.alerts__summary::-webkit-details-marker { display: none }   /* Safari marker */
.alerts__summary:focus-visible {
    outline: 2px solid currentColor;
    outline-offset: 2px;
    border-radius: var(--r-sm);
}
/* An alert with nothing to reveal renders no chevron, so it should not pretend
   to be interactive either. */
.alerts__item:not(:has(.alerts__detail)) .alerts__summary { cursor: default }

.alerts__mark { flex: 0 0 auto; line-height: 0 }
.alerts__mark svg { width: 20px; height: 20px; display: block }
.alerts__event {
    flex: 1 1 auto; min-width: 0;
    font: 700 13px/1.3 var(--ff-body);
    letter-spacing: .01em;
}
.alerts__until { font-weight: 500; opacity: .85 }

.alerts__chev { flex: 0 0 auto; line-height: 0; opacity: .7 }
.alerts__chev svg {
    width: 16px; height: 16px; display: block;
    transition: transform .18s ease;
}
.alerts__item[open] .alerts__chev svg { transform: rotate(180deg) }
@media (prefers-reduced-motion: reduce) {
    .alerts__chev svg { transition: none }
}

.alerts__detail {
    display: flex; flex-direction: column; gap: 4px;
    padding: 0 14px 10px 44px;     /* left aligns under the event, past the icon */
}
.alerts__headline {
    margin: 0;
    font-size: 12px; line-height: 1.4;
    overflow-wrap: anywhere;
}
.alerts__area {
    margin: 0;
    color: var(--ink-2);
    font-size: 11.5px; line-height: 1.35;
    overflow-wrap: anywhere;
}

/* The caution stamp — "GO*": your limits are met, but an alert is out.
   Deliberately NOT the green of a clean GO and NOT the red of a NO-GO — it is a
   third thing, and reusing either colour would misreport it.

   --buoy is the warnings token, and it is the right one here: it is what the
   advisory banner directly above the stamp already uses, so the mark and the
   thing that caused it are the same colour. It also reads more amber than
   --brass against the cream ground, which is what a caution should look like.
   A literal yellow is not available — gold on cream fails contrast, which is
   the whole reason --brass was deepened to #7A5C12 in the first place. */
.gng--caution {
    color: var(--buoy);
    border-color: var(--buoy);
}

/* The footnote the asterisk promises. Quiet, because it qualifies a Go rather
   than overturning it. */
.gng-explain__asterisk {
    margin: 12px 0 0;
    font: 400 13px/1.55 var(--ff-body);
    color: var(--ink-2);
}
.gng-explain__asterisk strong { color: var(--buoy); font-weight: 600 }

/* ---------------------------------------------------------------------------
   Environment banner (staging / local only — production renders nothing).

   In normal flow, not fixed: it pushes the page down rather than covering it,
   and there is no dismiss control on purpose. Staging serves markup identical
   to production from a hostname one word different, on a copy of the live
   database — the banner is the only thing on screen that distinguishes them.
   --------------------------------------------------------------------------- */
.envbar {
    display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
    padding: 8px 16px;
    padding-top: calc(8px + env(safe-area-inset-top));   /* clears the iOS status bar */
    background: var(--tide);
    border-bottom: 2px solid var(--signal);
    color: var(--ink-2);
    font: 12px/1.4 var(--ff-body);
}
.envbar__tag {
    font: 700 10px/1 var(--ff-mono); letter-spacing: .14em; text-transform: uppercase;
    color: var(--signal);
    border: 1px solid var(--signal);
    border-radius: 999px;
    padding: 4px 9px;
    white-space: nowrap;
}
.envbar__note { flex: 1 1 auto; min-width: 0 }
.envbar__link { color: var(--brass); white-space: nowrap; text-decoration: underline }
/* Local is informational; staging is the one that can be mistaken for live, so
   it keeps the louder signal colour. */
.envbar--local { border-bottom-color: var(--brass) }
.envbar--local .envbar__tag { color: var(--brass); border-color: var(--brass) }

/* ---------------------------------------------------------------------------
   Auth pages — the real logo instead of a text bubble, and room to breathe.

   These pages are reached from an emailed reset link or a QR flyer, often with
   no other context, so they lead with the mark rather than two lines of type.

   The top padding is the fix for "content sits too high on a phone": the auth
   pages send viewport-fit=cover with a black-translucent status bar, so in an
   iOS home-screen install the page starts UNDER the clock. env(safe-area-inset-top)
   is 0 in a normal browser, so this costs desktop nothing.
   --------------------------------------------------------------------------- */
.auth__brand {
    display: flex; flex-direction: column; align-items: center; gap: 10px;
    padding: 28px 0 22px;
    text-align: center;
}
.auth__logo { display: inline-block; line-height: 0 }
.auth__eyebrow {
    margin: 0;
    font: 600 10px/1 var(--ff-mono);
    letter-spacing: .18em;
    text-transform: uppercase;
    color: var(--ink-2);
}
@media (max-width: 720px) {
    .auth__brand { padding-top: calc(36px + env(safe-area-inset-top)) }
}

/* ---------------------------------------------------------------------------
   Account page — one card per section, one item per record.

   The page used to be a single card holding ports, vessels and the password
   form in one scroll, with each vessel rendered as a flat wrap of numeric
   inputs behind tiny inline labels. On a phone those wrapped into a pile where
   nothing said which number belonged to which heading. Records are now
   self-contained items, and every field carries a label that stacks ABOVE its
   input on narrow screens instead of sitting beside it.
   --------------------------------------------------------------------------- */

/* A count chip beside a section heading, so "how many do I have" is answered
   without counting rows. */
.acct__count {
    display: inline-block; vertical-align: middle; margin-left: 8px;
    font: 600 10px/1 var(--ff-mono); letter-spacing: .08em;
    color: var(--ink-2);
    border: 1px solid rgba(var(--edge-rgb), .22);
    border-radius: 999px; padding: 4px 8px;
}

/* One editable record, in three states.

   A card nested inside a card needs enough edge to read as its own object —
   at .1 it disappeared into the section behind it. Focus and unsaved then build
   on the same axis, so the three states are one visual idea getting stronger
   rather than three unrelated treatments. */
.acct__item {
    display: flex; flex-direction: column; gap: 12px;
    padding: 14px;
    background: rgba(var(--panel-rgb), .4);
    border: 1px solid rgba(var(--edge-rgb), .18);
    border-radius: var(--r-md);
    transition: border-color .15s ease, box-shadow .15s ease, background .15s ease;
}

/* Editing inside this record. Answers "which one am I in?" while tabbing
   through a list of near-identical rows. */
.acct__item:focus-within {
    border-color: rgba(var(--edge-rgb), .38);
    background: rgba(var(--panel-rgb), .55);
    box-shadow: 0 2px 10px rgba(var(--edge-rgb), .08);
}

/* Unsaved changes. Brass, matching the Save button it is telling you to press —
   the colour IS the instruction. Beats :focus-within so it survives tabbing
   away from the field you just edited, which is exactly when you are about to
   walk off and lose it. */
.acct__item[data-dirty],
.acct__item[data-dirty]:focus-within {
    border-color: var(--brass);
    box-shadow: 0 0 0 1px var(--brass);
}

/* Admin rows carry the same risk and get the same language. */
.acct__row--profile {
    transition: border-color .15s ease, box-shadow .15s ease;
}
.acct__row--profile:focus-within { border-color: rgba(var(--edge-rgb), .38) }
.acct__row--profile[data-dirty],
.acct__row--profile[data-dirty]:focus-within {
    border-color: var(--brass);
    box-shadow: 0 0 0 1px var(--brass);
}
.acct__item-head { display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap }
.acct__item-foot {
    display: flex; justify-content: flex-end;
    border-top: 1px solid rgba(var(--edge-rgb), .1);
    padding-top: 10px;
}
.acct__fields { display: flex; flex-wrap: wrap; gap: 12px 20px }

/* One threshold on its own line with its note tucked under it. `.acct__item` is
   a flex column with a 12px gap, so a bare <p> after a field would sit the same
   distance from its own field as from the next one and stop reading as a note
   about anything in particular. Grouping them makes one flex child, and the gap
   then separates the GROUPS. */
.acct__fieldrow { display: flex; flex-direction: column; align-items: flex-start; gap: 5px }
.acct__hint--field { margin: 0; max-width: 46ch }

/* Label above input — the whole point. Beside-the-input reads fine on a desktop
   row and turns to soup the moment three of them wrap on a phone. */
.acct__field {
    display: inline-flex; flex-direction: column; align-items: flex-start; gap: 4px;
    color: var(--ink-2); font-size: 12px;
}
/* `.acct__in--label` carries `flex: 1 1 140px` from the old single-row layout.
   .acct__field is now flex-direction: column, and flex-basis applies to the MAIN
   axis — which in a column is HEIGHT. That 140px silently became a text box
   about 140px tall. Reset the flex on any input inside a field; they size from
   their own width rules, never from a basis meant for a horizontal row. */
.acct__field .acct__in { flex: 0 0 auto }

/* Grows to fill a narrow card but is capped, so the vessel name does not
   stretch the full width of a desktop card. */
.acct__field--grow { flex: 0 1 22rem; max-width: 100% }
.acct__field--grow .acct__in { width: 100% }

.acct__field-label {
    font: 600 10px/1 var(--ff-mono); letter-spacing: .1em; text-transform: uppercase;
    color: var(--ink-2);
}
/* Keeps the unit glued to its number when the label sits above. */
.acct__unit { display: inline-flex; align-items: center; gap: 6px; color: var(--ink-2) }

/* The "add" form, set apart from the list it appends to.
   Still used by the admin screens; the account page moved to a [+ Add] button
   in the card header opening a dialog, because a permanently-expanded five-field
   vessel form sat between the last vessel and its Save button, and on a card
   holding several records it sat below the fold as well. */
.acct__addbox {
    margin-top: 16px; padding-top: 16px;
    border-top: 1px dashed rgba(var(--edge-rgb), .22);
}
.acct__addbox-title {
    margin: 0 0 10px;
    font: 600 10px/1 var(--ff-mono); letter-spacing: .14em; text-transform: uppercase;
    color: var(--ink-2);
}

/* A boolean sitting under the numeric threshold grid on a vessel card. */
.acct__check {
    display: flex;
    align-items: flex-start;
    gap: 9px;
    margin-top: 12px;
    font: 500 13.5px/1.4 var(--ff-body);
    color: var(--ink);
    cursor: pointer;
}
.acct__check input { margin: 2px 0 0; flex: 0 0 auto }
.acct__check small { display: block; color: var(--ink-2); font-size: 12.5px; font-weight: 400 }

/* The (ON)/(OFF) beside a boolean's name. Lighter than the label and set in the
   tabular face, so it reads as the control's state rather than as more of the
   sentence. */
.acct__state {
    font: 600 11.5px/1 var(--ff-mono);
    letter-spacing: .06em;
    color: var(--ink-2);
}

/* ---------- Add-a-record button + dialog (account page) ---------- */

/* Adjacent cards only. A lone card keeps its own spacing from whatever the
   page put above it, so this cannot double up on the admin screens — it fires
   solely where two cards actually meet, which before this was a hard seam with
   both borders touching. */
.admin__card + .admin__card { margin-top: 22px }

/* Title on the left, [+ Add] on the right. It was a full-width dashed button
   under the list, which on a card holding several records put the control that
   starts the card's whole purpose below the fold. */
.acct__cardhead {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
    margin-bottom: 6px;
}
.acct__cardhead .admin__title { margin: 0 }


/* The arrow on the one nav link that leaves the page. */
.acct__nav-arrow { width: 13px; height: 13px; flex: 0 0 auto }

/* **Progressive enhancement, and the order matters.** By default the dialog is
   NOT a dialog: it renders as an ordinary block so the add forms are usable
   with no JavaScript, and the button that would open it is hidden because it
   would do nothing. forms.js stamps [data-modal-ready] on <html> once it has
   confirmed showModal() exists, and only then do the two swap over. Written
   this way round so the failure mode is a visible form rather than no way to
   add a port at all. */
.acct__btn--add { display: none }
[data-modal-ready] .acct__btn--add { display: inline-flex }

.acct-modal:not([open]) {
    display: block;
    position: static;
    max-width: none;
    width: auto;
    margin: 16px 0 0;
    box-shadow: none;
}
[data-modal-ready] .acct-modal:not([open]) { display: none }

.acct-modal[open] { max-width: 420px }
.acct-modal--wide[open] { max-width: 560px }

/* Left-aligned: the QR flyout centres a code and a URL, this holds labelled
   fields, and centred labels over left-aligned inputs read as a mistake.

   **Specificity 0,2,0 deliberately** — the same note `.qr-flyout.gng-explain`
   carries a few hundred lines up, and for the same reason. `.qr-flyout__inner`
   sets `text-align: center` and is declared LATER in this file, so a
   single-class selector here loses on source order and silently does nothing.
   It did exactly that on the first cut: the dialog rendered centred. */
.acct-modal .qr-flyout__inner { text-align: left }
.acct-modal__title {
    margin: 0 0 4px;
    font: 600 20px/1.2 var(--ff-display);
    color: var(--ink);
}
.acct-modal__lede { margin: 0 0 16px; color: var(--ink-2); font-size: 13.5px }
.acct-modal__form { max-width: none }
.acct-modal__grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px }
.acct-modal__actions {
    display: flex;
    justify-content: flex-end;
    gap: 10px;
    margin-top: 4px;
}
@media (max-width: 520px) {
    .acct-modal__grid { grid-template-columns: 1fr }
    .acct-modal__actions { flex-direction: column-reverse }
    .acct-modal__actions > * { width: 100% }
}

@media (max-width: 640px) {
    /* Actions get a full row of their own rather than crowding the name. */
    .acct__item-head { align-items: stretch }
    .acct__item-head .acct__actions { width: 100% }
    .acct__field--grow { flex: 1 1 100% }   /* full width is right on a phone */
    .acct__fields { gap: 12px 14px }
    .admin__form--inline { flex-direction: column; align-items: stretch }
    .admin__form--inline > div { width: 100% }
}

/* Admin v2 — status readouts, settings form, secondary metadata.
   Tokens only; both themes inherit automatically. */
.acct__badge--off { color: var(--ink-2); border-color: rgba(var(--edge-rgb), .3) }
.acct__coords     { flex: 0 1 auto; color: var(--ink-2); font-weight: 400; font-size: 12px }
.acct__hint--muted { opacity: .75; margin-top: -8px }

/* Definition list used by the Status and Account-detail screens. */
.admin__facts {
    display: grid; grid-template-columns: minmax(9rem, auto) 1fr;
    gap: 6px 18px; margin: 0 0 6px; font-size: 13px;
}
.admin__facts dt { color: var(--ink-2); font-size: 12px }
.admin__facts dd { margin: 0; color: var(--ink); overflow-wrap: anywhere }
.admin__facts dd small { color: var(--ink-2); font-size: 11.5px; margin-left: 6px }
@media (max-width: 34rem) {
    .admin__facts { grid-template-columns: 1fr; gap: 2px 0 }
    .admin__facts dd { margin-bottom: 8px }
}

.admin__setting { margin: 0 0 18px; max-width: 42rem }
.admin__setting > label { display: block; margin-bottom: 4px; color: var(--ink); font-size: 13px }
.admin__setting > label small { color: var(--ink-2) }
.admin__setting > input { width: 12rem; max-width: 100% }

.admin__pre {
    padding: 12px 14px; margin: 0 0 10px;
    background: rgba(var(--panel-rgb), .5);
    border: 1px solid rgba(var(--edge-rgb), .12);
    border-radius: var(--r-md);
    font: 12px/1.6 var(--ff-mono); color: var(--ink-2);
    overflow-x: auto;                 /* long log lines scroll, page never does */
}
.admin__pre--log { max-height: 18rem; overflow-y: auto; white-space: pre-wrap; word-break: break-word }

.admin__form--inline { display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap; margin-bottom: 14px }
.admin__form--inline > div { margin: 0 }

.acct__actions { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap }
.acct__actions form { display: inline }
.acct__btn {
    appearance: none; cursor: pointer;
    background: rgba(var(--panel-rgb), .55); border: 1px solid rgba(var(--edge-rgb), .15);
    color: var(--ink-2); border-radius: 999px; padding: 6px 12px; min-height: 32px;
    font: 600 10px/1 var(--ff-mono); letter-spacing: .08em; text-transform: uppercase;
    transition: color .15s ease, border-color .15s ease;
}
.acct__btn:hover { color: var(--brass); border-color: var(--brass) }
.acct__btn--danger:hover { color: var(--signal); border-color: var(--signal) }

/* **A filled variant of .acct__btn, not a new button.** Same pill geometry as
   Save / Remove / Make default, so the account page has one button shape and
   this is simply the emphatic one. Building it from scratch is how a fourth
   button style gets into a page that already has three.

   **It lives HERE, beside its siblings, because it did not work where it was
   written.** The first cut put it up with the card-header layout, hundreds of
   lines above `.acct__btn` — and `.acct__btn` sets background, border and color
   too, so at equal specificity the base won on source order and the button
   rendered as an ordinary grey pill. Same trap `.acct-modal .qr-flyout__inner`
   and `.qr-flyout.gng-explain` both carry notes about. A modifier belongs next
   to the class it modifies.

   `--tide` on `--brass` is readable in BOTH themes because the two tokens
   invert together: dark gold under near-white on paper, light salmon under
   near-black in Red mode. Same reasoning as the signed-in avatar, which uses
   the pair the other way round.

   No `display` here on purpose — that is owned by the progressive-enhancement
   pair above, which hides this button entirely when there is no dialog to open. */
.acct__btn--add {
    align-items: center;
    gap: 6px;
    background: var(--brass);
    border-color: var(--brass);
    color: var(--tide);
    transition: background .15s ease, border-color .15s ease;
}
.acct__btn--add svg { width: 12px; height: 12px; flex: 0 0 auto }
/* .acct__btn:hover turns the label brass, which on a brass fill is invisible.
   Deepen the fill instead and hold the label. */
.acct__btn--add:hover {
    background: var(--brass-2);
    border-color: var(--brass-2);
    color: var(--tide);
}
.acct__btn--add:focus-visible { outline: 2px solid var(--brass); outline-offset: 2px }

/* **Hover has to move the OTHER WAY in Red mode, and this was measured.**
   `--brass-2` is a deepening of `--brass`, which is right on paper — a dark
   gold going darker under a near-white label, 5.67:1 → 8.35:1. In Red mode the
   fill is already light salmon under a near-BLACK label, so deepening it closes
   the gap instead of opening it: #1c0606 on #b04a3e measures **3.61:1**, under
   AA for a 10px label. `--buoy` lifts rather than deepens and measures 7.65:1.

   This is the same shape as the `--panel-rgb` note at the top of the file: the
   same token pair has to travel in opposite directions in the two themes, which
   is exactly why it needs an override rather than one clever value. */
[data-aweigh="red"] .acct__btn--add:hover {
    background: var(--buoy);
    border-color: var(--buoy);
}

/* Save is the primary action in a record, so it outranks Remove and Make
   default even at rest — they were all identical pills, which is part of why
   Save was easy to miss. */
.acct__btn--save {
    color: var(--ink);
    border-color: rgba(var(--edge-rgb), .32);
    background: rgba(var(--edge-rgb), .05);
}

/* With unsaved changes it fills. brass-on-tide is the same pair used by the
   signed-in avatar, verified at ~5.6:1 in day mode and ~7:1 in Red — and the
   two tokens invert together, so neither is hardcoded. */
[data-dirty] .acct__btn--save {
    background: var(--brass);
    border-color: var(--brass);
    color: var(--tide);
}
[data-dirty] .acct__btn--save:hover { color: var(--tide); filter: brightness(1.08) }
.acct__field { display: inline-flex; align-items: center; gap: 6px; color: var(--ink-2); font-size: 12px }
.acct__in {
    background: rgba(var(--panel-rgb), .55); color: var(--ink);
    border: 1px solid rgba(var(--edge-rgb), .12); border-radius: var(--r-sm);
    padding: 8px 10px; font: 500 13px/1 var(--ff-body); min-height: 38px;
}
.acct__in--label { flex: 1 1 140px; min-width: 120px }
.acct__in--num   { width: 68px; font-family: var(--ff-mono) }
.acct__in:focus  { outline: none; border-color: var(--brass) }
.acct__add { margin-bottom: 6px }

/* Signup agreement checkbox */
.admin__check { display: flex; align-items: flex-start; gap: 10px; color: var(--ink-2); font-size: 13px; line-height: 1.4 }
.admin__check input { flex: 0 0 auto; margin-top: 2px; width: 18px; height: 18px; accent-color: var(--brass) }

/* Info-page prose: terms, privacy, sources, troubleshooting, consent.
   Enlarged from 13.5px to 16px on 2026-09-03 — these are pages people read
   start to finish, not form chrome, and 13.5px was the size of a field hint. */
.terms__body h3     { margin: 22px 0 8px; font: 600 17px/1.25 var(--ff-display); color: var(--ink) }
/* Sub-headings arrived with the 1.4 policy, which splits "Information we collect"
   into account / non-account. Quieter than h3 so the section still reads as one. */
.terms__body h4     { margin: 14px 0 6px; font: 600 15px/1.25 var(--ff-display); color: var(--ink-2) }
.terms__body p      { margin: 0 0 12px; color: var(--ink-2); font-size: 16px; line-height: 1.55 }
.terms__body li     { color: var(--ink-2); font-size: 16px; line-height: 1.55; margin: 0 0 4px }
.terms__body strong { color: var(--ink) }
/* The lede above the prose grows with it. `.admin__lede` is shared with the
   admin screens, so this is scoped to a card that holds info-page prose rather
   than to the class; on a browser without :has() the lede keeps the admin size. */
.admin__card:has(.terms__body) .admin__lede { font-size: 16px; line-height: 1.55 }

/* Flash messages */

.flash {
    padding: 11px 14px;
    border-radius: var(--r-md);
    margin: 0 0 14px;
    font: 600 13px/1.3 var(--ff-body);
}
.flash--ok  { background: rgba(127, 179, 164, .12); color: var(--seaglass);
              border: 1px solid rgba(127, 179, 164, .35) }
.flash--err { background: rgba(200, 69, 58, .12); color: var(--signal);
              border: 1px solid rgba(200, 69, 58, .35) }

/* ---------- QR-share flyout (native <dialog>) ---------- */

.qr-trigger { /* same shape as theme-toggle, just a different glyph */ }
.qr-trigger svg { width: 18px; height: 18px }

/* Admin link — an <a> wearing the theme-toggle shape, so kill the underline */
.admin-trigger { text-decoration: none }
.admin-trigger svg { width: 18px; height: 18px }

.qr-flyout {
    padding: 0;
    border: none;
    background: transparent;
    color: var(--ink);
    max-width: 380px;
    width: calc(100% - 32px);
    border-radius: var(--r-lg);
    box-shadow: 0 24px 60px rgba(0, 0, 0, .55);
}
.qr-flyout::backdrop {
    /* Fixed dark scrim, NOT a theme token. A backdrop's job is to dim the page
       behind the dialog; in day mode --panel-rgb is white, which would wash the
       page out instead of dimming it. Dark in both themes is correct. */
    background: rgba(16, 18, 26, .55);
    backdrop-filter: blur(4px);
}

.qr-flyout__inner {
    /* This panel was TRANSPARENT. The `background:` shorthand below used to set
       only a gradient, so it left background-color at its initial value, and
       the `background-image:` that followed then overwrote the gradient with
       the chart grid — leaving no fill at all. On the dark theme that was
       invisible (transparent over a dark scrim still looked dark); on paper the
       forecast rows read straight through the dialog.
       Fixed by painting all three layers explicitly: grid on top, the intended
       panel gradient beneath it, over an opaque colour. Order matters — the
       last background-image is the bottom layer. */
    background-color: var(--tide);
    background-image:
        linear-gradient(rgba(var(--edge-rgb), .04) 1px, transparent 1px),
        linear-gradient(90deg, rgba(var(--edge-rgb), .04) 1px, transparent 1px),
        linear-gradient(180deg, var(--tide), var(--tide-2));
    background-size: 40px 40px, 40px 40px, auto;
    border: var(--rule);
    border-radius: var(--r-lg);
    padding: 26px 22px 22px;
    text-align: center;
    position: relative;
    box-shadow: var(--shadow-card);
}

.qr-flyout__close {
    position: absolute;
    top: 10px; right: 10px;
    background: transparent;
    border: 1px solid transparent;
    color: var(--ink-2);
    width: 36px; height: 36px;
    border-radius: 50%;
    cursor: pointer;
    display: inline-flex;
    align-items: center; justify-content: center;
    transition: color .15s ease, border-color .15s ease, background .15s ease;
}
.qr-flyout__close svg { width: 16px; height: 16px }
.qr-flyout__close:hover {
    color: var(--ink);
    border-color: rgba(var(--edge-rgb), .12);
    background: rgba(var(--panel-rgb), .55);
}

.qr-flyout__eyebrow {
    display: block;
    font: 600 10px/1 var(--ff-body);
    letter-spacing: .26em;
    text-transform: uppercase;
    color: var(--brass);
    margin-bottom: 6px;
}
.qr-flyout__title {
    margin: 0 0 6px;
    font: 600 22px/1.2 var(--ff-display);
    color: var(--ink);
}
.qr-flyout__lede {
    margin: 0 0 18px;
    color: var(--ink-2);
    font-size: 13px;
}

.qr-flyout__code {
    display: inline-block;
    padding: 14px;
    background: #ffffff;
    border-radius: var(--r-md);
    margin-bottom: 18px;
    box-shadow: 0 2px 16px rgba(0, 0, 0, .25);
}
.qr-flyout__code img { display: block; width: 220px; height: 220px }

.qr-flyout__placeholder {
    width: 220px; min-height: 220px;
    display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    gap: 14px;
    padding: 18px;
    color: var(--ink-2);
    background: rgba(var(--panel-rgb), .55);
    border: 1px dashed rgba(var(--edge-rgb), .18);
    border-radius: var(--r-sm);
}
.qr-flyout__placeholder p {
    margin: 0;
    font: 500 12px/1.5 var(--ff-mono);
    color: var(--ink-2);
}
.qr-flyout__placeholder code {
    color: var(--brass);
    font-family: var(--ff-mono);
}

.qr-flyout__url {
    display: flex;
    gap: 10px;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    margin-top: 6px;
}
.qr-flyout__url code {
    font: 500 12px/1.3 var(--ff-mono);
    color: var(--ink);
    word-break: break-all;
}
.qr-flyout__copy {
    appearance: none;
    background: rgba(var(--panel-rgb), .55);
    border: 1px solid rgba(var(--edge-rgb), .12);
    color: var(--ink);
    padding: 8px 14px;
    border-radius: 999px;
    cursor: pointer;
    font: 600 10px/1 var(--ff-mono);
    letter-spacing: .12em;
    text-transform: uppercase;
    transition: color .15s ease, border-color .15s ease;
    min-height: 32px;
}
.qr-flyout__copy:hover { color: var(--brass); border-color: var(--brass) }
.qr-flyout__copy--copied { color: var(--seaglass); border-color: var(--seaglass) }

/* ---------- Add-to-Home-Screen hint ----------
   Trigger wears the .theme-toggle shape (like .qr-trigger); the dialog reuses
   .qr-flyout chrome and only overrides the numbered step list. */
.a2hs-trigger svg { width: 18px; height: 18px }

.a2hs__steps {
    list-style: none;
    margin: 4px 0 18px;
    padding: 0;
    text-align: left;
    display: flex;
    flex-direction: column;
    gap: 12px;
    counter-reset: a2hs;
}
.a2hs__step {
    display: flex;
    align-items: center;
    gap: 14px;
}
.a2hs__icon {
    flex: 0 0 auto;
    width: 44px; height: 44px;
    border-radius: var(--r-md);
    display: inline-flex;
    align-items: center; justify-content: center;
    color: var(--brass);
    background: rgba(var(--panel-rgb), .55);
    border: 1px solid rgba(var(--edge-rgb), .12);
}
.a2hs__icon svg { width: 22px; height: 22px }
.a2hs__text {
    font-size: 13.5px;
    line-height: 1.45;
    color: var(--ink-2);
}
.a2hs__text::before {
    counter-increment: a2hs;
    content: counter(a2hs) ". ";
    color: var(--brass);
    font-weight: 600;
}
.a2hs__text strong { color: var(--ink); font-weight: 600 }
.a2hs__dismiss { margin-top: 2px }

/* ---------- Collapsible account cards (My ports / My vessels / Delete) ----------
   All ship CLOSED. Ports and vessels between them were most of account.php, so
   everything genuinely below — password, signed-in devices, deleting your
   account — sat under two long lists you had already read. The delete card
   joined the fold on 2026-09-03: a control with no undo should be one deliberate
   tap away, not the largest thing at the foot of every visit. */

/* The card owns the padding, so the summary has to reach back out to the card
   edge to be tappable across its full width. Negative margin + matching padding
   rather than moving padding off .admin__card, which every other card shares. */
.acct-fold__sum {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin: -22px;
    padding: 22px;
    cursor: pointer;
    list-style: none;               /* Firefox */
    border-radius: var(--r-lg);
}
/* Safari/Chrome draw their own triangle and ignore list-style on the element. */
.acct-fold__sum::-webkit-details-marker { display: none }

.acct-fold__sum .admin__title { margin: 0 }

/* Restore the gap the summary's negative margin eats, but only when open —
   closed, the card should be exactly its header and nothing more. */
.acct-fold__d[open] .acct-fold__sum { margin-bottom: 0 }
.acct-fold__d[open] > :not(summary):first-of-type { margin-top: 22px }

.acct-fold__chev {
    width: 18px; height: 18px;
    flex: 0 0 auto;
    color: var(--ink-2);
    transition: transform .18s ease;
}
.acct-fold__d[open] .acct-fold__chev { transform: rotate(180deg) }

.acct-fold__sum:hover .acct-fold__chev,
.acct-fold__sum:hover .admin__title { color: var(--brass) }

/* The delete card's heading is --signal, and hovering must not turn it brass:
   that would swap the one colour saying "danger" for the one saying "action". */
.acct-fold__d--danger .acct-fold__sum:hover .admin__title,
.acct-fold__d--danger .acct-fold__sum:hover .acct-fold__chev { color: var(--signal) }
.acct-fold__d--danger .acct-fold__sum:focus-visible { outline-color: var(--signal) }

/* Keyboard focus has to be visible on the summary — it is now the only way in
   to two whole cards, and the default outline is suppressed by the reset. */
.acct-fold__sum:focus-visible {
    outline: 2px solid var(--brass);
    outline-offset: -4px;
}

/* Add moved out of the header row (a button inside <summary> toggles the card
   as well as firing), so its row no longer holds a title to sit beside. */
.acct__cardhead--infold { justify-content: flex-end }

@media (prefers-reduced-motion: reduce) {
    .acct-fold__chev { transition: none }
}

/* ---------- Delete your account ----------
   The one destructive control in the app, so it is the one card that does not
   look like its neighbours.

   **The colour is the weakest part of this warning, and the CSS should say so.**
   Red mode redefines --signal to #ff5040 on a near-black ground where every
   other token is also red — the same finding that made every NWS alert one
   colour rather than severity-by-hue. So the tint marks this card in day mode
   and stops being a signal at night. What survives monochrome is the heading,
   the biohazard mark and the button's own words, and those are what actually
   carry it. */
.acct-danger { border: 1px solid var(--signal) }
.acct-danger__title { color: var(--signal) }

/* A --signal variant of .aweigh-form__submit, declared AFTER it so the equal
   single-class specificity resolves on source order. That is the trap
   .acct__btn--danger carries a note about: a modifier written above the class
   it modifies loses silently and renders as the plain base button. */
.acct-danger__go {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    border-color: var(--signal);
    background: linear-gradient(180deg, var(--signal), var(--signal-2));
    /* The base class glows in --accent-rgb, which is brass. A warm halo under a
       red button reads as a rendering mistake, so this takes the neutral card
       shadow instead and keeps only the white inset the base uses. */
    box-shadow: var(--shadow-card), inset 0 1px 0 rgba(255, 255, 255, .18);
}
.acct-danger__go:hover {
    box-shadow: var(--shadow-card), inset 0 1px 0 rgba(255, 255, 255, .28);
}
.acct-danger__mark { width: 18px; height: 18px; flex: 0 0 auto }

/* ---------- Red mode (night) ---------- */
/*
 * Preserves dark adaptation when checking conditions at night.
 * Tokens swap to a deep-red-on-near-black palette. Semantic colors
 * stay distinguishable by intensity: --seaglass (GO) is muted dark red,
 * --signal (NO-GO) is vivid bright red.
 */

[data-aweigh="red"] {
    --ground:   #0a0202;
    --tide:     #1c0606;
    --tide-2:   #2c0a0a;
    --ink:      #ff6457;
    --ink-2:    #c8473c;
    --brass:    #ff8175;
    --brass-2:  #b04a3e;
    --seaglass: #8a2f28;
    --signal:   #ff5040;
    --signal-2: #b0372c;
    --buoy:     #ff7a6a;
    --rule: 1px solid rgba(255, 129, 117, .28);

    /* Panel fill SINKS here and lifts in day mode — same alpha, opposite
       direction. This one line replaces the fourteen-selector block that used
       to restate every background by hand. */
    --panel-rgb: 10, 2, 2;
    --edge-rgb: 255, 100, 87;
    --accent-rgb: 255, 129, 117;

    /* Near-black ground swallows a soft shadow; keep it tight and dark. */
    --shadow-card: 0 8px 22px rgba(0, 0, 0, .45);
}

/* Everything that used to be re-declared below this point — the fourteen
   background/border-color selectors, the chart-paper grid, and .dial__track —
   is gone. Those components now read rgba(var(--panel-rgb), …) and friends, so
   overriding the three triplets above re-tints them automatically. */

[data-aweigh="red"] .bridge__row[data-now] {
    outline-color: var(--ink);
}

[data-aweigh="red"] .dial__arc { filter: drop-shadow(0 0 6px rgba(255, 80, 64, .25)) }

/* Per-hour Go/No-Go pill — preserve intensity contrast */
[data-aweigh="red"] .row-gng[data-verdict="go"] {
    color: var(--seaglass);
    background: rgba(138, 47, 40, .25);
    border-color: rgba(138, 47, 40, .55);
}
[data-aweigh="red"] .row-gng[data-verdict="no"] {
    color: var(--signal);
    background: rgba(255, 80, 64, .18);
    border-color: rgba(255, 80, 64, .45);
}

/* Hourly detail panel — re-tint dividers + hover from cream to red */
[data-aweigh="red"] .bridge__detail { border-top-color: rgba(255, 100, 87, .18) }
[data-aweigh="red"] .bridge__detail-item { border-bottom-color: rgba(255, 100, 87, .1) }
[data-aweigh="red"] .bridge__summary:hover { background: rgba(255, 100, 87, .05) }

/* Compass rose stays subtle */

/* Active day-scroller pill keeps the brass→hull inversion (vivid red on near-black) */
[data-aweigh="red"] .day-scroller__pill--active {
    background: var(--brass);
    color: var(--ground);
}

/* QR flyout in red mode — keep the QR backing white (codes need contrast to scan),
 * but tint the placeholder, the backdrop, the chart-paper grid, and inputs.
 * The QR image itself is unaffected (it's a static PNG/SVG).
 */
[data-aweigh="red"] .qr-flyout::backdrop {
    background: rgba(10, 2, 2, .85);
}
[data-aweigh="red"] .qr-flyout__inner {
    background-image:
        linear-gradient(rgba(255, 100, 87, .04) 1px, transparent 1px),
        linear-gradient(90deg, rgba(255, 100, 87, .04) 1px, transparent 1px);
}
[data-aweigh="red"] .qr-flyout__copy,
[data-aweigh="red"] .qr-flyout__placeholder,
[data-aweigh="red"] .qr-flyout__close:hover,
[data-aweigh="red"] .a2hs__icon {
    background: rgba(10, 2, 2, .55);
    border-color: rgba(255, 100, 87, .15);
}

/* ---------- Accessibility / motion ---------- */

@media (prefers-reduced-motion: reduce) {
    .day-band__now::after {
        animation: none !important;
    }
}

/* ---------- Error pages (404 / 500) ---------- */
/* Rendered by aweigh/error_page.php. Tokens only — these pages must re-tint in
   Red mode like everything else, and they load no JS beyond the theme boot. */

.errpage { text-align: center; padding: 34px 22px 30px }

/* The numeral is decoration, not information — the heading carries the meaning,
   and the code is already in the status line and the brand bar. Marked
   aria-hidden in the markup, so it is styled purely for weight. */
.errpage__code {
    margin: 0 0 6px;
    font: 700 64px/1 var(--ff-display);
    color: var(--brass);
    opacity: .45;
    letter-spacing: .04em;
}

.errpage__heading { margin: 0 0 10px }

.errpage__lede {
    margin: 0 auto 22px;
    max-width: 46ch;
}

.errpage__actions { margin: 0 }

.errpage__home {
    display: inline-block;
    padding: 11px 22px;
    border-radius: var(--r-lg);
    border: var(--rule);
    background: linear-gradient(180deg, var(--tide), var(--tide-2));
    color: var(--brass-2);
    font: 600 12px/1 var(--ff-body);
    letter-spacing: .12em;
    text-transform: uppercase;
    text-decoration: none;
    box-shadow: var(--shadow-card);
}
.errpage__home:hover { color: var(--ink); border-color: var(--brass) }
.errpage__home:active { transform: translateY(1px) }

@media (max-width: 480px) {
    .errpage { padding: 26px 16px 24px }
    .errpage__code { font-size: 48px }
}

/* ------------------------------------------------------------------ *
 * Admin — data sources worksheet (admin/stations.php)
 *
 * Four states, and the WORD carries them, not the hue. --signal and --brass
 * measured 1.01:1 in Red mode, which is the finding that made every NWS alert
 * one color; this page is read at a desk but inherits the same theme, so a
 * state that can only be seen as a color is a state that disappears at night.
 * Every pill therefore prints "ok" / "cold" / "none" / "warn" in text.
 * ------------------------------------------------------------------ */

.stations__form { max-width: 34rem }
.stations__form .admin__actions { gap: 10px }

.stations__layers { display: grid; gap: 10px; margin: 4px 0 14px }

.stations__layer {
    padding: 12px 14px;
    border: 1px solid rgba(var(--edge-rgb), .14);
    border-left: 3px solid rgba(var(--edge-rgb), .22);
    border-radius: var(--r-sm);
    background: rgba(var(--panel-rgb), .5);
}
.stations__layer[data-state=ok]   { border-left-color: var(--seaglass) }
.stations__layer[data-state=warn] { border-left-color: var(--buoy) }
.stations__layer[data-state=none] { border-left-color: var(--signal) }
.stations__layer[data-state=cold] { border-left-color: var(--ink-2) }

.stations__layer-head {
    display: flex; align-items: center; gap: 10px;
    margin-bottom: 4px;
}
.stations__layer-name {
    font: 600 10px/1 var(--ff-body);
    letter-spacing: .18em;
    text-transform: uppercase;
    color: var(--brass);
}

.stations__pill {
    display: inline-block;
    padding: 2px 7px;
    border-radius: var(--r-sm);
    border: 1px solid currentColor;
    font: 600 10px/1.4 var(--ff-body);
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--ink-2);
}
.stations__pill[data-state=ok]   { color: var(--seaglass) }
.stations__pill[data-state=warn] { color: var(--buoy) }
.stations__pill[data-state=none] { color: var(--signal) }

.stations__value {
    font: 500 14px/1.4 var(--ff-mono);
    color: var(--ink);
    overflow-wrap: anywhere;
}
.stations__note {
    margin: 6px 0 0;
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--ink-2);
}

/* Nine columns of identifiers will not fit a phone and must never widen the
   page — the wide content scrolls inside its own box, not the body. */
.stations__scroll {
    overflow-x: auto;
    margin: 4px 0 10px;
    border: 1px solid rgba(var(--edge-rgb), .12);
    border-radius: var(--r-sm);
}
.stations__table {
    border-collapse: collapse;
    width: 100%;
    min-width: 60rem;
    font-size: 12.5px;
}
.stations__table--tight { min-width: 52rem }
.stations__table th,
.stations__table td {
    padding: 8px 10px;
    text-align: left;
    vertical-align: top;
    border-bottom: 1px solid rgba(var(--edge-rgb), .1);
}
.stations__table thead th {
    position: sticky; top: 0;
    background: var(--tide);
    font: 600 10px/1.3 var(--ff-body);
    letter-spacing: .12em;
    text-transform: uppercase;
    color: var(--brass);
    white-space: nowrap;
}
.stations__table tbody th {
    color: var(--ink);
    font-weight: 600;
    white-space: nowrap;
}
.stations__table tbody th small {
    display: block;
    font-weight: 400;
    color: var(--ink-2);
    font-size: 11px;
}
.stations__table tbody th a { color: var(--ink); text-decoration: none; border-bottom: 1px dotted var(--brass) }
.stations__table tbody th a:hover { color: var(--brass) }
.stations__table tbody tr:hover td { background: rgba(var(--edge-rgb), .04) }
.stations__table em { color: var(--ink-2); font-style: italic }

.stations__cellval {
    display: block;
    margin-top: 3px;
    color: var(--ink-2);
    font: 400 11.5px/1.35 var(--ff-mono);
    max-width: 15rem;
    overflow-wrap: anywhere;
}

/* ------------------------------------------------------------------ *
 * Provenance — "where these readings come from" (components/provenance.php)
 *
 * Sits directly under the results header, collapsed. A <details> rather than
 * the explainer's <dialog>: it computes nothing, so it must stay readable with
 * a dead script. Tokens only, so Red mode re-tints it with everything else.
 * ------------------------------------------------------------------ */

.prov {
    margin: -4px 0 18px;
    border-bottom: 1px solid rgba(var(--edge-rgb), .14);
}

.prov__summary {
    display: flex;
    align-items: baseline;
    gap: 10px;
    padding: 8px 2px 10px;
    cursor: pointer;
    list-style: none;
    color: var(--ink-2);
}
.prov__summary::-webkit-details-marker { display: none }
.prov__summary:hover .prov__hint { color: var(--brass) }
.prov__summary:focus-visible {
    outline: 2px solid var(--brass);
    outline-offset: 3px;
    border-radius: var(--r-sm);
}

.prov__loc {
    color: var(--ink);
    font: 600 13px/1.2 var(--ff-body);
}
.prov__hint {
    font: 500 11px/1.2 var(--ff-body);
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--ink-2);
}
.prov__chev {
    margin-left: auto;
    width: 16px; height: 16px;
    color: var(--ink-2);
    transition: transform .18s ease;
}
.prov__chev svg { width: 100%; height: 100%; display: block }
.prov[open] .prov__chev { transform: rotate(180deg) }

.prov__list {
    display: grid;
    grid-template-columns: minmax(9rem, auto) 1fr;
    gap: 8px 18px;
    margin: 2px 0 12px;
    padding: 12px 14px;
    border-radius: var(--r-sm);
    background: rgba(var(--panel-rgb), .5);
    border: 1px solid rgba(var(--edge-rgb), .1);
    font-size: 13px;
}
.prov__list dt {
    color: var(--ink-2);
    font-size: 11px;
    letter-spacing: .06em;
    text-transform: uppercase;
    padding-top: 1px;
}
.prov__list dd {
    margin: 0;
    color: var(--ink);
    font-weight: 500;
    overflow-wrap: anywhere;
}
.prov__list dd small {
    display: block;
    margin-top: 2px;
    color: var(--ink-2);
    font-weight: 400;
    font-size: 11.5px;
    line-height: 1.45;
}

.prov__foot {
    margin: 0 0 14px;
    font-size: 11.5px;
    color: var(--ink-2);
}
.prov__foot a { color: var(--brass); text-decoration: none; border-bottom: 1px solid transparent }
.prov__foot a:hover { border-bottom-color: var(--brass) }

@media (max-width: 34rem) {
    .prov__list { grid-template-columns: 1fr; gap: 2px 0 }
    .prov__list dd { margin-bottom: 10px }
    .prov__hint { display: none }
}
