/* public_html/assets/css/site.css
   Every colour lives in :root. When you sample the real hexes off mmo.show,
   change them here and the whole site follows.

   This file is deliberately complete: forms, tables, badges, the detail
   layout, the calendar grid and pagination are all defined up front so the
   remaining pages don't each drag their own styles in behind them. */

/* Dark is the default and light is the override, so the theme is one attribute
   on <html> and no rule below needs to know which is on.

   Two groups. The chrome — masthead, footer, hero, the phone tab bar, the staff
   door — is
   near-black in both themes and does not flip, which is why --chrome exists
   separately from --ink. --ink is the text colour and flips. Anything that was
   var(--ink) as a *background* is var(--chrome) now. */
:root {
    /* Constant in both themes */
    --chrome:     #14161A;   /* masthead, footer, hero, door */
    --chrome-2:   #2A2F38;   /* the phone tab bar and calendar day headings */
    --signal:     #CC0C0C;   /* the MMO red, sampled off the logo artwork */
    --signal-dark:#A00909;
    --cream:      #FCF6E4;   /* the logo's off-white */
    --amber:      #F2B134;   /* under-construction bar */
    --go:         #2E7D57;   /* the green behind white button text */

    /* Dark theme */
    --ink:        #E7EAEF;   /* body text */
    --ink-soft:   #C2C8D2;
    --slate:      #8C95A4;   /* secondary text */
    --hairline:   #2B3039;
    --surface:    #1B1E24;   /* cards, panels */
    --surface-2:  #21252C;   /* striped rows, hovers, dead calendar cells */
    --page:       #101216;

    /* Red at #CC0C0C is unreadable on a dark ground, so links carry their own
       token rather than reusing the brand red directly. */
    --link:       #FF6B60;
    --link-hover: #FF958C;

    /* Inverted chip: the current page in the pager, the active filter button. */
    --selected-bg:   var(--cream);
    --selected-text: #14161A;

    /* Status tints. Each is a background, a border and a text colour, because
       the pale versions of these only work on white. */
    --wait-bg:   #3A2E12;  --wait-line:   #6B551F;  --wait-text:   #F0C86A;
    --ok-bg:     #12301F;  --ok-line:     #1F5638;  --ok-text:     #6FCB9B;
    --no-bg:     #3A1615;  --no-line:     #6B2422;  --no-text:     #F09B96;
    --danger-bg: #241617;  --danger-line: #6B2422;
    --event-bg:  #33191B;  --event-hover: #45201F;
    /* Mail to our own approvers, on /admin/mail. Pink rather than --no-*,
       because red in that table already means a send that failed. */
    --staff-bg:  #3A1530;  --staff-line:  #B0457F;
    /* The venue box on a desktop meetup card: cream in both themes, so it is
       the one light thing on a dark card and still a warm patch on a white one. */
    --venue-bg:  #FCF6E4;  --venue-line:  #E3D5AE;  --venue-text: #181818;  --venue-soft: #6E675A;

    /* The guest panel is a warm cream card on the light theme; on dark it has
       to be a warm dark rather than a bright slab in the middle of the page. */
    --panel-warm: #221E16;

    /* The artwork behind the site, and the sheet the page sits on over it.
       --veil has to stay near-opaque: the backdrop is a busy photograph and
       body copy on top of it is unreadable at any lighter setting. */
    --scrim: rgba(8, 9, 12, .40);
    --veil:  rgba(16, 18, 22, .94);

    --radius: 6px;
    --shell: 1080px;

    --display: 'Barlow Condensed', 'Arial Narrow', system-ui, sans-serif;
    /* Home page title only. The fallbacks are the narrowest heavy faces likely
       to be sitting on a machine already, so a failed webfont still reads as a
       poster rather than as body copy. */
    --poster: 'Anton', 'Arial Narrow', 'Haettenschweiler', Impact, sans-serif;
    --body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    --mono: 'IBM Plex Mono', ui-monospace, 'Courier New', monospace;
}


/* Light. Only the tokens that differ are repeated. */
:root[data-theme="light"] {
    --ink:        #14161A;
    --ink-soft:   #2A2F38;
    --slate:      #5A6270;
    --hairline:   #D8DCE2;
    --surface:    #FFFFFF;
    --surface-2:  #FAFBFC;
    --page:       #F2F4F7;

    --link:       #CC0C0C;
    --link-hover: #A00909;

    --selected-bg:   #14161A;
    --selected-text: #FFFFFF;

    --wait-bg:   #FDF3DC;  --wait-line:   #E8CF8E;  --wait-text:   #8A6100;
    --ok-bg:     #E7F3EC;  --ok-line:     #A9CFBB;  --ok-text:     #2E7D57;
    --no-bg:     #FBE9E9;  --no-line:     #F0C6C6;  --no-text:     #A00909;
    --danger-bg: #FDF7F7;  --danger-line: #F0C6C6;
    --event-bg:  #FCEBEA;  --event-hover: #F8DCDA;
    --staff-bg:  #FCE6F1;  --staff-line:  #E38BB8;

    --panel-warm: var(--cream);

    /* The backdrop is a dark photograph either way, so the light theme lifts it
       with a pale scrim rather than trying to make white text work on it. */
    --scrim: rgba(232, 234, 239, .82);
    --veil:  rgba(255, 255, 255, .95);
}

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

/* The browser's own [hidden] rule is a bare element selector, so any class in
   this file that sets display beats it and the element stays on screen. That is
   a silent failure: `hidden` in the markup is how several things here start out
   before a script reveals them, and a `.btn` marked hidden would simply be
   visible and do nothing. One rule, once. */
[hidden] { display: none !important; }

body {
    margin: 0;
    background: var(--page);
    color: var(--ink);
    font-family: var(--body);
    font-size: 16px;
    line-height: 1.6;
    -webkit-text-size-adjust: 100%;

    /* Every word on this site was typed in by somebody, and a description on
       the site right now is one unbroken 137-character run. Without this it
       does not wrap - it walks straight out through the side of its card.
       break-word only splits a word that cannot fit on a line of its own, so
       ordinary prose is untouched. */
    overflow-wrap: break-word;
}

h1, h2, h3 {
    font-family: var(--display);
    font-weight: 700;
    letter-spacing: .01em;
    line-height: 1.08;
    margin: 0 0 .5em;
}
h1 { font-size: clamp(2rem, 5vw, 3.1rem); }
h2 { font-size: clamp(1.4rem, 3vw, 1.9rem); }
h3 { font-size: 1.15rem; }

a { color: var(--link); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--link-hover); }

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

.shell { max-width: var(--shell); margin: 0 auto; padding: 0 20px; }

/* --- under construction ------------------------------------------------- */

.construction {
    background: var(--amber);
    color: #2A2000;
    font-family: var(--mono);
    font-size: .78rem;
    letter-spacing: .06em;
    text-transform: uppercase;
    text-align: center;
    padding: 9px 20px;
}

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

