/*
 * See My Beer Codes.
 *
 * The token block below is lifted verbatim from seemy.beer-pdtrackerweb's
 * src/styles/tokens.css so the two sites stay visually identical. If that file
 * changes, this block should be updated to match rather than diverging.
 *
 * Everything after the token block is specific to this site. The tracker is a
 * fixed-height app shell with its own internal scrolling; this is an ordinary
 * scrolling document, so the base rules deliberately do NOT copy the tracker's
 * `overflow:hidden` body or its 100dvh grid.
 */

/* ==== Design tokens (shared with seemy.beer / tracker.seemy.beer) ========== */

:root {
  /* ---- Brand constants (identical in both themes) ---- */
  --beer-50:  #fbf3dd;
  --beer-100: #f6e4b4;
  --beer-200: #eccd78;
  --beer-300: #e2b74a;
  --beer-400: #d9a62c; /* primary brand amber */
  --beer-500: #c08f1e;
  --beer-600: #9c7016;
  --foam:     #f3eedd; /* cream head */
  --foam-dim: #e7dec4;

  /* ---- Type ---- */
  --font-display: 'Baloo 2', 'Trebuchet MS', system-ui, sans-serif;
  --font-body: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;

  /* ---- Radii / shadows / motion ---- */
  --radius-sm: 8px;
  --radius: 14px;
  --radius-lg: 22px;
  --radius-pill: 999px;
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06), 0 2px 8px rgba(0, 0, 0, 0.05);
  --shadow-md: 0 6px 24px rgba(0, 0, 0, 0.10);
  --shadow-glow: 0 10px 40px -12px rgba(217, 166, 44, 0.45);
  --ease: cubic-bezier(0.22, 1, 0.36, 1);

  /* ---- Layout ---- */
  --maxw: 1120px;
  --gutter: clamp(1.15rem, 4vw, 2.5rem);
}

:root,
[data-theme='light'] {
  --bg: #fbf7ee;
  --bg-elev: #ffffff;
  --bg-sunken: #f3ecdb;
  --surface: #ffffff;
  --surface-2: #f7f1e3;
  --border: #e7ddc7;
  --border-strong: #d8cbac;
  --text: #211d16;
  --text-soft: #5a5344;
  --text-mute: #8a8171;
  --accent: var(--beer-500);
  --accent-contrast: #2a2013;
  --on-accent: #211a0c;
  --hero-glow: rgba(217, 166, 44, 0.28);
  --header-bg: rgba(251, 247, 238, 0.78);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    --bg: #17160f;
    --bg-elev: #201e16;
    --bg-sunken: #100f0a;
    --surface: #211f16;
    --surface-2: #2a271c;
    --border: #34311f;
    --border-strong: #46412a;
    --text: #f4efe2;
    --text-soft: #c8c1ad;
    --text-mute: #928b76;
    --accent: var(--beer-400);
    --accent-contrast: var(--foam);
    --on-accent: #211a0c;
    --hero-glow: rgba(217, 166, 44, 0.22);
    --header-bg: rgba(23, 22, 15, 0.72);
  }
}

[data-theme='dark'] {
  --bg: #17160f;
  --bg-elev: #201e16;
  --bg-sunken: #100f0a;
  --surface: #211f16;
  --surface-2: #2a271c;
  --border: #34311f;
  --border-strong: #46412a;
  --text: #f4efe2;
  --text-soft: #c8c1ad;
  --text-mute: #928b76;
  --accent: var(--beer-400);
  --accent-contrast: var(--foam);
  --on-accent: #211a0c;
  --hero-glow: rgba(217, 166, 44, 0.22);
  --header-bg: rgba(23, 22, 15, 0.72);
}

/* Semantic colours, matching tracker.css so "good" and "bad" read the same
   across both sites. */
:root {
  --good: #4f9a34; --good-ink: #356321;
  --bad:  #c8503a; --bad-ink:  #8f3323;
  /* Amber, not red. "May already be claimed" is a caution, not a failure. */
  --warn-ink: #8a5f0d;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    --good: #7fb85a; --good-ink: #c7e6ab;
    --bad:  #e07a4e; --bad-ink:  #f0b79c;
    --warn-ink: #e0b64e;
  }
}
[data-theme='dark'] {
  --good: #7fb85a; --good-ink: #c7e6ab;
  --bad:  #e07a4e; --bad-ink:  #f0b79c;
  --warn-ink: #e0b64e;
}

/* ==== Base ================================================================ */

* { box-sizing: border-box; }

/* Same reasoning as the tracker: the UA stylesheet's [hidden]{display:none} is
   author-overridable, so any component display declaration silently beats it.
   This site toggles a lot of state with the hidden attribute, so make it win. */
[hidden] { display: none !important; }

body {
  margin: 0; background: var(--bg); color: var(--text);
  font-family: var(--font-body); font-size: 15px; line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  min-height: 100dvh; display: flex; flex-direction: column;
}
h1, h2, h3 { font-family: var(--font-display); font-weight: 700; letter-spacing: -0.01em; margin: 0; }
button { font: inherit; color: inherit; cursor: pointer; }
a { color: inherit; text-decoration: none; }
.tnum { font-variant-numeric: tabular-nums; }

.wrap { width: 100%; max-width: 760px; margin: 0 auto; padding: 0 var(--gutter); }

/* ==== Shared site nav ====================================================
 *
 * The estate-wide bar: brand mark plus one tab per site. Identical on
 * seemy.beer, tracker.seemy.beer and codes.seemy.beer, so a visitor can move
 * between them without going back to a landing page.
 *
 * DUPLICATED ON PURPOSE. Three sites live in three repos on two stacks (Astro
 * for the sign and the tracker, plain HTML here), so no component can be
 * shared across all of them. Serving the markup from a common origin was
 * considered and rejected: it would add a cross-origin runtime dependency to a
 * site whose CSP is deliberately tight, and the failure mode is the nav
 * vanishing rather than looking slightly stale. Keep the three copies in step
 * by hand. It is a dozen lines and drift is obvious.
 *
 * PURELY NAVIGATIONAL, on purpose. No page actions live here, so the bar is
 * byte-identical on all three sites. "Share a code" sits in the list toolbar
 * instead, where it is next to the thing it acts on and more prominent than it
 * would be tucked in a corner of the chrome.
 *
 * Sticky, because on the board it is the only chrome and the tabs should stay
 * reachable while scrolling a long list.
 */

.sitebar {
  display: flex; align-items: center; gap: clamp(10px, 3vw, 26px);
  min-height: 64px; padding: 0 clamp(12px, 2vw, 20px);
  background: var(--header-bg); backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--border);
  position: sticky; top: 0; z-index: 6;
}
/* Tabs sit flush to the bottom edge so the active underline reads as a tab,
   which needs the bar's own height rather than a fixed one. */
.sitebar > .sitetabs { align-self: stretch; }

/*
 * Stacked brand lockup: the wordmark with the product name under it.
 *
 * `align-items: stretch` on the column is what makes the product name span the
 * logo's width exactly, without hardcoding a number: the logo is the widest
 * item, so it sizes the column and the label centres inside that.
 *
 * The label is centred rather than letter-spaced out to the edges, because
 * filling the width would need a different font size per site ("Codes" is five
 * characters, "Price Tracker" is thirteen) and the lockup has to look the same
 * everywhere. Centred keeps one size across all three.
 */
.sitebar-home {
  display: flex; flex-direction: column; align-items: stretch;
  gap: 3px; flex: none;
}
/* width/height attributes on the <img> carry the intrinsic ratio so the row
   does not reflow once the SVG loads; these then set the rendered size. */
.sitebar-logo { height: 26px; width: auto; display: block; }
.sitebar-product {
  display: block; text-align: center;
  font-family: var(--font-display); font-weight: 700;
  font-size: 15px; line-height: 1; letter-spacing: 0.02em;
  color: var(--accent);
}

.sitetabs {
  display: flex; align-items: stretch;
  list-style: none; margin: 0; padding: 0; gap: 2px;
}
/* The <li> is the flex child, not the <a>. Without this the <li> stretches to
   the bar height while the <a> keeps its 22px content height and sits at the
   top of it, which top-aligns the label and floats the active underline in
   mid-air instead of sitting on the bar's bottom edge. */
.sitetabs li { display: flex; }
.sitetabs a {
  display: flex; align-items: center; padding: 0 clamp(9px, 2vw, 15px);
  font-size: 13.5px; font-weight: 600; color: var(--text-mute);
  border-bottom: 2px solid transparent;
  transition: color 120ms var(--ease), border-color 120ms var(--ease);
}
.sitetabs a:hover { color: var(--text-soft); }
.sitetabs a:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* aria-current is the source of truth for the active tab, so the styling
   cannot disagree with what a screen reader announces. */
.sitetabs a[aria-current='page'] { color: var(--accent); border-bottom-color: var(--accent); }

@media (max-width: 560px) {
  .sitebar { min-height: 56px; gap: 6px; }
  .sitebar-logo { height: 21px; }
  .sitebar-product { font-size: 12.5px; }
  .sitetabs a { font-size: 12.5px; padding: 0 8px; }
}


/* ==== App bar ============================================================= */

.appbar {
  display: flex; align-items: center; gap: 12px;
  padding: 0 clamp(12px, 2vw, 20px); height: 58px;
  background: var(--header-bg); backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--border);
  position: sticky; top: 0; z-index: 5;
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand .sep { width: 1px; height: 20px; background: var(--border-strong); }
.brand .tag {
  font-family: var(--font-display); font-weight: 700; font-size: 14px;
  color: var(--accent); letter-spacing: -0.01em; white-space: nowrap;
}
.brand .home { font-family: var(--font-display); font-weight: 800; font-size: 16px; }
.spacer { flex: 1; }
.back-btn {
  display: inline-flex; align-items: center; gap: 5px; height: 32px;
  padding: 0 11px 0 9px; font-size: 12.5px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-pill); color: var(--text-soft);
}
@media (max-width: 520px) { .back-btn span { display: none; } }