.masthead { background: var(--chrome); color: #fff; }
.masthead .shell {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    min-height: 72px;
    flex-wrap: wrap;
}

.brand { display: flex; align-items: center; gap: 11px; text-decoration: none; color: #fff; }
.brand img { height: 30px; width: auto; display: block; }
.brand span {
    font-family: var(--display);
    font-weight: 700;
    font-size: 1.5rem;
    letter-spacing: .05em;
    text-transform: uppercase;
}
.brand span em { font-style: normal; color: var(--signal); }

/* margin-left:auto rather than relying on space-between: the shell has three
   children now (wordmark, links, the signed-in corner) and the links belong
   beside the corner, not floating in the middle of the bar. */
.masthead nav { display: flex; gap: 20px; flex-wrap: wrap; margin-left: auto; }
.masthead nav a {
    color: #C9CED6;
    text-decoration: none;
    font-size: .82rem;
    letter-spacing: .08em;
    text-transform: uppercase;
    font-family: var(--mono);
}
.masthead nav a:hover, .masthead nav a[aria-current] { color: #fff; }

/* --- the settings menu -------------------------------------------------- */

/* A <details> dropdown. It sits in the masthead on the public site and in the
   back-office strip, both of which are near-black, so it is styled for a dark
   ground in either theme. */
.menu { position: relative; }
.menu > summary {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    list-style: none;
    cursor: pointer;
    color: #C9CED6;
    font: 500 .82rem/1 var(--mono);
    letter-spacing: .08em;
    text-transform: uppercase;
    padding: 7px 10px;
    border: 1px solid rgba(255, 255, 255, .22);
    border-radius: var(--radius);
    white-space: nowrap;
}
.menu > summary::-webkit-details-marker { display: none; }
.menu > summary:hover,
.menu[open] > summary { color: #fff; border-color: rgba(255, 255, 255, .5); }

/* Three dots, drawn rather than fetched. */
.menu-dots {
    width: 3px;
    height: 3px;
    border-radius: 50%;
    background: currentColor;
    box-shadow: 0 -5px 0 currentColor, 0 5px 0 currentColor;
}

.menu-panel {
    position: absolute;
    right: 0;
    top: calc(100% + 8px);
    z-index: 40;
    min-width: 210px;
    background: var(--surface);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    padding: 6px;
    box-shadow: 0 12px 28px rgba(0, 0, 0, .35);
}

.menu-item {
    display: block;
    width: 100%;
    text-align: left;
    background: none;
    border: 0;
    border-radius: 4px;
    padding: 9px 11px;
    font: 500 .92rem/1.2 var(--body);
    color: var(--ink);
    text-decoration: none;
    cursor: pointer;
}
.menu-item:hover { background: var(--surface-2); color: var(--ink); }
.menu-item-out { color: var(--link); }
.menu-item-out:hover { color: var(--link-hover); }

/* Separates leaving the site from changing it. */
.menu-item-out { border-top: 1px solid var(--hairline); margin-top: 4px; padding-top: 11px; border-radius: 0 0 4px 4px; }

/* Scoped to the mastheads, because the phone tab bar carries a menu of its own
   and it is a word rather than a mark - see the tab bar rules below. */
@media (max-width: 640px) {
    .masthead .menu-word { display: none; }
    .masthead .menu > summary { padding: 8px 12px; }
}

/* --- the back office shell ---------------------------------------------- */

/* The admin masthead is the public one with the navigation taken out and a
   signal rule under it, so there is never any doubt which side you are on. */
.masthead-admin { border-bottom: 3px solid var(--signal); }
.masthead-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

/* .btn-quiet is near-black on a light ground by default, which would be
   invisible up here. */
.masthead-actions .btn-quiet { color: #fff; border-color: rgba(255, 255, 255, .3); }
.masthead-actions .btn-quiet:hover { background: rgba(255, 255, 255, .12); color: #fff; }

.colophon-admin { padding: 18px 0 84px; font-size: .8rem; }

/* --- the view tabs ------------------------------------------------------ */

/* List / Calendar / Map / Newly added, rendered twice and shown one at a time.
   .viewtabs-inline sits under the heading of the page it switches, which is
   where these read as a control rather than as chrome; .viewtabs-bar is the
   phone tab bar, pinned to the bottom edge of every public page. The
   breakpoint below picks which one is on screen - they are never both. */
.viewtabs {
    display: flex;
    gap: 4px;
    flex-wrap: wrap;
    margin: 0 0 24px;
    border-bottom: 1px solid var(--hairline);
}
/* Direct children only: the phone bar's fourth slot is the settings menu, and
   the links inside the panel it opens are not tabs. */
.viewtabs > a {
    padding: 9px 14px;
    margin-bottom: -1px;
    border-bottom: 2px solid transparent;
    color: var(--slate);
    text-decoration: none;
    font: 500 .8rem/1 var(--mono);
    letter-spacing: .08em;
    text-transform: uppercase;
}
.viewtabs > a:hover { color: var(--ink); }
.viewtabs > a[aria-current] { color: var(--ink); border-bottom-color: var(--signal); }

/* Off screen until the breakpoint below turns it on - and after the rule
   above, since these two selectors carry the same weight. */
.viewtabs-bar { display: none; }

/* --- the human check ----------------------------------------------------
   Turnstile draws a fixed 300px iframe and will not go narrower. The sheet's
   content box is 100vw minus the shell's 40px of padding and its 1px border,
   so below about 342px the widget is wider than the column it sits in: it does
   not scroll the page, because the sheet contains it, it just loses the
   Cloudflare mark and the privacy links off the right edge.

   Scaled to fit rather than left clipped. 278px of room at 320px over the
   widget's own 300px is .927, and .92 keeps a hair in hand. Two things to know:
   the 300 is Cloudflare's figure and not ours, so re-measure if they change the
   widget; and transform does not affect layout, so the row still reserves its
   unscaled height and gives up a few pixels of slack underneath. That is
   cheaper than the alternatives - compact size is a 150x140 block, and
   flexible has the same 300px floor. */

@media (max-width: 342px) {
    .cf-turnstile {
        transform: scale(.92);
        transform-origin: left top;
    }
}

/* --- the meetups map ----------------------------------------------------
   The full-page map on /meetups/map. It opens on the whole country, and the
   country is about 1.8 times as wide as it is tall - so the shape of this box
   decides how much of the frame the United States actually fills.

   Desktop is wider than that ratio, so the fit is limited by height and the US
   fills it top to bottom. A phone is not: 348px across by 520px down fits the
   country by width and leaves it as a band across the middle with Canada above
   and Brazil below, which is a fair rendering of "the whole United States" and
   a poor one of what somebody wanted to see. Shorter down there instead. */
.mapfull {
    height: 520px;
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
}

/* --- the backdrop ------------------------------------------------------- */

/* The artwork sits behind the public site, fixed, with a scrim over it and the
   content on an opaque sheet - the picture is a crowd, a skyline and a street
   map, and nothing readable survives being laid straight on top of it.
   Two fixed pseudo-elements rather than background-attachment: fixed, which
   iOS ignores and Android renders expensively.
   The back office and the staff door keep their plain grounds. */
body:not(.admin):not(.bare)::before,
body:not(.admin):not(.bare)::after {
    content: "";
    position: fixed;
    inset: 0;
    pointer-events: none;
}
body:not(.admin):not(.bare)::before {
    z-index: -2;
    background: var(--chrome) url("/assets/img/backdrop.jpg") center top / cover no-repeat;
}
body:not(.admin):not(.bare)::after {
    z-index: -1;
    background: var(--scrim);
}

/* Everything the page says sits on this. */
main > .shell {
    background: var(--veil);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    padding-top: 30px;
    padding-bottom: 34px;
}

/* --- layout ------------------------------------------------------------- */

main { padding: 40px 0 64px; }
.stack > * + * { margin-top: 18px; }
.tight > * + * { margin-top: 10px; }

.card {
    background: var(--surface);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    padding: 26px;
}
.card-pad-sm { padding: 18px 20px; }

/* --- Cards that are one big link ------------------------------------------
   A meetup card is a link to that meetup, all of it. Rather than wrapping the
   article in an <a> - which would be invalid, since the card already contains
   a link to the venue - the title's own anchor grows a transparent layer over
   the whole card. One real link, so it is exactly as keyboard-navigable, as
   middle-clickable and as copyable as the title always was, and with the
   stylesheet missing it is still a plain heading link.

   The trade is text selection: the layer sits over the description, so
   dragging across the card no longer selects it. */

.card-link { position: relative; }

.card-link .card-hit::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    border-radius: var(--radius);
}

/* Anything else in the card that is genuinely its own link has to ride above
   that layer, or the card swallows it. The photo does not: it points at the
   same meetup, so letting the layer take the click is the same answer. The
   corner is in this list because the venue moved up there: a link that is on
   the card but under the stretched layer cannot be clicked at all. */
.card-link .hint a,
.card-link .card-corner a { position: relative; z-index: 2; }

.card-link:hover {
    border-color: var(--link);
    background: var(--surface-2);
}

/* The card is what got focus as far as anyone can see, so the card is what
   shows it. */
.card-link:focus-within {
    outline: 3px solid var(--link);
    outline-offset: 2px;
}
.card-link .card-hit:focus-visible { outline: none; }

/* --- Host photos ----------------------------------------------------------
   One picture per meetup, uploaded by the host and re-encoded to a 1200px JPEG
   by inc/image.php. Nothing here trusts the shape of what arrives: a portrait
   phone photo would otherwise push a whole listing off the screen.

   Everywhere a visitor sees one it is a square, cropped from the middle with
   object-fit. That is a display decision only - what image.php writes is still
   the host's own framing at its own ratio, which is what og:image serves and
   what an approver is shown, so this can be changed back without touching a
   stored file. The one place deliberately left uncropped is the approval queue:
   see .photo-review. */

.card-photo { display: flex; gap: 20px; align-items: flex-start; }
.card-photo .card-body { flex: 1 1 auto; min-width: 0; }

.card-thumb {
    flex: 0 0 auto;
    display: block;
    width: 160px;
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--surface-2);
}
.card-thumb img {
    display: block;
    width: 100%;
    aspect-ratio: 1;
    height: auto;
    object-fit: cover;
}

figure.photo { margin: 24px 0 0; }
figure.photo img {
    display: block;
    width: auto;
    height: auto;
    max-width: 100%;
    max-height: 60vh;
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
}
figure.photo figcaption { margin-top: 8px; }

/* The queue is looking at it to decide, not reading round it - and it is the
   one place that must see the whole frame rather than the square the site will
   crop out of it. An approver deciding whether a picture can be published needs
   the corners: they are what a square crop hides, and they are still served in
   og:image and still one click away at the full file. So this stays whole. */
.photo-review img { max-height: 340px; }

/* The form's "this is already attached" row, beside the file input. */
.photo-current {
    display: flex;
    gap: 14px;
    align-items: flex-start;
    margin-bottom: 12px;
}
.photo-current img {
    flex: 0 0 auto;
    width: 120px;
    height: 120px;
    object-fit: cover;
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
}

/* The back office, where a picture is evidence rather than decoration. */
img.thumb {
    display: block;
    width: 220px;
    max-width: 100%;
    aspect-ratio: 1;
    height: auto;
    object-fit: cover;
    margin-bottom: 6px;
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
}

/* --- Member icons ---------------------------------------------------------
   The generic set in assets/img/avatars. Every tile ships as an opaque square
   and is rounded off here rather than in the PNG, so there is no alpha for the
   CDN's re-encode to get wrong. The hairline matters more than it looks: two
   of the twelve grounds are near-black and two are cream, so without an edge
   one pair vanishes into the dark theme and the other into the light one. */

.avatar {
    flex: 0 0 auto;
    border-radius: 50%;
    border: 1px solid var(--hairline);
    background: var(--surface-2);
    vertical-align: text-bottom;
}

/* A name with its icon: the flex row is what keeps the two on one baseline
   however long the name is. */
.byline {
    display: flex;
    align-items: center;
    gap: 9px;
}

/* Mid-sentence instead, as in "Hosted by", where a flex row would break the
   line apart. */
.avatar-inline {
    margin-right: 3px;
    vertical-align: -5px;
}

/* A host's name, linked to their profile. It sits inside a card byline, a
   factbox and the dark meetup banner, so it takes its colour from whatever it
   is in rather than carrying one of its own, and earns an underline only when
   somebody is pointing at it. */
.hostlink {
    color: inherit;
    text-decoration: none;
}
.hostlink:hover,
.hostlink:focus-visible { text-decoration: underline; }

/* The head of a profile: one big icon beside the name. */
.profile-head {
    display: flex;
    align-items: center;
    gap: 18px;
}
.profile-head h1 { margin-top: 0; }
/* With a bio under the name the block runs long, and a centred icon ends up
   floating halfway down the text. Pin it to the top instead. */
.profile-head:has(.profile-bio) { align-items: flex-start; }
.avatar-lg { border-width: 2px; }

/* On a phone the icon gives up a quarter of itself: at 320px a 96px circle
   and its gap leave under 170px for the name and the line under it. */
@media (max-width: 640px) {
    .profile-head { gap: 14px; }
    .avatar-lg { width: 72px; height: 72px; }
}

.menu-who {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 4px 0 4px;
    padding: 9px 11px;
    border-top: 1px solid var(--hairline);
    font: 500 .88rem/1.2 var(--body);
    color: var(--ink-soft);
}

/* --- The avatar editor ---------------------------------------------------
   A round window over the uploaded picture. Round rather than square because
   that is how the icon is shown everywhere else, so what you frame is exactly
   what you get. With no JavaScript the img below fills the frame with CSS and
   the server takes the middle square, which is the same picture. */

.cropper { margin: 0 0 18px; }

.crop-frame {
    position: relative;
    width: 260px;
    max-width: 100%;
    aspect-ratio: 1;
    overflow: hidden;
    border-radius: 50%;
    border: 1px solid var(--hairline);
    background: var(--surface-2);
    touch-action: none;          /* or a drag scrolls the page on a phone */
    user-select: none;
}
.cropper.is-live .crop-frame { cursor: move; }

.crop-frame img {
    position: absolute;
    left: 0;
    top: 0;
    display: block;
    width: 100%;
    height: 100%;
    max-width: none;
    object-fit: cover;
    -webkit-user-drag: none;
}

.crop-zoom {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 260px;
    max-width: 100%;
    margin-top: 14px;
    font-size: .85rem;
    color: var(--ink-soft);
}
.crop-zoom input { flex: 1 1 auto; accent-color: var(--signal); }

/* The "this is you" row at the top of the settings panel. */
.avatar-now {
    display: flex;
    align-items: center;
    gap: 14px;
    margin: 0 0 18px;
}
.avatar-now .hint { margin: 0; }

/* The two halves of the set, labelled, because 22 faces in one run reads as a
   wall rather than a choice. */
.avatar-group {
    margin: 6px 0 8px;
    font-family: var(--mono);
    font-size: .68rem;
    letter-spacing: .09em;
    text-transform: uppercase;
    color: var(--slate);
}

.avatar-pick {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    margin: 0 0 18px;
}
.avatar-pick label { line-height: 0; cursor: pointer; }

/* Off-screen rather than display:none, so the radios keep working with a
   keyboard and the checked ring below has something to hang off. */
.avatar-pick input {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
}
.avatar-pick img {
    border-radius: 50%;
    border: 2px solid transparent;
    padding: 2px;
}
.avatar-pick input:checked + img { border-color: var(--signal); }
.avatar-pick input:focus-visible + img {
    outline: 2px solid var(--link);
    outline-offset: 2px;
}

/* The site's only checkbox, so it carries its own row here. */
.check {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: .9rem;
    color: var(--ink-soft);
    cursor: pointer;
}
.check input { accent-color: var(--signal); }

@media (max-width: 640px) {
    /* A 160px thumbnail beside text is a postage stamp on a phone, so it goes
       across the top of the card instead. Capped and centred rather than left
       to fill the width: a full-bleed square is 348px tall on a 390px screen,
       which buries the title and the date under the picture and turns a list
       of five meetups into five screens of scrolling. */
    /* The title goes above everything, the picture under it. On a phone the
       first line is all somebody gets before deciding whether to keep
       scrolling, and a 240px square is not it. Document order is untouched, so
       the title's anchor is still the first link in the card. */
    .card-photo { display: flex; flex-direction: column; }
    .card-photo > .card-body { order: 1; }
    .card-photo > .card-thumb { order: 2; margin-top: 4px; }
    .card-thumb {
        width: 100%;
        max-width: 240px;
        margin: 0 auto 16px;
    }
    .photo-current img { width: 96px; height: 96px; }

    /* --- what a meetup card says first, on a phone ----------------------
       The picture if there is one, then the title, then when and where. The
       card led with the date and the venue on one line above the title, which
       is right on a wide card where the whole thing is read at once and wrong
       on a narrow one, where the first line is all somebody gets before
       deciding to scroll past.

       Order rather than markup, so the desktop card and the document order are
       both untouched: the title's anchor is still the first link in the card,
       which is what the stretched .card-hit layer and every screen reader are
       reading. Scoped to .card-body, which only the two public lists use - the
       overview's condensed cards share .card-topline and are not this. */
    .card-link .card-body { display: flex; flex-direction: column; }
    .card-link .card-body > * { order: 3; }
    .card-link .card-body > h3 { order: 1; }

    /* Sized to the card rather than set at one size for every title: --tw is
       how many ems wide this title is (title_width_em() in layout.php), so
       dividing the room by it lands the words on the full width of the card.
       44px is the card's 20px of padding either side plus the widest left
       marker a card carries. The floor is there so a long title shrinks to two
       readable lines instead of one tiny one; the cap so "MMO" is a heading
       rather than a billboard. */
    .card-link .card-body > h3 {
        font-size: clamp(1.25rem, calc((100vw - 44px) / var(--tw, 12)), 2.8rem);
        line-height: 1.04;
    }
    .card-link .card-body > .card-topline { order: 2; }

    /* And it is the line that has to carry, so it stops being the small grey
       furniture it is on desktop. The inline margin-top:0 on that element is
       why only the bottom is set here. */
    .card-link .card-body > .card-topline {
        margin-bottom: 12px;
        font-size: .82rem;
        color: var(--ink);
    }
    .card-link .card-corner { color: var(--ink); }

    /* Everything under it is detail, and reads as detail. */
    .card-link .card-body > p:not(.card-topline) {
        font-size: .92rem;
        color: var(--ink-soft);
    }

    /* With a picture there is no room for detail at all: the card is already a
       240px square plus a title plus a line of when and where, and the rest of
       it belongs on the meetup's own page, which is where the card goes. */
    .card-photo .card-body > p:not(.card-topline) { display: none; }
}

.lede { font-size: 1.12rem; color: var(--ink-soft); max-width: 62ch; }

/* Dates and locations get the mono treatment — they're data, not prose. */
.slug-line {
    font-family: var(--mono);
    font-size: .78rem;
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--slate);
}

/* The hero carries the brand: near-black ground with the tower mark bled off
   the right edge as a watermark. The mark is a background image, so it gets
   its own single-colour cut (logo-watermark.svg) and opacity does the rest. */
.hero {
    position: relative;
    overflow: hidden;
    isolation: isolate;
    background: var(--chrome);
    color: var(--cream);
    border: 1px solid var(--hairline);
    border-left: 5px solid var(--signal);
    border-radius: var(--radius);
    padding: 44px 32px;
}
.hero::before {
    content: "";
    position: absolute;
    z-index: -1;
    top: 50%;
    right: -56px;
    transform: translateY(-50%);
    width: 360px;
    height: 360px;
    background: url("/assets/img/logo-watermark.svg") center / contain no-repeat;
    opacity: .07;
    pointer-events: none;
}

/* The line under the wordmark. Display face, cream, sitting tight to the h1 so
   the two read as one block rather than a heading plus a paragraph. */
/* The line that says what the site is for. Still bigger than it was when it
   was sized as a subtitle, but deliberately under the tagline above it now:
   that one makes the claim and this one explains it, so the order of the two
   sizes has to say so. */
.hero-sub {
    font-family: var(--display);
    font-size: clamp(1.2rem, 2.4vw, 1.7rem);
    font-weight: 500;
    line-height: 1.15;
    letter-spacing: .02em;
    color: var(--cream);
    opacity: .9;
    margin: 0 0 18px;
}

/* The sign-off. Full-strength cream where the body copy above it is dimmed,
   so it lands as the last thing said rather than another line of it. */
.hero-outro {
    font-family: var(--display);
    font-size: clamp(1.05rem, 2.2vw, 1.35rem);
    font-weight: 700;
    letter-spacing: .03em;
    color: var(--cream);
    margin: 22px 0 0;
}

/* Text inside the hero is on near-black, so the light-ground defaults for the
   lede and the quiet button have to be undone. */
.hero .lede { color: #C9CED6; }

/* Three short lines, not three paragraphs of prose - the default paragraph
   rhythm leaves them floating apart. */
.hero .lede { margin: 0 0 6px; }
.hero .lede:last-of-type { margin-bottom: 0; }
.hero .btn-quiet { color: var(--cream); border-color: rgba(252, 246, 228, .3); }
.hero .btn-quiet:hover { background: rgba(252, 246, 228, .1); color: var(--cream); }

/* The tagline. Cream on black with the signal rule leading into it, so it
   reads as the brand's line rather than as another bit of page furniture. */
.tagline {
    display: flex;
    align-items: center;
    gap: 12px;
    font-family: var(--mono);
    font-size: .8rem;
    font-weight: 500;
    letter-spacing: .18em;
    text-transform: uppercase;
    color: var(--cream);
    margin: 0 0 14px;
}
.tagline::before {
    content: "";
    flex: 0 0 auto;
    width: 30px;
    height: 3px;
    background: var(--signal);
}

/* In the hero this is not an eyebrow any more, it is the line the whole site is
   built round, and it leads. Three things follow from that, and all three are
   scoped to .hero - plain .tagline is still the footer's "Stay on the
   offensive", which keeps its rule and its label size, and there is a blanket
   .tagline size in the phone block that this has to out-specify here and
   nowhere else.

   No rule: the red dash marks a label, and this has stopped being one. That
   also makes the flex box pointless, since the dash was the only other item in
   it. Tracking comes down from .18em because that is a small-caps label
   figure - at display size it reads as gappy rather than deliberate, and the
   line is long enough that the width is worth having back.

   It wraps to two lines below about 500px. That is fine and it is why the rule
   had to go rather than merely shrink: a dash pinned to the middle of a
   two-line block reads as a mistake. */
.hero .tagline {
    display: block;
    font-size: clamp(1.4rem, 2.9vw, 2.2rem);
    letter-spacing: .1em;
    line-height: 1.2;
    margin-bottom: 10px;
}
.hero .tagline::before { display: none; }

/* On a phone the hero is that line and the one under it, and nothing else.
   Everything marked .hero-extra in home.php - the sub-heading, two of the three
   ledes and the sign-off - is hidden rather than cut, so the desktop hero is
   exactly as it was. What is left is the claim and what to do about it, with
   the buttons under it, which fits above the fold instead of being the fold.

   The line holds to ONE line at every phone width, which is the whole reason
   for the arithmetic. "Connection is protection" measures 15.12em in IBM Plex
   Mono 500 at .03em of tracking - measured in the browser, not guessed, the way
   .wordtitle's 10.98em was - so the size is the room available divided by 15.4,
   which is that figure plus a hair of slack. The room is 100vw less 80px: 20px
   of shell padding and 1px of its border either side, 16px of hero padding
   either side, and the hero's 5px signal edge and 1px right border.

   Tracking comes down from .05em to .03em because it is width: at 320px those
   two hundredths are the difference between a 15px line and a 16px one. And the
   min() caps it where the desktop clamp used to top out, so the last 100px
   before the breakpoint do not run away with it.

   Change the words, the face, the tracking or the padding and re-measure: this
   number is specific to this string set this way. */
@media (max-width: 640px) {
    .hero { padding: 24px 16px; }
    .hero .hero-extra { display: none; }
    .hero .tagline {
        font-size: min(2.1rem, (100vw - 80px) / 15.4);
        letter-spacing: .03em;
        margin-bottom: 12px;
    }
    .hero .lede { font-size: 1.05rem; margin-bottom: 0; }

    /* Both buttons on one row, sharing the width equally. Two calls to action
       stacked on a phone read as a first choice and a second one, and the
       second one here is a link off the site - so it gets a column rather than
       the position under the thing somebody came for.

       flex: 1 1 0 rather than auto, so they are the same width whatever they
       say, and min-width: 0 because a flex item will not otherwise shrink below
       its own text. At 390px both still fit on one line; below about 350px they
       take two, which is what the padding and the 1.25 line-height are for. */
    .hero .btn-row { flex-wrap: nowrap; gap: 8px; }
    .hero .btn-row .btn {
        flex: 1 1 0;
        min-width: 0;
        padding: 11px 12px;
        font-size: .9rem;
        line-height: 1.25;
        text-align: center;
    }
}

.empty {
    border: 1px dashed var(--hairline);
    border-radius: var(--radius);
    padding: 40px 26px;
    text-align: center;
    color: var(--slate);
    background: var(--surface);
}

/* Meetup detail: description on the left, the facts box on the right. */
.split {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 300px;
    gap: 34px;
    align-items: start;
}

.factbox { border-left: 1px solid var(--hairline); padding-left: 26px; }
.factbox section + section { margin-top: 26px; }
.factbox h4 {
    font-family: var(--mono);
    font-size: .72rem;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--slate);
    margin: 0 0 8px;
    font-weight: 500;
}
.factbox p { margin: 0 0 3px; }

/* --- sharing a meetup ---------------------------------------------------
   Six small pills in a column about 300px wide, so they wrap to three rows and
   the gap comes down to suit. Nothing here is a new button: they are .btn
   .btn-quiet .btn-sm, the vocabulary the rest of the site already uses.

   The line under them is kept even when it says nothing, because it fills in
   after a click and the row above it must not jump when it does. */
.share-row { gap: 8px; }
.share-row .btn-sm { padding: 7px 12px; }
.share-said {
    margin: 10px 0 0;
    min-height: 1.3em;
}

/* --- guest access ------------------------------------------------------- */

/* Shown to logged-out visitors only. Cream ground so it reads as an invitation
   rather than a warning, and the actions sit to the right on wide screens so
   the paragraph keeps a readable measure. */
.guestbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 26px;
    flex-wrap: wrap;
    margin-top: 22px;
    padding: 22px 26px;
    background: var(--panel-warm);
    border: 1px solid var(--hairline);
    border-left: 5px solid var(--signal);
    border-radius: var(--radius);
}
.guestbar-text { flex: 1 1 340px; }
.guestbar h2 { font-size: 1.35rem; margin: 0 0 6px; }
.guestbar p { margin: 0; color: var(--ink-soft); max-width: 56ch; font-size: .95rem; }
.guestbar-actions { flex: 0 0 auto; margin: 0; }
.guestbar .btn-quiet { background: var(--surface); }

/* --- buttons ------------------------------------------------------------ */

.btn {
    display: inline-block;
    background: var(--signal);
    color: #fff;
    border: 0;
    border-radius: var(--radius);
    padding: 12px 22px;
    font: 600 .95rem/1 var(--body);
    text-decoration: none;
    cursor: pointer;
}
.btn:hover { background: var(--signal-dark); color: #fff; }
.btn-quiet { background: transparent; color: var(--ink); border: 1px solid var(--hairline); }
.btn-quiet:hover { background: var(--surface-2); color: var(--ink); }
.btn-go { background: var(--go); }
.btn-go:hover { background: #246343; }
.btn-sm { padding: 8px 14px; font-size: .85rem; }
/* A quiet button showing which of a set is currently on. */
.btn-on { background: var(--selected-bg); color: var(--selected-text); border-color: var(--selected-bg); }
.btn-on:hover { background: var(--selected-bg); color: var(--selected-text); opacity: .88; }
.btn-block { display: block; width: 100%; text-align: center; }
.btn[disabled] { opacity: .5; cursor: not-allowed; }

.btn-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }

/* --- forms -------------------------------------------------------------- */

.form-narrow { max-width: 440px; }

/* The meetup report: a write-up can run to 4,000 characters, which is a long
   way down a 440px column. */
.form-mid { max-width: 640px; }

/* /report and /report?kind=idea. The page is a heading, a few lines of copy and
   one form, with nothing beside it - so on a wide screen it takes the whole
   shell and runs down the middle of it rather than sitting in a 440px column
   hard against the left edge. The copy keeps a readable measure inside that:
   it is the box that is full width, not the line, since a paragraph set across
   1040px is about 160 characters and nobody finds their way back to the start
   of the next one. Below 640px it is the same left-aligned column every other
   form on the site is. */
.form-report { max-width: 440px; }

@media (min-width: 641px) {
    .form-report { max-width: none; }
    .form-report h1,
    .form-report > p { text-align: center; }
    .form-report > p { max-width: 62ch; margin-left: auto; margin-right: auto; }
    .form-report .btn-row { justify-content: center; }
    /* A fixed 300px iframe, so it is centred by the box round it. */
    .form-report .cf-turnstile { display: flex; justify-content: center; }
}

.field { margin-bottom: 20px; }
.field > label {
    display: block;
    font-weight: 600;
    font-size: .92rem;
    margin-bottom: 6px;
}
.field .req { color: var(--signal); }

input[type=text], input[type=email], input[type=password], input[type=url],
input[type=tel], input[type=number], input[type=date], input[type=time],
input[type=datetime-local], select, textarea {
    width: 100%;
    padding: 11px 12px;
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
    color: var(--ink);
    font: 400 1rem/1.4 var(--body);
}
input:focus, select:focus, textarea:focus { border-color: var(--signal); outline-offset: 1px; }
textarea { min-height: 170px; resize: vertical; }
select { appearance: auto; }

.hint { font-size: .85rem; color: var(--slate); margin: 6px 0 0; }
.field-error { border-color: var(--signal) !important; }
.error-text { font-size: .85rem; color: var(--signal-dark); margin: 6px 0 0; font-weight: 500; }

/* Two-up form rows that collapse on phones. */
.form-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0 18px;
}

fieldset {
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    padding: 22px 22px 4px;
    margin: 0 0 24px;
}
legend {
    font-family: var(--mono);
    font-size: .74rem;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--slate);
    padding: 0 8px;
}

.checkline { display: flex; gap: 9px; align-items: flex-start; }
.checkline input { margin-top: 4px; }
.checkline label { font-weight: 400; font-size: .95rem; }

/* Honeypot — bots fill it, humans never see it. */
.gotcha {
    position: absolute;
    left: -9999px;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

/* --- badges ------------------------------------------------------------- */

.badge {
    display: inline-block;
    font-family: var(--mono);
    font-size: .68rem;
    letter-spacing: .09em;
    text-transform: uppercase;
    padding: 3px 9px;
    border-radius: 100px;
    border: 1px solid currentColor;
    vertical-align: middle;
}
.badge-pending   { color: var(--wait-text); background: var(--wait-bg); border-color: var(--wait-line); }
.badge-approved  { color: var(--ok-text); background: var(--ok-bg); border-color: var(--ok-line); }
.badge-rejected  { color: var(--no-text); background: var(--no-bg); border-color: var(--no-line); }
.badge-cancelled { color: var(--slate); background: var(--page); border-color: var(--hairline); }

/* --- tables ------------------------------------------------------------- */

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

.table {
    width: 100%;
    border-collapse: collapse;
    background: var(--surface);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
}
.table th, .table td {
    text-align: left;
    padding: 12px 14px;
    border-bottom: 1px solid var(--hairline);
    vertical-align: top;
}
.table th {
    font-family: var(--mono);
    font-size: .72rem;
    letter-spacing: .09em;
    text-transform: uppercase;
    color: var(--slate);
    font-weight: 500;
    white-space: nowrap;
}
.table tr:last-child td { border-bottom: 0; }
.table tbody tr:hover { background: var(--surface-2); }
.table .num { text-align: right; font-family: var(--mono); }

/* --- rsvp --------------------------------------------------------------- */

.rsvp-count {
    font-family: var(--display);
    font-size: 2.4rem;
    line-height: 1;
    font-weight: 700;
}
.rsvp-count small {
    display: block;
    font-family: var(--mono);
    font-size: .72rem;
    letter-spacing: .09em;
    text-transform: uppercase;
    color: var(--slate);
    font-weight: 400;
    margin-top: 6px;
}

/* --- calendar ----------------------------------------------------------- */

.cal-head { display: flex; align-items: baseline; justify-content: space-between; gap: 14px; margin-bottom: 14px; flex-wrap: wrap; }

/* The month steppers, one either side of the grid and centred on its height.
   The grid takes min-width: 0 or it refuses to shrink below its content and
   pushes the right-hand stepper off the shell. */
.cal-frame { display: flex; align-items: center; gap: 12px; }
.cal-frame .cal { flex: 1 1 auto; min-width: 0; }

.cal-step {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    border-radius: 50%;
    border: 1px solid var(--hairline);
    background: var(--surface);
    color: var(--ink);
    font-size: 1.2rem;
    line-height: 1;
    text-decoration: none;
}
.cal-step:hover,
.cal-step:focus-visible { background: var(--surface-2); border-color: var(--signal); color: var(--ink); }

.cal {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 1px;
    background: var(--hairline);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    overflow: hidden;
}
.cal-dow {
    background: var(--chrome-2);
    color: #C9CED6;
    font-family: var(--mono);
    font-size: .68rem;
    letter-spacing: .09em;
    text-transform: uppercase;
    text-align: center;
    padding: 8px 2px;
}
/* Square, and it stays square: a day with something on it must not stretch its
   whole row into a tall rectangle, so anything past what fits is clipped and
   counted by .cal-more instead. */
.cal-cell {
    position: relative;
    background: var(--surface);
    aspect-ratio: 1;
    overflow: hidden;
    padding: 7px 8px;
}
.cal-cell.is-outside { background: var(--surface-2); color: var(--slate); }
.cal-cell.is-today { box-shadow: inset 0 0 0 2px var(--signal); }
.cal-date { font-family: var(--mono); font-size: .76rem; color: var(--slate); }
/* One line, always. A cell is a seventh of the grid, so anything too long for
   it is ellipsised rather than wrapped - wrapping is what made the rows grow. */
.cal-event {
    display: block;
    margin-top: 5px;
    padding: 3px 6px;
    border-radius: 3px;
    background: var(--event-bg);
    border-left: 3px solid var(--signal);
    font-size: .74rem;
    line-height: 1.3;
    text-decoration: none;
    color: var(--ink);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.cal-event:hover { background: var(--event-hover); color: var(--ink); }

/* What did not fit. The list view is the only place that can show all of it. */
.cal-more {
    display: block;
    margin-top: 4px;
    padding: 0 2px;
    font-family: var(--mono);
    font-size: .66rem;
    color: var(--slate);
    text-decoration: none;
}
.cal-more:hover { color: var(--ink); }

/* Matches the dashed circle Leaflet draws for a town-centre pin. */
.map-key-rough {
    display: inline-block;
    width: 13px;
    height: 13px;
    border: 2px dashed var(--signal);
    border-radius: 50%;
    vertical-align: -2px;
    margin-right: 4px;
}

/* --- flashes ------------------------------------------------------------ */

.flash {
    border-radius: var(--radius);
    padding: 12px 16px;
    margin-bottom: 18px;
    border: 1px solid var(--hairline);
    background: var(--surface);
}
.flash-error { border-color: var(--signal); }
.flash-success { border-color: var(--go); }

/* --- pagination --------------------------------------------------------- */

.pager { display: flex; gap: 8px; justify-content: center; margin-top: 32px; flex-wrap: wrap; }
.pager a, .pager span {
    padding: 8px 13px;
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
    text-decoration: none;
    color: var(--ink);
    font-size: .9rem;
}
.pager a:hover { background: var(--page); }
.pager .is-current { background: var(--selected-bg); color: var(--selected-text); border-color: var(--selected-bg); }

/* --- prose (policy pages) ----------------------------------------------- */

.prose { max-width: 70ch; }
.prose h2 { margin-top: 1.8em; }
.prose li { margin-bottom: .35em; }

/* --- backstage ---------------------------------------------------------- */

/* The footer mark. Sits apart from the link row, dimmed until hovered, with no
   label — it reads as a colophon flourish to anyone not looking for it. */
.stagedoor {
    display: inline-flex;
    align-items: center;
    opacity: .28;
    padding: 4px;
    border-radius: var(--radius);
    transition: opacity .15s ease;
}
.stagedoor:hover, .stagedoor:focus-visible { opacity: 1; }

/* The door page itself: no masthead, no nav, nothing to orient a stranger. */
body.bare {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    background: var(--chrome);
    padding: 24px;
}
.bare-shell { width: 100%; max-width: 360px; }

.door {
    background: var(--surface);
    border-radius: var(--radius);
    border-top: 4px solid var(--signal);
    padding: 32px 28px;
    text-align: center;
}
.door-mark { opacity: .9; margin-bottom: 18px; }
.door label {
    display: block;
    text-align: left;
    font: 500 .78rem/1 var(--mono);
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--slate);
    margin: 16px 0 6px;
}
.door input {
    width: 100%;
    padding: 11px 12px;
    font: 400 .95rem/1.2 var(--body);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: #fff;
}
.door button { margin-top: 22px; }
.door-error {
    background: var(--signal);
    color: #fff;
    font-size: .88rem;
    padding: 9px 12px;
    border-radius: var(--radius);
    margin: 0 0 4px;
}
.door-note { color: var(--slate); font-size: .9rem; }
.door-back { margin: 20px 0 0; font-size: .82rem; }
.door-back a { color: var(--slate); text-decoration: none; }

/* The admin strip, directly under the masthead. Signal red so there is never any
   doubt about which side of the site you are looking at. */
.adminbar {
    display: flex;
    align-items: center;
    gap: 18px;
    flex-wrap: wrap;
    background: var(--signal);
    color: #fff;
    padding: 10px 16px;
    border-radius: var(--radius);
    margin-bottom: 26px;
}
.adminbar-label {
    font: 500 .74rem/1 var(--mono);
    letter-spacing: .16em;
    text-transform: uppercase;
    opacity: .8;
}
.adminbar nav { display: flex; gap: 8px; flex-wrap: wrap; }

/* Each tab is its own chip. Underlining the current one was not enough
   separation - at a glance the five labels ran together into one word. */
.adminbar a {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: #fff;
    text-decoration: none;
    font: 600 .85rem/1 var(--body);
    padding: 8px 14px;
    border-radius: var(--radius);
    background: rgba(0, 0, 0, .16);
    border: 1px solid rgba(255, 255, 255, .3);
    white-space: nowrap;
}
.adminbar a:hover { background: rgba(0, 0, 0, .3); color: #fff; }

/* The page you are on inverts: white chip, red text. */
.adminbar a[aria-current] {
    background: #fff;
    color: var(--signal);
    border-color: #fff;
}
.adminbar .pill {
    display: inline-block;
    background: #fff;
    color: var(--signal);
    border-radius: 20px;
    font: 600 .72rem/1 var(--body);
    padding: 3px 7px;
}

/* ...which would put a white pill on a white chip, so it inverts too. */
.adminbar a[aria-current] .pill { background: var(--signal); color: #fff; }

/* Per-row admin actions. Kept on one line so the column stays narrow. */
.row-actions { display: flex; gap: 6px; flex-wrap: wrap; white-space: nowrap; }
.row-actions form { margin: 0; }

/* --- admin furniture ---------------------------------------------------- */

.stat-row { display: flex; gap: 12px; flex-wrap: wrap; }
.stat {
    flex: 1 1 130px;
    background: var(--surface);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    padding: 16px 18px;
    text-decoration: none;
    color: var(--ink);
}
a.stat:hover { border-color: var(--slate); color: var(--ink); }
.stat-live { border-left: 4px solid var(--signal); }

/* The same trios on a figure. Left border rather than a wash: five tiles in a
   row, two of them coloured, and full backgrounds would read as five states. */
.stat.st-live {
    background: var(--ok-bg);
    border-color: var(--ok-line);
    border-left: 4px solid var(--ok-line);
}
.stat.st-wait {
    background: var(--wait-bg);
    border-color: var(--wait-line);
    border-left: 4px solid var(--wait-line);
}
.stat.st-off {
    background: var(--no-bg);
    border-color: var(--no-line);
    border-left: 4px solid var(--no-line);
}
.stat-n { display: block; font: 700 1.9rem/1 var(--display); }
.stat-l {
    display: block;
    font: 400 .78rem/1.3 var(--mono);
    color: var(--slate);
    margin-top: 6px;
}

.filterbar {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    align-items: center;
    margin: 18px 0;
}
.filterbar input, .filterbar select {
    padding: 8px 10px;
    font: 400 .9rem/1 var(--body);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
}
.filterbar input[type="search"] { min-width: 200px; }

.table-wrap { overflow-x: auto; }
.grid {
    width: 100%;
    border-collapse: collapse;
    background: var(--surface);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    font-size: .92rem;
}
.grid th {
    text-align: left;
    font: 500 .72rem/1 var(--mono);
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--slate);
    padding: 11px 12px;
    border-bottom: 1px solid var(--hairline);
    white-space: nowrap;
}
.grid td { padding: 11px 12px; border-bottom: 1px solid var(--hairline); vertical-align: top; }
.grid tr:last-child td { border-bottom: 0; }
.grid tbody tr:hover { background: var(--surface-2); }
/* What a listing is doing, as a colour, so a long table is read at a glance
   rather than by looking up every badge: **green** on the site, **amber**
   waiting on somebody, **red** not published. status_row() in helpers.php picks
   the class, and these are the same token trios the chips use, so a row and the
   badge inside it can never disagree - and both themes come free.

   The tint goes on the cells rather than the row: a td paints over its tr, so
   tinting the row and letting `tbody tr:hover` paint over it would flicker the
   colour away exactly when somebody is pointing at the thing they want to read.
   Hover gets the left edge instead. */
tr.st-live { --st-bg: var(--ok-bg);   --st-line: var(--ok-line); }
tr.st-wait { --st-bg: var(--wait-bg); --st-line: var(--wait-line); }
tr.st-off  { --st-bg: var(--no-bg);   --st-line: var(--no-line); }

tr.st-live > td,
tr.st-wait > td,
tr.st-off > td { background: var(--st-bg); }

tr.st-live > td:first-child,
tr.st-wait > td:first-child,
tr.st-off > td:first-child { box-shadow: inset 3px 0 0 var(--st-line); }

tr.st-live:hover > td:first-child,
tr.st-wait:hover > td:first-child,
tr.st-off:hover > td:first-child { box-shadow: inset 5px 0 0 var(--st-line); }

/* Not a status: the Recent attempts rows on /admin/mail that went to the
   approvers. Same cells-and-edge treatment, so it reads as a highlight. */
tr.mail-staff > td { background: var(--staff-bg); }
tr.mail-staff > td:first-child { box-shadow: inset 3px 0 0 var(--staff-line); }
tr.mail-staff:hover > td:first-child { box-shadow: inset 5px 0 0 var(--staff-line); }

/* The same three on a card, where the whole thing is one item rather than a
   row in a list. Lighter than a table cell on purpose: a card carries a lot of
   type, and the queue's cards are all one colour anyway. */
.card.st-live {
    background: var(--ok-bg);
    border-color: var(--ok-line);
    box-shadow: inset 4px 0 0 var(--ok-line);
}
.card.st-wait {
    background: var(--wait-bg);
    border-color: var(--wait-line);
    box-shadow: inset 4px 0 0 var(--wait-line);
}
.card.st-off {
    background: var(--no-bg);
    border-color: var(--no-line);
    box-shadow: inset 4px 0 0 var(--no-line);
}

/* Panels inside a tinted card have to step forward off it rather than sitting
   in their own colour: the change strip is amber on its own, and amber on
   amber is one flat block with a line through it. */
.card.st-wait .changed,
.card.st-wait .factbox,
.card.st-wait .field input,
.card.st-wait .field select { background: var(--surface); }

.mono-cell { font-family: var(--mono); font-size: .84rem; }
.nowrap { white-space: nowrap; }
.row-dim { opacity: .5; }
.row-actions form { display: flex; gap: 6px; flex-wrap: wrap; }

.tag {
    display: inline-block;
    font: 500 .7rem/1 var(--mono);
    letter-spacing: .06em;
    text-transform: uppercase;
    padding: 4px 7px;
    border-radius: 3px;
    background: var(--page);
    color: var(--slate);
    border: 1px solid var(--hairline);
}
.tag-go   { background: var(--ok-bg);   color: var(--ok-text);   border-color: var(--ok-line); }
.tag-wait { background: var(--wait-bg); color: var(--wait-text); border-color: var(--wait-line); }
.tag-no   { background: var(--no-bg);   color: var(--no-text);   border-color: var(--no-line); }

.factlist { margin: 0; }
.factlist dt {
    font: 500 .72rem/1 var(--mono);
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--slate);
    margin-top: 14px;
}
.factlist dt:first-child { margin-top: 0; }
.factlist dd { margin: 5px 0 0; }

.card-tight { padding: 16px 18px; }

/* The top of /account/settings: the profile as other members see it, on a card
   of its own so it reads as a picture of the page rather than as a form. */
.profile-preview {
    margin: 0 0 8px;
    padding: 20px 24px 24px;
    background: var(--surface);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
}
.profile-preview-label {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: 4px 16px;
    margin: 0 0 16px;
    font-family: var(--mono);
    font-size: .74rem;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--slate);
}
.profile-preview-label a { letter-spacing: .04em; }
.profile-preview-name { margin: 0 0 6px; font-size: 2rem; }

/* Public and Private: the two halves of the settings under the preview. */
.settings-group {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 36px 0 4px;
    font-size: 1.35rem;
}
.settings-group .tag { font-size: .7rem; }
.settings-group-note { margin: 0 0 16px; }

@media (max-width: 640px) {
    .profile-preview { padding: 16px 16px 18px; }
    .profile-preview-name { font-size: 1.6rem; }
}

/* A meetup report: the host's write-up, and the recording if there is one. */
.report {
    margin: 28px 0 0;
    padding: 20px 22px;
    background: var(--surface-2);
    border: 1px solid var(--hairline);
    border-left: 4px solid var(--signal);
    border-radius: var(--radius);
}
.report h2 { margin-top: 0; }
/* The line on /account saying how many nights are still unwritten, and the one
   on /admin/meetups pointing at the approval queue. Amber, the colour
   everything waiting on somebody wears. One rule because they are the same
   thing said on two pages. */
.report-nudge,
.queue-nudge {
    margin: 12px 0 0;
    padding: 12px 16px;
    background: var(--wait-bg);
    border: 1px solid var(--wait-line);
    border-left: 4px solid var(--wait-line);
    border-radius: var(--radius);
    color: var(--wait-text);
    font-size: .95rem;
    max-width: 68ch;
}
.report-flag { margin-left: 6px; }
/* The theme's link colour was picked for the page ground, not for an amber
   panel, so this inherits the nudge's own ink the way the card's venue box
   does. Both themes come free, since --wait-text flips with them. */
.queue-nudge a { color: inherit; text-decoration: underline; font-weight: 600; }
.queue-nudge a:hover { text-decoration: none; }

/* The copy-across card on an admin meetup page. Facebook has no API for making
   events, so the job is to make hand-copying quick: a label, the value already
   shaped the way their form wants it, and a button per line. Mono, because
   every one of these is data being moved rather than prose being read. */
.fbrow {
    display: grid;
    grid-template-columns: 130px minmax(0, 1fr) auto;
    gap: 10px;
    align-items: start;
    margin-bottom: 10px;
}
.fbrow > label {
    padding-top: 9px;
    font-family: var(--mono);
    font-size: .74rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--ink-soft);
}
.fbrow .fbfield { margin: 0; font-family: var(--mono); font-size: .88rem; }
.fbrow textarea.fbfield { min-height: 0; }
.fbrow .btn { margin-top: 2px; }
/* The label goes above the value once there is no room beside it. */
@media (max-width: 640px) {
    .fbrow { grid-template-columns: minmax(0, 1fr) auto; }
    .fbrow > label { grid-column: 1 / -1; padding-top: 0; }
}

/* The past meetups shown under the upcoming ones on /meetups. The rule across
   the top is the whole point: everything below it has already happened. */
.pastblock {
    margin-top: 44px;
    padding-top: 26px;
    border-top: 3px solid var(--hairline);
}
.pastblock-head { margin-bottom: 4px; }
.card-past { border-left: 3px solid var(--slate); }

/* Drawn for desktop, not for a phone. The home page uses it to keep to five
   cards down there; see $phonePastSlots in home.php. */
@media (max-width: 640px) {
    .phone-hide { display: none; }
}
.tag-past {
    background: var(--chrome-2);
    border-color: var(--hairline);
    color: var(--cream);
}
.report-body {
    white-space: pre-line;
    max-width: 68ch;
}
.report-audio {
    display: block;
    width: 100%;
    max-width: 520px;
    margin: 4px 0 2px;
}
.check-line {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 0 0 12px;
    font-size: .95rem;
}
.check-line input { width: auto; }

/* A member's own words on their profile, with their line breaks kept. */
.profile-bio {
    margin: 10px 0 0;
    max-width: 60ch;
    white-space: pre-line;
    color: var(--ink);
}

/* A card's top line with something pinned to the far end of it - the venue on
   the queue's condensed cards. Wraps to its own line on a phone rather than
   squeezing the left half. */