/* ==== Main =============================================================== */

main { flex: 1; padding: clamp(18px, 4vw, 34px) 0 40px; }

.intro { margin-bottom: 18px; }
.intro h1 { font-size: clamp(22px, 5vw, 30px); margin-bottom: 6px; }
.intro p { color: var(--text-soft); font-size: 14.5px; margin: 0; }

.toolbar { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin: 20px 0 14px; }
.toolbar h2 { font-size: 15px; color: var(--text-soft); font-weight: 700; }
/* Below this the count and two buttons cannot share a line without the count
   wrapping mid-phrase, so give the buttons their own row. */
@media (max-width: 460px) {
  .toolbar { gap: 8px; }
  .toolbar .spacer { display: none; }
  .toolbar h2 { flex: 1 0 100%; }
}

/* ==== Buttons ============================================================= */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  height: 40px; padding: 0 18px; border: 1px solid var(--border);
  background: var(--surface); color: var(--text);
  border-radius: var(--radius-pill); font-size: 14px; font-weight: 600;
  transition: filter 120ms var(--ease), border-color 120ms var(--ease);
}
.btn:hover { border-color: var(--border-strong); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn[disabled] { opacity: 0.55; cursor: not-allowed; }
.btn-primary {
  background: var(--accent); border-color: transparent; color: var(--on-accent);
  font-family: var(--font-display); font-weight: 700;
}
.btn-primary:hover { filter: brightness(1.05); }
.btn-sm { height: 32px; padding: 0 13px; font-size: 12.5px; }

/* ==== Code cards ========================================================== */

.codes { display: grid; gap: 12px; list-style: none; padding: 0; margin: 0; }

.card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 16px 17px; box-shadow: var(--shadow-sm);
}

/* Market indicator. Same treatment as tracker.css so the two sites read alike. */
.market-static {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 13px; color: var(--text-soft); white-space: nowrap;
}

/* Shown instead of the board to a blocked visitor. Plain and unstyled-looking
   on purpose: this is a notice, not a card in a list of cards. */
.blocked {
  border: 1px solid color-mix(in srgb, var(--bad) 35%, var(--border));
  background: color-mix(in srgb, var(--bad) 6%, var(--surface));
  border-radius: var(--radius);
  padding: 18px 20px;
  margin: 16px 0;
}
.blocked h2 { font-family: var(--font-display); font-size: 18px; margin: 0 0 8px; }
.blocked p { margin: 0 0 10px; max-width: 62ch; font-size: 14px; color: var(--text-soft); }
.blocked p:last-child { margin-bottom: 0; }

/* ---- "Probably gone" ----------------------------------------------------
 *
 * Dimmed rather than removed. Taking these off the board is what left it empty
 * within four hours of launch, which told every new arrival the site was dead
 * when it was actually working. A card that says plainly it has probably gone
 * is honest and still useful: it shows the board is alive, and somebody who
 * reads it and tries anyway is making their own decision.
 *
 * Deliberately duller than .is-claimed, which is amber and means "hurry".
 * This one means "do not get your hopes up", so it recedes instead. Text stays
 * at full contrast: the card is de-emphasised, not made hard to read.
 */
.card.is-spent {
  background: color-mix(in srgb, var(--text-mute) 6%, var(--surface));
  border-color: var(--border);
}
.card.is-spent .card-display { color: var(--text-soft); }
.card.is-spent .card-warn {
  background: color-mix(in srgb, var(--text-mute) 10%, transparent);
  color: var(--text-soft);
}
.card.is-spent .card-warn-sign { color: var(--text-mute); }

/* A gone card carries no actions at all, so there is no button to restyle. */

/* ---- "May already be claimed" ------------------------------------------- */

/*
 * Single use, and somebody has already revealed it. Amber rather than red,
 * deliberately: red would read as "this is dead", and the honest claim is only
 * that it might be. The code is still worth trying, which is why the card stays
 * fully legible and every control on it stays live.
 *
 * Built from the brand amber via color-mix over --surface rather than a fixed
 * yellow, so it lands correctly in both themes without a second rule: in dark
 * mode the same mix resolves against the dark surface.
 */
.card.is-claimed {
  background: color-mix(in srgb, var(--beer-300) 16%, var(--surface));
  border-color: color-mix(in srgb, var(--beer-500) 45%, var(--border));
}

.card-warn {
  display: flex; align-items: flex-start; gap: 8px;
  margin: 10px 0 0; padding: 9px 11px;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--beer-300) 22%, transparent);
  font-size: 12.5px; line-height: 1.45; color: var(--text-soft);
}
.card-warn-sign {
  flex: none; font-size: 15px; line-height: 1.25;
  color: var(--warn-ink);
}

/* The amber is carrying meaning, so it has to survive a high contrast setting
   rather than flattening back to an ordinary card. */
@media (prefers-contrast: more) {
  .card.is-claimed { border-width: 2px; border-color: var(--beer-600); }
}

.card-head { display: flex; align-items: flex-start; gap: 10px; }
.card-display { font-family: var(--font-display); font-weight: 700; font-size: 17px; line-height: 1.3; }
.card-meta {
  display: flex; flex-wrap: wrap; gap: 5px 12px; margin-top: 8px;
  font-size: 12px; color: var(--text-mute);
}

.badge {
  display: inline-flex; align-items: center; height: 22px; padding: 0 9px;
  border-radius: var(--radius-pill); font-size: 11px; font-weight: 700;
  letter-spacing: 0.02em; white-space: nowrap;
  color: var(--text-soft); background: var(--surface-2); border: 1px solid var(--border);
}
/* Offer shape. Neutral rather than good/bad coloured: "% off" is not better
   or worse than "money off", it is just a different shape, and colouring it
   would compete with the single/multi badge which does carry a judgement. */
.badge.cat { color: var(--text-soft); background: var(--surface-2); border-color: var(--border-strong); }
.badge.single { color: var(--bad-ink); background: color-mix(in srgb, var(--bad) 14%, transparent); border-color: color-mix(in srgb, var(--bad) 34%, transparent); }
.badge.multi  { color: var(--good-ink); background: color-mix(in srgb, var(--good) 15%, transparent); border-color: color-mix(in srgb, var(--good) 35%, transparent); }

/* Revealed code. Monospace because these get read aloud, retyped and compared
   character by character, and an ambiguous 0/O or 1/l costs a failed checkout. */
.reveal-box {
  display: flex; align-items: center; gap: 10px; margin-top: 13px;
  padding: 11px 13px; background: var(--bg-sunken);
  border: 1px dashed var(--border-strong); border-radius: var(--radius-sm);
}
.reveal-code {
  flex: 1; font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
  font-size: 16px; font-weight: 600; letter-spacing: 0.06em; word-break: break-all;
}
.card-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }

.card.is-gone { opacity: 0.62; }
.card.is-gone .card-display { text-decoration: line-through; text-decoration-thickness: 1px; }

/* ==== Empty / status ====================================================== */

.note {
  padding: 11px 13px; border-radius: var(--radius-sm); font-size: 13.5px;
  border: 1px solid var(--border); background: var(--surface-2); color: var(--text-soft);
}
.note.error {
  color: var(--bad-ink); background: color-mix(in srgb, var(--bad) 12%, transparent);
  border-color: color-mix(in srgb, var(--bad) 34%, transparent);
}
.note.ok {
  color: var(--good-ink); background: color-mix(in srgb, var(--good) 13%, transparent);
  border-color: color-mix(in srgb, var(--good) 32%, transparent);
}
.empty { text-align: center; padding: 40px 20px; color: var(--text-mute); }

/* ==== Submission form ===================================================== */

.sheet {
  position: fixed; inset: 0; z-index: 50; display: grid; place-items: center;
  padding: 20px; background: color-mix(in srgb, var(--bg) 86%, transparent);
  overflow-y: auto;
}
.sheet-card {
  width: min(460px, 100%); background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: 24px 22px; box-shadow: var(--shadow-md);
}
.field { display: flex; flex-direction: column; gap: 5px; margin-bottom: 14px; }
.field label { font-size: 12px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-mute); }
.field input, .field textarea {
  font: inherit; font-size: 16px; padding: 11px 12px; color: var(--text);
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.field input:focus-visible, .field textarea:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent);
}
/* The code field is the one place where character-level accuracy matters most. */
#subCode { font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace; text-transform: uppercase; letter-spacing: 0.06em; }
.field .hint { font-size: 12px; color: var(--text-mute); font-weight: 400; text-transform: none; letter-spacing: 0; }
.counter { font-variant-numeric: tabular-nums; }
.counter.over { color: var(--bad-ink); font-weight: 700; }
.sheet-actions { display: flex; gap: 9px; justify-content: flex-end; margin-top: 18px; }

/* The route to a human. Quiet, but present. */
.contact-note { opacity: 0.85; }

/* ==== Age gate (lifted from tracker.css) ================================== */

/*
 * No JavaScript means no decision, so default to gated rather than open.
 *
 * gate.js removes `no-js` and then decides. If it never runs, the class never
 * clears, and a scriptless visitor gets the gate instead of the offer. Without
 * this rule the failure mode was the other way round: the alcohol promotion
 * rendered in full to anyone with JS disabled, because nothing had run to hide
 * it.
 */
html.no-js body > *:not(.agegate) { display: none; }

html.gated body > *:not(.agegate) { display: none; }

/*
 * :not(.no-js) matters and is not decoration. This rule and the no-js one
 * above have identical specificity, so whichever is written last wins. Without
 * the exclusion this hides the gate on a scriptless visit while the rule above
 * hides everything else, and the page is blank in both directions rather than
 * gated in one.
 */