.card-topline {
    display: flex;
    gap: 6px 16px;
    align-items: baseline;
    justify-content: space-between;
    flex-wrap: wrap;
}
.card-corner {
    margin-left: auto;
    text-align: right;
    color: var(--ink-soft);
    text-transform: none;
    letter-spacing: 0;
}
.card-corner em { display: block; font-style: normal; color: var(--slate); }
.card-corner-label { margin-right: 6px; color: var(--slate); }
.card-corner a { color: inherit; }
.card-corner a:hover { color: var(--link); }

@media (max-width: 520px) {
    .card-corner { margin-left: 0; text-align: left; }
}

/* On desktop the public cards are nearly a thousand pixels wide, and the three
   things somebody skims them for - when, where, what - were a 12px grey line
   and a title barely bigger than the pitch under it. So above the phone
   breakpoint they are sized as the point of the card: the date large and in
   ink, the venue labelled and set on a cream box so it is the one light thing
   on the card, and the title as a heading.

   Scoped to .card-link .card-body like the phone block, so it reaches home.php
   and browse.php only; the overview's condensed cards share .card-topline and
   stay as they are. The topline only sets its bottom margin, because it carries
   an inline margin-top; the title keeps its inline 6px under it. */
@media (min-width: 641px) {
    .card-link .card-body > .card-topline {
        gap: 12px 32px;
        align-items: flex-start;
        margin-bottom: 14px;
        font-size: 1.15rem;
        letter-spacing: .03em;
        line-height: 1.35;
        color: var(--ink);
    }
    /* Nudged down so the date sits level with the venue name inside its box. */
    .card-link .card-body > .card-topline > span:first-child { font-weight: 600; padding-top: 11px; }

    .card-link .card-corner {
        padding: 10px 16px 11px;
        background: var(--venue-bg);
        border: 1px solid var(--venue-line);
        border-radius: var(--radius);
        font-family: var(--body);
        font-size: 1.3rem;
        font-weight: 700;
        line-height: 1.25;
        text-align: left;
        color: var(--venue-text);
    }
    .card-link .card-corner-label {
        margin-right: 8px;
        font-family: var(--mono);
        font-size: .72rem;
        font-weight: 500;
        letter-spacing: .1em;
        text-transform: uppercase;
        vertical-align: 2px;
        color: var(--venue-soft);
    }
    /* The theme's link colours are chosen for the card, not for cream. */
    .card-link .card-corner a { color: inherit; }
    .card-link .card-corner a:hover { color: var(--signal); }
    .card-link .card-corner em {
        margin-top: 4px;
        font-family: var(--mono);
        font-size: .8rem;
        font-weight: 500;
        letter-spacing: .06em;
        text-transform: uppercase;
        color: var(--venue-soft);
    }
    .card-link .card-body > h3 { font-size: 1.85rem; }
}
.quote {
    border-left: 3px solid var(--hairline);
    padding-left: 12px;
    color: var(--ink-soft);
    font-style: italic;
    margin: 10px 0 0;
}