html:not(.gated):not(.no-js) .agegate { display: none; }
.agegate {
  position: fixed; inset: 0; z-index: 100; display: grid; place-items: center;
  padding: 20px; background: var(--bg); overflow-y: auto;
}
.agegate-card {
  width: min(420px, 100%); background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 26px 24px; box-shadow: var(--shadow-md);
}
.agegate-title { font-size: 22px; margin-bottom: 8px; }
.agegate-intro { color: var(--text-soft); font-size: 14.5px; margin: 0 0 18px; }
/* min-inline-size:0 is required: a <fieldset> has an intrinsic minimum width based
   on its contents and will NOT shrink as a flex container without it. */
.agegate-dob { display: flex; flex-wrap: wrap; gap: 10px; border: 0; padding: 0; margin: 0 0 12px; min-inline-size: 0; }
.agegate-dob legend { font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--text-mute); padding: 0; margin-bottom: 8px; }
.agegate-field { display: flex; flex-direction: column; gap: 4px; flex: 1 1 0; min-width: 0; }
.agegate-field--year { flex: 1.5; }
.agegate-field label { font-size: 11.5px; color: var(--text-mute); }
.agegate-field input {
  height: 44px; padding: 0 12px; font: inherit; font-size: 16px; text-align: center;
  background: var(--bg); color: var(--text); border: 1px solid var(--border);
  border-radius: var(--radius-sm); font-variant-numeric: tabular-nums;
}
.agegate-field input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
.agegate-error {
  color: var(--bad-ink); background: color-mix(in srgb, var(--bad) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--bad) 34%, transparent);
  border-radius: var(--radius-sm); padding: 8px 11px; font-size: 13.5px; margin: 0 0 12px;
}
.agegate-remember { display: flex; gap: 9px; align-items: flex-start; font-size: 13px; color: var(--text-soft); margin-bottom: 16px; cursor: pointer; }
.agegate-remember input { margin-top: 2px; flex: none; }
.agegate-submit {
  display: block; width: 100%; height: 46px; border: 0; border-radius: var(--radius-pill);
  background: var(--accent); color: var(--on-accent); font-family: var(--font-display);
  font-weight: 700; font-size: 15px; text-align: center; line-height: 46px;
}
.agegate-submit:hover { filter: brightness(1.05); }
.agegate-submit:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
.agegate-out { text-decoration: none; }
.agegate-fine { margin: 16px 0 0; font-size: 12px; color: var(--text-mute); line-height: 1.5; }
.agegate-fine a { text-decoration: underline; }

/* ==== Legal strip (lifted from tracker.css) =============================== */

.legal-strip {
  flex: none; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px;
  padding: 8px clamp(12px, 2vw, 20px); border-top: 1px solid var(--border);
  background: var(--bg-sunken); color: var(--text-mute); font-size: 11px; line-height: 1.45;
}
.legal-strip a { text-decoration: underline; }
.legal-strip a:hover { color: var(--text-soft); }

@media (max-width: 560px) {
  .legal-strip { font-size: 9.5px; line-height: 1.3; gap: 1px 7px; padding: 6px clamp(12px, 2vw, 20px); }
}

@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }

.admin-why { color: var(--text-mute); font-size: 11px; }

/* Country a submission arrived from. Amber, not red: a mismatch is a prompt to
   read it properly, never a verdict. */
.admin-from { font-variant-numeric: tabular-nums; font-size: 12px; }
.admin-from.is-foreign {
  color: var(--warn-ink);
  font-weight: 600;
}

/* ---- Bans (moderation page) --------------------------------------------- */

.ban-section { margin: 24px 0; }
.ban-section h2 { font-size: 1.05rem; margin: 0 0 4px; }
.ban-section .hint { margin: 0 0 12px; max-width: 68ch; }

.bans { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }

.ban-row {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  background: var(--surface);
}

/* Expired bans are kept for the history, so they are legible but visibly not
   in force. Reusing .is-gone keeps them consistent with removed codes. */
.ban-row.is-gone { opacity: 0.55; }

.ban-head { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.ban-ref { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.85rem; }
.ban-when, .ban-count { font-size: 0.8rem; color: var(--text-mute); }
.ban-reason { margin: 6px 0 0; font-size: 0.85rem; }
.ban-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }

/* The inline length picker that replaces the Ban button. */
.ban-picker { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.ban-picker-label { font-size: 0.8rem; color: var(--text-mute); }

/* Submitter inspection banner on the moderation page. */
.submitter-note { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }

/* Ban is the only destructive control on the site, so it is the only one that
   looks it. Everything else is reversible with one click. */
.btn-danger {
  color: var(--bad-ink);
  border-color: color-mix(in srgb, var(--bad) 40%, transparent);
  background: color-mix(in srgb, var(--bad) 8%, transparent);
}
.btn-danger:hover { border-color: color-mix(in srgb, var(--bad) 60%, transparent); }

/* ==== Prose pages (privacy) ===============================================
 *
 * Only used by /privacy today. Kept minimal on purpose: the board is a list of
 * cards and has no running text, so these are the only rules in the file that
 * exist for reading rather than scanning.
 */
/* Shared seemy.beer long-form scale — the same values are set in
   seemy.beer-web's global.css (.prose) and seemy.beer-pdtrackerweb's
   legal.astro (.legal-page). Three repos, two stacks, so like the site nav
   this cannot be a shared file: change one, change all three.

   These pages are read as one set with /privacy on the sign and /legal on the
   tracker, and were 46em/14.5px against 72ch/16px and 68ch/14.5px. */
.prose { max-width: 68ch; }
.prose h2 {
  font-size: 18px; margin: 35px 0 6px; color: var(--accent);
}
.prose p, .prose li {
  color: var(--text-soft); font-size: 15px; line-height: 1.7;
}
.prose p { margin: 0 0 12px; }
.prose ul { margin: 0 0 12px; padding-left: 1.2em; }
.prose li { margin-bottom: 7px; }
/* The dated line at the foot of a legal page. Same treatment on all three. */
.prose .note {
  margin-top: 28px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
  color: var(--text-mute);
  font-size: 12.5px;
}
.prose a { text-decoration: underline; }
.prose strong { color: var(--text); }
/* Cookie names are copied and compared character by character, same reasoning
   as the reveal box and the moderation table. */
.prose code {
  font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
  font-size: 13px; background: var(--surface-2);
  border: 1px solid var(--border); border-radius: 5px; padding: 1px 5px;
}

/* ==== Moderation view ===================================================== */

/* The moderation table has seven columns, so it needs more room than the
   reading-width board. */
.wrap-wide { max-width: 1180px; }

.filter { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--text-soft); }
.filter select {
  font: inherit; font-size: 13px; padding: 5px 9px; color: var(--text);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm);
}

/* Narrow screens scroll the table rather than the page. A seven column table
   cannot reflow usefully, and a horizontally scrolling body is worse. */
.table-scroll { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); }

.admin-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.admin-table th, .admin-table td { padding: 9px 12px; text-align: left; vertical-align: top; }
.admin-table thead th {
  font-size: 11px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase;
  color: var(--text-mute); border-bottom: 1px solid var(--border); white-space: nowrap;
}
.admin-table tbody tr { border-top: 1px solid var(--border); }
.admin-table tbody tr:first-child { border-top: 0; }
.admin-table tr.is-gone { opacity: 0.55; }

/* The submitter's own words are the deciding evidence on this page, so they
   get the room. The parser's rewrite sits beside them for comparison and is
   muted, because it is derived rather than authoritative. */
.admin-submitted { max-width: 24em; }
.admin-display { max-width: 18em; color: var(--text-mute); font-size: 12.5px; }
/* Monospace for the same reason as the reveal box: these get compared
   character by character, and an ambiguous 0/O costs a failed checkout. */
.admin-code {
  font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
  letter-spacing: 0.04em; white-space: nowrap;
}
.has-reports { color: var(--bad-ink); font-weight: 700; }
.admin-actions { display: flex; gap: 6px; flex-wrap: wrap; }

/*
 * Turnstile container.
 *
 * Deliberately NOT hidden and NOT sized to zero. An invisible-mode widget
 * renders nothing visible on its own, so there is nothing to hide, and
 * Turnstile can refuse to run inside a zero-size or display:none container.
 * A previous version set width:0;height:0;overflow:hidden here and the
 * challenge silently never resolved, which surfaced as the reveal button
 * hanging on "Revealing...".
 *
 * If a challenge ever does need to become interactive (Cloudflare can escalate
 * an invisible widget), this container is in normal flow at the end of the
 * body and can paint. It collapses to nothing when empty.
 */
#cf-turnstile-host:empty { display: none; }
#cf-turnstile-host { margin: 0 auto; }

/* ---- Moderation on a phone ---------------------------------------------
 *
 * Eight columns and up to five buttons a row cannot share a 390px screen, and
 * the horizontal scroller they lived in meant the actions were off the edge of
 * every row: you scrolled right, lost sight of which code you were looking at,
 * then pressed something. On a moderation page that is how the wrong code gets
 * removed.
 *
 * So below 720px the table stops behaving like one. Each row becomes a card,
 * each cell puts its column heading in front of its value, and the actions get
 * a full-width row of proper touch targets underneath. The markup does not
 * change: it is still a real <table>, which keeps it sane on a desktop and
 * keeps the semantics for a screen reader on both.
 */
@media (max-width: 720px) {
  /* The scroller has nothing left to scroll, and its border would double up
     with the per-card borders below. */
  .table-scroll { overflow-x: visible; border: 0; background: none; border-radius: 0; }

  .admin-table, .admin-table tbody, .admin-table tr, .admin-table td { display: block; width: auto; }

  /* Headings move into the cells via data-label, so the real header row goes.
     clip rather than display:none so it stays available to a screen reader. */
  .admin-table thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }

  .admin-table tbody tr {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
    padding: 12px 14px;
    margin-bottom: 10px;
  }

  .admin-table td { padding: 3px 0; max-width: none; }

  /* The label, then the value. Grid rather than a float so a long value wraps
     under its own heading instead of around it. */
  .admin-table td::before {
    content: attr(data-label);
    display: block;
    font-size: 10.5px; font-weight: 700; letter-spacing: 0.05em;
    text-transform: uppercase; color: var(--text-mute);
  }

  /* The submitted text is the deciding evidence, so it leads and stays big
     enough to actually read. */
  .admin-submitted { font-size: 14px; }
  .admin-display { font-size: 13px; }

  /* Actions last, separated, and thumb-sized. 44px is the smallest target
     that is reliably hittable without zooming. */
  /* td.admin-actions, not .admin-actions: the `.admin-table td { display: block }`
     above is more specific than the base `.admin-actions { display: flex }` and
     would otherwise collapse the button row into a stack of full-width blocks. */
  .admin-table td.admin-actions {
    display: flex; flex-wrap: wrap; gap: 8px;
    margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border);
  }
  /* The heading is a flex item here, so it needs a full row of its own. */
  .admin-actions::before { flex-basis: 100%; margin-bottom: 2px; }
  .admin-actions .btn { min-height: 44px; padding-inline: 14px; }

  /* The destructive one gets its own line, so Ban is never the button your
     thumb lands on by accident next to Remove. */
  .admin-actions .btn-danger { flex-basis: 100%; }

  /* The length picker appears in place of the Ban button and would otherwise
     push a four-button row off the side. */
  .ban-picker { width: 100%; }
  .ban-picker .btn { flex: 1 1 auto; min-height: 44px; }

  .ban-actions .btn { min-height: 44px; }
  .ban-head { gap: 4px 10px; }
}