.card.danger { border-color: var(--danger-line); background: var(--danger-bg); }
.btn-danger { background: var(--signal-dark); }
.btn-danger:hover { background: #7C0707; }
.card.danger input[type="text"] {
    display: block;
    width: 100%;
    max-width: 340px;
    margin: 8px 0 12px;
    padding: 9px 11px;
    font: 400 .92rem/1 var(--body);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
}

/* --- footer ------------------------------------------------------------- */

.colophon {
    background: var(--chrome);
    color: #97A0AD;
    font-size: .85rem;
    padding: 30px 0;
}

/* The reserve, and only when the button is really on the page. The bottom
   right corner belongs to the floating report button while it is switched on,
   so the stage door and the admin log-out link are not underneath it - but
   that button is config-driven and skipped on two pages, so the room it needs
   is asked for by report_float_shows() rather than left standing here. Take
   the class away and the footer closes back up on its own. */
.colophon-float { padding-bottom: 96px; }
.colophon .shell { display: flex; justify-content: space-between; gap: 18px; flex-wrap: wrap; }
.colophon a { color: #C9CED6; text-decoration: none; }
.colophon a:hover { color: #fff; }
.colophon nav { display: flex; gap: 18px; flex-wrap: wrap; }
.colophon .tagline { color: #C9CED6; font-size: .72rem; margin: 0 0 6px; }
.colophon .tagline::before { width: 22px; height: 2px; }

/* A labelled rule between two ways of doing the same thing, so "or fill it in
   yourself" reads as an alternative rather than as more fields to complete. */
.rule-label {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 26px 0 18px;
    color: var(--slate);
    font: 500 .74rem/1 var(--mono);
    letter-spacing: .1em;
    text-transform: uppercase;
}
.rule-label::before,
.rule-label::after {
    content: "";
    flex: 1 1 auto;
    height: 1px;
    background: var(--hairline);
}

/* Which way of giving an address the meetup form is asking for. The three ways
   used to be stacked, all open at once, which reads as three things to fill in
   rather than a choice between them. Hidden until the script that switches the
   panels has run, since without it the choice does nothing. */
.where-pick {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px 18px;
    margin-bottom: 22px;
}
.where-pick > span {
    font-weight: 600;
    font-size: .92rem;
    flex: 1 0 100%;
}
.where-pick label {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    font-size: .95rem;
    cursor: pointer;
}
.where-pick input {
    width: auto;
    margin: 0;
    accent-color: var(--signal);
}

/* Old against new on an approval card. Not a strike-through: half of these
   are a whole paragraph, and struck-through prose is unreadable. The two lines
   are labelled instead, and the one that is proposed carries the weight. */
.diff { margin: 14px 0 0; }
.diff-row {
    display: grid;
    grid-template-columns: 116px minmax(0, 1fr);
    gap: 4px 14px;
    padding: 12px 0;
    border-top: 1px solid var(--hairline);
}
.diff-row:first-child { border-top: 0; padding-top: 4px; }
.diff-k {
    font: 500 .72rem/1.6 var(--mono);
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--slate);
}
.diff-v { min-width: 0; }
.diff-was,
.diff-now {
    display: block;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}
.diff-was { color: var(--slate); }
.diff-now { color: var(--ink); font-weight: 500; margin-top: 5px; }
.diff-was em,
.diff-now em {
    display: block;
    font: 500 .68rem/1.6 var(--mono);
    letter-spacing: .08em;
    text-transform: uppercase;
    font-style: normal;
    color: var(--slate);
}
.diff-now em { color: var(--signal); }

@media (max-width: 640px) {
    .diff-row { grid-template-columns: 1fr; gap: 2px; }
}

/* An input with a button on its end: the address line and its lookup. Wraps
   rather than squeezing the input on a phone. */
.with-button { display: flex; gap: 8px; align-items: stretch; flex-wrap: wrap; }
.with-button input { flex: 1 1 220px; }
.with-button .btn { flex: 0 0 auto; white-space: nowrap; }

/* --- the floating report button ----------------------------------------- */

/* Temporary furniture for the shakeout, so it is loud on purpose: signal red,
   over everything including the phone tab bar. report_float() in layout.php
   decides whether it is there at all. */
/* The pair in the bottom corner. The row is what is fixed, not the pills: one
   of them missing would otherwise leave the other sitting beside a gap. It is
   the mirror of .topfloats, and shares its z-index for the same reason. */
.bugfloats {
    position: fixed;
    right: 16px;
    bottom: 16px;
    z-index: 1300;
    display: flex;
    align-items: center;
    gap: 8px;
}
.report-float {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 10px 15px;
    border-radius: 999px;
    background: var(--signal);
    border: 1px solid rgba(255, 255, 255, .2);
    box-shadow: 0 6px 18px rgba(0, 0, 0, .38);
    color: #fff;
    font: 600 .78rem/1 var(--body);
    text-decoration: none;
}
.report-float:hover { background: var(--signal-dark); color: #fff; }

/* Quiet beside the red one: --chrome does not flip with the theme, so cream on
   it holds in both. Same trick as the "Log in" pill in the top corner. */
.idea-float {
    background: var(--chrome);
    border-color: rgba(252, 246, 228, .35);
    color: var(--cream);
}
.idea-float:hover { background: #2A2A2A; color: var(--cream); }
.idea-float .report-float-mark { background: var(--cream); color: var(--chrome); }
.report-float-mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 17px;
    height: 17px;
    border-radius: 50%;
    background: #fff;
    color: var(--signal);
    font: 700 .72rem/1 var(--body);
}

/* The version, wherever it is printed. */
.version {
    font-family: var(--mono);
    font-size: .72rem;
    color: var(--slate);
    white-space: nowrap;
}

/* --- responsive --------------------------------------------------------- */

@media (max-width: 860px) {
    /* minmax(0, …) rather than a bare 1fr, the way the desktop rule above
       already has it. A bare 1fr floors at min-content, and the global
       overflow-wrap: break-word deliberately does not shrink min-content - so
       one long unbroken value (a venue's website address, in the case that
       turned this up) sets a floor wider than the phone and pushes the whole
       page sideways. Measured on /admin/meetup/{id} at 390px: 550px of content
       in a 390px window before this, 390 after. */
    .split { grid-template-columns: minmax(0, 1fr); gap: 26px; }
    .factbox { border-left: 0; border-top: 1px solid var(--hairline); padding-left: 0; padding-top: 22px; }
}

@media (max-width: 640px) {
    .masthead .shell { padding-top: 14px; padding-bottom: 14px; }
    /* A phone gets the smaller cut of the backdrop. */
    body:not(.admin):not(.bare)::before {
        background-image: url("/assets/img/backdrop-760.jpg");
    }
    /* .hero's phone padding is not here any more: it is 24px 16px, up in the
       block that sizes the tagline, because that size is derived from it. Two
       rules setting the same padding in two blocks would mean the later one
       silently wins, which is how the tagline would come to wrap again. */
    .hero::before { width: 240px; height: 240px; right: -70px; opacity: .06; }
    main > .shell { padding-top: 22px; padding-bottom: 26px; }
    .tagline { letter-spacing: .12em; font-size: .74rem; }
    .card { padding: 20px; }
    .guestbar { padding: 20px; }

    /* The meetup tiles go edge to edge down here. At 390px the shell's 20px of
       padding either side was spending a tenth of the screen on a margin nobody
       reads, and a list of cards floating in it looks like a desktop page that
       has been shrunk. The card keeps its own padding, so the words do not
       touch the glass.

       21px is the shell's 20px of padding plus the 1px border on main > .shell:
       change either and this has to change with it. Scoped to .card-link inside
       a .stack, which is the two public lists and nothing else - an admin card
       or the guest box is not a tile and stays in its column. */
    .stack > .card-link {
        margin-left: -21px;
        margin-right: -21px;
        border-left-width: 0;
        border-right-width: 0;
        border-radius: 0;
    }

    /* The one side border that stays: it is the marker saying this already
       happened, not decoration. */
    .stack > .card-link.card-past {
        border-left: 3px solid var(--slate);
    }
    .masthead-admin .shell { padding-top: 12px; padding-bottom: 12px; }
    .adminbar { gap: 10px; padding: 10px 12px; }
    .adminbar nav { gap: 6px; }
    .adminbar a { padding: 7px 11px; font-size: .8rem; }
    .stat { flex: 1 1 100%; }
    .guestbar-actions { width: 100%; }
    .guestbar-actions .btn { flex: 1 1 auto; text-align: center; }
    .form-row { grid-template-columns: 1fr; }
    /* The steppers shrink hard down here and the gap with them: at 390px a
       44px pair plus 24px of gaps takes a fifth of the row, and every pixel of
       that comes out of seven columns that are already tight. */
    /* See .mapfull: at 348px across, this is the height at which the country
       fills about three quarters of the frame instead of a third. */
    .mapfull { height: 300px; }

    .cal-frame { gap: 5px; }
    .cal-step { width: 26px; height: 26px; font-size: .95rem; }

    /* A seventh of a phone is about 50px, so the chip loses its padding and
       most of its type and is usually a truncated town. It is still a link,
       and the day it sits in is still square. */
    .cal-cell { padding: 4px 3px; }
    .cal-date { font-size: .68rem; }
    .cal-event {
        margin-top: 3px;
        padding: 2px 3px;
        border-left-width: 2px;
        font-size: .58rem;
    }

    /* One chip is all that fits, so the rest of the day becomes a count. The
       list view is right under the grid for anyone who wants the detail. */
    .cal-event ~ .cal-event { display: none; }
    .cal-more { display: none; }
    /* Beside the date rather than under the chip: the bottom of a cell this
       small is already spoken for, and anything put there is clipped. */
    .cal-cell[data-extra]::after {
        content: "+" attr(data-extra);
        position: absolute;
        top: 4px;
        right: 4px;
        font: 500 .58rem/1 var(--mono);
        color: var(--slate);
    }

    /* The inline strip gives way to the tab bar, which is chrome and so keeps
       the dark ground in both themes. It sits above Leaflet's panes and
       controls (400-1000), or the map page would paint straight over it - and
       above the two floats at 1300 as well, because the settings panel opens
       upward into the same corner the bug button is pinned to. The bar itself
       never reaches the floats: they sit clear of its height. */
    .viewtabs-inline { display: none; }
    .viewtabs-bar { display: flex; }

    .viewtabs {
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 1400;
        margin: 0;
        gap: 0;
        flex-wrap: nowrap;
        background: var(--chrome-2);
        border-bottom: 0;
        border-top: 1px solid rgba(255, 255, 255, .14);
        box-shadow: 0 -6px 18px rgba(0, 0, 0, .3);
        padding-bottom: env(safe-area-inset-bottom);
    }
    .viewtabs > a,
    .viewtabs .menu > summary {
        flex: 1 1 0;
        min-width: 0;
        text-align: center;
        justify-content: center;
        padding: 12px 4px;
        margin: 0;
        border: 0;
        border-radius: 0;
        border-bottom: 0;
        border-top: 2px solid transparent;
        font: 500 .66rem/1.25 var(--mono);
        letter-spacing: .05em;
        text-transform: uppercase;
        color: #AEB6C2;
    }
    .viewtabs > a:hover,
    .viewtabs .menu > summary:hover { color: #fff; }
    .viewtabs > a[aria-current] {
        color: #fff;
        border-top-color: var(--signal);
        background: rgba(255, 255, 255, .07);
    }

    /* Lit, but not in signal red: the red bar means "the page you are on", and
       an open menu is not a page. The tint alone says which tab is open, and
       the panel hanging off it says the rest. */
    .viewtabs .menu[open] > summary {
        color: #fff;
        border-top-color: rgba(255, 255, 255, .45);
        background: rgba(255, 255, 255, .07);
    }

    /* The fourth tab: settings, where "Newly added" used to be. The <details>
       has to be a flex item in its own right, or the summary inside it cannot
       take an equal quarter of the row. */
    .viewtabs .menu { flex: 1 1 0; min-width: 0; display: flex; }
    .viewtabs .menu-word { display: inline; }

    /* Opening upward, since the bar is on the bottom edge. Inset from the right
       rather than flush with it, so the panel reads as hanging off the tab. */
    .viewtabs .menu-panel {
        top: auto;
        bottom: calc(100% + 8px);
        right: 6px;
        box-shadow: 0 -12px 28px rgba(0, 0, 0, .45);
    }

    /* Room for the bar, so the footer isn't stuck under it. The back office
       has no tab bar, so its own footer keeps its padding. */
    .colophon:not(.colophon-admin) {
        padding-bottom: calc(30px + 62px + env(safe-area-inset-bottom));
    }

    /* Above the tab bar rather than on top of it. */
    .bugfloats {
        right: 12px;
        bottom: calc(12px + 56px + env(safe-area-inset-bottom));
        gap: 6px;
    }
    /* Both pills keep their words down here: an icon on its own says nothing,
       and "what is this button" is the wrong question to leave somebody on a
       phone. They are tightened instead - smaller text, less padding, a smaller
       gap - which fits the pair inside 320px. Re-measure if either label
       changes. */
    .report-float { padding: 8px 10px; font-size: .7rem; gap: 6px; }
    .report-float-mark { width: 16px; height: 16px; font-size: .68rem; }
}

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

/* --- the meetup banner --------------------------------------------------
   A meetup page used to open with a grey slug line, a heading and a sentence
   of prose, which is a lot of reading before you know whether you can make it.
   The banner puts the four things somebody actually decides on - the date, the
   title, the host and the headcount - on one dark card at the top.

   Same near-black ground as .hero, and for the same reason: --chrome does not
   flip with the theme, so this reads as brand furniture in both. The red date
   block is the only saturated thing on the page, which is what makes it the
   first thing seen. */

.mhero {
    position: relative;
    overflow: hidden;
    isolation: isolate;
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: 28px;
    background: var(--chrome);
    color: var(--cream);
    border: 1px solid var(--hairline);
    border-left: 5px solid var(--signal);
    border-radius: var(--radius);
    padding: 26px 30px;
    margin-bottom: 34px;
}

/* The same watermark the home hero carries, bled off the right edge. */
.mhero::before {
    content: "";
    position: absolute;
    z-index: -1;
    top: 50%;
    right: -118px;
    transform: translateY(-50%);
    width: 300px;
    height: 300px;
    background: url("/assets/img/logo-watermark.svg") center / contain no-repeat;
    opacity: .04;
    pointer-events: none;
}

/* The date block: a torn-off calendar page in the signal red. It repeats what
   the line under the title says, which is deliberate - one of them is read and
   the other is glanced at, and they are not the same gesture. */
.mhero-date {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 96px;
    margin: 0;
    padding: 13px 6px 11px;
    background: var(--signal);
    color: #fff;
    border-radius: var(--radius);
    line-height: 1;
    box-shadow: 0 6px 16px rgba(0, 0, 0, .3);
}
.mhero-mon {
    font: 700 .82rem/1 var(--mono);
    letter-spacing: .14em;
    text-transform: uppercase;
}
.mhero-day {
    font-family: var(--display);
    font-size: 2.9rem;
    font-weight: 700;
    margin: 4px 0 5px;
}
.mhero-dow {
    font: 500 .7rem/1 var(--mono);
    letter-spacing: .12em;
    text-transform: uppercase;
    opacity: .82;
}

.mhero-body { min-width: 0; }
.mhero-place { color: #9AA3B0; margin: 0 0 6px; }

/* The h1 default runs to 3.1rem, which is too much shouting inside a card that
   already has a red block in it. */
.mhero h1 {
    font-size: clamp(1.8rem, 4vw, 2.5rem);
    color: var(--cream);
    margin: 0 0 10px;
}

.mhero-when {
    font-family: var(--mono);
    font-size: .88rem;
    color: #D8DDE5;
    margin: 0 0 14px;
}
.mhero-sep { color: var(--signal); margin: 0 4px; }
.mhero-tz {
    display: inline-block;
    margin-left: 8px;
    padding: 2px 8px;
    border: 1px solid rgba(252, 246, 228, .28);
    border-radius: 100px;
    font-size: .7rem;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--cream);
}

.mhero-host { margin: 0; color: #C9CED6; font-size: .95rem; }
.mhero-host strong { color: var(--cream); font-weight: 600; }
.mhero-host .avatar { border-color: rgba(252, 246, 228, .25); }

/* Headcount and the way in, kept together on the right so the button is beside
   the number it belongs to rather than at the bottom of the page. */
.mhero-side {
    flex: 0 0 auto;
    text-align: center;
    padding-left: 26px;
    border-left: 1px solid rgba(252, 246, 228, .16);
}
.mhero-count {
    font-family: var(--display);
    font-size: 2.6rem;
    line-height: 1;
    font-weight: 700;
    color: var(--cream);
    margin: 0 0 14px;
}
.mhero-count small {
    display: block;
    font: 400 .68rem/1.3 var(--mono);
    letter-spacing: .09em;
    text-transform: uppercase;
    color: #8C95A4;
    margin-top: 7px;
}
.mhero-count-done { margin-bottom: 0; }

/* One that has been and gone. The red block would be a call to action on a
   night nobody can go to any more. */
.mhero-past .mhero-date { background: var(--chrome-2); box-shadow: none; }
.mhero-past .mhero-date .mhero-mon,
.mhero-past .mhero-date .mhero-dow { color: #9AA3B0; }
.mhero-past { border-left-color: var(--chrome-2); }

/* --- the rest of a meetup page ------------------------------------------ */

/* The photo leads the column now that the banner carries the time, so it wants
   the margin off the top of it. */
figure.photo-lead { margin-top: 0; }

/* Square, like the card that led here. max-height has to be cleared or the
   60vh cap on figure.photo above wins on a short window and letterboxes it
   back to a rectangle. */
figure.photo-lead img {
    width: 100%;
    aspect-ratio: 1;
    height: auto;
    max-height: none;
    object-fit: cover;
}

/* The weekly report as it will arrive, on /admin/mail. Monospace and scrolling
   inside its own box: the body is written to a fixed width, and letting it wrap
   to the page would show something nobody receiving it will see. */
.mail-preview {
    font-family: var(--mono);
    font-size: .8rem;
    line-height: 1.6;
    white-space: pre;
    overflow: auto;
    max-height: 420px;
    margin: 12px 0 0;
    padding: 14px 16px;
    background: var(--surface-2);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    color: var(--ink-soft);
}

/* The HTML digest drawn as it arrives. Its own colours are baked into the
   message, so the frame does not follow the theme. */
.mail-preview-html {
    display: block;
    width: 100%;
    height: 640px;
    margin: 12px 0 0;
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: #EFECE4;
}

/* Whatever the host typed, with their line breaks kept. */
.meetup-copy {
    white-space: pre-wrap;
    font-size: 1.02rem;
    color: var(--ink-soft);
    margin-top: 22px;
    max-width: 66ch;
}

.where { margin-top: 36px; }
.where h2 { font-size: 1.35rem; margin-bottom: 12px; }

/* The preview map. isolation matters: Leaflet's panes sit at z-index 400, and
   without a stacking context of its own this would paint over the settings
   dropdown whenever the two overlap. */
.minimap {
    position: relative;
    isolation: isolate;
    z-index: 0;
    height: 300px;
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface-2);
}
.where-go { margin: 14px 0 0; }

/* The facts column as a stack of small cards rather than a bare list under a
   rule, so the page has something on the right of it. Only the meetup page
   asks for this; /venue keeps the plain column. */
.factbox-cards {
    border-left: 0;
    padding-left: 0;
    display: grid;
    gap: 14px;
}
.factbox-cards section {
    margin: 0;
    padding: 18px 20px;
    background: var(--surface);
    border: 1px solid var(--hairline);
    border-left: 3px solid var(--signal);
    border-radius: var(--radius);
}
.factbox-cards section + section { margin-top: 0; }

/* The RSVP form is the point of the page, so it gets the same red edge as the
   cards pointing at it. */
.rsvp-card {
    margin-top: 40px;
    max-width: 560px;
    border-left: 5px solid var(--signal);
}
.rsvp-card h2 { margin-bottom: 6px; }

/* --- the signed-in corner ------------------------------------------------
   Logged in, the top right of both mastheads is your own icon: it replaces
   both the three dots and the name that used to sit in the nav beside them.
   The panel behind it still carries the name, which is what the phone gets
   when .menu-word goes. */

.menu-me > summary { padding: 4px 12px 4px 5px; gap: 9px; }
.menu-me > summary .menu-face {
    width: 30px;
    height: 30px;
    border: 1px solid rgba(252, 246, 228, .3);
    vertical-align: middle;
}
.menu-me[open] > summary .menu-face,
.menu-me > summary:hover .menu-face { border-color: var(--signal); }
.menu-me > summary .menu-word { text-transform: none; letter-spacing: .03em; }

@media (max-width: 860px) {
    /* The facts column is full width down here, so the cards would be four
       wide slabs. Back to the plain list the rest of the site uses. */
    .factbox-cards { display: block; }
    .factbox-cards section {
        padding: 0;
        background: none;
        border: 0;
        border-left: 0;
    }
    .factbox-cards section + section { margin-top: 26px; }
}

@media (max-width: 720px) {
    /* The headcount drops under the date and the title rather than squeezing
       a third column into a phone. */
    .mhero {
        grid-template-columns: auto minmax(0, 1fr);
        gap: 18px 20px;
        padding: 20px;
    }
    .mhero-side {
        grid-column: 1 / -1;
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 16px;
        text-align: left;
        padding: 16px 0 0;
        border-left: 0;
        border-top: 1px solid rgba(252, 246, 228, .16);
    }
    .mhero-count { margin: 0; font-size: 2rem; }
    .mhero-count small { display: inline; margin-left: 8px; }
    .mhero-date { width: 74px; padding: 10px 5px 9px; }
    .mhero-day { font-size: 2.2rem; }
    .mhero-when { font-size: .8rem; }
    .mhero-tz { margin-left: 0; margin-top: 6px; }
    .minimap { height: 240px; }
}


/* --- the phone masthead -------------------------------------------------
   A phone has no top nav. Five words in a row that wrapped onto two lines was
   the worst of both: too small to hit and too big to ignore. Everything in it
   has somewhere better to be, so nothing is lost by taking it away:

     Find a meetup  ->  the bar pinned to the bottom of the window
     Host one       ->  .host-float, the green button in the top corner
     The show       ->  the episode card in the hero, and the footer credit
     Register / Log in -> the settings panel behind the icon

   The show briefly had a big red button of its own down here. The hero card
   that pulls the latest episode off the feed says the same thing better - it
   names the episode and shows its artwork - so the button went, and with it the
   only reason the wordmark and the icon were on separate rows.

   .topfloats is declared hidden here and turned on inside the 640px block, so
   a desktop never pays for it. */

.topfloats { display: none; }

@media (max-width: 640px) {

    .masthead nav { display: none; }


    /* Top right, over the page, and still there after you have scrolled. Same
       z-index as the report button at the other end of the window; they cannot
       overlap, so they do not need to be ordered against each other.

       The row rather than the pills is what is fixed, so neither pill has to
       know the other's width: "Host a Meetup" is not drawn on the host's own
       form and "Log in" goes the moment somebody is logged in, and a pill
       placed by a hard-coded offset would then sit beside a gap. */
    .topfloats {
        display: flex;
        align-items: center;
        gap: 8px;
        position: fixed;
        top: 10px;
        right: 10px;
        z-index: 1300;
    }

    .host-float, .login-float {
        display: inline-block;
        padding: 9px 14px;
        border-radius: 999px;
        box-shadow: 0 6px 18px rgba(0, 0, 0, .38);
        font: 600 .78rem/1 var(--body);
        white-space: nowrap;
        text-decoration: none;
    }
    .host-float {
        background: var(--go);
        border: 1px solid rgba(255, 255, 255, .22);
        color: #fff;
    }
    .host-float:hover, .host-float:active { background: #246343; color: #fff; }

    /* Quiet beside it, and deliberately so: hosting is the thing the site
       actually wants, and two pills of equal weight in one corner is a choice
       rather than a call to action. --chrome does not flip with the theme, so
       cream on it reads in both. */
    .login-float {
        background: var(--chrome);
        border: 1px solid rgba(252, 246, 228, .3);
        color: var(--cream);
    }
    .login-float:hover, .login-float:active { background: #2A2E35; color: var(--cream); }

    /* The home page says all of this at length in .guestbar. Down here the pill
       above is the whole of it: the box is four lines and three buttons about
       not needing an account, on the screen somebody is trying to get past. */
    .guestbar { display: none; }

    /* The corner belongs to the floats, so nothing else may compete for it.
       The full wordmark measures 203px, and with both pills up there the row's
       left edge lands at x=188 on a 390px screen - so the two cannot share a
       row while the wordmark says both words.

       It does not need to. The page title underneath now says MEETUPS in letters
       an inch high, so repeating it up here on a phone was the least useful
       thing on the screen. Dropping to "MMO" ends the wordmark at x=100, which
       holds at every width: the row is anchored to the right edge, so at 320px
       it starts at x=118 and there is still 18px of daylight between them.

       Keep that in step with the floats: measured, not guessed. Anything added
       to that row, or a longer word in either pill, eats into those 18px. */
    .masthead:not(.masthead-admin) .brand span em { display: none; }

    /* And nothing is left up there to reserve it for. The menu that used to
       hold that corner is the fourth tab in the bar at the bottom of the
       window now, so the whole row is the wordmark and the float over it. The
       back office keeps its corner: it has no float and no tab bar. */
    .masthead:not(.masthead-admin) .masthead-actions { display: none; }
    .masthead .shell { gap: 8px 12px; }

    /* The banner is the one thing left underneath the floats, and it cannot be
       moved - it is the first element on the page. So it gives up the width
       instead. Signed out there are two pills up there rather than one, which
       is what the :has() asks: it is the row's own width either way, and the
       banner sits above the floats in the document, so a sibling selector
       could not have said it. Temporary either way: it goes when
       site.under_construction does. */
    .construction { padding-right: 132px; }
    body:has(.login-float) .construction { padding-right: 200px; }
}


/* --- what changed, in the approval queue --------------------------------
   Only drawn on a re-submission. A host editing an approved listing puts it
   back in the queue, and without this the card is identical to a brand-new
   one: an approver had to read the whole thing again to find out that all
   that happened was a picture being added.

   Amber ground rather than a plain panel, because it is the one thing on the
   card that is news. It uses the --wait-* trio the pending chips use, so it
   reads as the same kind of information in both themes. */

.changed {
    margin: 0 0 18px;
    padding: 12px 14px;
    background: var(--wait-bg);
    border: 1px solid var(--wait-line);
    border-left: 3px solid var(--wait-text);
    border-radius: var(--radius);
}
.changed-head {
    margin: 0 0 9px;
    font: 500 .74rem/1.4 var(--mono);
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--wait-text);
}
.changed-flags {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin: 0;
}

/* The chips sit on the amber panel rather than on the card, so the plain one
   needs a ground of its own or it reads as a hole in the panel. */
.changed-flags .tag { background: var(--surface); border-color: var(--wait-line); }
.changed-flags .tag-wait { background: var(--surface); color: var(--wait-text); border-color: var(--wait-text); font-weight: 700; }


/* --- the latest episode, in the home hero -------------------------------
   Only drawn when inc/show.php has an answer, so .hero has to hold up without
   it: the words keep the whole width until .hero-show says otherwise, which is
   why the split lives on that modifier rather than on .hero itself. */

.hero-show {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: 38px;
}
.hero-words { min-width: 0; }

/* The label and the card. It is a heading over the card rather than a line
   inside it: the card is the episode, and "listen to the latest show" is what
   to do with it, which is not something the episode says about itself. The
   card's own art still starts the link, so the first thing in it is the thing
   it is. */
.showbox {
    display: grid;
    justify-items: center;
    gap: 8px;
}

/* One link wrapping the art and the words, so there is a single target and no
   argument about which half is clickable. */
.showcard {
    display: grid;
    justify-items: center;
    gap: 2px;
    width: 240px;
    padding: 16px 16px 18px;
    background: rgba(0, 0, 0, .28);
    border: 1px solid rgba(252, 246, 228, .18);
    border-radius: var(--radius);
    text-align: center;
    text-decoration: none;
    color: var(--cream);
}
.showcard:hover {
    color: var(--cream);
    border-color: var(--signal);
    background: rgba(0, 0, 0, .4);
}

/* Square, because podcast art is, and stored square by show_store_art(). The
   width/height on the tag are the stored 480 and the box is 208, so the aspect
   ratio is what reserves the space rather than a guess. */
.showcard-art {
    display: block;
    width: 208px;
    height: auto;
    aspect-ratio: 1;
    object-fit: cover;
    border-radius: 4px;
    margin-bottom: 12px;
    background: var(--chrome-2);
}

/* The label is the instruction, so it is the loud half. It is a <p> outside
   the anchor now, hence the margin reset, and it still lights up with the card
   under it - :has(), because it is no longer inside the thing being hovered. */
.showcard-label {
    font-family: var(--mono);
    font-size: .72rem;
    font-weight: 500;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--signal);
    margin: 0;
}
.showbox:has(.showcard:hover) .showcard-label { color: #FF6B60; }

/* The episode title is somebody else's typing and can run long, so it is
   allowed two lines and clipped after that rather than growing the card. */
.showcard-title {
    font-family: var(--display);
    font-size: 1.08rem;
    font-weight: 700;
    line-height: 1.2;
    letter-spacing: .01em;
    margin-top: 4px;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.showcard-when {
    font-family: var(--mono);
    font-size: .68rem;
    letter-spacing: .06em;
    color: #9AA3B0;
    margin-top: 6px;
}

@media (max-width: 860px) {
    /* Under the words rather than beside them, and centred, which is the only
       thing that looks deliberate once it has the full width to sit in. The
       box is what carries the width now; the card fills it. */
    .hero-show { grid-template-columns: minmax(0, 1fr); gap: 26px; }
    .showbox { width: 100%; max-width: 260px; justify-self: center; }
    .showcard { width: 100%; }
}

/* On a phone the episode card lies down: a small square of art on the left and
   the title beside it, instead of a 260px column of picture. The hero is two
   lines and two buttons down here and the card was taller than all of it. */
@media (max-width: 640px) {
    .showbox { max-width: none; gap: 6px; justify-items: stretch; }
    .showcard {
        grid-template-columns: 56px minmax(0, 1fr);
        justify-items: start;
        align-items: center;
        gap: 0 12px;
        padding: 10px 12px;
        text-align: left;
    }
    .showcard-art {
        width: 56px;
        margin-bottom: 0;
        grid-row: span 2;        /* the words stack beside it, not under it */
    }
    .showcard-title { font-size: .95rem; margin-top: 0; }
    .showcard-when { margin-top: 2px; }
    .showcard-label { font-size: .66rem; }
}


/* --- the home page's title ----------------------------------------------
   The show's name, spelled out, at the top of its own site: it sits directly
   under the nav and above the hero, and it is the only place Anton is used.

   Anton ships one weight and that weight is already heavy, so the font-weight
   the h1 rule sets has to be undone - ask a browser for 700 of a face that has
   only 400 and it fakes it by smearing the letters sideways, which on a face
   this tight closes the counters up.

   "Meetups" is red and on a line of its own for the same reason it is red in
   the wordmark up in the masthead: it is the same lockup, said larger. The show
   names itself on the first line and the site says what it is on the second,
   which is also what keeps four words off one line - all four at this size runs
   past the shell on any screen narrower than about 1300px. */

.wordtitle {
    font-family: var(--poster);
    font-weight: 400;
    font-size: clamp(2.3rem, 8.4vw, 5.4rem);
    line-height: .92;
    letter-spacing: .004em;
    text-transform: uppercase;
    color: var(--ink);
    text-align: center;
    margin: 0 0 28px;
}

/* Sized in em rather than rem, so it stays in proportion to the line above it
   at every step of that clamp instead of needing a second one of its own - and
   so the phone gets the same treatment for free. The centring is inherited
   from .wordtitle now and repeated here only so this block still reads on its
   own: both lines are centred, which is what makes them one lockup rather than
   a heading with a word parked under its left edge. */
.wordtitle em {
    display: block;
    text-align: center;
    font-size: 1.62em;
    line-height: .86;
    font-style: normal;
    color: var(--signal);
}

@media (max-width: 640px) {
    /* Sized to the width rather than clamped to a floor, so the show's name is
       one line that fills the shell instead of breaking after "Media". Anton
       sets those three words at 10.98em wide, measured; 11 leaves a hair of
       slack, and 46px is the shell's 20px either side plus its border. The
       clamp's 8.4vw was already about right - it was the 2.3rem floor under it
       that overflowed everything narrower than 438px. */
    .wordtitle {
        font-size: calc((100vw - 46px) / 11);
        margin-bottom: 20px;
    }

    /* Room between the two lines. On desktop the first line is large enough
       that the tight leading reads as one lockup; shrunk to fit a phone it
       reads as MEETUPS sitting on top of the name. The margin is in the em's
       own em, so it scales with the line it is pushing down rather than with
       the smaller line above it. */
    .wordtitle em { margin-top: .18em; }
}