/* ==== Fleet notice banner and /notices ======================================
 *
 * Same rules as seemy.beer-web and the tracker carry, kept in step by hand — see the note
 * at the top of /notices-lib.js about why these three sites duplicate rather than share.
 *
 * The banner needs no age-gate special case: styles.css already hides every body child that
 * is not .agegate until the gate has run and passed, so the rule that hides the alcohol
 * offer hides the notice with it.
 */
  /*
   * ⭐ IN FLOW, NEVER FIXED. The banner pushes the page down rather than covering any of it.
   * That is the website's echo of R3.3's never-brick rule on the sign: a notice may take
   * space, never take over. It also means no layout jump on a page that has no banner,
   * because the element is `hidden` until there is something to say.
   */
  .notice-banner {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 4px 12px;
    padding: 14px clamp(1.15rem, 4vw, 2.5rem);
    font-size: 15px;
    line-height: 1.4;
    background: var(--bg-sunken);
    color: var(--text);
    border-bottom: 1px solid var(--border);
  }
  .notice-banner[hidden] {
    display: none;
  }
  /* Severity is the palette and nothing else: the position, the flow and the dismissal rules
     are identical at every level, and severity no longer decides whether there is a banner
     here at all — `web_show` does. An `info` banner is a legitimate, quiet banner. */
  .notice-banner[data-severity='warning'] {
    background: #f6d98a;
    color: #3b2f12;
    border-bottom-color: #e0c268;
  }
  .notice-banner[data-severity='urgent'] {
    background: #b4361f;
    color: #fff4ea;
    border-bottom-color: #8e2917;
  }
  .notice-banner-title {
    font-weight: 700;
    /* Inherits the banner's colour rather than the link colour: it is the headline first and
       a link second, and a severity banner already carries its own foreground. */
    color: inherit;
    text-decoration: none;
  }
  .notice-banner-body {
    opacity: 0.92;
  }
  .notice-banner-link {
    font-weight: 600;
    color: inherit;
    text-decoration: underline;
    white-space: nowrap;
  }
  /*
   * ⭐ THE SUMMARY GOES ON A PHONE, AND THE TITLE HAS TO CARRY THE NOTICE ALONE THERE.
   *
   * .notice-banner is `flex-wrap: wrap`, so on a narrow viewport its three children stack:
   * title, summary, "Read more". The summary is a full sentence and wraps to two lines by
   * itself, which makes the banner around four lines tall and pushes the page down. It is in
   * flow by design - a notice may take space, never take over - so the height is paid by the
   * page, not by an overlay. Measured on seemy.beer at 375px: 121px before, 75px after.
   *
   * Hiding rather than truncating: a clamped sentence cut mid-phrase reads as broken, and
   * "Read more" already exists for anyone who wants the rest.
   *
   * 560px is the breakpoint this stylesheet already uses elsewhere, so it is not a fourth
   * number to remember.
   *
   * ⚠️ THE CONSEQUENCE IS EDITORIAL, NOT VISUAL. The summary is the line that carries the
   * information on a banner; a title is capped at 50 characters and drawn from a restricted
   * glyph set, so it is already terse. After this, a title that only makes sense with its
   * summary beside it makes no sense at all on a phone. Write titles that stand alone.
   */
  @media (max-width: 560px) {
    .notice-banner-body,
    .notice-banner-link {
      display: none;
    }
    /* The title is the only affordance left once the CTA is gone, so it has to look like
       one. Underlined rather than recoloured: the banner's foreground is set by severity,
       and a link colour that ignores that would read as a rendering fault on the urgent
       palette. */
    .notice-banner-title {
      text-decoration: underline;
    }
  }
  /*
   * ⭐ is:global IS LOAD-BEARING, NOT A SHORTCUT. Astro scopes <style> by stamping a
   * data-astro-* attribute onto elements IN THE TEMPLATE. Every element below is created by
   * the script at runtime from the feed, so none of them carries that attribute and a scoped
   * rule matches nothing: the list rendered as unstyled text, with the severity stripe and
   * the body's paragraph breaks both silently missing. Caught by looking at it.
   *
   * Every selector here is prefixed `notice`/`notices`, so going global costs nothing that
   * scoping was buying.
   */
  .notices-page {
    max-width: 68ch;
    margin: 0 auto;
    padding: clamp(2rem, 6vw, 4rem) clamp(1.15rem, 4vw, 2.5rem);
  }
  .notices-intro {
    color: var(--text-soft);
    margin-bottom: 2rem;
  }
  .notices-gone {
    padding: 12px 14px;
    border-radius: var(--radius-sm);
    background: var(--bg-sunken);
    border: 1px solid var(--border);
    margin-bottom: 1.5rem;
  }
  .notices-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
  }
  .notice-entry {
    padding: 1.15rem 1.25rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
    /* The severity stripe is the only place severity shows here. The page is for reading,
       not for alarming: the banner already did the shouting.

       ⭐ SEVERITY IS A CUSTOM PROPERTY, NOT A BORDER COLOUR, AND THAT IS THE BUG FIX. It
       used to be set directly as border-left-color, which meant `.is-target` below could
       and did overwrite it: both selectors are (0,2,0), is-target is declared later, and
       it sets the `border-color` SHORTHAND, which repaints all four edges including the
       stripe. So an URGENT notice reached from a red banner drew an AMBER stripe and an
       amber ring - the one moment the colour matters most was the one moment it was
       wrong, because arriving via a link is exactly what adds is-target.

       Held in a variable, the severity is stated once and every rule that wants it reads
       it. Nothing downstream can silently win the cascade against a value it has to name
       to use. */
    --notice-hue: var(--border-strong);
    --notice-ring: var(--accent);
    --notice-glow: rgba(192, 143, 30, 0.14);
    border-left: 4px solid var(--notice-hue);
  }
  .notice-entry[data-severity='warning'] {
    --notice-hue: #d9a62c;
    --notice-ring: #d9a62c;
    --notice-glow: rgba(217, 166, 44, 0.16);
  }
  .notice-entry[data-severity='urgent'] {
    --notice-hue: #b4361f;
    --notice-ring: #b4361f;
    --notice-glow: rgba(180, 54, 31, 0.18);
  }
  /* The one a link pointed at, so somebody arriving from a banner can see which is theirs.

     ⭐ AND IT NOW CARRIES THE SEVERITY, WHICH IS THE WHOLE POINT OF THE HIGHLIGHT. A ring
     in the brand amber said "this is the one you clicked" and nothing else; on an urgent
     notice it actively contradicted the red banner that sent you. Ringing it in the
     notice's own colour answers both questions at once - which one is yours, and how
     serious it is - and matches what the sign now does on its own /messages page.

     border-left-color is restated AFTER the shorthand so the stripe survives: at `info`
     the ring is the brand amber while the stripe stays deliberately quiet, and without
     this line the shorthand would flood the quiet stripe with it. */
  .notice-entry.is-target {
    border-color: var(--notice-ring);
    border-left-color: var(--notice-hue);
    box-shadow: 0 0 0 3px var(--notice-glow);
  }
  .notice-entry-title {
    margin: 0 0 0.35rem;
    font-size: 1.25rem;
  }
  .notice-entry-when {
    margin: 0 0 0.75rem;
    font-size: 0.85rem;
    color: var(--text-mute);
  }
  .notice-entry-body {
    margin: 0;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
  }
  .notice-entry-link {
    display: inline-block;
    margin-top: 0.75rem;
    font-weight: 600;
  }
  .notices-empty {
    color: var(--text-soft);
  }

/* ==== The archive: disclosure entries and the record ======================= */

/* The summary IS the row, so it carries the pointer and the whole title/date block. The
   default marker is removed and replaced below: a triangle beside a heading reads as a
   bullet, not a control. */
.notice-entry > details > summary {
  cursor: pointer;
  list-style: none;
}
.notice-entry > details > summary::-webkit-details-marker { display: none; }

/* Generated rather than markup so open and closed cannot drift apart, and so a collapsed
   row costs nothing to say it can be opened. */
.notice-entry > details > summary::after {
  content: 'Read more';
  display: block;
  margin-top: 6px;
  font-size: 14px;
  font-weight: 600;
  color: var(--accent);
}
.notice-entry > details[open] > summary::after { content: 'Show less'; }

.notice-entry-detail { margin-top: 10px; }
.notice-entry-detail:empty { margin-top: 0; }

/* The record. Set apart from the current-conditions list above it, because the two answer
   different questions and a reader scanning for an outage should not have to wade through a
   year of release notes to be sure there isn't one. */
.notices-history-heading {
  margin-top: 42px;
  padding-top: 26px;
  border-top: 1px solid var(--border);
}
