/* ============================================================================
   Weekly Shenanigans — styles
   New England coastal, "oyster shack": the sign-painted navy of a Long Island
   Sound shellfish trailer, sage-green script, and the warm cream of the paper
   it is printed on. Coral-rose kept from the shell's lip as the one warm accent.
   Mobile-first. No frameworks.
   ========================================================================== */

/* ---- Design tokens ------------------------------------------------------- */
:root {
  /* Brand + UI colors (New England coastal — "oyster shack" palette).

     Three colours carry the whole thing, the way the sign does: deep navy for
     anything that matters, sage for anything supporting, cream underneath both.
     Sage is DECORATIVE ONLY at these values — it is far too light for body text
     on cream, which is exactly why the sign uses it on navy and never the other
     way round. Where sage has to carry words, --seaglass-ink is the readable
     one. */
  --paper:        #FAF7F0;   /* printed cream — page background */
  --surface:      #FFFDF8;   /* the paper stock itself — cards */
  --surface-2:    #EFE9DD;   /* a shade deeper — fills, chips */
  --ink:          #1B2B44;   /* sign-painted navy — primary text */
  --ink-soft:     #4A5768;   /* weathered navy — secondary text */
  --ink-faint:    #5F6B7B;   /* faded ink — meta text (dark enough for 4.5:1) */
  /* ---- PLACEHOLDER INK, LIGHTER THAN META INK ON PURPOSE ------------------
     Owner, 13 Sep 2026: "make the font color a little lighter than this so it
     doesnt confuse users because now it looks like someone really typed that."

     Exactly the trouble with using --ink-faint here: that colour is tuned to
     4.5:1 because meta text is text somebody has to READ. A placeholder is not
     read, it is glanced at and then typed over - and at meta weight it looks
     like a value already in the box, so people tab past a field they have not
     filled in.

     3.6:1 on paper, 3.7:1 on card. Above the 3:1 floor for non-body text, and
     a clear step lighter than the ink beside it, which is the whole point:
     the difference is what says "nothing here yet". Text a reader actually has
     to read keeps --ink-faint - see .field__hint, which is real guidance and
     sits OUTSIDE the box where nobody can mistake it for input. */
  --ink-hint:     #7B8694;   /* placeholder ink — 3.6:1, deliberately not 4.5 */
  --line:         #E2DACC;   /* bleached driftwood — borders */
  --line-strong:  #C9BFAF;

  --primary:      #1C3557;   /* the trailer's navy — links, buttons, active */
  --primary-ink:  #12233D;
  --primary-tint: #DCE3EC;   /* navy washed out to a haze */
  --primary-tint-2:#C2CFDE;  /* one step deeper — calendar days that have something on */
  --accent:       #984F42;   /* shell lip — coral-rose; highlights, "today" */
  --accent-tint:  #F5E4DE;   /* blush inside the shell */
  /* SUNSET, and the reason it exists is that --accent reads as RED at scale.
     Fine as an icon or a word; wrap a whole card in it and the paid placement
     looks like an error state. This is the same warmth turned toward orange —
     unmistakably not a warning, still muted enough to sit beside the navy and
     the sage without shouting. Used for the Highlight of the Week card. */
  --sunset:       #C4703A;
  --sunset-tint:  #FBEADC;
  /* THE SAME SUNSET, DARK ENOUGH TO BE READ AS TEXT, which is the same split
     as --seaglass / --seaglass-ink and exists for the same reason. --sunset is
     3.6:1 on the card - fine behind a whole panel, under the 4.5:1 a 12px bold
     figure needs. This is 5.0:1 and the identical hue. */
  --sunset-ink:   #A85A28;

  /* Sage: the script on the sign. Calm guidance, and the one hue here that is
     neither navy nor coral, so tips stay distinct from both actions and alarms. */
  --seaglass:     #7D9E97;   /* sage green (decorative accents) */
  --seaglass-ink: #3F5B55;   /* readable sage for text on the tint */
  --seaglass-tint:#E4EDEA;   /* pale sage — the "here's a tip" panels and the
                                Highlight tiles. Never red, which reads as an
                                error rather than guidance. */
  --destructive:  #B33A2B;   /* error / alarm red */
  /* THE FOUR SIGNAL COLOURS, for the capacity traffic lights. Kept apart from
     the brand palette on purpose: these have to mean "fine / plan / soon / now"
     at a glance, which is a different job from looking like us. The green is
     not --seaglass-ink — that is a sage chosen to be legible as TEXT, and at
     lamp size it reads as a dark smudge rather than as green. */
  /* THE THREE STATUS COLOURS, DARKENED TO BE READABLE (Aug 2026).
     They are used as `color:` on status icons and lamps — .hrow__icon,
     .lamp--* — so the contrast that matters is against the paper behind them,
     and the originals did not have it: green 2.89:1, yellow 1.79:1, orange
     2.73:1, where even non-text UI wants 3:1.

     Held to 4.5:1 rather than 3:1, deliberately: these ARE read as
     information, not decoration — the lamp is the whole message on a row that
     says whether something is healthy — and the stricter bar costs nothing
     here but a slightly deeper shade of the same hue.

     Yellow moves furthest because yellow always does: there is no light yellow
     that is legible on cream, so a readable one is necessarily closer to
     amber. The rows carrying it also have a border and a background tint, so
     the colour was never the only signal. */
  --signal-green:  #1E7A44;   /* 5.00:1 on paper */
  --signal-yellow: #8A6410;   /* 5.02:1 */
  --signal-orange: #A85A16;   /* 4.74:1 */
  --signal-red:    var(--destructive);
  /* Sea-glass green, used ONLY for the pay-per-post promotion: the brush stroke
     over the old price and the "40% off" tag. Deliberately outside the plain
     slate/coral palette so a live offer is legible at a glance and never gets
     mistaken for ordinary UI. White on --paint is 5.2:1. */
  --paint:        #2F7A57;
  --paint-ink:    #1F5A3E;

  --on-primary:   #FFFFFF;
  --focus:        #1C3557;

  /* Typography */
  --font-display: "Fraunces", Georgia, "Times New Roman", serif;
  --font-body: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  /* Spacing scale */
  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px; --sp-4: 16px;
  --sp-5: 24px; --sp-6: 32px; --sp-7: 48px; --sp-8: 64px;

  --radius:   14px;
  --radius-sm: 9px;
  --radius-pill: 999px;

  --shadow: 0 1px 2px rgba(38, 48, 58, .05), 0 4px 16px rgba(38, 48, 58, .06);
  --shadow-hover: 0 2px 4px rgba(38, 48, 58, .07), 0 10px 28px rgba(38, 48, 58, .10);

  --maxw: 720px;          /* content column — keeps it scannable */
  --maxw-wide: 1040px;    /* venues grid, calendar */
}

/* ---- Reset --------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.55;
  color: var(--ink);
  background: var(--paper);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
img { max-width: 100%; display: block; }
a { color: var(--primary); text-decoration: none; }
a:hover { text-decoration: underline; }
button { font: inherit; cursor: pointer; }
h1, h2, h3 { font-family: var(--font-display); font-weight: 600; line-height: 1.15; margin: 0; }

/* ---- Layout -------------------------------------------------------------- */
.wrap { max-width: var(--maxw); margin: 0 auto; padding: 0 var(--sp-4); }
.wrap--wide { max-width: var(--maxw-wide); }
.section { padding: var(--sp-6) 0; }

/* ---- Skip link ----------------------------------------------------------- */
.skip-link {
  position: absolute; left: -9999px; top: 0;
  background: var(--ink); color: #fff; padding: 10px 16px;
  border-radius: 0 0 var(--radius-sm) 0; z-index: 100;
}
.skip-link:focus { left: 0; }

/* ---- Header -------------------------------------------------------------- */
.site-header {
  position: sticky; top: 0; z-index: 40;
  background: rgba(239, 237, 228, .86);
  backdrop-filter: saturate(1.2) blur(10px);
  border-bottom: 1px solid var(--line);
  /* UNDER THE PHONE'S CLOCK, NOT BEHIND IT. Owner, 10 Sep 2026, from the
     app on their own iPhone: "the Join and the Weekly Shenanigans get mixed
     with the phone's information, the time, the battery." Inside the app,
     and on the home-screen install, the page runs edge to edge and the
     status bar sits over whatever is at the top - which was the masthead.
     This is the height of that bar on the device in hand (zero in an
     ordinary browser tab, where the browser already keeps clear of it), so
     the masthead starts below the clock and its own tinted, blurred ground
     fills the strip behind it. Needs viewport-fit=cover on the page, which
     every page carries. */
  padding-top: env(safe-area-inset-top, 0px);
}

/* ---- THE HEADER GETS OUT OF THE WAY ONCE YOU ARE READING ------------------

   Asked for as "fixed on that position, especially when the user scrolls up and
   down the page" — about the Filters and town buttons, which pin further down
   this file. This half is what makes that affordable.

   THE ARITHMETIC IS THE WHOLE ARGUMENT. The header is already sticky and
   measures 133px on a desktop and 184px on a phone. Pinning a 60px control bar
   under it unchanged would freeze about 244px of a 375x812 screen — very nearly
   a third of the viewport — before a single listing showed. Two sticky things
   stacked is the problem; one slim strip is the feature.

   So the masthead condenses the moment you scroll: the tagline goes, the mark
   shrinks, the padding tightens. It has done its job by the time somebody is
   800px into a list of what is on, and nothing it drops is a control — the
   nav, the town and Sign in all stay exactly where they were.

   Not a transform, and not a height animation on `auto`: the parts that go are
   `display: none`, and the row keeps its own min-height, so the strip settles
   at one predictable height rather than easing through a range that the pinned
   bar underneath would have to chase. */
   MEASURED, so the trade is a fact rather than a hope. The header was two
   rows: .site-header__inner (brand, Sign in, town) and .section-nav (the
   tabs). The second row is gone entirely now, and what condensing still takes
   is only what is not a control: the tagline, the newsletter link, and a few
   pixels of padding either side. */
body.is-scrolled .site-header { background: rgba(239, 237, 228, .94); }
body.is-scrolled .site-header__inner { min-height: 46px; }
body.is-scrolled .brand__tag { display: none; }
body.is-scrolled .brand__mark { width: 30px; height: 30px; }
body.is-scrolled .brand__name { font-size: 18px; }
/* Both are asides, and neither is worth a row of somebody's screen while they
   are reading a list. The tabs, the town and Sign in all stay. */
body.is-scrolled .news-link { display: none; }
body.is-scrolled .header-slogan { display: none; }
body.is-scrolled .section-nav__bar { padding-top: 2px; padding-bottom: 2px; }
body.is-scrolled .site-nav a { padding-top: 6px; padding-bottom: 6px; min-height: 34px; }
.site-header__inner, .brand__mark, .brand__name {
  transition: min-height .16s, width .16s, height .16s, font-size .16s;
}

@media (prefers-reduced-motion: reduce) {
  .site-header__inner, .brand__mark, .brand__name { transition: none; }
}
.site-header__inner {
  display: flex; align-items: center; gap: var(--sp-4);
  min-height: 60px; max-width: var(--maxw-wide);
  margin: 0 auto; padding: 0 var(--sp-4);
  /* Wrap, so the tools drop below the wordmark instead of pushing the document
     sideways. Without this the brand plus Sign in plus the town button came to
     more than a 390px phone and every page scrolled horizontally. */
  flex-wrap: wrap;
}
/* min-width:0 lets the lockup shrink inside the flex row rather than forcing
   its natural width; the tagline truncates instead of overflowing. */
.brand { display: inline-flex; align-items: center; gap: 10px; color: var(--ink); min-width: 0; }
.brand__lockup { min-width: 0; }
.brand:hover { text-decoration: none; }
.brand__mark { width: 40px; height: 40px; flex: none; }
.brand__name { font-family: var(--font-display); font-weight: 600; font-size: 20px; letter-spacing: -.01em; }
.brand__name b { color: var(--accent); font-weight: 600; }
/* Wordmark + tagline stack, so the header stays one line tall. */
.brand__lockup { display: flex; flex-direction: column; justify-content: center; line-height: 1.1; }
.brand__tag {
  font-family: var(--font-body); font-size: 10px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .07em; color: var(--ink-faint);
  margin-top: 1px;
}

/* BIGGER LOCKUP ON A REAL SCREEN. The 40px mark above is sized for a 390px
   phone, where the brand shares the row with Sign in and the town button. On a
   laptop that row has room to spare and the logo read as an afterthought, so
   the mark, the wordmark and the tagline all step up together and the row grows
   to match. Phones keep the old numbers.

   The is-scrolled rules are repeated here on purpose: they are set further up
   the file at the same specificity, so the shrunk-header sizes have to be
   restated inside the query or the header would jump back to 30px the moment
   somebody scrolled. */
@media (min-width: 760px) {
  .site-header__inner { min-height: 72px; }
  .brand { gap: 12px; }
  .brand__mark { width: 54px; height: 54px; }
  .brand__name { font-size: 24px; }
  .brand__tag { font-size: 11px; }

  body.is-scrolled .site-header__inner { min-height: 52px; }
  body.is-scrolled .brand__mark { width: 38px; height: 38px; }
  body.is-scrolled .brand__name { font-size: 20px; }
}

.site-nav { margin-left: auto; display: flex; gap: var(--sp-1); }
.site-nav a {
  color: var(--ink-soft); font-weight: 600; font-size: 15px;
  padding: 8px 12px; border-radius: var(--radius-sm);
  min-height: 40px; display: inline-flex; align-items: center;
}
.site-nav a:hover { background: var(--surface-2); color: var(--ink); text-decoration: none; }
.site-nav a[aria-current="page"] { color: var(--primary-ink); background: var(--primary-tint); }

/* ---- Intro / hero-lite ---------------------------------------------------
   A two-column grid: heading and subtitle on the left, the Filters button
   pinned top-right in the space the heading was never using. The filter panel
   is a third row spanning the full width, so opening it pushes the results
   down only while it is open.

   The button is injected here by NL.filterShell({intro:true}); `display:
   contents` on the injected .filterbar lets its two children take part in
   THIS grid directly, which is what allows the button and its panel to sit in
   different rows without a wrapper boxing them together. */
.intro {
  padding: var(--sp-6) 0 var(--sp-4);
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: start; column-gap: var(--sp-5);
}
.intro h1, .intro p { grid-column: 1; }
/* THE FLOOR IS WHAT DECIDES WHETHER THE HEADING WRAPS.

   It was 28px, then 23px so the longest heading would hold one line beside the
   Filters button. That constraint is gone — the controls have their own row
   now — but a new one took its place: "What's happening near you" is 343px at
   23px type, which fits a 375px phone exactly and overflows a 320px one, and
   320px is what an iPhone with Display Zoom turned on reports.

   The owner, twice: "I don't want that text to wrap and cover two lines. I
   definitely don't want that."

   The smaller floor lives in the phone block near the top of this file rather
   than here, so a tablet keeps the type it has always had. */
.intro h1 { font-size: clamp(23px, 6vw, 40px); letter-spacing: -.02em; }
.intro p { color: var(--ink-soft); margin: var(--sp-3) 0 0; font-size: 17px; }

/* The promise, folded into the subtitle rather than banner-ed above it. */
.intro__no-feed { color: var(--ink-faint); font-weight: 600; white-space: nowrap; }

.intro .filterbar { display: contents; }
.intro .filterbar__btn { grid-column: 2; grid-row: 1; justify-self: end; }
.intro .filterbar__panel { grid-column: 1 / -1; margin-top: var(--sp-4); }

/* ONE COLUMN ON PHONES. This block used to keep the two columns here too, so
   the controls could sit beside the heading and save a row — "that is where
   getting the results above the fold matters most". It ended up doing the
   exact opposite, and badly.

   The right-hand column is sized `auto`, and the rules below only ever placed
   `.filterbar__btn`. When the button was wrapped in a `.filterbar__row`
   alongside the town picker, that row auto-placed into the auto column and
   asked for its intrinsic width: 291px of a 343px line. Column one was left
   with THIRTY-NINE PIXELS. The heading fell into a four-line vertical ribbon,
   the subtitle into eight lines, and the two of them took 317px of height to
   say two short sentences — pushing the first event 865px down, a full screen
   and a bit, on a 375x812 phone.

   Reported by the owner from their own iPhone with a screenshot, 31 Aug 2026,
   as "the text is wrapped, that's obviously very bad" and "that text and the
   filters take over half the page view on my phone". Measured before and
   after rather than eyeballed.

   So the controls get their own row here and the words get the whole width.
   That costs one row and saves about 200px, which is the trade the original
   comment thought it was making. */
@media (max-width: 620px) {
  .intro {
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--sp-3);
    /* 8px at the foot rather than 16. With the subtitle gone this padding sits
       between the filter row and the first day heading and does nothing but
       separate two things that are already separated. */
    padding: var(--sp-5) 0 var(--sp-2);
  }
  /* AND THE FIRST DAY GROUP STOPS PAYING FOR A SEPARATION IT DOES NOT NEED.

     `.day-group` carries 32px on top to hold Today apart from the events of
     the day before it. The FIRST one has nothing above it but the filters, so
     that 32 plus the intro's own padding left a 48px band of empty page
     between the controls and the first result — visible in a screenshot in a
     way it never was in a measurement, and exactly the "stuff up top" this
     pass is about. Only the first; the rest keep their gap. */
  #results > .day-group:first-child,
  .day-group:first-child { margin-top: var(--sp-3); }

  /* THE HEADING STOPS WRAPPING, WHICH IS THE WHOLE POINT OF THIS BLOCK.

     The floor on `.intro h1` is 23px. "What's happening near you" measures
     343px at 23px type: exactly a 375px phone, and over a 320px one — and 320
     is what an iPhone with Display Zoom reports. So it held one line in
     testing and broke onto two on the owner's actual phone. Twice: "I don't
     want that text to wrap and cover two lines. I definitely don't want that."

     An 18px floor lets the vw term carry it down. Measured with a Range rather
     than scrollWidth, which reports the box and not the glyphs and had been
     telling me there was no room to spare when there were 58 pixels:

       320px   18.2px type, 230px of text in 288  (58px spare)
       375px   21.4px type, 269px of text in 343  (74px spare)

     Scoped here so a tablet and a desktop keep the type they had. */
  .intro h1 { font-size: clamp(18px, 5.7vw, 40px); }
  /* Everything in one column, including the row wrapper that has no explicit
     placement of its own. Left-aligned: it reads as a control belonging to the
     list below it rather than as decoration floated off the heading. */
  /* `stretch`, NOT `start`, AND THAT ONE WORD WAS A VISIBLE BUG.

     `start` shrink-wraps the row to its two buttons. Unpinned that is
     invisible, because the background is transparent. Pinned it is not: the
     row becomes `position: fixed` with `left: 0; right: 0` and a full-bleed
     background, and a browser applies grid self-alignment to an
     absolutely-positioned grid child, so `start` beat the offsets and sized it
     to content anyway. Measured at 375px: 323px of bar in a 375px window, a
     52px strip of bare page at the right end of an otherwise solid bar.

     Reported on scrolling: "the bar with the filters is only half made, the
     bar is cut in half."

     So the ROW fills the line and `justify-content` decides where the buttons
     sit inside it, which is the right division of labour and the one the
     original `space-between` was already using. */
  .intro .filterbar__row {
    grid-column: 1; justify-self: stretch; justify-content: flex-start;
    /* NOWRAP IS WHAT MAKES THE SHRINKING BELOW HAPPEN AT ALL. While this row
       could wrap, flex had a cheaper way to fit three controls than making any
       of them smaller: put one on a second line. Every min-width:0 and
       text-overflow below was correct and did nothing, because nothing was
       ever asked to shrink. */
    flex-wrap: nowrap;
  }
  /* Pinned, they stay exactly where they were unpinned. The rule further down
     spreads them apart at this width, which would slide the town button to the
     far edge at the moment somebody scrolls — and the note beside that rule is
     itself about not moving these two when they pin. */
  .intro .filterbar__row.is-pinned { justify-content: flex-start; }

  /* THE VIEW TOGGLE, SHARING THIS ROW. Pushed to the right end so the two
     controls that narrow the list stay together on the left and the one that
     changes its shape sits apart from them.

     ICON ONLY HERE, and only here. The filter button and the town picker
     already ask for 213px of a 343px line; the toggle with its words is 161px
     more and the row would break. Without them it is 76px and everything fits
     with room to spare. `aria-pressed` on each button and the group's own
     aria-label carry it for a screen reader, which the words never did
     anything for anyway. */
  .filterbar__view { margin-left: auto; flex: none; }
  .filterbar__view .toggle__lbl { display: none; }
  .filterbar__view .toggle button { padding: 8px 12px; min-height: 38px; }
  /* NOTHING TO DO FOR THE RESET AT THIS WIDTH ANY MORE. It used to lose its
     word here, the way the toggle above loses its own; it has no word now and
     no place in the row, so the corner it is pinned to is the same corner at
     every size. */

  /* THE TOWN BUTTON GIVES WAY, because it is the only one of the three that
     can. Filters is 59px and the toggle 88px, both fixed; the town button is
     as wide as the town is long, and "All of Fairfield County" comes to 276.
     59 + 276 + 88 + gaps is 423 of a 343px line, so the row broke in two and
     the saving went with it.

     Allowed to shrink with an ellipsis rather than capped at a number: a real
     town name like Norwalk is 145px and the three sit on one line untouched,
     and only the long county-wide label loses its tail. The full name is on
     the button's own title attribute and in the menu it opens. */
  /* "Fairfield County", not "All of Fairfield County". The preamble is the only
     part of that label carrying nothing: the pin beside it says this is where,
     and a county with no town in front of it already means all of it. Asked
     for directly, and it is the difference between the three controls sharing
     a line comfortably and only just. */
  /* Two classes, because the plain defaults for these live ~800 lines BELOW
     this block and a media query adds no specificity of its own. Fourth time
     in this file today. */
  .city-btn .city-btn__full { display: none; }
  .city-btn .city-btn__short { display: inline; }

  /* THE TOWN MENU HANGS OFF THE LEFT OF THE SCREEN, AND THE ANCHOR IS WHY.

     `.city-menu` is `right: 0` against `.city-select`, which was right for the
     old header where that button sat at the right-hand end of the bar: the
     menu grew leftwards into the page. In the filter row the button is in the
     MIDDLE, so a 270px menu hung off the left edge — 7px at 375, and about 50
     on a phone reporting 320. The owner's screenshot shows "Bridgeport" with
     its B cut off.

     Fixed by moving the anchor, not by nudging the offset. `.city-select` goes
     static so the row becomes the menu's containing block, and the menu opens
     from the row's left edge, which is the page gutter at every width. When
     the bar is pinned the row is `position: fixed` and therefore still the
     containing block, so the menu follows it up the screen. */
  .intro .filterbar__row { position: relative; }
  .intro .filterbar__row .city-select { position: static; }
  .intro .filterbar__row .city-menu {
    left: 0; right: auto; max-width: calc(100vw - 32px);
  }

  /* ---- THE TOWN PILL OVERFLOWED ITS OWN WRAPPER AND SAT ON THE BADGE -----

     REPORTED, with a photograph: the filter chip and the town picker
     overlapping, the "2" on the chip half-hidden behind the white pill.

     THE THREE RULES BELOW WERE ALREADY HERE AND WERE ALREADY RIGHT. What
     defeated them is one element nobody had constrained: the markup is

         .filterbar__town  >  .city-select  >  .city-btn

     and `.city-select` is a plain block with `min-width: auto`. So the town
     WRAPPER shrank as intended, the button's `max-width: 100%` resolved
     against the middle element rather than against the wrapper, and
     `.city-select` simply kept its intrinsic width. Measured at 375px with a
     filter on: the wrapper was 155px starting at x=108, and the pill inside it
     was 183.7px starting at x=79.3 - twenty-nine pixels of white pill painted
     back over the badge at 68-88.

     Which is why nothing looked wrong until a filter was set. With no count on
     the chip the row needs 343px of 343 and every box is exactly where it
     should be; the badge adds twenty-five and there is nowhere for them to go.

     `min-width: 0` is the fix and `max-width: 100%` is the belt to it. Flex is
     asked to shrink a chain, and every link in that chain has to agree. */
  .intro .filterbar__row .filterbar__town { min-width: 0; flex: 0 1 auto; }
  .intro .filterbar__row .filterbar__town .city-select {
    display: flex; min-width: 0; max-width: 100%;
  }
  .intro .filterbar__row .filterbar__town .city-btn { max-width: 100%; min-width: 0; }
  .intro .filterbar__row .filterbar__town .city-btn__label {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
  }

  /* ---- AND SMALLER TYPE, SO IT RARELY COMES TO TRUNCATING ----------------

     ASKED FOR: "my recommendation is perhaps making the font smaller on both
     buttons, especially the location selector button, that way you have more
     space." Right on both counts, and it is worth being clear about which
     problem each half solves.

     The rule above makes the overlap IMPOSSIBLE: past a certain narrowness the
     label ellipsises instead, which is the correct failure and the reason the
     full name lives on the button's title and in the menu it opens. This half
     is what stops that happening at ordinary widths.

     MEASURED RATHER THAN CHOSEN. At 375px with a filter on, the row wants
     372px of the 343 it has. Type alone got it to 357 - not enough on its own,
     which is why the padding and the icon gaps come in with it. At 13px, 9px
     and 5px the row wants 344, so the label gives up a pixel; at 8px padding
     it wants 340 and nothing truncates at all.

     8px is the choice, and it keeps the 44px tap target: the height is set by
     min-height on both controls, so narrowing the sides costs nothing a thumb
     can feel.

     THE TOWN PICKER GOES ONE STEP SMALLER THAN THE FILTER CHIP, as asked. It
     is the longest label on the row and the only one that can truncate, so a
     pixel spent here buys more than a pixel spent anywhere else. */
  .intro .filterbar__row .filterbar__btn {
    font-size: 13.5px; padding-left: 8px; padding-right: 8px; gap: 5px;
  }
  .intro .filterbar__row .filterbar__town .city-btn {
    font-size: 13px; padding-left: 8px; padding-right: 8px; gap: 5px;
  }
  /* The badge shrinks with the type it sits in. It is the one thing on the
     chip that must stay legible, so it loses a pixel of diameter and none of
     its weight. */
  .intro .filterbar__row .filterbar__n {
    min-width: 18px; height: 18px; padding: 0 5px; font-size: 11.5px;
  }
  .intro .filterbar__btn { grid-column: 1; grid-row: auto; justify-self: start; }
  .intro .filterbar__panel { grid-column: 1; }
  /* 44, matching the town button it now sits beside. It was 40 to save a few
     pixels of height, from before the two controls shared a row — so on a
     phone the pair were visibly different sizes, which reads as one of them
     being the lesser control. Height only; the width is untouched, so the
     heading beside it still keeps its two columns. */
  .intro .filterbar__btn { padding: 8px 12px; min-height: 44px; }
}
/* Small phones: the word drops, the icon stays. Anything wider than the icon
   pushes the longest heading ("Classes & Workshops") onto a second line, which
   costs more height than the whole row this move saved. The count badge stays
   — it is the part that says a filter is on. */
@media (max-width: 480px) {
  .intro .filterbar__btn > span:not(.filterbar__n) { display: none; }
  .intro .filterbar__btn { gap: 5px; padding: 8px 11px; }
}

/* ---- "How it is going", on the Today page --------------------------------

   The Numbers tab's own tiles, a chosen few of them, on the admin front door.
   Everything here reuses .mtiles/.mtile so the two pages look like one
   product; only the two-column split for neighbours and organizations is new,
   and it is new because Today is a narrower column than the Numbers page.

   THE HEADINGS EARN THEIR SPACE by naming three different questions -
   visiting, signing up, using it - which is the difference between a card
   somebody reads and a row of numbers they skim past. */
/* ---- THREE BANDS, NOT ONE LIST -------------------------------------------

   The three groups used to be separated by a margin and nothing else, and
   their headings were the same size, weight and colour as the NEIGHBOURS and
   ORGANIZATIONS labels nested INSIDE one of them. Four identical headings in
   one column with no rule anywhere: "it all feels like it is all the same
   story, same stats."

   A rule and real space give the eye the seams; the darker heading puts a band
   title above the labels it contains. The caption under each heading is doing
   the actual work - see grp() in admin-hub.js. */
.hstat__grp { margin-top: var(--sp-5); padding-top: var(--sp-4);
  border-top: 1px solid var(--line); }
.hstat__grp:first-of-type { margin-top: var(--sp-3); padding-top: 0; border-top: 0; }
/* --ink-soft against the --ink-faint of the labels inside the band: one step
   of contrast is all a heading needs to outrank the ones it contains, and more
   would make three quiet section titles shout over the figures. */
.hstat__grp > .mhead { color: var(--ink-soft); margin-bottom: 2px; }
.hstat__cap { font-size: 12.5px; color: var(--ink-faint); margin: 0 0 var(--sp-3); }
/* `.hstat .mhead { margin-top: var(--sp-4) }` used to live here and is gone.
   It was the ONLY thing separating the groups, and every heading it applied to
   now sits inside a band that owns its own spacing - so it survived only to
   add a fourth gap under the new rule. */
.hstat__split { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-4); }
.hstat__col .mhead { margin-top: 0; font-size: 13px; }
/* Two tiles in each half, and they must not wrap to one per line while there
   is room for both - that is what turns a compact card into a tall one. */
.hstat__col .mtiles { grid-template-columns: 1fr 1fr; }
@media (max-width: 760px) {
  /* One question per row before the tiles inside them start squeezing. */
  .hstat__split { grid-template-columns: 1fr; gap: var(--sp-3); }
}

/* ---- Controls (search + filters + view toggle) --------------------------- */
.controls { position: relative; z-index: 5; }

/* ---- SEARCH BEHIND A BUTTON, ON A PHONE ONLY -----------------------------

   "Hide the search bar for the mobile versions, because it takes a lot of
   space and it takes longer for the user to see the very first result. I do
   prefer that they use the filters. However some users like the search. So
   maybe this goes to the very top right, next to Sips & Bites, kind of like a
   loupe button that when the user clicks, the search bar appears."

   The bar was 55px plus its margins above the results, on every one of the
   four pages that has one. Behind a button it costs nothing until it is
   wanted, and the button sits in the empty right-hand end of the tab row,
   which has been spare space since that row went down to two tabs.

   `body:has(.search--inline)` IS THE WHOLE GATE. The button is drawn into the
   header on every page, and the header knows nothing about which pages have a
   search bar. Rather than tell it, the button simply does not appear unless
   there is something for it to reveal, which keeps the knowledge in one place
   and cannot go stale when a fifth page grows a search box.

   Desktop is untouched at every step: no button, bar always visible. */
.nav-search { display: none; }
@media (max-width: 620px) {
  body:has(.search--inline) .nav-search {
    display: inline-flex; align-items: center; justify-content: center;
    flex: none; margin-left: auto; width: 44px; height: 44px;
    border: 0; background: none; cursor: pointer;
    color: var(--ink-soft); border-radius: var(--radius-pill);
  }
  body:has(.search--inline) .nav-search:hover { color: var(--ink); background: var(--surface-2); }
  body:has(.search--inline) .nav-search[aria-expanded="true"] {
    color: var(--primary); background: var(--primary-tint);
  }
  /* Hidden rather than removed, so the page keeps rendering it and the value
     inside it survives a filter change.

     `.search.search--inline`, BOTH CLASSES, for weight. This block sits above
     the base `.search { display: flex }` rule, and a media query adds no
     specificity, so a single-class selector here loses to the plain later one
     and the bar never hid at all. The same trap caught the stock tiles an hour
     earlier in this file. */
  .search.search--inline { display: none; }
  body.search-open .search.search--inline { display: flex; }
}

.search {
  position: relative; display: flex; align-items: center;
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--radius); box-shadow: var(--shadow);
  transition: border-color .15s, box-shadow .15s;
}
/* Inline placement: sits just above each page's results, so it needs its own gap. */
.search--inline { margin: var(--sp-4) 0; }

/* ---- THE SEARCH BOX IN THE TAB ROW (owner, 17 Sep 2026) ------------------

   "Let's move the search bar up here, on the right on this tab bar. This will
   give us more space overall when we bring in the advertisement tile."

   The element is the page's own `.search--inline`, moved here by
   NL.hoistSearch after the page draws it, so everything below is about making
   a 52px page control sit in a 44px row: no margins, a shorter field, and
   type one step down. Nothing about what it searches changes.

   COLLAPSED WHEN EMPTY, which is every page that does not hoist and every
   phone. `:empty` rather than a class, so the row spends no gap on a slot with
   nothing in it and no page has to say anything about itself.

   IT SHRINKS BEFORE THE TABS DO. `flex: 0 1 380px` against the tabs'
   `flex: 1` means the box gives up width first as the window narrows, down to
   its own min-width, and the two tabs - the thing people came to press - are
   never squeezed to make room for a search field. */
.nav-search-host { flex: 0 1 380px; min-width: 0; }
.nav-search-host:empty { display: none; }
.nav-search-host .search--inline { margin: 0; }
.nav-search-host .search { box-shadow: none; }
.nav-search-host .search input {
  padding: 8px 40px; min-height: 38px; font-size: 14.5px;
}
.nav-search-host .search__icon { left: 12px; }
.nav-search-host .search__icon svg { width: 17px; height: 17px; }
.nav-search-host .search__clear { right: 5px; }
/* A tablet has the tabs, the box and nothing else to spare - see the note
   above about which of the two gives way. */
@media (max-width: 860px) {
  .nav-search-host { flex: 0 1 260px; }
}
/* A PHONE: A LINE OF ITS OWN, AND ONLY WHILE IT IS OPEN.

   The box is moved up here at every width (NL.hoistSearch knows nothing about
   the screen), so this is where the phone's arrangement is decided. The loupe
   in this same row is what opens it - see "SEARCH BEHIND A BUTTON" above - and
   while it is shut the slot is taken out of the row altogether, so a closed
   search costs a phone nothing at all, not even a flex gap.

   Open, it wraps onto a full-width second line under the tabs, immediately
   below the button that revealed it, and at the full 44px touch height rather
   than the desktop row's 38px. It is inside the sticky header, so it stays put
   while the results scroll - which is the right way round for a control the
   reader deliberately opened. */
@media (max-width: 620px) {
  .section-nav__bar { flex-wrap: wrap; }
  .nav-search-host { display: none; }
  body.search-open .nav-search-host {
    display: block; flex: 1 0 100%; order: 3; margin: 2px 0 4px;
  }
  body.search-open .nav-search-host .search input { min-height: 44px; font-size: 16px; }
}
.search:focus-within { border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-tint); }
.search__icon { position: absolute; left: 14px; color: var(--ink-faint); pointer-events: none; }
.search input {
  flex: 1; border: 0; background: transparent; outline: none;
  font: inherit; color: var(--ink); padding: 14px 44px 14px 44px; min-height: 52px;
}
.search input::placeholder { color: var(--ink-hint); }
.search__clear {
  position: absolute; right: 8px; border: 0; background: transparent;
  color: var(--ink-faint); width: 36px; height: 36px; border-radius: var(--radius-pill);
  display: none; align-items: center; justify-content: center;
}
.search__clear:hover { background: var(--surface-2); color: var(--ink); }
.search.has-value .search__clear { display: inline-flex; }

/* Collapsed filter shell ---------------------------------------------------
   Every section used to open with two or three rows of chips, so the first
   real result sat below the fold. The filters now live behind one button and
   the list starts at the top. The button carries a count so a filter can
   never be quietly in effect while the panel is shut. */
.filterbar { margin: 0 0 var(--sp-4); }
/* LOUD ENOUGH TO BE FOUND, QUIET ENOUGH TO IGNORE.

   Reported by the owner about their own site: "even I often forget that I have
   set one a certain way or forget about the ability to filter." Two different
   misses, and they want different answers.

   FINDING IT: the button was a pale outline in --ink-soft on a cream page,
   which reads as a label rather than as something you press. It keeps the same
   footprint — no bigger, no colour block — and gains the three cues that say
   "control": the icon in navy (the town button beside it already did this,
   which is why that one was the easier of the two to see), full-strength ink
   for the word, and a shadow that lifts it off the page instead of lying flat
   in it. Weight goes to 650, not 700: bolder than the body text around it,
   short of shouting.

   Deliberately NOT done: a filled navy button, a bigger hit area, an animation.
   This sits above a list of what is on in town, and the list is the page. */
.filterbar__btn {
  display: inline-flex; align-items: center; gap: 8px;
  border: 1px solid var(--line-strong); background: var(--surface);
  color: var(--ink); font-weight: 650; font-size: 14.5px;
  padding: 9px 15px; min-height: 44px; border-radius: var(--radius-pill);
  box-shadow: var(--shadow);
  transition: background .15s, color .15s, border-color .15s, box-shadow .15s;
}
.filterbar__btn > svg:first-child { color: var(--primary); }
.filterbar__btn:hover {
  background: var(--surface-2); border-color: var(--ink-faint);
  box-shadow: var(--shadow-hover);
}
/* SOMETHING IS IN EFFECT — the other half of what was reported, and the half
   worth more. A filter that is quietly on looks exactly like one that is off,
   and the reader blames the town for having nothing on. Navy ground, navy
   border, and the count already sitting in .filterbar__n. */
.filterbar__btn.has-active {
  border-color: var(--primary); color: var(--primary-ink); background: var(--primary-tint);
  font-weight: 700;
}
.filterbar__btn.has-active > svg:first-child { color: var(--primary-ink); }
.filterbar__btn.has-active:hover {
  background: var(--primary-tint-2); border-color: var(--primary);
}
/* THE WAY BACK, AND IT IS MEANT TO BE QUIET.

   Asked for as "a very discrete reset button", then placed exactly: "put the
   reset icon on the top left of the Filters button and no need to spell out
   reset, just leave the icon there."

   So it is a glyph on a corner, and everything about it is smaller and fainter
   than the pill it sits on: no shadow, muted ink, a hairline ring only so it
   reads as a control rather than as decoration on the page behind it. Undoing
   is not what anybody came here to do.

   PINNED, NOT IN THE ROW. Absolute against .filterbar__row, whose first child
   is the Filters button — so the row's top-left corner IS the button's. Taking
   it out of the flow is what makes it survivable on a phone, where the row is
   `nowrap` and the two pills already want 213px of a 343px line: a control with
   no width can always be there, at every size, without the word.

   It is only in the markup when something is actually non-default (see
   resetHTML), so there is no disabled state to style — and nothing sits on that
   corner on a page nobody has filtered. */
.filterbar__row { position: relative; }
/* THE BOX THE PIN IS MEASURED FROM. It wraps the Filters button and nothing
   else, so its corner is the button's corner at every width and in both
   states - including pinned, where the row itself stretches to the window and
   used to take the glyph out to the screen's edge with it. */
.filterbar__filters { position: relative; flex: none; display: inline-flex; }
.filterbar__reset {
  position: absolute; top: -9px; left: -9px; z-index: 2;
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; padding: 0;
  border: 1px solid var(--line); border-radius: 50%;
  background: var(--surface); box-shadow: none;
  color: var(--ink-faint); cursor: pointer;
  transition: color .15s, background .15s, border-color .15s;
}
.filterbar__reset:hover {
  color: var(--ink); background: var(--surface-2); border-color: var(--ink-faint);
}
.filterbar__reset:focus-visible {
  color: var(--ink); background: var(--surface-2); border-color: var(--ink-faint);
}

.filterbar__n {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 20px; height: 20px; padding: 0 6px; border-radius: var(--radius-pill);
  background: var(--primary); color: var(--on-primary); font-size: 12px; font-weight: 700;
}
.filterbar__caret { transition: transform .18s; }
.filterbar__btn[aria-expanded="true"] .filterbar__caret { transform: rotate(180deg); }
.filterbar__panel {
  margin-top: var(--sp-3); display: grid; gap: var(--sp-4);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: var(--sp-4);
}
.filterbar__panel[hidden] { display: none; }

/* Filter groups */
.filters { margin-top: var(--sp-4); display: grid; gap: var(--sp-4); }
.filter-group { display: grid; gap: var(--sp-2); }
.filter-group__label {
  font-size: 12px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .07em; color: var(--ink-faint);
}
/* ---- TWO COLUMNS WHEREVER TWO WILL FIT (owner, 12 Sep 2026) -------------

   ASKED FOR: "this looks very busy being just one long column instead of two
   where... wherever we can squeeze two side by side."

   FLEX-WRAP COULD NOT DO IT. Pills sized to their own text wrap when the next
   one does not fit, so a column of long labels - "Nightlife & Karaoke",
   "Holiday & Seasonal" - put one chip per row with a ragged half-empty gutter
   beside it, while two short ones happened to pair up. The result reads as a
   list that cannot make its mind up.

   A GRID DECIDES IT ONCE. auto-fit with a minimum means the browser fits as
   many equal columns as the width allows: two on a phone, five or six in the
   filter panel on a laptop, and never a row with one chip and a hole. The
   chips stretch to their column, which is what squares the right-hand edge.

   THE MINIMUM IS SET BY THE LONGEST LABEL, not by how many columns look nice.
   Too small and the browser fits another column by making every chip too
   narrow for its own text - at 148px a laptop took six columns and half the
   names wrapped to two lines, which is a worse kind of busy than the one this
   replaced. 172px holds "Older Adults (65+)" and "Nightlife & Karaoke" on one
   line at the full type size, and the phone query below drops both the minimum
   and the type together so two columns still fit at 375px. */
.chips {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(172px, 1fr));
  gap: var(--sp-2); align-items: stretch;
}
/* The label takes the room and the count sits at the end, so a column of
   stretched chips lines its numbers up instead of scattering them. */
/* LEFT, INCLUDING THE ONES THAT WRAP. A stretched chip whose label runs to two
   lines centres that text by default, so "Older Adults (65+)" sat centred in a
   column of left-aligned neighbours and the grid stopped looking like a grid.
   One rule fixes both states. */
.chips .chip { justify-content: flex-start; min-width: 0; text-align: left; }
.chips .chip .chip__count { margin-left: auto; flex: none; }
.chip {
  border: 1px solid var(--line-strong); background: var(--surface);
  color: var(--ink-soft); font-weight: 600; font-size: 14px;
  padding: 9px 14px; min-height: 40px; border-radius: var(--radius-pill);
  display: inline-flex; align-items: center; gap: 7px;
  transition: background .15s, color .15s, border-color .15s, transform .05s;
}
.chip:hover { border-color: var(--line-strong); background: var(--surface-2); }
.chip:active { transform: scale(.97); }
.chip[aria-pressed="true"] {
  background: var(--primary); border-color: var(--primary); color: var(--on-primary);
}
.chip[aria-pressed="true"]:hover { background: var(--primary-ink); }
.chip__dot { width: 9px; height: 9px; border-radius: 50%; flex: none; background: var(--c, var(--ink-faint)); }
.chip[aria-pressed="true"] .chip__dot { background: rgba(255,255,255,.9); }

/* Toolbar row: result count + view toggle */
.toolbar {
  margin-top: var(--sp-5); display: flex; align-items: center; gap: var(--sp-3);
  flex-wrap: wrap; border-top: 1px solid var(--line); padding-top: var(--sp-4);
}
.result-count { font-size: 14px; color: var(--ink-faint); font-weight: 600; }
.result-count strong { color: var(--ink); }
.toolbar__spacer { flex: 1; }
.link-btn {
  border: 0; background: transparent; color: var(--primary); font-weight: 600;
  font-size: 14px; padding: 8px 10px; border-radius: var(--radius-sm); min-height: 40px;
}
.link-btn:hover { background: var(--primary-tint); }

.toggle {
  display: inline-flex; background: var(--surface-2); border-radius: var(--radius-pill);
  padding: 3px; border: 1px solid var(--line);
}
.toggle button {
  border: 0; background: transparent; color: var(--ink-soft); font-weight: 600;
  font-size: 14px; padding: 7px 14px; min-height: 38px; border-radius: var(--radius-pill);
  display: inline-flex; align-items: center; gap: 7px; transition: background .15s, color .15s;
}
.toggle button[aria-pressed="true"] { background: var(--surface); color: var(--ink); box-shadow: var(--shadow); }

/* ONE ROW ON A PHONE, AND THE COUNT IS WHAT PAYS FOR IT.

   Asked for: "remove the count from the two tabs, and better utilise the
   spacing by bringing up the List and Calendar buttons to where this text was.
   We may need those buttons a bit smaller so it all fits on that same line."

   The toolbar was three rows and 115px tall on a 375px screen: the count and
   the per-page picker, then the view toggle underneath. Measured with the
   count gone, the remaining two want 351px of a 343px line — over by eight
   pixels, which is why they would not sit together and why the owner's guess
   that the buttons needed trimming was right.

   Ten pixels of horizontal padding instead of fourteen takes 16px off the
   toggle. Nothing else shrinks: the font, the height and the tap target are
   untouched, because these are the two controls that switch the whole view.

   `order: -1` puts the toggle first so it lands where the count used to be,
   on the left, and the spacer pushes the per-page picker to the right edge.

   THE COUNT IS HIDDEN THE ACCESSIBLE WAY, clipped rather than `display: none`,
   which is the same trick `.report-fab__lbl` uses further down. It carries
   `aria-live="polite"` and is the only thing that announces "136 things to do"
   when a filter changes; `display: none` would take it out of the
   accessibility tree and somebody filtering by ear would get silence. Clipped,
   it takes no space and still speaks. Still visible at desktop widths, where
   there is room for it. */
@media (max-width: 620px) {
  .toolbar {
    /* The spacer goes below, so the two controls are pushed apart by the
       container instead of by an empty flex item that costs its own gap. */
    justify-content: space-between;
  }
  .toolbar .result-count {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
  }
  /* AND THE SPACER GOES. With the count clipped out of flow, the row held the
     toggle, this empty div and the per-page picker: three items, so TWO 12px
     gaps. 172 + 151 + 24 came to 347 in a 343px line and they stayed on
     separate rows over four pixels. `space-between` does the same job for
     nothing. */
  .toolbar .toolbar__spacer { display: none; }
  .toolbar .toggle { order: -1; }

  /* ---- THE TOOLBAR ROW GOES TO THE BOTTOM ---------------------------------

     "There's some space there that I think is unnecessary... the page count
     could be all the way at the bottom and share space with the buttons to go
     to the next pages. And the List and Calendar buttons could be moved
     somewhere else, and the results would show up sooner."

     Both halves of that. The toggle leaves for the filter row (see
     NL.placeViewToggle), and what is left — the per-page picker — moves below
     the results, where the pager already is. The row between the filters and
     the first card is gone.

     `:has(> .toolbar)` RATHER THAN NAMING THE CONTAINER. Four pages draw this
     bar into four differently-named roots, and `order` needs the PARENT to be
     a flex box. Matching the direct parent of any toolbar covers all of them
     and any page added later. The toolbar itself is the only child given an
     order, so it goes last whatever else is in there. */
  main :has(> .toolbar) { display: flex; flex-direction: column; }
  .toolbar {
    order: 2; margin-top: var(--sp-4);
    border-top: 0; padding-top: 0; justify-content: flex-end;
  }
  /* 8px of padding and 13.5px type, down from 14 and 14. `space-between` sets
     where these two sit; it does NOT remove the flex `gap`, which is a
     MINIMUM and still applies between them. That 12px is what kept the row
     broken after the arithmetic said it fitted: 284px of controls plus 12 is
     296 in a 288px line. Gap to 8 and the buttons a shade smaller leaves
     enough slack to stop this being decided by a rounding error. */
  .toolbar { gap: var(--sp-2); }
  .toolbar .toggle button { padding: 7px 8px; font-size: 13.5px; }

  /* AND "SHOW" GOES, which is what finally makes this hold on a small phone.
     The pair fitted at 375px and still wrapped for the owner, because a phone
     with Display Zoom turned on reports 320px: a 288px line for 323px of
     controls. Dropping the word takes the per-page picker from 151px to about
     111px and the row comes to ~283px, which fits. The select keeps its
     aria-label, so it still announces as "Items per page". */
  .perpage__show { display: none; }
}

/* THE SUBTITLE UNDER THE HEADING, GONE FROM THE TWO BROWSING TABS ON A PHONE.

   Asked for: "let's remove that text from the tabs in the mobile version."
   It is a sentence about what the page is, on the page itself, above the thing
   the reader came for. On a desktop it costs nothing; on a phone it was two
   lines and 53px between the heading and the events.

   SCOPED TO THE TWO TABS BY data-section, NOT to `.intro p` generally, and
   that matters: business.html and newsletter.html use this same block, and on
   newsletter.html the paragraph IS the pitch — the reason somebody would give
   their address. Hiding it there would be deleting the argument and keeping
   the form. directory.html has no paragraph to hide. */
@media (max-width: 620px) {
  body[data-section="things"] .intro p,
  body[data-section="dining"] .intro p { display: none; }

  /* AND THE HEADING ITSELF, ON THOSE TWO PAGES ONLY.

     "No need to show this text because the name of the tabs already implies
     what the page is about." True: the tab reading "Things to do" is lit and
     directly above a heading reading "What's happening near you", and "Sips &
     Bites" sits above a heading reading "Sips & Bites".

     CLIPPED, NOT `display: none`, AND THAT IS NOT A DETAIL. This is the page's
     <h1>. `display: none` takes it out of the accessibility tree, so a screen
     reader loses the one element that names the page, and it is the tag search
     engines lean on hardest for what a page is about — on the two pages this
     site most wants found. Clipped, it takes no space, still announces, and is
     still there to be indexed.

     Only these two. Every other page keeps a visible heading, because nothing
     else has a lit tab saying the same words. */
  body[data-section="things"] .intro h1,
  body[data-section="dining"] .intro h1 {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
  }
}

/* ---- Event list ---------------------------------------------------------- */
.day-group { margin-top: var(--sp-6); }
.day-head {
  display: flex; align-items: baseline; gap: 10px;
  padding-bottom: var(--sp-3); margin-bottom: var(--sp-3);
  border-bottom: 2px solid var(--line);
}
.day-head__weekday { font-family: var(--font-display); font-size: 20px; font-weight: 600; }
.day-head__date { font-size: 14px; color: var(--ink-faint); font-weight: 600; }
/* Today's heading is coloured rather than badged. `.day-head__badge` and its
   pill lived here and said "TODAY" beside a heading whose first word is
   already "Today" — NL.relativeDayLabel returns it for the current day. The
   colour is the whole signal now, and it is not the only one: the date sits
   right beside it. */
.day-head--today .day-head__weekday { color: var(--accent); }

.event-card {
  display: grid; grid-template-columns: 64px 1fr; gap: var(--sp-4);
  /* See `.event-card:has(.event-org)` below for the wider column an EVENT tile
     gets. This narrower default is what the two cards that carry something
     else on the left keep — the highlight's photo thumb and the promote
     preview's time column, neither of which has a logo to make room for. */
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: var(--sp-4);
  margin-bottom: var(--sp-3); box-shadow: var(--shadow);
  transition: box-shadow .15s, border-color .15s, transform .06s;
  color: inherit;
}
/* A WIDER LEFT COLUMN, AND ONLY WHERE THERE IS SOMETHING TO PUT IN IT.

   The organization's logo was 44px in a 64px column, which was as small as a
   logo can be and still be a logo — several of them are wordmarks and simply
   could not be read. It is bigger now, and the category glyphs sit under it, so
   the text starts further right and the left of the tile stops being a margin.

   `:has(.event-org)` rather than widening `.event-card` outright: the highlight
   mini-card and the promote preview share this chrome with a photo thumb and a
   time column in that slot, and neither of those wants 96px. Same test the
   cancelled-listing rules a few thousand lines below already use. */
.event-card:has(.event-org) { grid-template-columns: 76px 1fr; }
.event-card:hover { box-shadow: var(--shadow-hover); border-color: var(--line-strong); text-decoration: none; }
.event-card:active { transform: translateY(1px); }
.event-card:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }

.event-time {
  text-align: center; border-right: 1px solid var(--line); padding-right: var(--sp-3);
  display: flex; flex-direction: column; justify-content: center;
}
.event-time__t { font-family: var(--font-display); font-size: 17px; font-weight: 600; color: var(--ink); line-height: 1.1; }
.event-time__ampm { font-size: 12px; font-weight: 700; color: var(--ink-faint); text-transform: uppercase; letter-spacing: .04em; }
.event-time__all { font-size: 12px; font-weight: 700; color: var(--primary); text-transform: uppercase; letter-spacing: .03em; }
/* "Sep 2 @ 2:30 PM", on one line. The column was a stacked flex - a big time
   over a small meridiem - and the date now leads it, so it becomes a row that
   wraps only when it has to. Quieter than the time it introduces: the hour is
   what somebody is scanning for and the day is the qualifier. */
/* ONE PHRASE, ONE LINE. "Sep 2" and "2:30 PM" are one answer to one question,
   and split over two rows they read as two facts stacked up. `nowrap` keeps
   them together and the column takes the width it needs - measured at about
   100px once the time drops from 17px to 15px, against 67px before, so it
   costs a real title roughly a word. */
.event-time {
  flex-direction: row; align-items: baseline; gap: 4px;
  flex-wrap: nowrap; white-space: nowrap;
}
.event-time .event-time__t { font-size: 15px; }
.event-time__day {
  font-size: 12.5px; font-weight: 700; color: var(--ink-soft);
  letter-spacing: .02em; white-space: nowrap;
}
.event-time__at { font-size: 12px; color: var(--ink-faint); }

.event-body { min-width: 0; }
.event-title { font-family: var(--font-display); font-size: 18.5px; font-weight: 600; letter-spacing: -.01em; color: var(--ink); }
.event-card:hover .event-title { color: var(--primary-ink); }
/* THE PIN STAYS ON THE VENUE'S LINE (owner, 10 Sep 2026, from the app on
   their phone). This was a wrapping flex row, so when a long venue did not
   fit beside the pin the whole name dropped to the next line and the pin was
   left alone above it. The line is ordinary text now: the pin is an inline
   mark before the first word, and a long name wraps within its own words,
   as a sentence does. */
/* WHERE IT IS, AT FULL TEXT COLOUR. Owner, 14 Sep 2026: "the venue and
   location text can be of normal text color. i just want to try to hide a bit
   the description text."

   It was --ink-soft, one step down, back when the blurb below it was the same
   tone and the pair receded together. The blurb has since gone quieter still
   (--ink-faint, one line), so matching it here would have pushed the WHERE of
   a listing into the background alongside the write-up - and the venue is one
   of the three things somebody scans a tile for, with the title and the time.
   The description is the only thing meant to recede. */
.event-meta { margin-top: 4px; font-size: 14px; color: var(--ink); line-height: 1.45; }
.event-meta > svg { color: var(--ink-faint); vertical-align: -2px; margin-right: 6px; }
/* ON A PHONE THE TILE IS TITLE, TIME AND VENUE; the write-up is on the event
   page. Inside the app (`.in-app`, set on the root by NL.inAppShell) and on
   any phone-width screen, after the owner saw the app's tiles and asked for
   the same on the mobile site (10 Sep 2026). A desk keeps the two lines. */
.in-app .event-desc { display: none; }
/* ---- The blurb on a tile, quieter and shorter ----------------------------

   Owner, 14 Sep 2026: "can you make the text for the listing description in the
   tiles a bit lighter gray? i am just trying to think of ways to make the page
   feel less busy. also, we should only stick to the first line and then add the
   '...' so we see less text."

   ONE LINE, CLAMPED RATHER THAN CUT. -webkit-line-clamp measures the rendered
   line, so the ellipsis lands wherever the column actually runs out - at any
   width, in any font, on a title that wraps or one that does not. Truncating by
   character count instead would be a guess that is wrong on every screen except
   the one it was tuned on.

   --ink-faint, NOT --ink-hint, and the distinction is the palette's own. Faint
   is the meta-text tone and holds 4.5:1; hint is the placeholder ink, and its
   note in the tokens says it is "deliberately not 4.5" because a placeholder is
   "glanced at and then typed over". This is a sentence somebody actually reads
   to decide whether an evening is for them. Lighter, as asked - one step, not
   two, and not into the tone reserved for text nobody reads. */
.event-desc {
  margin-top: var(--sp-2); font-size: 14.5px; color: var(--ink-faint); line-height: 1.5;
  display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden;
}
/* AFTER the rule above, not before it: same specificity, so the later one
   wins, and the phone rule sat first and lost to `display: -webkit-box`.
   The app rule survived only because `.in-app` outranks it. */
@media (max-width: 620px) {
  .event-desc { display: none; }
}
/* AND WITH THE DESCRIPTION GONE, THE CHIPS GO TO THE FOOT OF THE TILE (owner,
   10 Sep 2026: "move those category tags to the bottom of the tile, and make
   sure the placement and alignment makes sense"). The left rail - logo,
   glyphs, save and share - sets the tile's height; the text column was
   shorter, so the chips sat mid-tile over dead space. The body is a column
   now and the footer takes the slack, so the chips rest on the tile's bottom
   edge, level with the rail's buttons, and the price keeps the right end of
   that row. Phone widths and the app; a desk keeps the description and its
   own rhythm. */
@media (max-width: 620px) {
  .event-card .event-body { display: flex; flex-direction: column; }
  .event-card .event-foot { margin-top: auto; padding-top: var(--sp-3); }
  .event-card .event-meta { margin-top: 6px; }
}
.in-app .event-card .event-body { display: flex; flex-direction: column; }
.in-app .event-card .event-foot { margin-top: auto; padding-top: var(--sp-3); }
/* THE RAIL USES ITS WHOLE HEIGHT (owner, 10 Sep 2026): "share and calendar
   side by side at the bottom left, a little smaller, and bring the category
   glyph to the middle between the logo and those buttons." The logo holds
   the top, the two buttons hold the bottom on one row, and the glyph floats
   to the middle of whatever is left - so a long title makes the tile taller
   and the rail spreads with it instead of leaving the space below the
   buttons empty. Specific enough to beat the stacked-column rule that sits
   later in this file. */
@media (max-width: 620px) {
  .event-card .event-org { height: 100%; justify-content: space-between; gap: 10px; }
  .event-card .event-org .event-acts { flex-direction: row; flex-wrap: nowrap; gap: 6px; }
  .event-card .event-org .event-acts .event-share,
  .event-card .event-org .event-acts .event-react { width: 30px; height: 30px; }
}
.in-app .event-card .event-org { height: 100%; justify-content: space-between; gap: 10px; }
.in-app .event-card .event-org .event-acts { flex-direction: row; flex-wrap: nowrap; gap: 6px; }
.in-app .event-card .event-org .event-acts .event-share,
.in-app .event-card .event-org .event-acts .event-react { width: 30px; height: 30px; }
.event-tags { margin-top: var(--sp-3); display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
/* Resident reaction ("Interested") on each event card */
.event-foot { margin-top: var(--sp-3); display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); flex-wrap: wrap; }
.event-foot .event-tags { margin-top: 0; }
/* Both card actions are deliberately small — they sit under every listing, so
   weight here costs more than it buys. */
.event-react {
  display: inline-flex; align-items: center; gap: 5px; flex: none; font: inherit;
  font-size: 12px; font-weight: 700; color: var(--ink-soft); cursor: pointer;
  background: var(--surface-2); border: 1px solid var(--line-strong); border-radius: var(--radius-pill);
  padding: 4px 9px; transition: color .15s, border-color .15s, background .15s;
}
.event-react:hover { border-color: var(--accent); color: var(--accent); }
.event-react svg { color: var(--ink-faint); transition: color .15s; }
.event-react:hover svg { color: var(--accent); }
/* SAVED IS A FILLED RED HEART (owner's call, Aug 2026). The tile button has no
   words any more, so the state has to be carried by the mark itself — an
   outline heart for "not saved", the same heart filled in for "saved".

   This replaces a sea-glass pill whose note argued for "a 'this is yours'
   colour rather than the coral that means action". That reasoning held while
   the button was a labelled pill among other labelled pills. With only a heart
   left, red is the thing every reader already knows a filled heart to mean,
   and a heart in an unfamiliar colour would need the legend the words used to
   provide.

   --accent, not --destructive: the palette's warm coral-rose reads as the red
   of a heart, while --destructive is the alarm colour and borrowing it for a
   favourite would make "saved" and "something is wrong" the same red. It
   passes AA on paper at 5.56:1.

   `fill: currentColor` is what fills it. Every icon in this set is drawn as an
   outline with fill:none, so one path gives both states and they cannot drift
   apart the way two hand-drawn hearts would. */
.event-react.is-on {
  background: var(--accent-tint); border-color: var(--accent); color: var(--accent);
}
.event-react.is-on svg { color: var(--accent); fill: currentColor; }
.event-react.is-on:hover { border-color: var(--accent); color: var(--accent); }
.event-react.is-on:hover svg { color: var(--accent); }

/* THE TWO ICON-ONLY CONTROLS ON A TILE. Square, so the heart and the share
   arrow sit on the same rhythm, and 34px with the row's own padding around it
   — visibly lighter than the labelled pills they replace while staying a real
   target on a phone. The detail-page --lg variants keep their words and their
   44px and are untouched below. */
.event-react:not(.event-react--lg),
.event-share:not(.event-share--lg) {
  width: 34px; height: 34px; padding: 0; gap: 0; justify-content: center;
}
/* NO CIRCLE AROUND THE CALENDAR (owner, 10 Sep 2026): "similar to how the
   share icon doesn't have anything around it" - the tile-size save button is
   as plain as the share button beside it, on every screen, and only borrows
   a tint on hover the way share does. Saved is still the filled coral mark;
   the state lives in the icon, not in a ring around it. The detail page's
   large version keeps its words and its pill. */
.event-react:not(.event-react--lg) { background: none; border-color: transparent; }
.event-react:not(.event-react--lg):hover { border-color: var(--primary); background: var(--primary-tint); color: var(--primary); }
.event-react:not(.event-react--lg):hover svg { color: var(--primary); }
.event-react.is-on:not(.event-react--lg) { background: none; border-color: transparent; }
.event-react.is-on:not(.event-react--lg):hover { background: var(--accent-tint); border-color: var(--accent); }
/* The signed-out state kept its own ring (see .is-locked below); at tile size
   it is plain too, and shows "locked" by its fade alone. */
.event-react.is-locked:not(.event-react--lg) { background: none; border-color: transparent; }
.event-react.is-locked:not(.event-react--lg):hover { border-color: var(--line-strong); background: var(--surface-2); }
/* Detail-page version: same control, promoted to a real button. This is the
   page where people decide, so saving is the primary action here — the .ics
   export next to it steps back to a ghost button. */
.event-react--lg {
  padding: 11px 18px; min-height: 44px; font-size: 15px;
  background: var(--primary); border-color: var(--primary); color: var(--on-primary);
}
.event-react--lg:hover { background: var(--primary-ink); border-color: var(--primary-ink); color: var(--on-primary); }
/* THE MARK KEEPS ITS OWN COLOURS ON THIS BUTTON, IN EVERY STATE.

   It used to be knocked out to solid white here (filter: brightness(0)
   invert(1)) because the bird is drawn in the same slate as the button under
   it. That had two problems. The small one: .is-locked repaints the button to
   a pale --surface-2 pill, the knockout went on applying anyway, and a white
   bird on a cream button is the faint smudge the owner circled on
   15 Sep 2026 — which, signed out or signed in as an organization rather
   than a resident, was every visit. The large one: a knockout throws the
   orange beak away, and the beak is the half of the mark people recognise.

   ASKED FOR (owner, 15 Sep 2026): "the logo in both places need to be blue
   with the orange beak, just like the normal logo." So the logo is never
   recoloured. On the solid slate button it gets a small light tile to sit on
   instead — the ordinary answer for a logo on a dark field, and the same one
   the toast uses. On the two light states (.is-on, .is-locked) it needs no
   tile and gets none.

   SIZED IN CSS, not by the 28 that calLabel asks for, so the tile fits inside
   the pill rather than spilling past it the way the bare mark could. 24px
   with -4px block margins sits inside the 20px line the button already has,
   so nothing grows and the height does not jump as the state changes. */
.event-react--lg .brand-inline { width: 24px; height: 24px; margin: -4px 2px; top: 0; }
.event-react--lg:not(.is-on):not(.is-locked) .brand-inline {
  padding: 2px; background: var(--surface); border-radius: 6px;
}
.event-react--lg.is-on {
  font-size: 15px; padding: 11px 18px;
  background: var(--seaglass-tint); border-color: var(--seaglass); color: var(--seaglass-ink);
}
.event-react--lg.is-on:hover { background: var(--seaglass-tint); border-color: var(--seaglass-ink); }
/* ---- SEND IT TO THE CALENDAR YOU ALREADY USE -----------------------------
   The tile twin of the dashboard's send button, so the two pages carry one
   control with one mark. Same 34px square as the heart beside it. */
.event-cal {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  width: 34px; height: 34px; padding: 0; font: inherit; cursor: pointer;
  background: var(--surface-2); border: 1px solid var(--line-strong);
  border-radius: var(--radius-pill); color: var(--ink-soft);
  transition: color .15s, border-color .15s, background .15s;
}
.event-cal:hover { border-color: var(--accent); color: var(--accent); }
.event-cal svg { color: var(--ink-faint); transition: color .15s; }
.event-cal:hover svg { color: var(--accent); }

/* ---- ALREADY IN THEIR OWN CALENDAR ---------------------------------------
   REPORTED: "when I came back to my dashboard, I couldn't tell... if I had
   already clicked on the icon to send it to my personal calendar."

   --seaglass is the palette's own "this went well" colour - the same one
   .vr-ask__row.is-done uses, and the note there warns against inventing a
   --good that would resolve to nothing. It also keeps this distinct from the
   heart beside it, which owns the coral. Filled rather than outlined so it
   reads as a state at a glance down a column of rows -
   the difference has to survive being seen at thumbnail size and in the
   corner of the eye, which a colour change alone does not. The glyph swaps to
   a tick in NL.sendToCalButton for the same reason.

   NOT dimmed or disabled: sending it again is an ordinary thing to want - a
   second calendar, a new phone, an entry deleted by mistake. */
.event-cal.is-sent {
  background: var(--seaglass-tint); border-color: var(--seaglass); color: var(--seaglass-ink);
}
.event-cal.is-sent svg { color: var(--seaglass-ink); }
.event-cal.is-sent:hover { border-color: var(--seaglass); color: var(--seaglass-ink); }
.event-cal.is-sent:hover svg { color: var(--seaglass-ink); }

/* ---- NOT YOURS YET -------------------------------------------------------
   ASKED FOR: "the button should be there, visible always, but only functionable
   if I am logged in. If I click on it and I'm not logged in, it should be a
   little grayed out, and it should explain you need an account."

   FADED, NOT `disabled`. The tap is the only way the explanation is ever
   reached on a phone - there is no hover to carry a tooltip - so the button
   stays live and the dialog does the explaining. `cursor: help` is the honest
   pointer for a control that answers rather than acts.

   NOT A HOVER-LESS DEAD THING EITHER: it lifts a little under the pointer, so
   somebody who tries it can see it is willing to respond. */
.event-react.is-locked, .event-cal.is-locked {
  opacity: .55; background: var(--surface-2); border-color: var(--line);
  color: var(--ink-faint); cursor: help;
}
.event-react.is-locked svg, .event-cal.is-locked svg { color: var(--ink-faint); }
.event-react.is-locked:hover, .event-cal.is-locked:hover {
  opacity: .8; border-color: var(--line-strong); color: var(--ink-soft);
}
.event-react.is-locked:hover svg, .event-cal.is-locked:hover svg { color: var(--ink-soft); }
/* The big one is a solid slate button, so faded has to mean unfilled here
   rather than see-through - a 55% primary on a white card reads as broken. */
.event-react--lg.is-locked {
  opacity: 1; background: var(--surface-2); border-color: var(--line-strong); color: var(--ink-soft);
}
.event-react--lg.is-locked svg { color: var(--ink-faint); }
.event-react--lg.is-locked:hover {
  background: var(--surface); border-color: var(--ink-faint); color: var(--ink);
}
/* The labelled twin, on the detail page. A ghost button beside the solid
   favorite one: keeping it is the primary act here, sending it on is not. */
.event-cal--lg.is-locked { opacity: .6; cursor: help; }
.event-cal--lg.is-locked:hover { opacity: .85; }
.detail-tags .event-like { margin-left: auto; }
.event-react__n { font-variant-numeric: tabular-nums; }
/* The two card actions travel together, so the thumb sits immediately left of
   Interested and they wrap as one unit. */
/* `flex: none` kept this row at its natural width, so on a 360px phone the
   three actions (like · share · favorite) came to 254px and pushed past the
   card. Allowed to wrap and shrink instead — the row breaks onto two lines
   rather than the page scrolling sideways. */
.event-acts { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
/* ---- ON A PHONE THEY STACK, IN ONE STRAIGHT LINE -------------------------
   REPORTED, with the wrapping circled: "the placement for these on the mobile
   web also looks bad. Can we make them smaller and make them fit in a straight
   line vertically, for the mobile web only?"

   THE COLUMN THEY LIVE IN IS ABOUT 44px WIDE, so three 34px controls at six
   pixels apart needed 114px and wrapped two-and-one - which is the shape that
   looked broken: not a row, not a column, just a control that fell off the
   end. `nowrap` and a column make it a deliberate stack, and 30px keeps three
   of them inside the tile's height while staying a real thumb target.

   PHONE ONLY. On a wider screen the row has the space and reads better
   horizontally, which is why this is a media query and not a rewrite. */
@media (max-width: 620px) {
  .event-acts {
    flex-direction: column; flex-wrap: nowrap;
    align-items: center; justify-content: flex-start; gap: 5px;
  }
  .event-react:not(.event-react--lg),
  .event-cal:not(.event-cal--lg),
  .event-share:not(.event-share--lg) { width: 30px; height: 30px; }
}
/* ---- BUY TICKETS, IN THE CORNER OF A TILE --------------------------------

   The third control in .event-acts, and the only one of the three that is not
   housekeeping: save and share act on this reader's own week, this one goes
   and buys something. So it is the one that wears a colour — the same coral
   .tag--tickets has used since the ticket link shipped, so a resident who has
   seen the chip recognises the mark.

   IT CARRIES ITS WORDS. Save and share are bare icons because a heart and a
   share arrow need no teaching; a ticket stub in a corner does, and "an icon
   ... that says buy tickets" was the ask. A pill rather than a square,
   therefore, sized to sit on the same 34px rhythm as the two beside it.

   AND DROPS THEM ON A PHONE, where .event-acts becomes a 30px-wide stack in a
   70px column — see the note above it. A pill with a word in it cannot live
   there, so below 620px this is a square with the stub alone, which is the
   same trade save and share already made at every width. */
.event-tix {
  display: inline-flex; align-items: center; gap: 5px; flex: none; font: inherit;
  font-size: 12px; font-weight: 700; letter-spacing: .01em; cursor: pointer;
  height: 34px; padding: 0 11px; white-space: nowrap;
  color: var(--accent); background: var(--accent-tint);
  border: 1px solid transparent; border-radius: var(--radius-pill);
  transition: color .15s, border-color .15s, background .15s;
}
.event-tix svg { color: currentColor; flex: none; }
.event-tix:hover { border-color: var(--accent); background: var(--accent); color: var(--on-primary, #fff); }
.event-tix:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
@media (max-width: 620px) {
  .event-tix { width: 30px; height: 30px; padding: 0; gap: 0; justify-content: center; }
  .event-tix .event-tix__t { display: none; }
}

/* Thumbs up: a plain tally anyone can add to. Kept lighter than .event-react —
   no fill, no pill background — so "Interested" stays the primary action. */
.event-like {
  display: inline-flex; align-items: center; gap: 4px; flex: none; font: inherit;
  font-size: 11.5px; font-weight: 700; color: var(--ink-faint); cursor: pointer;
  background: none; border: 1px solid transparent; border-radius: var(--radius-pill);
  padding: 4px 8px; transition: color .15s, border-color .15s, background .15s;
}
.event-like:hover { color: var(--primary); border-color: var(--primary); background: var(--primary-tint); }
.event-like svg { color: var(--ink-faint); transition: color .15s; }
.event-like:hover svg { color: var(--primary); }
.event-like.is-on { color: var(--primary-ink); border-color: var(--primary); background: var(--primary-tint); }
.event-like.is-on svg { color: var(--primary); fill: var(--primary); }
.event-like:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
.event-like__n { font-variant-numeric: tabular-nums; }
/* Share sits between the tally and the save button, styled like the thumb —
   both are secondary to "Add to my favorites", which stays the one filled pill. */
.event-share {
  display: inline-flex; align-items: center; gap: 5px; flex: none; font: inherit;
  font-size: 11.5px; font-weight: 700; color: var(--ink-faint); cursor: pointer;
  background: none; border: 1px solid transparent; border-radius: var(--radius-pill);
  padding: 4px 8px; transition: color .15s, border-color .15s, background .15s;
}
.event-share:hover { color: var(--primary); border-color: var(--primary); background: var(--primary-tint); }
.event-share svg { color: var(--ink-faint); transition: color .15s; }
.event-share:hover svg { color: var(--primary); }
.event-share:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
/* Detail page: a full-size ghost button, so reset the compact card sizing. */
.event-share--lg {
  font-size: 15px; padding: 11px 18px; min-height: 44px; gap: 8px;
  color: var(--ink); background: var(--surface); border-color: var(--line-strong);
}
.event-share--lg:hover { background: var(--surface-2); border-color: var(--line-strong); color: var(--ink); }
.event-share--lg svg { color: var(--ink); }

/* ---- Share sheet (desktop fallback for the OS share sheet) -------------- */
.share-rows { margin-top: var(--sp-4); display: grid; gap: 8px; }
.share-row {
  display: flex; align-items: center; gap: 12px; width: 100%; text-align: left;
  padding: 12px 14px; border: 1px solid var(--line-strong); border-radius: var(--radius);
  background: var(--surface); color: var(--ink); text-decoration: none; font: inherit;
  transition: border-color .15s, background .15s;
}
.share-row:hover { border-color: var(--primary); background: var(--primary-tint); text-decoration: none; }
.share-row:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
.share-row > svg:first-child { color: var(--primary); flex: none; }
.share-row > svg:last-child { margin-left: auto; color: var(--ink-faint); flex: none; }
.share-row strong { display: block; font-size: 15px; font-weight: 700; }
.share-row em { display: block; font-style: normal; font-size: 13px; color: var(--ink-soft); margin-top: 1px; }
.share-copy { margin-top: var(--sp-4); display: flex; gap: 8px; align-items: center; }
.share-copy input {
  flex: 1; min-width: 0; font-size: 13px; color: var(--ink-soft);
  padding: 9px 11px; border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  background: var(--surface-2); text-overflow: ellipsis;
}
.share-copy .btn { flex: none; }

/* Level 2 of the promotions filter. Indented and tinted so it reads as living
   INSIDE the business type above it, rather than as a second, equal filter. */
.promo-lvl2 {
  margin-top: 2px; padding: var(--sp-3) var(--sp-4) var(--sp-2);
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: var(--radius); border-left: 3px solid var(--primary);
}
.promo-lvl2 .filter-group { margin-top: 0; }
/* The kind of offer — the thing residents actually scan for, so it carries the
   accent rather than the muted grey the audience tag uses. */
.tag--promo { color: var(--accent); background: var(--accent-tint); font-weight: 700; }

/* A trade with nothing on this week: present, so the range is visible, but
   clearly quieter than the ones you can actually browse. */
.chip.is-muted { color: var(--ink-faint); border-style: dashed; opacity: .8; }
.chip.is-muted .chip__dot { opacity: .45; }
.chip.is-muted:hover { opacity: 1; }
.promo-more { margin-top: 8px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.chip--more { font-weight: 700; color: var(--primary); border-color: var(--primary); background: var(--primary-tint); }
.chip--more:hover { background: var(--surface-2); }
.promo-more__note { font-size: 12.5px; color: var(--ink-faint); }
/* Shown in place of the "Who it's for" chips when Food & Drinks is the chosen
   category and an audience is still set. Same size and colour as the note
   above, because it does the same job: explaining a filter that is not doing
   what the reader might expect. */
.filter-note { font-size: 12.5px; color: var(--ink-faint); margin: 0 0 10px; }

/* ---- Registration: the optional profile block --------------------------
   Collapsed by default so the required form still reads as four boxes and a
   button. Opening it is a choice, which is the honest framing for questions
   nobody has to answer. */
.join-more {
  margin-top: var(--sp-5); border: 1px solid var(--line-strong);
  border-radius: var(--radius); background: var(--surface-2);
}
.join-more summary {
  display: flex; align-items: center; gap: 11px; cursor: pointer; list-style: none;
  padding: var(--sp-4); color: var(--ink);
}
.join-more summary::-webkit-details-marker { display: none; }
.join-more summary > svg:first-child { color: var(--primary); flex: none; }
.join-more summary > svg:last-child { margin-left: auto; flex: none; color: var(--ink-faint); transition: transform .18s; }
.join-more[open] summary > svg:last-child { transform: rotate(180deg); }
.join-more summary strong { display: block; font-family: var(--font-display); font-size: 16.5px; font-weight: 600; }
.join-more summary em { display: block; font-style: normal; font-size: 13.5px; color: var(--ink-soft); margin-top: 2px; }
.join-more summary:focus-visible { outline: 3px solid var(--focus); outline-offset: -3px; border-radius: var(--radius); }
.join-more__body { padding: 0 var(--sp-4) var(--sp-4); }
.join-more__body .field { margin-top: var(--sp-4); }

/* The "what happens next" explainer on a synced member's dashboard. Each step
   leads with when it happens, so the panel can be skimmed down the bold edge
   without reading a word of the rest. */
.dash-guide { margin-bottom: var(--sp-5); }
.dash-guide .dash-guide__step { margin: 0 0 var(--sp-3); font-size: 15px; line-height: 1.55; color: var(--ink-soft); }
.dash-guide .dash-guide__step strong { color: var(--ink); }
.dash-guide .dash-guide__step em { font-style: normal; font-weight: 600; color: var(--ink); }
.dash-guide .form-note { margin-top: var(--sp-4); }
/* The plan's own bullets, inside the guide (16 Sep 2026). */
.dash-guide__list { margin: 0 0 var(--sp-3); padding-left: 1.25em; font-size: 14.5px; line-height: 1.55; color: var(--ink-soft); }
.dash-guide__list li { margin: 0 0 4px; }
.dash-guide__list strong { color: var(--ink); }

/* Interest picker: small wrap-around pills, sized to their own label. An
   earlier version borrowed the browse-filter chip (colour dot and all) and it
   read as a wall of big boxes with the text falling out of them — these are
   twelve quick optional taps, not a navigation control, so they stay small. */
/* NB: named .intpick, not .pick — `.pick` is already the Weekender's numbered
   pick card (a `grid-template-columns: 44px 1fr` layout). The collision forced
   every label into a 44px column, which is what made this render as big empty
   boxes with the text falling out. */
.intpick-grid { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 6px; }
.intpick { position: relative; display: inline-flex; flex: none; cursor: pointer; }
.intpick input { position: absolute; width: 1px; height: 1px; opacity: 0; margin: -1px; }
.intpick__lbl {
  display: inline-block; padding: 6px 12px; border-radius: var(--radius-pill);
  border: 1px solid var(--line-strong); background: var(--surface);
  font-size: 13px; font-weight: 600; line-height: 1.25; color: var(--ink-soft);
  white-space: nowrap; transition: border-color .15s, background .15s, color .15s;
}
.intpick__lbl:hover { border-color: var(--primary); color: var(--primary); }
.intpick input:checked + .intpick__lbl {
  border-color: var(--primary); background: var(--primary-tint); color: var(--primary-ink);
}
.intpick input:focus-visible + .intpick__lbl { outline: 3px solid var(--focus); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .join-more summary > svg:last-child { transition: none; } }
/* Visually hidden label, so the count is not read out as a bare number. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
/* "Highlight of the Week" — product name, always visually distinct */
.hotw { color: var(--seaglass-ink); font-weight: 700; }
/* Newsletter page: "get featured" CTA for businesses */
.news-biz-cta {
  margin-top: var(--sp-5); display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4);
  flex-wrap: wrap; background: var(--accent-tint); border: 1px solid var(--accent);
  border-radius: var(--radius); padding: var(--sp-4) var(--sp-5);
}
.news-biz-cta__title { font-family: var(--font-display); font-size: 18px; font-weight: 600; color: var(--ink); display: flex; align-items: center; gap: 8px; }
.news-biz-cta__title svg { color: var(--accent); }
.news-biz-cta__text { margin-top: 4px; font-size: 14px; color: var(--ink-soft); }
/* The label stays visible at every width: it used to sit next to a count, so
   hiding it left something readable behind. Now it IS the button. */
.event-react__lbl { font-weight: 600; white-space: nowrap; }
/* The brand mark set into a line of text, deliberately larger than the type it
   sits in. The negative block margins stop it from growing the line box, so it
   spills out of the pill (top and bottom) instead of inflating the button —
   which is the point: the mark should be big enough to recognise. */
.brand-inline {
  display: inline-block; vertical-align: middle;
  margin: -12px 0; position: relative; top: -1px;
}
.tag {
  font-size: 12.5px; font-weight: 600; padding: 3px 10px; border-radius: var(--radius-pill);
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--surface-2); color: var(--ink-soft);
}
.tag__dot { width: 8px; height: 8px; border-radius: 50%; background: var(--c, var(--ink-faint)); flex: none; }
.tag--aud { background: transparent; border: 1px solid var(--line-strong); color: var(--ink-faint); }
.tag--price { margin-left: auto; color: var(--primary-ink); background: var(--primary-tint); font-weight: 700; }
.tag--tickets { color: var(--accent); background: var(--accent-tint); font-weight: 700; display: inline-flex; align-items: center; gap: 4px; }

/* ---- Empty state --------------------------------------------------------- */
.empty {
  text-align: center; padding: var(--sp-8) var(--sp-4); color: var(--ink-soft);
  border: 1px dashed var(--line-strong); border-radius: var(--radius); margin-top: var(--sp-6);
  background: var(--surface);
}
.empty svg { color: var(--line-strong); margin-bottom: var(--sp-3); }
.empty h3 { font-size: 20px; margin-bottom: var(--sp-2); }

/* ---- Venue directory ----------------------------------------------------- */
.venue-grid { display: grid; grid-template-columns: 1fr; gap: var(--sp-4); margin-top: var(--sp-5); }
/* Venue tiles reuse the event-card chrome so every tab reads the same. A place
   has no start time, so the left column carries the venue icon instead. */
.venue-card .event-time { color: var(--ink-faint); }
.venue-card .event-time svg { color: var(--seaglass-ink); }
.venue-card__site {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 12.5px; font-weight: 600; color: var(--primary);
}
.venue-card__site:hover { text-decoration: underline; }
.venue-card__addr { margin-top: 6px; font-size: 13.5px; color: var(--ink-faint); display: flex; gap: 6px; align-items: flex-start; }
.venue-card__addr svg { flex: none; margin-top: 2px; }
.venue-card__blurb { margin-top: var(--sp-3); color: var(--ink-soft); font-size: 15px; }
.venue-card__tags { margin-top: var(--sp-3); display: flex; flex-wrap: wrap; gap: 6px; }
.venue-card__actions { margin-top: auto; padding-top: var(--sp-4); display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; }
/* Heart sits opposite the website button, mirroring the event cards. */
.venue-card__actions .event-react { margin-left: auto; }

/* ---- Buttons ------------------------------------------------------------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  padding: 11px 18px; min-height: 44px; border-radius: var(--radius-pill);
  font-weight: 700; font-size: 15px; border: 1px solid transparent; transition: background .15s, border-color .15s, transform .05s;
}
.btn:active { transform: scale(.98); }
.btn:hover { text-decoration: none; }
.btn--primary { background: var(--primary); color: var(--on-primary); }
.btn--primary:hover { background: var(--primary-ink); }
.btn--ghost { background: var(--surface); border-color: var(--line-strong); color: var(--ink); }
.btn--ghost:hover { background: var(--surface-2); }
.btn--sm { padding: 8px 14px; min-height: 40px; font-size: 14px; }
/* For actions that live inside a row rather than under it. */
.btn--xs { padding: 6px 11px; min-height: 34px; font-size: 13px; gap: 5px; }

/* ---- Event detail page --------------------------------------------------- */
.back-link { display: flex; width: fit-content; max-width: 100%; align-items: center; gap: 6px; color: var(--ink-soft); font-weight: 600; font-size: 14px; margin-bottom: var(--sp-4); }
.back-link:hover { color: var(--ink); text-decoration: none; }
/* The reading measure, for the whole column rather than for one block in it.
   See the note on `.detail-desc` for why it moved here. `margin: 0 auto`
   centres it, so a wide screen puts the air on both sides where it reads as
   margin, not all of it on the right where it reads as a mistake. */
.detail { max-width: 66ch; margin-left: auto; margin-right: auto; }
.detail-cat { font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; display: inline-flex; align-items: center; gap: 7px; color: var(--ink-soft); }
/* AIR BETWEEN THE TWO OF THEM. A cross-posted listing prints both categories
   here, and the spans are concatenated with nothing between - so "SEAFOOD" ran
   straight into the second one's coloured dot and read as one long word. */
.detail-cat + .detail-cat { margin-left: var(--sp-4); }
.detail h1 { font-size: clamp(26px, 6vw, 38px); letter-spacing: -.02em; margin-top: var(--sp-3); }
.detail-when {
  margin-top: var(--sp-4); display: flex; align-items: center; gap: var(--sp-3);
  font-family: var(--font-display); font-size: 19px; color: var(--ink);
}
.detail-when svg { color: var(--accent); flex: none; }
.detail-facts { margin-top: var(--sp-5); border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); overflow: hidden; }
.fact { display: flex; gap: var(--sp-3); padding: var(--sp-4); border-bottom: 1px solid var(--line); }
.fact:last-child { border-bottom: 0; }
.fact svg { color: var(--ink-faint); flex: none; margin-top: 2px; }
.fact__k { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-faint); }
.fact__v { font-size: 15.5px; color: var(--ink); margin-top: 2px; }
/* THE DESCRIPTION, GIVEN ROOM TO BE READ.

   REPORTED: it "feels very busy, there is no formation, no spacing... it is
   just really hard to read". Three things were doing that, and all three are
   fixed here rather than by editing anybody's words:

     NO PARAGRAPHS. It was one <p>, always — see NL.richText, which now breaks
     the text where the writer did, or at sentence ends when they left it as
     one run.

     NO MEASURE. Full-width text on a laptop is 110-odd characters a line, and
     the eye loses its place coming back to the left margin. 66ch is the
     ordinary reading measure and it is why this is the only block on the page
     that does not fill the column.

     TOO TIGHT. 1.65 with no space between paragraphs is a wall even when the
     paragraphs exist. 1.75, with a full line of air between them.

   The first paragraph is a shade darker and larger — a lead. It costs nothing
   and it gives the block a top, which is most of what "no formation" meant. */
/* ONE RIGHT EDGE, NOT TWO.

   REPORTED, with the gap ringed: "the text is just ending before the page ends
   on the sides, so you have the space that I circled that somehow the text is
   not continuing."

   Exactly right, and the cause was a measure in the wrong place. `max-width:
   66ch` lived HERE, on the description alone, so the paragraph stopped at
   about 535px while the WHERE/PRICE card beside it ran the full 688px column.
   Two right edges on one page, and the shorter one looks like text that failed
   to reach the end rather than like a reading measure.

   The measure itself is right - a line much longer than this is genuinely
   harder to read coming back to the left margin. So it moves UP to the column,
   on `.detail` below: the heading, the picture, the facts card, the paragraph,
   the buttons and the tags now all stop in the same place, and the block reads
   as one rectangle centred on the page instead of a ragged one. */
.detail-desc {
  margin-top: var(--sp-5);
  font-size: 17px; line-height: 1.75; color: var(--ink-soft);
}
.detail-desc p { margin: 0 0 var(--sp-4); }
.detail-desc > :last-child { margin-bottom: 0; }
/* NO LEAD PARAGRAPH. The first one used to be 18px in full ink while the rest
   were 17px in --ink-soft, which is a magazine standfirst: it works when
   somebody WROTE a standfirst, and these descriptions are pasted from a
   venue's own page. So the treatment landed on whatever sentence happened to
   come first, and a reader saw one description in two sizes and two colours
   for no reason the text supported.

   Reported as exactly that. One size and one colour now, the quieter pair,
   which is the one the owner asked to keep. Removing the rule rather than
   restyling it is the fix, because `.detail-desc` already sets both and every
   surface that shows a description uses that class — the event page, the
   private invitation preview and the promotion preview all correct together. */
.detail-desc ul { margin: 0 0 var(--sp-4); padding-left: 1.35em; list-style: disc; }
.detail-desc li { margin-bottom: 6px; }
.detail-desc li::marker { color: var(--ink-faint); }
/* ---- THREE BUTTONS, ONE LINE ---------------------------------------------

   REPORTED: "I'm pretty sure that button fits on the same line and it would
   look much better. Right now it looks kinda odd and displaced - especially if
   you make the buttons and the font slightly smaller, you can definitely fit
   three next to each other."

   MEASURED RATHER THAN GUESSED, on the live page at 1280px. The column is
   569px. Add to my favorites (203) + Add to my calendar (201) + Google
   Calendar (180) plus two 12px gaps needs 608 - over by 39, which is why the
   third one dropped and sat alone looking like a mistake.

   14px type, 13px of side padding and an 8px gap bring it to 544: it fits with
   25px to spare. The slack is the point. Exactly-fitting would break on the
   next label a listing produces - "More at Westport Library" is 244px on its
   own - and this row carries different buttons on different listings.

   STILL wrap, and it must: at phone width three of these cannot share a line
   at any readable size, and they stack there as they always did. */
/* MEASURED AGAIN (4 Sep 2026) after the first button became "Add to
   favorites & calendar", which is about twenty pixels wider than the label
   the numbers above were taken from, and the third button dropped again.
   13.5px type, 10px of side padding, 16px icons and a 6px gap: the three
   come to about 540 in a 569 column, so the slack is back. */
.detail-actions { margin-top: var(--sp-6); display: flex; flex-wrap: wrap; gap: 6px; }
.detail-actions .btn { font-size: 13.5px; padding: 9px 9px; gap: 5px; }
.detail-actions .btn svg { width: 15px; height: 15px; }
/* SIX TAGS ON ONE LINE (owner, 4 Sep 2026: "Grown-ups (46-64) fits in the
   same line as the others"). Measured at 566 in a 569 column with the old
   gap and padding, which is a fit only until the next label; 6px gaps and a
   pixel less padding either side bring six to about 540. */
.detail-tags { margin-top: var(--sp-5); display: flex; flex-wrap: wrap; gap: 6px; }
.detail-tags .tag { padding: 3px 9px; }

/* WHEN THE LISTING WENT UP, kept deliberately quiet: last on the page, small,
   and in the faintest ink the palette has. Somebody reading to the bottom
   finds it; nobody scanning for Saturday's plans notices it.

   `--ink-faint` is a real token and is documented as dark enough for 4.5:1.
   An invented `--muted` would resolve to nothing and render at FULL ink —
   louder than the title of the page — which is how ten of those ended up
   shouting earlier in this project. */
.detail-stamp {
  margin-top: var(--sp-6);
  font-size: 12px;
  color: var(--ink-faint);
}

/* "Is this yours?" - deliberately the quietest thing on the page, and to the
   right, where a footnote goes rather than where an action goes. It shares the
   stamp's size and colour because it belongs to the same margin. */
.detail-claim {
  margin: var(--sp-2) 0 0;
  text-align: right;
  font-size: 12px;
}
.detail-claim a { color: var(--ink-faint); text-decoration: none; }

/* The logo control in the owner strip: the mark, then the two buttons. Its own
   line on a phone, where the strip already wraps, and the mark sits with them
   rather than above them because it IS the answer to "did that work". */
.owner-logo { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.owner-logo__mark { display: inline-flex; flex: none; }
.owner-logo .org-mark { width: 34px; height: 34px; font-size: 12px; }
/* THE TOOLBAR, RIDING IN THE FILTER ROW on anything wider than a phone.

   `margin-left: auto` on the slot rather than `justify-content: space-between`
   on the row: the row also holds the town picker, and space-between would push
   THAT to the middle of an empty line whenever the toolbar is absent - which
   it is on a page with no results, and on a phone. An auto margin only acts
   when there is something to push. */
@media (min-width: 621px) {
  .filterbar__view:has(.toolbar) { margin-left: auto; min-width: 0; }
  .filterbar__view .toolbar {
    margin: 0; padding: 0; border: 0; gap: var(--sp-3);
    display: flex; align-items: center; flex-wrap: wrap; justify-content: flex-end;
  }
  /* The spacer earned its keep in a full-width row where it pushed the view
     switch to the far edge. In here the row is already right-aligned and it
     only adds a gap in the middle of the group. */
  .filterbar__view .toolbar .toolbar__spacer { display: none; }
}

/* THE PREVIEW IS THE WIDTH OF THE PAGE IT PREVIEWS.

   REPORTED: "the preview page has glitchy formatting, with the description
   text being wrapped oddly."

   It was not the wrapping. `.detail-desc` carries `max-width: 66ch`, which is
   a reading measure and right where it lives - on the real listing page the
   column is about that wide anyway, so it never shows. The preview modal is
   much wider than that column, so the paragraph broke at 530px inside a 910px
   card and read as stranded text rather than as a measure.

   Widening the text would have made the preview LIE, since the guest's page
   really does wrap at 66ch. So the card narrows to the same measure instead,
   and what is shown is what arrives. */
.detail--preview { max-width: 68ch; margin-left: auto; margin-right: auto; }

/* A town that has not opened yet, on the preferences form. Ticking it is a
   waiting list, so it reads as information rather than as a refusal. */
.check__soon { font-style: normal; font-size: 11.5px; color: var(--ink-faint); }
/* The whole county, above the towns it contains. */
.check--all { font-weight: 600; }
/* ---- A tick somebody is agreeing to, inside a warning box ------------------
   Heavier than an ordinary .check because it gates a button: it is the thing
   being read before an irreversible choice, not one option among several. The
   box it sits in already carries the warning colour, so this only has to be
   findable and comfortable to hit. */
.check--confirm {
  display: flex; gap: 9px; align-items: flex-start;
  margin: var(--sp-3) 0 var(--sp-2); padding: var(--sp-2) 0;
  font-size: 14.5px; font-weight: 600; line-height: 1.45; cursor: pointer;
}
.check--confirm input { margin-top: 3px; flex: none; width: 17px; height: 17px; cursor: pointer; }

/* ---- WHAT YOU JUST ATTACHED ---------------------------------------------
   A picture, because the question is "did the right thing attach" and only a
   picture answers it. Small enough to sit under a file input without pushing
   the form around, big enough to recognise a flyer from. */
.attached {
  display: flex; align-items: center; gap: 10px; margin-top: 8px;
  padding: 6px 8px; border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface-2);
}
.attached__img {
  width: 56px; height: 56px; flex: none; object-fit: cover;
  border-radius: var(--radius-sm); border: 1px solid var(--line);
  background: var(--surface);
}
.attached__say { font-size: 12.5px; color: var(--ink-soft); }
.attached__x { margin-left: auto; font-size: 12px; color: var(--ink-faint); }
.attached__x:hover { color: var(--primary); }

/* ---- SEVERAL PICTURES, in the scan dialog --------------------------------
   A row of thumbnails rather than a stack of identical sentences: six posters
   pasted one after another are told apart by looking, not by reading "png,
   240 KB" six times. */
.shots { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 8px 0 0; }
.shots__one { position: relative; display: inline-flex; }
.shots__img {
  width: 62px; height: 62px; object-fit: cover;
  border-radius: var(--radius-sm); border: 1px solid var(--line);
  background: var(--surface);
}
/* On the corner of the picture it removes, which is the only place it cannot
   be mistaken for removing a different one. */
.shots__x {
  position: absolute; top: -6px; right: -6px; width: 20px; height: 20px;
  border: 1px solid var(--line-strong); border-radius: 50%;
  background: var(--surface); color: var(--ink-soft);
  font-size: 11px; line-height: 1; cursor: pointer; padding: 0;
}
.shots__x:hover { background: var(--surface-2); color: var(--ink); }
.shots__say { flex: 1 0 100%; font-size: 12.5px; color: var(--seaglass-ink); }

/* ---- THE LOGO DESK, in Advanced settings --------------------------------
   A row per place with no logo: its mark, who it is, and the three things that
   settle it. The link box takes the width that is left, because a URL is the
   longest thing on the line and the one being typed. */
.lg-list { list-style: none; margin: var(--sp-4) 0 0; padding: 0; display: grid; gap: 6px; }
.lg-row {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 8px 10px; border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface);
}
.lg-row__mark { flex: none; }
.lg-row .org-mark { width: 30px; height: 30px; font-size: 11px; }
.lg-row__who { display: flex; flex-direction: column; min-width: 140px; }
.lg-row__meta { font-style: normal; font-size: 11.5px; color: var(--ink-faint); }
.lg-row__do { display: flex; align-items: center; gap: 6px; flex: 1 1 260px; flex-wrap: wrap; }
.lg-row__link { flex: 1 1 160px; min-width: 0; font-size: 13px; }
/* The file input inside is hidden, so the label IS the button. */
.lg-row__up { cursor: pointer; }
/* Quietest of the three: it is the one you press when you are NOT doing the
   work, and it must not look like the primary way out of the row. */
.lg-row__skip { color: var(--ink-faint); font-size: 12px; }
.lg-row__skip:hover { color: var(--primary); }
.lg-row__said {
  flex: 1 0 100%; font-size: 12px; color: var(--seaglass-ink);
  display: flex; align-items: center; gap: 5px;
}
.lg-row__said.is-bad { color: var(--destructive); }

/* Why there are no buttons here. Quiet, and the same size as the strip's own
   label, because it is an explanation and not a warning. */
.owner-logo__note {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12.5px; color: var(--ink-faint);
}
.owner-logo__note svg { flex: none; color: var(--ink-faint); }
.detail-claim a:hover { color: var(--primary); text-decoration: underline; }

/* ---- Footer -------------------------------------------------------------- */
.site-footer {
  margin-top: var(--sp-8); border-top: 1px solid var(--line);
  background: var(--surface-2); padding: var(--sp-6) 0;
}
.site-footer .wrap { display: flex; flex-wrap: wrap; gap: var(--sp-4); align-items: baseline; }
.site-footer p { margin: 0; font-size: 13.5px; color: var(--ink-faint); }
.site-footer .brand__name { font-size: 17px; }
.site-footer nav { margin-left: auto; display: flex; gap: var(--sp-4); }
.site-footer nav a { color: var(--ink-soft); font-size: 14px; font-weight: 600; }

/* ---- Responsive ---------------------------------------------------------- */
@media (min-width: 620px) {
  .venue-grid { grid-template-columns: 1fr 1fr; }
  .event-card { grid-template-columns: 78px 1fr; }
  .event-card:has(.event-org) { grid-template-columns: 96px 1fr; }
}
@media (min-width: 900px) {
  .filters { grid-template-columns: 1fr; }
}

@media (max-width: 400px) {
  .brand__tag { display: none; }   /* keep the wordmark readable on tiny screens */
  .site-nav a { padding: 8px 9px; font-size: 14px; }
}

/* ---- Motion preferences -------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}

/* ---- Focus visibility everywhere ---------------------------------------- */
:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; border-radius: 4px; }


/* ==========================================================================
   v2 additions: city selector, section nav, section layouts
   ========================================================================== */

/* Header now stacks a top row + a section-nav row. */
.site-header__inner { min-height: 58px; }

/* ---- City / region selector --------------------------------------------- */
.city-select { position: relative; margin-left: auto; }
.city-btn {
  display: inline-flex; align-items: center; gap: 7px;
  background: var(--surface); border: 1px solid var(--line-strong);
  /* 44px, not 40. The header controls are the ones a thumb reaches for on a
     phone while walking, and 40 was close enough to look fine and miss often.
     Raised on min-height rather than padding so the text keeps its position
     and nothing about the layout moves. */
  border-radius: var(--radius-pill); padding: 7px 13px; min-height: 44px;
  font-weight: 650; font-size: 14.5px; color: var(--ink);
  /* Matched to the Filters button it sits beside — same shadow, same lift off
     the cream. The pair are one control in two halves (what KIND / WHERE) and
     were drawn to two different weights. */
  box-shadow: var(--shadow);
  transition: background .15s, border-color .15s, box-shadow .15s;
}
.city-btn:hover {
  background: var(--surface-2); border-color: var(--ink-faint);
  box-shadow: var(--shadow-hover);
}
.city-btn svg:first-child { color: var(--primary); }

/* A TOWN IS PICKED, and until now nothing said so but the words themselves.

   "All of Fairfield County" and "Norwalk" were the same pale pill, so the most
   consequential filter on the site could be narrowed to one town and look
   exactly like it was showing everything — which is the state somebody is in
   when they tell you the site is empty. Set by NL.markCitySet in layout.js,
   off whenever the selection is "everywhere". Same navy the Filters button
   uses for the same fact, so the two are learned once. */
.city-btn.is-set {
  border-color: var(--primary); color: var(--primary-ink); background: var(--primary-tint);
  font-weight: 700;
}
.city-btn.is-set .city-btn__caret { color: var(--primary); }
.city-btn.is-set:hover { background: var(--primary-tint-2); border-color: var(--primary); }
.city-btn__label { max-width: 46vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The button carries both forms of the town label and CSS picks one. Long is
   the default; the phone rule near the top of this file swaps them. See
   NL.cityLabelHTML. */
.city-btn__full { display: inline; }
.city-btn__short { display: none; }
.city-btn__caret { color: var(--ink-faint); }
.city-btn[aria-expanded="true"] .city-btn__caret { transform: rotate(180deg); }

.city-menu {
  position: absolute; right: 0; top: calc(100% + 8px); width: 270px; max-width: 84vw;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow-hover); padding: 8px; z-index: 60;
  /* IT SCROLLS ITSELF. REPORTED, 5 Sep 2026: "the filter is not allowing
     for scrolling. When I scroll, the page behind it scrolls." Seventeen
     towns at 44px each is taller than a laptop window, and the menu had no
     height of its own, so the wheel went to the page and the bottom of the
     list was unreachable. Capped at what fits below the button, with the
     scroll kept inside so it never spills over to the page behind. */
  max-height: min(70vh, calc(100vh - 230px)); overflow-y: auto; overscroll-behavior: contain;
}
.city-menu__head {
  margin: 4px 8px 6px; font-size: 11px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .07em; color: var(--ink-faint);
}
.city-menu ul { list-style: none; margin: 0; padding: 0; }
.city-opt {
  width: 100%; display: flex; align-items: center; gap: 10px; text-align: left;
  padding: 10px; min-height: 44px; border: 0; background: transparent;
  border-radius: var(--radius-sm); font: inherit; font-weight: 600; color: var(--ink);
}
.city-opt span { flex: 1; }
.city-opt svg { color: var(--ink-faint); }
.city-opt:hover:not(:disabled) { background: var(--surface-2); }
.city-opt.is-selected { color: var(--primary-ink); }
.city-opt.is-selected svg { color: var(--primary); }
.city-opt__check { color: var(--primary); }
/* Not-yet-launched towns stay clickable — clicking explains why. */
.city-opt.is-soon { color: var(--ink-faint); }
.city-opt.is-soon:hover { background: var(--surface-2); color: var(--ink-soft); }
.modal--soon { max-width: 420px; text-align: center; }
.modal__badge {
  display: inline-flex; align-items: center; gap: 5px; margin-bottom: var(--sp-3);
  font-size: 11.5px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  color: var(--seaglass-ink); background: var(--seaglass-tint);
  border: 1px solid var(--seaglass); border-radius: var(--radius-pill); padding: 3px 10px;
}
.modal--soon .modal__actions { justify-content: center; }
/* multi-select checkbox box (small square; must beat `.city-opt span{flex:1}`) */
.city-opt .city-box {
  flex: 0 0 16px; width: 16px; height: 16px; border-radius: 5px; border: 1.5px solid var(--line-strong);
  background: var(--surface); display: inline-flex; align-items: center; justify-content: center;
}
.city-box.is-on { background: var(--primary); border-color: var(--primary); }
.city-box svg { color: #fff; }
.city-opt.is-selected .city-box svg { color: #fff; }
.city-opt .soon {
  font-size: 9.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  color: var(--ink-soft); background: var(--surface-2); padding: 2px 7px; border-radius: var(--radius-pill);
  white-space: nowrap;
}

/* ---- Section nav (tabs left, Newsletter right) -------------------------- */
.section-nav { border-top: 1px solid var(--line); }
.section-nav__bar {
  max-width: var(--maxw-wide); margin: 0 auto; padding: 7px var(--sp-4);
  display: flex; align-items: center; gap: var(--sp-3);
}
.section-nav__inner {
  flex: 1; min-width: 0; display: flex; gap: 4px; overflow-x: auto; scrollbar-width: none;
}
.section-nav__inner::-webkit-scrollbar { display: none; }

/* One label per tab, chosen by width. Both are in the markup; `display:none`
   takes the unused one out of the accessibility tree as well as off screen. */
.tab__short { display: none; }
@media (max-width: 560px) {
  .tab__full { display: none; }
  .tab__short { display: inline; }
}

/* Belt and braces for phones narrower than the labels can be shortened for
   (320px, and anyone running a large text size). The tabs still scroll, but the
   right edge now fades instead of slicing a word clean in half with no hint
   that a whole section is out there. The mask is only applied while the row
   actually overflows — see `is-scrollable`, set in layout.js — so a row that
   fits is never dimmed for no reason. */
.section-nav__inner.is-scrollable {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent 100%);
  mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent 100%);
}
.section-nav__aside { flex: none; }
.section-nav__aside:not(.is-active) { color: var(--primary); }
.section-nav__aside:not(.is-active) svg { color: var(--primary); }
/* Business directory: same line as the tabs but deliberately quiet — it is a
   utility link, not a fourth section. */
.section-nav__dir {
  flex: none; display: inline-flex; align-items: center; gap: 6px;
  font-size: 13px; font-weight: 600; color: var(--ink-faint); white-space: nowrap;
}
.section-nav__dir svg { color: var(--ink-faint); }
.section-nav__dir:hover { color: var(--primary); text-decoration: none; }
.section-nav__dir:hover svg { color: var(--primary); }
@media (max-width: 620px) { .section-nav__dir span { display: none; } }
.tab {
  display: inline-flex; align-items: center; gap: 7px; white-space: nowrap;
  padding: 8px 14px; min-height: 44px; border-radius: var(--radius-pill);
  color: var(--ink-soft); font-weight: 600; font-size: 14.5px; transition: background .15s, color .15s;
}
.tab svg { color: var(--ink-faint); }
.tab:hover { background: var(--surface-2); color: var(--ink); text-decoration: none; }
.tab:hover svg { color: var(--ink-soft); }
.tab.is-active { background: var(--primary); color: var(--on-primary); }
.tab.is-active svg { color: #fff; }
.tab--soon { opacity: .75; }
/* "year" on Premium's allowance. The whole difference between that plan and
   the others is the period, and it was the one word in the sentence with no
   visual weight at all. */
.plan__year {
  display: inline-block; padding: 1px 7px; border-radius: var(--radius-pill);
  background: var(--accent); color: var(--on-primary);
  font-size: .92em; font-weight: 800; letter-spacing: .02em;
}
.tab__soon, .dash-soon-tag, .plan__soon {
  font-size: 9.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  color: var(--ink-soft); background: var(--surface-2); border: 1px solid var(--line-strong);
  border-radius: var(--radius-pill); padding: 1px 6px; margin-left: 4px; white-space: nowrap;
}
/* Sits inline at the end of a plan feature line. */
.plan__soon { display: inline-block; vertical-align: 1px; }
.dash-soon-tag { font-size: 10px; padding: 2px 9px; vertical-align: middle; }
.dash-card--soon { opacity: .72; }
/* "Coming soon" page placeholder */
.coming-soon { text-align: center; padding: var(--sp-7) var(--sp-4); max-width: 540px; margin: var(--sp-6) auto; }
.coming-soon > svg { color: var(--accent); }
.coming-soon__tag {
  display: inline-block; margin-top: var(--sp-3); font-size: 12px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .06em; color: var(--accent); background: var(--accent-tint); border: 1px solid var(--accent);
  border-radius: var(--radius-pill); padding: 3px 12px;
}
.coming-soon__title { font-family: var(--font-display); font-size: clamp(26px, 6vw, 34px); margin-top: var(--sp-3); }
.coming-soon__text { margin-top: var(--sp-3); color: var(--ink-soft); font-size: 16px; line-height: 1.6; }

/* Local Businesses, while it is closed. The trade list is the point of the
   page: a gym owner has to see "Gyms & Fitness" to know this is for them. */
/* The directory before anyone has registered. Not an error state — it is the
   model working as intended, so it explains rather than apologises. */
/* The "which are you?" sign-in chooser. Bigger than a share row on purpose —
   this is a fork in the road, not a menu item, and there are only two. */
/* ---- The unbooked Highlight slot -----------------------------------------
   Shown whenever nobody has taken the week. Borrows .hl-tile so it is exactly
   the shape and size a real Highlight occupies — the first version was a
   full-width banner that dwarfed the listings beneath it and made an unsold
   week the loudest thing on the page. The empty slot should look like the
   space it is offering, not like an advert for it.

   Dashed and tinted rather than solid and white, so a resident reads it as a
   space waiting to be filled instead of a listing worth clicking. */
.hl-tile--invite {
  border: 2px dashed var(--line-strong); background: var(--surface-2);
  box-shadow: none;
}
.hl-tile--invite:hover { border-color: var(--primary); background: var(--primary-tint); }
/* The gull, doing the thing the example describes.

   SCOPED TO .hl-tile--invite, and that specificity is the fix rather than a
   flourish. The base rule sizes the media image to 150x94 with object-fit
   COVER — right for a photograph, wrong for an illustration: it cropped the
   top of the bird's head off. A single class could not outrank it, so the
   first override silently lost and the crop stayed.

   Contained, with room around it. The box is padded and the image is capped
   below the box height, so the bob has somewhere to move into and the bird
   never touches an edge. .hl-tile has overflow:hidden, so anything that did
   would be sliced rather than merely tight. */
.hl-tile--invite .hl-tile__media {
  flex: 0 0 132px; padding: 12px 10px;
  background: var(--surface); border-right: 1px solid var(--line);
}
.hl-tile--invite .hl-tile__media img,
.hl-tile--invite .hl-tile__media .hl-skit {
  width: 100%; height: auto; max-height: 84px;
  object-fit: contain; display: block; margin: 0 auto;
}
/* ---- The placeholder skits ------------------------------------------------
   Six performances, one per empty Highlight slot; see assets/hl-skit.js for
   the markup and brand/HIGHLIGHT-SKITS.md for what each one is meant to show.

   EVERY TIMELINE RESOLVES TO THE RESTING POSE. `.is-playing` is added, held,
   and removed — so the tile settles back into exactly the picture it started
   as, and removing the class mid-flight is always safe. Props that have no
   business in the resting picture sit at opacity 0 until the skit runs.

   ONE LOOP EACH, and the JS plays left then right. Nothing here uses
   `infinite`: a page that keeps moving after you have arrived is a page you
   cannot read. The old whole-image bob is gone for the same reason.
   ------------------------------------------------------------------------- */
.hl-skit { width: 100%; height: auto; max-height: 84px; display: block; margin: 0 auto; }
.hl-skit text { font: 700 5px/1 var(--font-body, sans-serif); fill: var(--primary); }

/* Rotation and scaling read from each element's OWN box rather than the
   viewBox corner, so "spin the crab" means about the crab. */
.hl-skit .hs-crab, .hl-skit .hs-pair, .hl-skit .hs-bird, .hl-skit .hs-jaw,
.hl-skit .hs-beak-top, .hl-skit .hs-brush { transform-box: fill-box; transform-origin: 50% 50%; }

/* Anything that arrives during the skit starts invisible. */
.hl-skit .hs-n, .hl-skit .hs-steam { opacity: 0; }

/* ---- 1. Events, left: dancing on the umbrella --------------------------- */
.hs-umbrella-dance.is-playing .hs-dance { animation: hsSway 2.8s ease-in-out .2s 1 both; }
.hs-umbrella-dance.is-playing .hs-a1 { animation: hsNote 1.5s ease-out .5s 1 both; }
.hs-umbrella-dance.is-playing .hs-a2 { animation: hsNote 1.5s ease-out 1.0s 1 both; }
.hs-umbrella-dance.is-playing .hs-a3 { animation: hsNote 1.5s ease-out 1.5s 1 both; }
@keyframes hsSway {
  0%, 100% { transform: rotate(0) translateY(0); }
  20%      { transform: rotate(-7deg) translateY(-1px); }
  45%      { transform: rotate(7deg)  translateY(-2px); }
  70%      { transform: rotate(-5deg) translateY(-1px); }
}
@keyframes hsNote {
  0%   { opacity: 0; transform: translate(0, 0) scale(.7); }
  25%  { opacity: 1; }
  100% { opacity: 0; transform: translate(3px, -7px) scale(1); }
}

/* ---- 2. Events, right: two birds, spinning ------------------------------ */
.hs-pair-spin.is-playing .hs-pair { animation: hsSpinPair 3.2s cubic-bezier(.5,0,.5,1) .2s 1 both; }
.hs-pair-spin.is-playing .hs-a1 { animation: hsNote 1.6s ease-out .8s 1 both; }
.hs-pair-spin.is-playing .hs-a2 { animation: hsNote 1.6s ease-out 1.5s 1 both; }
.hs-pair-spin.is-playing .hs-a3 { animation: hsNote 1.6s ease-out 2.1s 1 both; }
/* A held pose, one full turn, and a settle — flamenco rather than a fan. */
@keyframes hsSpinPair {
  0%   { transform: rotate(0) scale(1); }
  12%  { transform: rotate(-8deg) scale(1.02); }
  62%  { transform: rotate(352deg) scale(1.02); }
  80%  { transform: rotate(368deg) scale(1); }
  100% { transform: rotate(360deg) scale(1); }
}

/* ---- 3. Classes, left: little ones jumping ------------------------------ */
/* Staggered, and never in unison — three birds hopping on the same beat reads
   as a machine, three off the beat reads as a playground. */
.hs-kids-play.is-playing .hs-kid1 { animation: hsHopKid 1.1s ease-in-out .2s 2 both; }
.hs-kids-play.is-playing .hs-kid2 { animation: hsHopKid 1.1s ease-in-out .55s 2 both; }
.hs-kids-play.is-playing .hs-kid3 { animation: hsHopKid 1.1s ease-in-out .9s 2 both; }
@keyframes hsHopKid {
  0%, 100% { transform: translateY(0) rotate(0); }
  35%      { transform: translateY(-5px) rotate(-6deg); }
  60%      { transform: translateY(0) rotate(3deg); }
  75%      { transform: translateY(-1.5px) rotate(0); }
}

/* ---- 4. Classes, right: painting ---------------------------------------- */
/* The canvas fills in as he works. Without the marks appearing, a still frame
   is a bird standing near a board. */
/* NOT hidden at rest. The marks were opacity:0 by default, so the resting
   picture — which is what the tile shows almost all of the time — was a bird
   standing beside a blank board. The finished painting is the payoff and
   belongs in the still frame, exactly as the pasta bowl does next door.

   `both` rather than `forwards` on the marks: it holds each one at opacity 0
   through its delay as well as after, so the canvas still fills in stroke by
   stroke during the skit and is complete again the moment it ends. */
.hs-painting.is-playing .hs-brush { animation: hsStroke 2.6s ease-in-out .2s 1 both; }
.hs-painting.is-playing .hs-p1 { animation: hsInk .5s ease-out .9s 1 both; }
.hs-painting.is-playing .hs-p2 { animation: hsInk .5s ease-out 1.6s 1 both; }
.hs-painting.is-playing .hs-p3 { animation: hsInk .5s ease-out 2.3s 1 both; }
@keyframes hsStroke {
  0%, 100% { transform: rotate(0) translate(0, 0); }
  20%      { transform: rotate(-12deg) translate(0, 2px); }
  45%      { transform: rotate(9deg)  translate(1px, -1px); }
  70%      { transform: rotate(-6deg) translate(0, 1px); }
}
@keyframes hsInk { from { opacity: 0; transform: scale(.6); } to { opacity: 1; transform: scale(1); } }
/* The painting stays up once made — he finished it. */
.hs-painting .hs-p1, .hs-painting .hs-p2, .hs-painting .hs-p3 { transform-box: fill-box; transform-origin: 50% 50%; }

/* ---- 5. Dining, left: menu out, pasta in -------------------------------- */
/* At rest the bowl is on the table: the payoff is the picture worth showing
   when nothing is moving. The menu only exists during the skit. */
.hs-menu-pasta .hs-menu { opacity: 0; }
.hs-menu-pasta.is-playing .hs-menu { animation: hsMenu 2.2s ease-in-out .2s 1 both; }
.hs-menu-pasta.is-playing .hs-bowl { animation: hsServe 1.1s cubic-bezier(.2,.9,.3,1.2) 2.1s 1 both; }
.hs-menu-pasta.is-playing .hs-diner { animation: hsRead 3.4s ease-in-out .2s 1 both; }
.hs-menu-pasta.is-playing .hs-s1 { animation: hsSteam 1.5s ease-out 3.0s 1 both; }
.hs-menu-pasta.is-playing .hs-s2 { animation: hsSteam 1.5s ease-out 3.35s 1 both; }
@keyframes hsMenu {
  0%   { opacity: 0; transform: translateY(4px); }
  18%  { opacity: 1; transform: translateY(0); }
  74%  { opacity: 1; transform: translateY(0); }
  100% { opacity: 0; transform: translateX(9px); }
}
@keyframes hsServe {
  0%   { opacity: 0; transform: translate(7px, 5px) scale(.85); }
  100% { opacity: 1; transform: translate(0, 0) scale(1); }
}
/* A read is a tilt down and back up, not a nod. */
@keyframes hsRead {
  0%, 100% { transform: rotate(0); }
  22%, 55% { transform: rotate(5deg); }
  80%      { transform: rotate(-2deg); }
}
@keyframes hsSteam {
  0%   { opacity: 0; transform: translateY(1px) scaleY(.7); }
  35%  { opacity: .85; }
  100% { opacity: 0; transform: translateY(-4px) scaleY(1.15); }
}

/* ---- 6. Dining, right: the crab toss ------------------------------------ */
/* Two nested transforms on purpose. .hs-arc carries the flight path and
   .hs-crab the spin, so the two never compete for one transform property —
   the same split brand-skit.js makes between the hop and the body. */
.hs-crab-toss .hs-arc { transform: translate(21.5px, 12.4px); }
.hs-crab-toss.is-playing .hs-arc  { animation: hsToss 2.4s cubic-bezier(.3,0,.4,1) .35s 1 both; }
.hs-crab-toss.is-playing .hs-crab { animation: hsCrabSpin 2.4s linear .35s 1 both; }
.hs-crab-toss.is-playing .hs-tosser { animation: hsLaunch 2.4s ease-in-out .35s 1 both; }
.hs-crab-toss.is-playing .hs-beak-top { animation: hsGape 2.4s ease-in-out .35s 1 both; }
@keyframes hsToss {
  0%   { transform: translate(21.5px, 12.4px); }
  12%  { transform: translate(21px, 14.5px); }  /* the dip before the throw */
  55%  { transform: translate(23px, 1.5px); }   /* apex */
  92%  { transform: translate(21.5px, 12.9px); } /* down into the open beak */
  100% { transform: translate(21.5px, 12.4px); }
}
@keyframes hsCrabSpin {
  0%   { transform: rotate(0); }
  12%  { transform: rotate(0); }
  92%  { transform: rotate(700deg); }
  100% { transform: rotate(720deg); }
}
/* A throw comes from the whole body, so he crouches and rises with it. */
@keyframes hsLaunch {
  0%, 100% { transform: translateY(0) rotate(0); }
  12%      { transform: translateY(1.5px) rotate(2deg); }
  30%      { transform: translateY(-1px) rotate(-3deg); }
  88%      { transform: translateY(0) rotate(-1deg); }
}
/* The beak opens to receive it and closes once it lands. */
@keyframes hsGape {
  0%, 20%  { transform: rotate(0); }
  45%, 88% { transform: rotate(-19deg); }
  100%     { transform: rotate(0); }
}


/* ---- Something that is built but not switched on yet ----------------------
   Collapsed and deliberately muted: it is a promise being kept in view, not a
   feature competing for attention. Greyed rather than hidden so a member can
   open it, see exactly what is coming, and stop looking for a link that is not
   there — a plan bullet promising analytics with nothing to point at is worse
   than an honest "not yet". */
.soon-group {
  margin-top: var(--sp-4); border: 1px dashed var(--line-strong);
  border-radius: var(--radius); background: var(--surface-2);
}
.soon-group > summary {
  display: flex; align-items: center; gap: 8px; cursor: pointer;
  padding: 12px 14px; font-weight: 700; color: var(--ink-soft);
  list-style: none;
}
.soon-group > summary::-webkit-details-marker { display: none; }
.soon-group > summary::after { content: '▾'; margin-left: auto; font-size: 11px; opacity: .7; }
.soon-group[open] > summary::after { content: '▴'; }
.soon-group > summary:hover { color: var(--ink); }
.soon-group > summary svg { flex: none; color: var(--ink-faint); }
/* Matches `.check__soon`, which is how this site already says "not yet" on the
   coming-soon towns: unbolded, parenthesised, quiet. The first version here was
   a bold uppercase pill — louder than the thing it was labelling, and the exact
   opposite of the discreet treatment agreed for the towns. One format for one
   idea, or the site looks like it was built by two people. */
.soon-tag { font-weight: 400; font-size: 13px; color: var(--ink-faint); }
.soon-group__body { padding: 0 14px 14px; }
/* The list of what is coming, held at reduced contrast so it reads as a
   preview rather than as something you can act on right now. */
.soon-list { list-style: none; margin: var(--sp-3) 0 0; padding: 0; display: grid; gap: 6px; }
.soon-list li { display: flex; gap: 8px; align-items: flex-start; font-size: 13.5px; color: var(--ink-faint); }
.soon-list svg { flex: none; margin-top: 3px; opacity: .6; }

/* ---- "Left for you", made actionable -------------------------------------
   The venue resolver's third tier refuses to guess, correctly, and used to be
   a plain list — a place plainly on the site with nowhere to give it a logo.
   Each row now carries the one thing no crawler has: the owner knowing where
   that venue's website is.

   Louder than .soon-list on purpose. That style is for things being reported;
   this is a list of things to do, and the two should not look alike. */
.vr-ask { list-style: none; margin: var(--sp-3) 0 0; padding: 0; display: grid; gap: 8px; }
.vr-ask__row {
  display: flex; flex-wrap: wrap; gap: 8px var(--sp-3); align-items: center;
  justify-content: space-between;
  padding: 10px 12px;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface);
}
.vr-ask__who { min-width: 220px; flex: 1 1 260px; font-size: 14px; color: var(--ink); }
.vr-ask__who strong { font-weight: 600; }
.vr-ask__n {
  margin-left: 6px; font-size: 12px; font-weight: 600; color: var(--ink-soft);
  white-space: nowrap;
}
/* The reason sits under the name rather than beside it: it is the longest text
   in the row and wrapping it around the controls makes the row unreadable. */
.vr-ask__who em {
  display: block; margin-top: 2px;
  font-style: normal; font-size: 12.5px; line-height: 1.45; color: var(--ink-faint);
}
.vr-ask__act { display: flex; gap: 8px; align-items: center; flex: 0 1 auto; flex-wrap: wrap; }
.vr-ask__act .field__input { width: 260px; max-width: 100%; font-size: 13.5px; }
.vr-ask__act .btn { flex: none; }
/* Done, and it says what happened. A row that looks untouched after a
   successful write is a row somebody presses twice. */
/* --seaglass is the palette's own 'this went well' colour; an invented
   --good would resolve to nothing and take the border with it. */
.vr-ask__row.is-done { border-color: var(--seaglass); background: var(--seaglass-tint); }
.vr-ask__done {
  display: flex; gap: 7px; align-items: flex-start; margin: 0;
  font-size: 13px; line-height: 1.5; color: var(--ink-soft);
}
.vr-ask__done svg { flex: none; margin-top: 2px; color: var(--seaglass-ink); }
@media (max-width: 560px) {
  .vr-ask__act { width: 100%; }
  .vr-ask__act .field__input { flex: 1 1 auto; width: auto; }
}

/* ---- Site health ---------------------------------------------------------
   Five rows, each resolving to a word and a sentence. Colour is a supporting
   signal only — the icon and the words carry it, so the panel still works in
   a screenshot, in monochrome, and for a colour-blind reader. */
.health__headline { font-size: 17px; font-weight: 700; margin-top: var(--sp-2); }
.health--green  .health__headline { color: var(--seaglass-ink); }
.health--yellow .health__headline { color: #B98410; }
.health--orange .health__headline { color: #C06A15; }
.health--red    .health__headline { color: var(--destructive); }

/* TWO COLUMNS ONCE THERE IS ROOM FOR TWO.

   Five rows in one column left half the card empty on a desktop and pushed the
   last two readings below the fold — on a panel whose whole job is to be taken
   in at a glance. `auto-fit` with a 380px floor does the switch without a
   breakpoint: one column on a phone, two the moment the card is wide enough to
   hold two without the sentences inside going narrow enough to hyphenate.

   `align-items: start` matters. Without it every row in a pair stretches to the
   height of the taller one, and a green row with one line of advice grows a
   pocket of empty space to match the amber one beside it. */
.hlist {
  list-style: none; margin: var(--sp-4) 0 0; padding: 0;
  display: grid; gap: 8px; align-items: start;
  /* `min(380px, 100%)`, not a bare 380px. A bare floor is a floor even when
     the container is narrower than it, so on a 375px phone the single column
     was still 380px wide and pushed the whole page into sideways scrolling.
     min() lets the track shrink to the container and keeps the floor
     everywhere it fits. */
  grid-template-columns: repeat(auto-fit, minmax(min(380px, 100%), 1fr));
}
.hrow {
  display: grid; gap: 2px 12px; align-items: baseline;
  /* The light column is a FIXED width, not `auto`. Each row is its own grid —
     tracks do not align between separate <li> elements — so an auto column
     sized itself to each row's own contents and the four dots landed at four
     different distances from the edge. A fixed track lines them up.

     72px is the housing measured, not guessed: four 10px lamps, three 5px
     gaps, 7px padding each side and a 1px border. */
  grid-template-columns: auto 1fr auto 72px;
  padding: 12px 14px; border: 1px solid var(--line);
  border-left: 4px solid var(--line-strong);
  border-radius: var(--radius); background: var(--surface);
}
/* FOUR LIGHTS. Orange is its own colour rather than a lighter red: "sort this
   out this week" and "do it today" are different instructions and should not
   look like the same one at two brightnesses. */
.hrow--green  { border-left-color: var(--signal-green); }
.hrow--yellow { border-left-color: var(--signal-yellow); background: #FDF6E4; }
.hrow--orange { border-left-color: var(--signal-orange); background: #FDEFE2; }
.hrow--red    { border-left-color: var(--destructive); background: var(--destructive-tint, #FDEDEA); }
.hrow--unknown { border-left-color: var(--ink-faint); }
/* EVERY ROW-1 ITEM NAMES ITS COLUMN. Only the icon and the light used to carry
   `grid-row: 1`, and an item with an explicit row is placed BEFORE the ones
   that auto-flow. So the light was put in the first free cell of row 1 —
   column 2 — and the name and number were pushed along behind it: tick, lamp,
   "Database", with the value overflowing the narrow last track. It read as a
   layout accident because it was one. Naming all four columns removes the
   auto-placement pass that caused it. */
.hrow__icon  { grid-row: 1; grid-column: 1; color: var(--ink-soft); }
.hrow__name  { grid-row: 1; grid-column: 2; }
.hrow__num   { grid-row: 1; grid-column: 3; }
.hrow__light { grid-row: 1; grid-column: 4; }
.hrow--green  .hrow__icon { color: var(--signal-green); }
.hrow--yellow .hrow__icon { color: #B98410; }
.hrow--orange .hrow__icon { color: #C06A15; }
.hrow--red    .hrow__icon { color: var(--destructive); }

/* THE WHOLE SCALE, NOT JUST YOUR PLACE ON IT. A single dot tells you its
   colour but not what the colour is measured against — amber means nothing
   until you can see there is an orange and a red above it, and green means
   nothing until you can see you are at the bottom of the run. So the housing
   carries all four lamps like a real traffic light: the unlit ones are dim but
   still plainly their own colour, and one is lit.

   Unknown lights none of them, which says "we could not read this" more
   honestly than a grey dot sitting where a colour belongs. */
.hrow__light {
  align-self: center; justify-self: end;
  display: flex; align-items: center; gap: 5px;
  padding: 4px 7px; border-radius: 999px;
  background: var(--surface-2); border: 1px solid var(--line);
}
.lamp {
  width: 10px; height: 10px; border-radius: 50%;
  background: currentColor;
  /* Dim, not grey: the point of showing an unlit lamp is that you can still
     tell which colour it would be. */
  opacity: .28;
}
/* A green you can read at 10px. The sage in --seaglass-ink is a text colour —
   dark enough to be legible as type, which is exactly what makes it ambiguous
   as a lamp: at this size it reads as "some dark thing", not as "green". */
.lamp--green  { color: var(--signal-green); }
.lamp--yellow { color: var(--signal-yellow); }
.lamp--orange { color: var(--signal-orange); }
.lamp--red    { color: var(--signal-red); }
.lamp.is-on {
  opacity: 1;
  /* Glows in its own colour — currentColor resolves per lamp, so this is one
     rule rather than four. The white ring keeps it from smearing into its
     neighbour. */
  box-shadow: 0 0 0 2px var(--surface), 0 0 7px currentColor;
}
/* ON A PHONE THE NUMBER DROPS TO ITS OWN LINE. Four things across does not fit
   375px: the name track cannot shrink below its longest word, and the number
   carries `nowrap` so "23 MB of 100.0 GB" cannot fold either — together they
   pushed the row 40px past the screen and the whole page scrolled sideways.

   The number is what gives way, because it is the one thing on the row that is
   repeated in the sentence underneath. The name says which limit this is and
   the light says how worried to be; both stay where the eye lands first. */
@media (max-width: 560px) {
  .hrow { grid-template-columns: auto 1fr 72px; }
  .hrow__num {
    grid-row: 2; grid-column: 2 / -1;
    white-space: normal; font-size: 13px; color: var(--ink-soft);
  }
  .hrow__body { grid-row: 3; }
}
@media (prefers-reduced-motion: no-preference) {
  .lamp.is-on { transition: opacity .2s ease, box-shadow .2s ease; }
}
.hrow__name { font-weight: 700; }
.hrow__name em { display: block; font-style: normal; font-weight: 400; font-size: 12.5px; color: var(--ink-faint); }
.hrow__num { font-variant-numeric: tabular-nums; font-weight: 600; white-space: nowrap; }
.hrow__per { font-weight: 400; color: var(--ink-faint); font-size: 12.5px; }
/* The sentence spans the full width underneath — it is the part that is
   actually read, and squeezing it into a column would cost it its meaning. */
/* The four answers, stacked full-width under the name and number. Squeezing
   them into columns would cost them the thing that makes them useful. */
.hrow__body { grid-column: 1 / -1; margin-top: 6px; display: grid; gap: 3px; font-size: 13.5px; }
.hrow__head { font-weight: 700; color: var(--ink); }
.hrow--yellow .hrow__head { color: #B98410; }
.hrow--orange .hrow__head { color: #C06A15; }
.hrow--red    .hrow__head { color: var(--destructive); }
.hrow__what { color: var(--ink-faint); }
/* The service's own words on a row that could not be read. Monospaced and
   faint: it is a diagnostic, not a sentence, and it should look like one so
   nobody mistakes it for advice they are meant to act on directly. */
.hrow__why {
  color: var(--ink-faint); font-size: 12px; font-family: ui-monospace, SFMono-Regular,
  Menlo, Consolas, monospace; word-break: break-word;
}
.hrow__risk { color: var(--ink-soft); }
/* The instruction. Boxed at warn and above so the eye lands on the thing to
   actually go and do, rather than on the numbers explaining why. */
.hrow__do { color: var(--ink-soft); }
.hrow--yellow .hrow__do, .hrow--orange .hrow__do, .hrow--red .hrow__do {
  margin-top: 4px; padding: 8px 10px; border-radius: var(--radius-sm, 8px);
  background: var(--surface-2); color: var(--ink); font-weight: 600;
}
.hrow--red .hrow__do { background: var(--surface); color: var(--destructive); }
.hrow__do svg { vertical-align: -2px; }

/* ---- Report something ----------------------------------------------------
   A quiet, permanent way to say "this should not be here". Fixed to the
   bottom-left so it is reachable on every page without sitting under the
   thumb on the right, where the reading hand and the primary actions are.

   Deliberately understated: it must be findable when somebody is upset about
   something they have just seen, and invisible the rest of the time. */
/* A CIRCLE, NOT A PILL. Shrinking the pill twice did not fix it, because the
   problem was never the size — it was that a control used once a year was
   wearing a word and sitting over the footer text. A 30px circle is a mark you
   can find when you want it and stop seeing when you do not.

   The word is not deleted, only hidden: it is still in the DOM for a screen
   reader and still in the `title` for a hover. Nothing is lost except the
   space it took. */
.report-fab {
  /* RIGHT, not left. On the left it sat over the footer text and the start of
     every line — the side the eye returns to on each pass. The right margin is
     the quietest corner of a left-aligned page. */
  /* left: auto is not redundant. This rule moved from left to right, and any
     stale `left` still in effect would win over `right` on a fixed element of
     auto width — which is exactly what happened while testing against a
     layered stylesheet. Stating both makes the position unambiguous whatever
     came before it. */
  position: fixed; right: 14px; left: auto; bottom: 14px; z-index: 60;
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; padding: 0;
  border-radius: 50%;
  border: 1px solid var(--line); background: var(--surface);
  color: var(--ink-faint); font: inherit;
  box-shadow: 0 1px 4px rgba(0,0,0,.08);
  cursor: pointer; text-decoration: none;
  /* Faint enough to stop registering as an element, present enough to find
     when you go looking. It comes back to full strength on hover and focus —
     rare use is the argument for hiding it, not for making it unusable. */
  opacity: .35; transition: opacity .15s, border-color .15s, color .15s;
}
/* 24px is below the comfortable touch target, so a phone gets a bigger one and
   a little more presence — there is no hover there to bring it back. */
@media (hover: none) and (pointer: coarse) {
  .report-fab { width: 34px; height: 34px; opacity: .5; }
}
.report-fab:hover, .report-fab:focus-visible {
  opacity: 1; color: var(--destructive); border-color: var(--destructive);
  text-decoration: none;
}
.report-fab:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
.report-fab svg { flex: none; }
/* On a phone it is the icon alone — a labelled pill that wide would sit over
   the content it is there to report. */
/* Hidden at every width now, and hidden the accessible way — clipped rather
   than display:none, so it is still announced. The tooltip carries it for a
   sighted reader. */
.report-fab__lbl {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
@media print { .report-fab { display: none; } }

/* ---- What you have sent us ----------------------------------------------
   The receipt half of the "send us your events" card. A green tick on a form
   proves the form worked; this proves somebody has the thing. */
.sub-head {
  font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  color: var(--ink-faint); margin-top: var(--sp-5); padding-top: var(--sp-4);
  border-top: 1px dashed var(--line);
}
.sub-list { list-style: none; margin: var(--sp-3) 0 0; padding: 0; display: grid; gap: 8px; }
.sub-row {
  display: grid; gap: 2px 12px; padding: 10px 12px;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-2);
  font-size: 13px;
}
.sub-row__what { font-weight: 600; color: var(--ink); }
.sub-row__what svg, .sub-row__state svg { vertical-align: -2px; }
.sub-row__size { font-weight: 400; color: var(--ink-faint); }
.sub-row__when { color: var(--ink-faint); font-size: 12.5px; }
.sub-row__state { color: var(--ink-soft); }
/* Their own note, quoted back. Clamped because somebody will paste a whole
   season in here and it should not push the rest of the page away. */
.sub-row__note {
  margin-top: 4px; padding-left: 10px; border-left: 2px solid var(--line-strong);
  color: var(--ink-soft); white-space: pre-wrap;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
@media (min-width: 620px) {
  .sub-row { grid-template-columns: 1fr auto; align-items: baseline; }
  .sub-row__state, .sub-row__note { grid-column: 1 / -1; }
}

/* The two ways into an account. Not `.share-row`, because each of these
   carries two links and a row that IS a link cannot contain them. */
.signin-opts { margin-top: var(--sp-4); display: grid; gap: 10px; }
/* TWO CARDS THAT WERE TOO ALIKE. This dialog is the fork in the whole site —
   a resident and a business go different ways for good from here — and it was
   two identical grey boxes you had to read to tell apart.

   The character is deliberately cheap: a tinted disc behind each icon, a 3px
   rule down the side in the same family, and a lift on hover. No new colours
   were invented — sage is the "this is yours" colour the Highlight of the Week
   already uses, navy is what every business surface is painted in — so the
   dialog now previews where each door leads rather than decorating itself. */
.signin-opt {
  display: flex; align-items: flex-start; gap: 12px;
  padding: var(--sp-4); border: 1px solid var(--line-strong);
  border-left-width: 3px;
  border-radius: var(--radius); background: var(--surface);
  transition: border-color .15s, box-shadow .15s, transform .15s;
}
/* The whole card lifts, not just the button. It is a choice between two
   things, so the thing that should feel pickable is the card. */
.signin-opt:hover { box-shadow: var(--shadow-hover); transform: translateY(-1px); }

.signin-opt__badge {
  flex: none; width: 40px; height: 40px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
}
.signin-opt__badge svg { color: inherit; }

/* THE FILL IS MIXED FROM THE SAME TOKEN, not typed as a second colour.

   color-mix keeps one source of truth: change --seaglass and the wash follows
   it. A hand-picked rgba() would be a copy to drift, which is the mistake this
   file has already made with the price presets and the audience label.

   8% and 6% — navy is much darker than sage, so an equal share of it would
   read as a stronger fill for the same intent. Both land a hair off the paper:
   enough that the two cards are different objects, not so much that they
   compete with the buttons inside them.

   `background: var(--surface)` above stays as the fallback. A browser without
   color-mix ignores the line below and keeps the plain card, which is exactly
   what it had yesterday. */
/* TWO COLOURS YOU CANNOT MISTAKE (owner, 16 Sep 2026: "make these two boxes
   different colours. I keep logging into the wrong type of account all the
   time"). The 8% and 6% washes read as the same beige; the cards now wear
   the full tints, a coloured border all round, a coloured title and, on the
   neighbour's, a sea-glass button - so the two differ before a word is read. */
.signin-opt--you {
  border-color: var(--seaglass); border-left-color: var(--seaglass-ink);
  background: var(--seaglass-tint);
}
.signin-opt--you .signin-opt__body strong { color: var(--seaglass-ink); }
.signin-opt--you .signin-opt__badge { background: var(--surface); color: var(--seaglass-ink); }
.signin-opt--you .btn--primary { background: var(--seaglass-ink); border-color: var(--seaglass-ink); }
.signin-opt--you .signin-opt__alt a { color: var(--seaglass-ink); }
.signin-opt--you:hover { border-color: var(--seaglass-ink); }

/* Navy — what every plan, dashboard and business surface already is. 9.58:1. */
.signin-opt--biz {
  border-color: var(--primary); border-left-color: var(--primary-ink);
  background: var(--primary-tint);
}
.signin-opt--biz .signin-opt__body strong { color: var(--primary-ink); }
.signin-opt--biz .signin-opt__badge { background: var(--surface); color: var(--primary); }
.signin-opt--biz:hover { border-color: var(--primary-ink); }

/* Somebody who prefers less motion still gets the colour and the border; they
   just do not get the card moving under the pointer. */
@media (prefers-reduced-motion: reduce) {
  .signin-opt { transition: none; }
  .signin-opt:hover { transform: none; }
}
.signin-opt__body { min-width: 0; }
.signin-opt__body strong { display: block; font-size: 16.5px; font-weight: 700; }
.signin-opt__body em { display: block; font-style: normal; font-size: 13px; color: var(--ink-soft); margin-top: 2px; }
/* Wraps rather than pushing the dialog wide: "Create a free account" beside
   "or sign in" is close to the full width of the modal on a phone. */
/* THE SECOND WAY IN IS A CONTROL, NOT A FOOTNOTE STUCK TO A BUTTON.

   "The sign in is very small and way too close to the button to create an
   account... we need to make it some sort of a more discrete button, but not
   overlap with other existing buttons." Both platforms.

   It was 13px of grey text with a 10px gap between it and a filled navy
   button, so on a phone it read as spillage off the corner of the CTA rather
   than as the other choice. It is also the door for every returning member,
   which is most of the people who ever open this dialog.

   So it becomes an outlined pill the same height as the button beside it —
   quiet, because the primary action is still the filled one, but plainly its
   own control with its own edge. `gap` goes to a full 12px, and the row keeps
   wrapping, so on a narrow card the two sit one above the other rather than
   crushed together. */
.signin-opt__acts {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px 12px;
  margin-top: var(--sp-4);
}
.signin-opt__alt {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 13.5px; color: var(--ink-soft);
  padding: 7px 14px; min-height: 38px;
  border: 1px solid var(--line-strong); border-radius: var(--radius-pill);
  background: var(--surface); white-space: nowrap;
}
.signin-opt__alt a { font-weight: 700; color: var(--primary); }
.signin-opt__alt:hover { border-color: var(--primary); }

.dir-empty {
  text-align: center; max-width: 560px; margin: var(--sp-6) auto;
  padding: var(--sp-7) var(--sp-4);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
}

/* The same panel while we are still asking, so the swap to the real answer is
   a change of words rather than a change of shape. Dimmed a little because it
   is a state, not a statement — see NL.nothingYet. */
.dir-empty--waiting { opacity: .72; }
.dir-empty--waiting .dir-empty__title { font-weight: 600; }

.dir-empty > svg { color: var(--ink-faint); }
.dir-empty__title { font-family: var(--font-display); font-size: 22px; font-weight: 600; margin-top: var(--sp-3); }
.dir-empty__text { margin-top: var(--sp-3); color: var(--ink-soft); font-size: 15.5px; line-height: 1.6; }
.dir-empty .mycal-cta { justify-content: center; }

.soon-block { max-width: 640px; margin: var(--sp-7) auto 0; }
.soon-trades { display: flex; flex-wrap: wrap; gap: 8px; margin-top: var(--sp-3); }
.soon-trade {
  display: inline-flex; align-items: center; gap: 7px;
  border: 1px solid var(--line); background: var(--surface);
  color: var(--ink-soft); font-size: 13.5px; font-weight: 600;
  padding: 7px 12px; border-radius: var(--radius-pill);
}
.soon-form { display: flex; gap: var(--sp-3); align-items: flex-start; margin-top: var(--sp-3); }
.soon-form .field { flex: 1; margin: 0; }
.soon-form .btn { min-height: 52px; white-space: nowrap; }
@media (max-width: 520px) {
  .soon-form { flex-direction: column; }
  .soon-form .btn { width: 100%; }
}

/* ---- Section headings (used inside Families / Weekender / Dining) -------- */
.section-title {
  font-family: var(--font-display); font-size: 24px; font-weight: 600; letter-spacing: -.01em;
  margin: var(--sp-7) 0 var(--sp-2); display: flex; align-items: center; gap: 10px;
}
.section-title:first-of-type { margin-top: var(--sp-5); }
.section-title svg { color: var(--primary); }
.section-sub { color: var(--ink-soft); font-size: 15px; margin: 0 0 var(--sp-4); }

/* ---- Venue "type" chip (Bars & Restaurants + family guide) --------------- */
.venue-card__type {
  font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
  color: var(--accent); background: var(--accent-tint); align-self: flex-start;
  padding: 3px 10px; border-radius: var(--radius-pill); margin-bottom: 8px;
}

/* ---- Bars & Restaurants: inline upcoming events on each venue card ------- */
.mini-events { margin-top: var(--sp-4); border-top: 1px solid var(--line); padding-top: var(--sp-3); }
.mini-events__label {
  font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
  color: var(--ink-faint); margin-bottom: var(--sp-2);
}
.mini-event {
  display: flex; align-items: baseline; gap: 10px; padding: 8px 0; color: inherit;
  border-bottom: 1px solid var(--line);
}
.mini-event:last-child { border-bottom: 0; }
.mini-event:hover { text-decoration: none; }
.mini-event:hover .mini-event__title { color: var(--primary-ink); text-decoration: underline; }
.mini-event__date {
  flex: none; width: 56px; font-size: 12.5px; font-weight: 700; color: var(--accent);
  text-transform: uppercase; letter-spacing: .02em;
}
.mini-event__title { font-size: 14.5px; font-weight: 600; color: var(--ink); }
.mini-event__cat { font-size: 12.5px; color: var(--ink-faint); }
.mini-empty { color: var(--ink-faint); font-size: 13.5px; font-style: italic; }

/* ---- Weekender: editorial pick list ------------------------------------- */
/* Hero kept deliberately compact (~25% less real estate than a full hero). */
.weekender-hero {
  background: var(--primary); color: #fff; border-radius: var(--radius);
  padding: var(--sp-4) var(--sp-5); margin-top: var(--sp-5);
}
.weekender-hero__kicker {
  font-size: 11.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .1em;
  color: var(--primary-tint); display: inline-flex; align-items: center; gap: 7px;
}
.weekender-hero h1 { color: #fff; font-size: clamp(22px, 5vw, 30px); margin-top: var(--sp-2); letter-spacing: -.02em; }
.weekender-hero p { color: rgba(255,255,255,.9); margin-top: var(--sp-2); font-size: 15px; }

.pick {
  display: grid; grid-template-columns: 44px 1fr; gap: var(--sp-4);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  padding: var(--sp-5); margin-top: var(--sp-4); box-shadow: var(--shadow);
}
.pick__rank { font-family: var(--font-display); font-size: 30px; font-weight: 600; color: var(--accent); line-height: 1; }
.pick__note {
  font-family: var(--font-display); font-size: 18px; font-style: italic; line-height: 1.4;
  color: var(--ink); margin: 0 0 var(--sp-3);
}
.pick__meta { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 14px; color: var(--ink-soft); margin-bottom: var(--sp-3); }
.pick__meta span { display: inline-flex; align-items: center; gap: 6px; }
.pick__meta svg { color: var(--ink-faint); }

/* ---- Empty section note -------------------------------------------------- */
.section-empty {
  text-align: center; padding: var(--sp-7) var(--sp-4); color: var(--ink-soft);
  border: 1px dashed var(--line-strong); border-radius: var(--radius); background: var(--surface); margin-top: var(--sp-4);
}

@media (min-width: 620px) {
  .weekender-hero { padding: var(--sp-5) var(--sp-6); }
}

/* ---- Subcategory filter bar (Families / Bars & Restaurants) -------------- */
.filter-group--sub { margin: var(--sp-5) 0 var(--sp-2); }

/* A long taxonomy (the dining "Type" list) — slightly tighter chips so the
   whole set lands on two lines instead of spilling onto a third. */
/* Two columns on a phone are only worth having if the longest label still
   fits on one line in one of them. This is the same step-down the compact
   variant below already uses, applied by width rather than by caller. */
@media (max-width: 430px) {
  .chips { grid-template-columns: repeat(auto-fit, minmax(136px, 1fr)); gap: 6px; }
  /* Tighter than the compact variant on purpose: every pixel taken off the
     padding and the count is a pixel the LABEL gets, and the label is the only
     part a reader is looking for. Two columns are worth having only if most
     names still fit on one line inside one. */
  .chips .chip { padding: 8px 9px; font-size: 12.5px; gap: 5px; min-height: 36px; }
  .chips .chip__count { font-size: 10.5px; padding: 1px 4px; }
  .chips .chip__dot { width: 7px; height: 7px; }
}

.filter-group--compact .chips { gap: 6px; }
.filter-group--compact .chip { padding: 8px 11px; font-size: 13.5px; }
.filter-group--compact .chip__count { font-size: 11px; padding: 1px 5px; }

.chip__count {
  font-size: 12px; font-weight: 700; color: var(--ink-faint);
  background: var(--surface-2); border-radius: var(--radius-pill);
  padding: 1px 7px; margin-left: 2px; min-width: 20px; text-align: center;
}
.chip[aria-pressed="true"] .chip__count { background: rgba(255,255,255,.22); color: #fff; }

/* Cuisine label on a dining card */
.venue-card__cuisine {
  font-size: 13px; font-weight: 600; color: var(--ink-faint);
  display: inline-flex; align-items: center; gap: 6px; margin-top: 2px;
}
.venue-card__cuisine svg { color: var(--accent); }

/* Small note under a section filter showing sort order */
.sort-note { font-size: 13px; color: var(--ink-faint); margin: var(--sp-3) 0 0; }

/* ---- Header tools cluster (resident button / city selector / claim chip) - */
.header-tools { margin-left: auto; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
/* Newsletter link — its own line beneath the town button, right-aligned.
   flex-basis:100% is what drops it to the second row of .header-tools.
   Small and low-contrast on purpose: it collects interest in something that
   does not exist yet, so it must not outrank Sign in or the sections. */
.news-link {
  flex-basis: 100%; display: inline-flex; align-items: center; justify-content: flex-end;
  gap: 5px; margin-top: 2px;
  font-size: 12.5px; font-weight: 600; color: var(--ink-faint); text-decoration: none;
}
.news-link:hover { color: var(--primary); text-decoration: none; }
.news-link svg { color: var(--primary); }
/* GONE FROM THE PHONE HEADER ENTIRELY. It used to shed its word here and keep
   the envelope, on the reasoning that the header was already tight. It was
   tighter than that: `.news-link` takes `flex-basis: 100%`, so inside
   `.header-tools` it claimed a line of its own and set that block's width to
   94px. With `.brand` at 245px and a 16px gap, the header row needed 355px of
   a 343px screen — over by twelve pixels — so the whole tools block wrapped
   under the wordmark and the header stood 124px tall instead of 58.

   Two-thirds of a header, spent on an envelope, on every page.

   The owner asked for it to go from the phone ("I think we do not need that
   newsletter up there", 31 Aug 2026). Removing it also unwraps the header,
   which is the larger win and was not why they asked. Still there on a desktop,
   where there is room for it; the footer link is the way to it on a phone. */
@media (max-width: 620px) {
  .news-link { display: none; }
}
/* IN SPACE THAT ALREADY EXISTS. It began under Newsletter, which added a row
   to a header that has plenty — and a site about what is happening this week
   should not spend more of the screen on itself. The tabs bar is one line with
   a wide empty right-hand end on every page, and `.section-nav__inner` already
   takes `flex: 1`, so this lands against the right edge without a rule.

   SENTENCE CASE. The tagline under the wordmark is uppercase because it is a
   label; this is something somebody said, and small caps would flatten it into
   another label. */
.header-slogan {
  flex: none; font-size: 12px; font-style: italic; color: var(--ink-faint);
  white-space: nowrap;
}
@media (max-width: 900px) {
  /* Out well before it can crowd the tabs, which are the thing people came to
     press. The wordmark's own tagline still says most of this. */
  .header-slogan { display: none; }
}

/* The resident's way in. Ghost-weight on purpose: browsing needs no account,
   so this must never look like a wall. The count is the actual argument. */
.me-btn {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 7px 12px; min-height: 44px; border: 1px solid var(--line-strong); border-radius: var(--radius-pill);
  background: var(--surface); color: var(--ink-soft);
  font-size: 14px; font-weight: 700; text-decoration: none; white-space: nowrap;
  transition: border-color .15s, color .15s, background .15s;
  /* Signed out this is a <button> (it opens the chooser) and signed in it is an
     <a>. Both must look identical, so the font is not left to the UA default. */
  font-family: inherit; cursor: pointer;
}
.me-btn:hover { border-color: var(--primary); color: var(--primary); background: var(--surface-2); text-decoration: none; }
.me-btn:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
.me-btn.is-in { color: var(--primary-ink); border-color: var(--primary); background: var(--primary-tint); }
.me-btn__n {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 19px; height: 19px; padding: 0 5px; border-radius: var(--radius-pill);
  background: var(--accent); color: #fff; font-size: 11px; font-weight: 800;
}
.me-btn.is-in .me-btn__n { background: var(--primary); }
.me-btn__tight { display: none; }
@media (max-width: 560px) {
  .me-btn__lbl { display: none; }
  .me-btn { padding: 7px 10px; }
  /* The signed-out invitation keeps a word where the rest go icon-only. */
  .me-btn__tight { display: inline; }
  /* The signed-in organization is a person glyph in its tinted pill, and that
     is the whole control at this width. Two earlier attempts lived here — a
     shop glyph, then the account's initials — and the note in residentBtn
     records why neither survived. */
  .me-btn--org { justify-content: center; min-width: 44px; }
  .me-btn--org {
    font-size: 12.5px; font-weight: 800; letter-spacing: .02em;
    justify-content: center; min-width: 44px;
  }
}
.claim-chip {
  display: inline-flex; align-items: center; gap: 6px; max-width: 60vw;
  font-size: 14px; font-weight: 700; color: var(--accent); background: var(--accent-tint);
  border: 1px solid var(--accent); border-radius: var(--radius-pill); padding: 7px 13px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.claim-chip svg { color: var(--accent); flex: none; }
.city-select { position: relative; margin-left: 0; }

/* ---- Footer: main + legal rows ------------------------------------------ */
.site-footer__main, .site-footer__legal { display: flex; flex-wrap: wrap; gap: var(--sp-4); align-items: baseline; }
.site-footer__main nav { margin-left: auto; display: flex; flex-wrap: wrap; gap: var(--sp-4); }
.site-footer__main nav a { color: var(--ink-soft); font-size: 14px; font-weight: 600; }
/* STACKED, AND TUCKED INTO THE CORNER. The copyright sat opposite the links,
   holding down the whole left end of the row for a line nobody reads. Under
   them and right-aligned it stops being one of two things competing across the
   footer's width and becomes what it is: the last, smallest mark on the page.

   Column direction with `align-items: flex-end` rather than `text-align`,
   because the nav is a flex row of its own and only the box alignment moves
   it. The nav comes first in the MARKUP too — reordering with `order` would
   leave the owner-door button in the copyright taking focus before the links
   that sit above it. */
/* `.site-footer .site-footer__legal`, TWO CLASSES ON PURPOSE. `.site-footer
   .wrap` up at line 658 also matches this element and sets align-items:
   baseline — two classes to one, so it wins on specificity no matter how far
   down the file this sits. A single-class rule here looked correct, was later
   in the sheet, and silently did nothing. */
.site-footer .site-footer__legal {
  margin-top: var(--sp-5); padding-top: var(--sp-4); border-top: 1px solid var(--line-strong);
  flex-direction: column; align-items: flex-end; gap: var(--sp-2);
}
/* Same reason: `.site-footer p` sets 13.5px with two classes' worth of weight. */
.site-footer .site-footer__legal p {
  margin: 0; font-size: 11.5px; color: var(--ink-faint); text-align: right;
}
/* flex-wrap, like the nav above it. Without it the seven legal links sat on one
   unbreakable line ~426px wide, which on a 390px phone pushed the whole
   document sideways — the page scrolled horizontally on every screen. */
/* `justify-content: flex-end`, not `margin-left: auto`. The auto margin was
   what pushed the nav right while this row was a horizontal flex; in a column
   it does nothing useful, and it would leave the links flush LEFT on a phone
   where they wrap onto two rows — with the copyright still tucked right
   underneath them. Both ends of the block line up now, at every width. */
.site-footer .site-footer__legal nav {
  display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--sp-4);
  margin-left: 0;
}
.site-footer__legal nav a { color: var(--ink-soft); font-size: 13.5px; font-weight: 600; }

/* THE FOOTER LINKS, FOLDED ON A PHONE. See footerHTML and foldFooterNav in
   assets/layout.js, which sets `open` above 621px so this is a plain row on a
   laptop and a single line on a phone. Nothing here hides the links; the
   element does that itself when it is shut, which is why the attribute is set
   in script rather than faked in CSS. */
.foot-nav { min-width: 0; }
/* AND THE SHUT FOLD HAS TO BE TOLD TO STAY SHUT.

   A browser hides a closed <details>'s contents with a rule of its own, of the
   shape `details:not([open]) > *:not(summary) { display: none }`. Any author
   rule that sets `display` on that child beats it, and there is one a few
   lines up: `.site-footer .site-footer__legal nav { display: flex }`. So the
   fold reported itself closed, the caret pointed the right way, and all seven
   links sat there in the open anyway.

   Four classes deep on purpose, to outrank the two-class rule above rather
   than to fight it with !important. */
.site-footer .site-footer__legal .foot-nav:not([open]) > nav { display: none; }
.foot-nav > summary {
  cursor: pointer; list-style: none; display: inline-flex; align-items: center;
  gap: 6px; padding: 8px 0; min-height: 44px;
  color: var(--ink-soft); font-size: 13.5px; font-weight: 600;
}
.foot-nav > summary::-webkit-details-marker { display: none; }
/* A caret drawn in CSS rather than an icon, because this summary is written in
   footerHTML as a plain string and has no place to put one. */
.foot-nav > summary::after {
  content: ''; width: 7px; height: 7px; margin-top: -3px;
  border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: rotate(45deg); transition: transform .15s;
}
.foot-nav[open] > summary::after { transform: rotate(-135deg); margin-top: 2px; }
.foot-nav > summary:hover { color: var(--ink); }
/* Above phone width the fold is always open, so the word would be a label on a
   row that needs no label. */
@media (min-width: 621px) {
  .foot-nav > summary { display: none; }
}
@media (max-width: 620px) {
  /* Open, the links stack rather than spreading over two ragged rows. */
  .site-footer .site-footer__legal .foot-nav nav {
    flex-direction: column; align-items: flex-start; gap: var(--sp-3);
    margin-top: var(--sp-2);
  }
}

/* The owner's door: the separator in the copyright line, which is a link.

   Inherits its colour and weight so it is genuinely the punctuation it looks
   like. The padding is the real work — a bare "·" is a ~4px target, which is
   unhittable on a phone; the negative margin spends that padding without
   moving the text a pixel. Hover and focus give it away deliberately: the
   owner has to be able to FIND it, and a control nobody can locate on purpose
   is not discreet, it is broken. */
.owner-door {
  /* A <button> now (see layout.js), so the browser defaults have to be undone
     for it to read as the punctuation it is. */
  appearance: none; background: none; border: 0;
  font: inherit; line-height: inherit;
  color: inherit; text-decoration: none; cursor: default;
  /* The tap area stays generous vertically and is pulled back to nothing
     horizontally, so the glyph sits hard against the "s" the way a full stop
     does. Padding that reads as a gap is what made it look like a control. */
  padding: 10px 8px 10px 1px; margin: -10px -8px -10px -1px;
  border-radius: 4px;
}
/* NO HOVER STATE. It used to colour up and take a tint, which was meant to help
   the owner find it and had the side effect of announcing it to anyone whose
   cursor crossed the copyright line. Concealment was the point; a control that
   lights up is not concealed.

   Focus IS still shown, and that is not an oversight. A focusable control with
   no visible focus is unusable by keyboard, and hiding it from that one group
   would buy nothing — see the note in layout.js about what actually protects
   the page. */
.owner-door:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ---- Business directory ------------------------------------------------- */
.dir-note {
  display: flex; gap: 8px; align-items: flex-start; margin: var(--sp-4) 0 0;
  font-size: 14px; line-height: 1.55; color: var(--ink-soft); background: var(--surface-2);
  border-radius: var(--radius-sm); padding: 11px 13px;
}
.dir-note svg { color: var(--accent); flex: none; margin-top: 2px; }
.dir-town { margin-top: var(--sp-6); }
.dir-cat { margin-top: var(--sp-6); }
.dir-cat__title {
  font-family: var(--font-display); font-size: 22px; font-weight: 600; color: var(--accent);
  letter-spacing: -.01em; padding-bottom: var(--sp-2); border-bottom: 2px solid var(--accent-tint);
  display: flex; align-items: baseline; gap: 8px;
}
.dir-cat__count {
  font-family: var(--font-body); font-size: 12px; font-weight: 700; color: var(--accent);
  background: var(--accent-tint); border-radius: var(--radius-pill); padding: 1px 8px;
}
.dir-list { list-style: none; margin: 0; padding: 0; }
.dir-item {
  display: flex; align-items: center; gap: var(--sp-3); justify-content: space-between;
  padding: 11px 2px; border-bottom: 1px solid var(--line); min-height: 48px;
}
.dir-item:last-child { border-bottom: 0; }
.dir-item__name {
  font-size: 15.5px; font-weight: 600; color: var(--ink);
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 3px 8px; min-width: 0;
}
.dir-item__addr { font-style: normal; font-size: 13px; font-weight: 400; color: var(--ink-faint); }
/* Provenance: how much to trust this row, and when we last had reason to. */
.dir-verify {
  font-size: 11.5px; font-weight: 700; letter-spacing: .02em; color: var(--seaglass-ink);
  background: var(--seaglass-tint); border-radius: var(--radius-pill); padding: 1px 8px;
  display: inline-flex; align-items: center; gap: 3px; white-space: nowrap;
}
.dir-verify--owner { color: var(--primary-ink); background: var(--primary-tint); }
.dir-verify--soft { color: var(--ink-soft); background: var(--surface-2); }
.dir-verify--closed { color: var(--destructive); background: var(--accent-tint); }
.dir-verify svg { color: currentColor; }
.dir-item.is-closed .dir-item__name { text-decoration: line-through; color: var(--ink-faint); }
.dir-report {
  display: inline-flex; align-items: center; gap: 4px; font-size: 12.5px;
  color: var(--ink-faint); white-space: nowrap;
}
.dir-report:hover { color: var(--destructive); text-decoration: none; }
.dir-report svg { color: currentColor; }
.dir-source {
  display: flex; gap: 8px; align-items: flex-start; margin-top: var(--sp-6);
  padding-top: var(--sp-4); border-top: 1px solid var(--line);
  font-size: 12.5px; line-height: 1.6; color: var(--ink-faint);
}
.dir-source svg { flex: none; margin-top: 3px; }
.dir-item__link {
  flex: none; display: inline-flex; align-items: center; gap: 5px; font-size: 13.5px; font-weight: 600;
  color: var(--primary); white-space: nowrap;
}
.dir-item__link svg { color: currentColor; }
.dir-item__link--soft { color: var(--ink-faint); }
.dir-item__link--soft:hover { color: var(--ink-soft); }
.dir-item__meta { flex: none; display: inline-flex; align-items: center; gap: var(--sp-3); }
/* Claim / claimed indicator */
.dir-claim {
  display: inline-flex; align-items: center; gap: 4px; font-size: 12.5px; font-weight: 700;
  padding: 3px 9px; border-radius: var(--radius-pill); white-space: nowrap;
}
a.dir-claim { color: var(--accent); background: var(--accent-tint); }
a.dir-claim:hover { background: var(--surface); text-decoration: none; box-shadow: inset 0 0 0 1px var(--accent); }
.dir-claim.is-claimed { color: var(--primary-ink); background: var(--primary-tint); }
.dir-claim svg { color: currentColor; }
@media (max-width: 520px) {
  .dir-item { flex-wrap: wrap; align-items: flex-start; }
  .dir-item__name { flex: 1 1 100%; }
  .dir-claim { font-size: 12px; }
  .dir-item__meta { gap: var(--sp-2); }
}

@media (min-width: 640px) {
  .dir-list { columns: 2; column-gap: var(--sp-6); }
  .dir-item { break-inside: avoid; }
}

/* ---- Business account signup ------------------------------------------- */
.req-star { color: var(--destructive); font-weight: 700; }
.field__opt { color: var(--ink-faint); font-weight: 600; font-size: 12px; }
/* "recommended", for a settings field that is worth filling in and will never
   stop a save. NOT the red asterisk: red says something is wrong, and an empty
   field somebody has not got round to is not wrong. Sits at the same weight as
   "(optional)" because both are the same kind of aside — a note about the
   field, not a verdict on it. Unbracketed so the two are still telling you
   different things at a glance. */
.field__rec {
  color: var(--ink-faint); font-weight: 600; font-size: 12px;
  font-style: italic; letter-spacing: .01em;
}
.signup-plan {
  display: inline-flex; align-items: center; gap: 7px; margin-top: var(--sp-4);
  font-size: 15px; color: var(--ink-soft); background: var(--surface-2);
  padding: 7px 14px; border-radius: var(--radius-pill);
}
.signup-plan svg { color: var(--accent); }
.signup-plan__tag {
  display: inline-block; margin-left: 6px; font-size: 11.5px; font-weight: 700;
  color: var(--seaglass-ink); background: var(--seaglass-tint);
  border: 1px solid var(--seaglass); border-radius: var(--radius-pill); padding: 2px 9px;
}
.signup-plan strong { color: var(--ink); }
.biz-match { font-size: 13px; font-weight: 600; display: none; }
.biz-match.ok, .biz-match.new { display: block; }
.biz-match.ok { color: var(--primary-ink); }
.biz-match.new { color: var(--accent); }
.signup-optional { margin-top: var(--sp-5); padding-top: var(--sp-4); border-top: 1px solid var(--line); }

/* "Is there a better page for us to read?" — collapsed by default.
   Most venues have an ordinary events page and should skim past this; the ones
   who need it are the ones who already know their site is unusual. Closed keeps
   the form short for the majority without hiding it from the minority. */
.signup-hint { margin-top: var(--sp-3); border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); }
.signup-hint > summary {
  display: flex; align-items: center; gap: 8px; cursor: pointer;
  padding: var(--sp-3) var(--sp-4); font-weight: 600; font-size: 14.5px;
  list-style: none;
}
.signup-hint > summary::-webkit-details-marker { display: none; }
.signup-hint > summary svg { flex: none; color: var(--primary); }
.signup-hint > summary em {
  font-style: normal; font-weight: 500; color: var(--ink-faint); margin-left: 4px;
}
.signup-hint > summary:hover { color: var(--primary); }
.signup-hint[open] > summary { border-bottom: 1px solid var(--line); }
.signup-hint__body { padding: var(--sp-4); }
.signup-hint__body .field:last-child { margin-bottom: 0; }
.signup-hint textarea, #set-notes { resize: vertical; min-height: 68px; font: inherit; }
.signup-subhead { font-family: var(--font-display); font-size: 16px; font-weight: 600; color: var(--ink); margin: 0; }
/* AIR ABOVE A SECTION HEADING THAT FOLLOWS SOMETHING.

   This had margin: 0 flat, so on the join form "What we send you" sat with a
   measured ZERO-pixel gap against the bottom of the "Tell us a bit more" panel
   — the heading of one section touching the box of the previous one, which
   reads as a rendering fault rather than as a new section.

   It was also inconsistent with itself: .signup-subhead--sub, the QUIETER
   nested variant, already claimed 24px. The subordinate heading had more air
   than the section heading above it.

   :not(:first-child), because most of the other seventeen uses open a card and
   a top margin there would push the heading off its own panel. Only a subhead
   with something before it starts a new section. */
.signup-subhead:not(:first-child) { margin-top: var(--sp-5); }
/* A heading inside a section (e.g. "Billing address" within Card on file) —
   quieter than the section head so the nesting is obvious. */
.signup-subhead--sub {
  font-family: var(--font-body); font-size: 12px; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase; color: var(--ink-faint);
  margin-top: var(--sp-5);
}
/* align-items:end so a field carrying an extra hint line does not push its
   input out of line with the one beside it. */
/* TWO FIELDS SIDE BY SIDE, AND minmax(0, …) IS NOT DECORATION.

   A bare `1fr` carries an automatic minimum of MIN-CONTENT, so a grid column
   will refuse to shrink below whatever its widest unbreakable thing needs and
   simply overflow instead. `<input type="time">` is exactly that: measured
   here, one column of the Start/End pair will not go under 141px, so the pair
   plus the 12px gap wants 294px. A 320px phone gives the row 288. Six pixels
   short, and the second field runs off the edge of the card.

   Reported with a photo: "the boxes here are just overlapping on each other,
   and that's very poor design... it looks fine on a computer, but here for
   some reason it doesn't adjust automatically."

   minmax(0, 1fr) removes that floor so a row can never push past its
   container at ANY width. The stack below is what stops the fields being
   squeezed into something too narrow to use. Both, because they answer
   different halves of the problem. */
.field-row { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--sp-3); margin-top: var(--sp-4); align-items: end; }
.field-row .field { margin-top: 0; }
/* Date, start, end. Stacks below 480px, where three time fields side by side
   are narrower than the picker each one opens. */
.field-row--three { grid-template-columns: 1.2fr minmax(0, 1fr) minmax(0, 1fr); }

/* A TIME FIELD HAS TO FIT ITS OWN CONTENTS, and inside a modal it was not.

   REPORTED, with the two time boxes circled: "the formatting is poor, it's
   eating up some of the PM word and the time". The three-up row divides
   whatever width it is given, and a modal is a good deal narrower than the
   page - so "03:00 PM" plus the browser's own clock button had less room than
   it needs and the meridiem was clipped.

   The columns stop being purely fractional and get a floor. A time input needs
   about 11.5em for the widest thing it can hold, and `minmax` means the row
   still shares space evenly whenever there IS enough. Below that the row wraps
   to one column, which the rule underneath already does on a phone and which
   is the right answer at any width where three of these will not fit. */
/* AUTO-FIT, SO IT CAN NEVER OVERFLOW ITS CONTAINER.

   My first attempt at the clipped "PM" gave these columns a floor and then
   stacked them with a VIEWPORT media query - which was the wrong question. The
   thing running out of room is the MODAL, which caps itself at a few hundred
   pixels no matter how wide the screen is, so on a desktop the row kept its
   three columns, demanded more width than the dialog had, and the whole modal
   scrolled sideways. Reported: "the formatting looks bad, I do not want to
   scroll to the side."

   auto-fit asks the container instead: as many columns as fit at 11.5em each -
   three on the page, two in a dialog, one on a phone - and it is arithmetic
   rather than a guess about where the edges are. */
.field-row--three { grid-template-columns: repeat(auto-fit, minmax(11.5em, 1fr)); }
/* The clock button the browser draws sits INSIDE the field, so a time input
   that is exactly as wide as its text has none left for it. */
input[type="time"], input[type="date"] { min-width: 0; }

/* ONE COLUMN ON A PHONE, FOR EVERY FIELD ROW AND NOT JUST THE THREE-UP ONE.

   The `--three` variant has stacked here since somebody hit this with three
   time fields. The ordinary two-up row was left at `1fr 1fr` and has been
   overflowing on phones ever since, which is the same bug one field earlier.

   480px rather than the 620px the rest of this pass uses, because that is
   where the existing decision already sits and two thresholds for the same
   idea is how a stylesheet starts disagreeing with itself. Every iPhone is
   well inside it.

   THIS IS A SWEEP, NOT ONE FIX. `.field-row` is rendered by ten files —
   promote, settings, my-settings, invite, sync, highlight, event, admin-post,
   admin-promos and common — so the start/end time pair that was reported is
   one instance of something that was wrong everywhere. */
@media (max-width: 480px) {
  .field-row { grid-template-columns: 1fr; }
}
/* THE THREE-UP ROW NEEDS MORE ROOM THAN 480px, which is what the old rule
   assumed. Found while checking this: between 481 and roughly 560 the three
   columns compute to about 133px each while the date input inside one still
   demands 173. minmax lets the COLUMN shrink; it does nothing about the
   control sitting in it, so the fields simply spilled over each other in a
   band of widths nobody had looked at. Three fields want about 500px of row
   before they are worth having side by side. */
@media (max-width: 560px) {
  .field-row--three { grid-template-columns: 1fr; }
}
/* AND A FLOOR UNDER ALL OF IT. Even with the stacking above, a field row is
   rendered by ten different files and the next one added may hold something
   wider than a time picker. `min-width: 0` lets the control shrink with its
   column instead of hanging out of it, so the worst case is a cramped field
   rather than a broken card. */
.field-row .field { min-width: 0; }
.field-row input, .field-row select, .field-row textarea { min-width: 0; max-width: 100%; }

/* AN EXAMPLE HAS TO LOOK LIKE AN EXAMPLE. Placeholders inherited the browser
   default, which in Chrome is dark enough to read as a real value — so "School
   PTO meeting" looked like a filled-in field somebody had to clear rather than
   a suggestion. --ink-faint is the same grey the site's other meta text uses,
   and it still clears 4.5:1 on our paper. */
/* EVERY placeholder on the site, which is what was asked for - "all these
   types of guidance text in the forms throughout the website". `opacity: 1`
   stays: Firefox dims placeholders by default, and dimming a colour that has
   already been chosen for its contrast is how you end up below the floor
   without changing a value. */
input::placeholder,
textarea::placeholder { color: var(--ink-hint); opacity: 1; }

/* Raised, smaller cents on any price (e.g. $29⁹⁹) */
sup.cents { font-size: 0.58em; font-weight: 700; vertical-align: top; line-height: 1; margin-left: 1px; }
/* A price is one word. See NL.centsHTML. */
.money { white-space: nowrap; }

/* ---- Business dashboard ------------------------------------------------- */
.dash-plan-badge {
  display: inline-flex; align-items: center; gap: 7px; margin-top: var(--sp-3);
  font-size: 15px; color: var(--ink-soft); background: var(--surface-2);
  padding: 7px 14px; border-radius: var(--radius-pill);
}
.dash-plan-badge svg { color: var(--accent); }
.dash-plan-badge strong { color: var(--ink); }
/* When the rate is an intro rate: how long, and what it becomes. Quiet, but
   present on every dashboard load — an expiring price should never be a
   surprise on a bill. */
.dash-plan-term {
  display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
  margin-top: var(--sp-2); font-size: 13.5px; color: var(--ink-faint);
}
.dash-plan-term svg { color: var(--ink-faint); flex: none; }
.dash-plan-term strong { color: var(--ink); }
/* The badge carries a painted price now, which brings its own line-height. */
.dash-plan-badge .paint { vertical-align: baseline; }
/* .dash-switch, .dash-switch__label, .dash-switch__btn and .dash-preview-note
   were removed with the plan switcher (Aug 2026). Left behind they would style
   nothing, and the next person searching for "dash-switch" would find rules
   for a control that no longer exists and go looking for the markup. */

/* The owner's controls on a live listing. Set apart from the page rather than
   dressed to match it: these are not part of the event, they act ON it, and a
   resident should never wonder what they are. Dashed border and the faintest
   ink say "not for you" without needing a colour nobody can see. */
.owner-strip {
  margin-top: var(--sp-6);
  padding: var(--sp-4);
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface-2);
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3);
}
.owner-strip__label {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink-faint);
}
/* No `:empty` rule here, and that is deliberate rather than an omission. The
   placeholder div carries NO class until ownerBoot returns an owner, so for
   every resident it is an unstyled, zero-height, borderless element — measured
   at 0px. A `.owner-strip:empty` rule could never match, and a rule that
   cannot fire is the dead code this file already had four of. */

/* Dashboard top row + Settings link */
.dash-top { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
.dash-settings {
  display: inline-flex; align-items: center; gap: 6px; font-weight: 600; font-size: 14px;
  color: var(--ink-soft); border: 1px solid var(--line-strong); border-radius: var(--radius-pill);
  padding: 7px 14px; min-height: 38px; white-space: nowrap;
}
.dash-settings:hover { color: var(--primary); border-color: var(--primary); text-decoration: none; }
.dash-settings svg { color: var(--ink-faint); }
.dash-settings:hover svg { color: var(--primary); }

/* ---- Account settings --------------------------------------------------- */
.settings-card { margin-top: var(--sp-5); }

/* ---- Settings, grouped and shut ------------------------------------------

   THE GROUP IS THE CARD NOW, and the forms inside it are sub-sections. The
   alternative — a bordered box holding eleven bordered boxes — is the thing
   that makes a settings page feel like a filing cabinet inside a filing
   cabinet, and it was already close to that with the cards on their own.

   So .set-group carries the border, the surface and the shadow, and the cards
   inside give theirs up and are separated by a hairline instead. Closed, the
   page is six tidy rows; open, one card with its parts laid out in it.

   The chevron is the only moving thing: it rotates, which is the cheapest
   possible statement of what the control does. Nothing animates the panel
   itself — <details> has no transition and faking one with max-height is a
   guess about content height that is wrong the moment a form grows.
--------------------------------------------------------------------------- */
.set-group {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
  margin-top: var(--sp-4);
}
.set-group > summary {
  display: flex; align-items: center; gap: 11px; cursor: pointer;
  padding: var(--sp-4) var(--sp-5); list-style: none;
}
.set-group > summary::-webkit-details-marker { display: none; }
.set-group > summary > svg:first-child { color: var(--primary); flex: none; }
.set-group > summary strong {
  display: block; font-family: var(--font-display);
  font-size: 17px; font-weight: 600; color: var(--ink);
}
.set-group > summary em {
  display: block; font-style: normal; font-size: 13.5px;
  color: var(--ink-soft); margin-top: 2px;
}
.set-group > summary:hover strong { color: var(--primary); }
.set-group > summary:focus-visible {
  outline: 3px solid var(--focus); outline-offset: -3px; border-radius: var(--radius);
}
.set-group[open] > summary { border-bottom: 1px solid var(--line); }
.set-group__chev {
  margin-left: auto; flex: none; display: inline-flex;
  color: var(--ink-faint); transition: transform .18s;
}
.set-group[open] > summary .set-group__chev { transform: rotate(180deg); }
.set-group__body { padding: 0 var(--sp-5) var(--sp-5); }

/* ---- The town picker, folded ---------------------------------------------
   Twenty-four towns is more vertical space than every other question on the
   signup form put together, and most of them are not open yet. Folded at the
   owner's request, 14 Sep 2026.

   BUILT LIKE A FIELD, NOT LIKE A SECTION. `.set-group` above is a page
   division with its own card and shadow; this sits INSIDE a field, under that
   field's label, and has to read as the control answering it. So it borrows
   the summary mechanics - flex row, hidden marker, chevron that turns - and
   none of the chrome.

   IT LOOKS LIKE THE OTHER INPUTS ON THE FORM because it is one: same border,
   same radius, same 44px hit area a select gets, so a reader scanning the
   column of questions does not meet one that looks like furniture. */
.towns {
  border: 1px solid var(--line-strong); border-radius: var(--radius);
  background: var(--surface); margin-top: var(--sp-2);
}
.towns__summary {
  display: flex; align-items: center; gap: 10px; cursor: pointer;
  padding: 12px var(--sp-4); min-height: 44px; list-style: none;
  font-size: 15px; font-weight: 600; color: var(--ink);
}
.towns__summary::-webkit-details-marker { display: none; }
/* The chosen towns, or the prompt. --ink-hint is the placeholder ink, so an
   untouched field reads as unanswered rather than as filled in - the same
   distinction data.js draws for every other empty box on the site. */
.towns:not([open]) .towns__chosen { color: var(--ink); }
.towns__chosen:empty::after { content: 'Choose your town'; color: var(--ink-hint); }
.towns__chev {
  margin-left: auto; flex: none; display: inline-flex;
  color: var(--ink-faint); transition: transform .18s;
}
.towns[open] .towns__chev { transform: rotate(180deg); }
/* The list itself keeps the two-column grid it already had; it is only the
   padding that has to come back, since the summary owns the top edge now. */
.towns > .checks { padding: 0 var(--sp-4) var(--sp-4); }
/* Focus belongs on the summary, which is the thing being operated. */
.towns__summary:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; border-radius: var(--radius); }
/* An invalid field turns its box, so the error and the control agree. */
.field.is-invalid .towns { border-color: var(--destructive); }
@media (prefers-reduced-motion: reduce) { .towns__chev { transition: none; } }

/* ---- Smart Draft, as a panel a form can carry ----------------------------
   The private invitation form had no drafting at all, and it is the form most
   likely to be somebody's first: the details are already in an email or on a
   photograph of a flyer, and retyping them is the only work the page asks.

   SHUT BY DEFAULT, and styled as an offer rather than a step. Most people know
   what they are inviting people to and will simply type it, so this must not
   read as the first thing to fill in. It borrows .set-group's summary
   behaviour — one disclosure pattern on the site, not two — and takes the
   accent tint so it reads as help rather than as another question. */
/* SAGE, NOT CORAL, and the palette at the top of this file says why in as many
   words: --accent "reads as RED at scale. Fine as an icon or a word; wrap a
   whole card in it and it looks like an error state." This is a whole card, and
   it was wrapped in it. Reported as exactly that.

   --seaglass-tint is the documented colour for a panel offering help rather
   than raising an alarm, which is what this is: an offer to save you typing.
   Its own note reads "the here's-a-tip panels... never red." */
.smartdraft {
  border: 1px solid var(--seaglass); border-radius: var(--radius);
  background: var(--seaglass-tint); margin-top: var(--sp-5);
}
.smartdraft__head {
  display: flex; align-items: center; gap: 11px; cursor: pointer;
  padding: var(--sp-3) var(--sp-4); list-style: none;
}
.smartdraft__head::-webkit-details-marker { display: none; }
.smartdraft__head > svg:first-child { color: var(--seaglass-ink); flex: none; }
.smartdraft__head strong {
  display: block; font-family: var(--font-display);
  font-size: 16px; font-weight: 600; color: var(--ink);
}
.smartdraft__head em {
  display: block; font-style: normal; font-size: 13.5px;
  color: var(--ink-soft); margin-top: 2px;
}
.smartdraft__head:hover strong { color: var(--seaglass-ink); }
.smartdraft__head:focus-visible {
  outline: 3px solid var(--focus); outline-offset: -3px; border-radius: var(--radius);
}
.smartdraft[open] > .smartdraft__head { border-bottom: 1px solid var(--line); }
.smartdraft[open] > .smartdraft__head .set-group__chev { transform: rotate(180deg); }
.smartdraft__body { padding: var(--sp-3) var(--sp-4) var(--sp-4); }
.smartdraft__body .field { margin-top: var(--sp-3); }
.smartdraft__body .form-actions { margin-top: var(--sp-4); }

/* The cards give up their own chrome inside a group. `margin-top` stays so the
   first one clears the summary's rule and the rest keep their rhythm. */
.set-group .news-card.settings-card {
  background: transparent; border: 0; box-shadow: none;
  border-radius: 0; padding: 0; margin-top: var(--sp-5);
}
.set-group .news-card.settings-card + .news-card.settings-card {
  border-top: 1px solid var(--line); padding-top: var(--sp-5);
}
/* Closing an account is the one thing here that should not look like changing
   a preference, and the group heading is where that has to show — the card
   inside it no longer has a border to be red. */
.set-group#sec-close > summary > svg:first-child { color: var(--destructive); }
.set-group#sec-close[open] { border-color: var(--destructive); }

@media (prefers-reduced-motion: reduce) { .set-group__chev { transition: none; } }
@media (max-width: 560px) {
  .set-group > summary { padding: var(--sp-4); }
  .set-group__body { padding: 0 var(--sp-4) var(--sp-4); }
}

.settings-head { display: flex; align-items: center; gap: 9px; }
.settings-head svg { color: var(--primary); }
.settings-line { margin: var(--sp-2) 0 0; color: var(--ink-soft); font-size: 15px; }
.settings-line strong { color: var(--ink); }
/* The sentence under a control that says what pressing it actually does -
   a consequence rather than an instruction, so it is quieter than the line
   above it and never competes with the button. */
.settings-line--quiet { font-size: 13.5px; color: var(--ink-faint); }
.settings-pill {
  display: inline-flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 700;
  color: var(--primary); background: var(--primary-tint); border: 1px solid var(--primary);
  padding: 5px 12px; border-radius: var(--radius-pill);
}
.settings-pill--off { color: var(--ink-soft); background: var(--surface-2); border-color: var(--line-strong); }
.settings-cardform, .settings-billing { margin-top: var(--sp-4); padding-top: var(--sp-4); border-top: 1px solid var(--line); }
.settings-subhead { font-size: 13px; font-weight: 700; color: var(--ink-faint); text-transform: uppercase; letter-spacing: .05em; }
.settings-status { margin-top: var(--sp-3); font-size: 14px; font-weight: 600; display: none; align-items: center; gap: 6px; }
/* TWO NAMES FOR THE SAME TWO STATES, because both are already in the codebase
   and only one of them was ever styled. settings.js and my-settings.js write
   .ok/.bad; sync.js, myevents.js and admin-orgs.js write .is-on/.is-bad — and
   those three were writing a class that left the element display:none, so every
   "Saving…", every validation message and every save error on the clients, my
   events and organizations pages was written to an invisible element. Aliasing
   here fixes all of them at once and cannot break the pages that were already
   right. */
.settings-status.ok, .settings-status.bad,
.settings-status.is-on, .settings-status.is-bad { display: inline-flex; }
.settings-status.ok, .settings-status.is-on { color: var(--primary); }
/* Last, so "is-on is-bad" reads as bad rather than inheriting the on colour. */
.settings-status.bad, .settings-status.is-bad { color: var(--destructive); }

/* ---- The logo card ------------------------------------------------------
   The mark itself at the size a member is deciding about, with everything that
   can be said or done about it beside it. It stacks under 520px, where a 72px
   square and a paragraph beside it stop fitting on one line. */
.logo-row {
  display: flex; align-items: flex-start; gap: var(--sp-4);
  margin-top: var(--sp-4);
}
.logo-row__body { flex: 1; min-width: 0; }
/* Bigger than a card's 44px on purpose: this is the one place somebody looks
   AT the mark rather than past it, and a logo that is wrong is wrong at a size
   you can see. */
.logo-mark .org-mark { width: 72px; height: 72px; border-radius: 16px; font-size: 23px; }
.logo-note {
  display: flex; align-items: flex-start; gap: 7px;
  color: var(--ink-soft); font-size: 14px; line-height: 1.5;
}
.logo-note svg { flex: none; margin-top: 2px; color: var(--seaglass); }
.logo-acts { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-3); }
/* A <label> pointing at the hidden file input, so the button says what we want
   rather than "Choose File / no file selected" in whatever the browser's
   language is. */
.logo-pick { cursor: pointer; }
.logo-say { margin-top: var(--sp-2); font-size: 13px; font-weight: 600; color: var(--ink-faint); }
.logo-say svg { vertical-align: -2px; }
/* A live region has to EXIST before it has anything to announce, so this
   paragraph is in the markup from the first paint and empty nearly always. An
   empty block is still a box: its own top and bottom margins were stacking
   under the hint and pushing Save logo away for no reason a member could see.
   Reserving the space would be right if the message replaced something; it
   appears once and briefly, so collapse until it has text. */
.logo-say:empty { display: none; }

@media (max-width: 520px) {
  .logo-row { flex-direction: column; gap: var(--sp-3); }
}

/* ---- Prepay term chooser (signup) ---------------------------------------
   Three cards rather than a radio list, because each option has to carry three
   numbers — what leaves the card, what that is per month, and what it saves —
   and a radio label has room for none of them. */
.term-grid { display: grid; gap: var(--sp-3); margin-top: var(--sp-3);
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }
.term-card { position: relative; display: flex; flex-direction: column; gap: 3px;
  padding: var(--sp-4); text-align: left; cursor: pointer;
  background: var(--surface); border: 1.5px solid var(--line); border-radius: var(--radius);
  font: inherit; color: inherit; }
.term-card:hover { border-color: var(--primary); }
/* The chosen one has to be obvious at a glance — this is a decision about
   money, and "which did I pick?" should never need a second look. */
.term-card.is-on { border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-tint); }
/* Extra top padding on any card carrying a corner badge, so the badge sits in
   the margin rather than on top of the term name. */
.term-card:has(.term-card__off) { padding-top: calc(var(--sp-4) + 8px); }
.term-card__flag { position: absolute; top: -9px; right: var(--sp-3);
  background: var(--primary); color: #fff; font-size: 11px; font-weight: 700;
  padding: 2px 8px; border-radius: 999px; letter-spacing: .02em; }
/* THE DISCOUNT, top-left and green. Green because it is the one thing on this
   card that is money in their pocket, and the site's navy is already carrying
   "Best value" on the opposite corner — two badges in one colour would read as
   one long label rather than two separate facts. */
.term-card__off { position: absolute; top: -9px; left: var(--sp-3);
  /* --seaglass-ink, not --seaglass: the latter is a pale decorative sage and
     white text on it does not carry. This is the readable one. */
  background: var(--seaglass-ink); color: #fff; font-size: 11px; font-weight: 700;
  padding: 2px 8px; border-radius: 999px; letter-spacing: .02em; white-space: nowrap; }

/* ---- The signup total, as a receipt --------------------------------------
   A column of figures with the total at the foot, rather than a sentence with
   four numbers in it. Each discount gets its own row so it can be checked one
   at a time. */
.signup-coupon { margin-top: var(--sp-4); }
.sum { margin-top: var(--sp-4); padding: var(--sp-4);
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: var(--radius); }
.sum-row { display: flex; justify-content: space-between; align-items: baseline;
  gap: var(--sp-3); padding: 5px 0; font-size: 15px; }
.sum-row__label { color: var(--ink-faint); }
.sum-row__n { font-variant-numeric: tabular-nums; white-space: nowrap; }
.sum-row.is-off .sum-row__n { color: var(--seaglass-ink); font-weight: 600; }
.sum-row__code { font-size: 12px; font-weight: 700; letter-spacing: .04em;
  background: var(--line); padding: 1px 6px; border-radius: 4px; }
.sum-row.is-total { border-top: 1px solid var(--line); margin-top: 4px; padding-top: 10px;
  font-size: 17px; }
.sum-row.is-total .sum-row__label { color: inherit; }
.sum-row.is-total .sum-row__n { font-weight: 700; font-family: var(--font-display, inherit); }
.sum__save { display: flex; align-items: center; gap: 6px; margin: var(--sp-3) 0 0;
  font-size: 14px; font-weight: 700; color: var(--seaglass-ink); }
/* The renewal footnote. Quiet, but not hidden — it is the one thing on this
   panel that describes a charge they have not agreed to yet. */
.sum__foot { margin: var(--sp-3) 0 0; font-size: 13px; line-height: 1.5; color: var(--ink-faint); }
.term-card__name { font-size: 13px; font-weight: 700; color: var(--ink-faint);
  text-transform: uppercase; letter-spacing: .04em; }
.term-card__total { font-family: var(--font-display, inherit); font-size: 26px;
  font-weight: 700; line-height: 1.15; }
.term-card__per { font-size: 13px; color: var(--ink-faint); }
.term-card__blurb { font-size: 13px; margin-top: 2px; }
.term-card__save { margin-top: 4px; font-size: 13px; font-weight: 700; color: var(--primary); }
.signup-terms { margin-top: var(--sp-5); }

/* ---- Metrics page -------------------------------------------------------
   No charting library. Every bar here is a div with a width, which is enough
   for comparing a dozen values and costs nothing to load, keep current or
   audit. */
/* Was a flex row pairing this sentence with the window picker. The picker
   moved into the two cards it actually governs, so what is left is a
   paragraph and wants nothing but the space under it. */
.mtop { margin-bottom: var(--sp-5); }
/* A card's title with its own control on the same line. The control belongs to
   the card, not the page — which is the whole point of it being here. Wraps on
   a phone so the buttons drop under the heading rather than squeezing it. */
.card-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-3); flex-wrap: wrap;
}
.card-head .dash-card__title { margin: 0; }
/* The same row one level down, for a control that governs part of a card
   rather than all of it — the metrics page's traffic card opens on four fixed
   periods and only the half below this line follows the window. */
.card-head .mhead { margin: 0; }
.mcloser {
  margin-top: var(--sp-5); padding-top: var(--sp-4);
  border-top: 1px dashed var(--line);
}
.mwindow { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
/* Names what the buttons beside it govern. Without it the group reads as a
   control for the whole page, including the totals above it that it has never
   touched — see windowPicker() in finances.js. */
.mwindow__lbl { font-size: 12.5px; font-weight: 600; color: var(--ink-faint); margin-right: 2px; }
.mwindow .btn.is-on { background: var(--primary); color: #fff; border-color: var(--primary); }

.mtiles { display: grid; gap: var(--sp-3); margin-top: var(--sp-4);
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
.mtile { display: flex; flex-direction: column; gap: 2px; padding: var(--sp-3) var(--sp-4);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  text-decoration: none; color: inherit; }
.mtile__n { font-family: var(--font-display, inherit); font-size: 28px; font-weight: 700;
  line-height: 1.1; }
.mtile__label { font-size: 13px; color: var(--ink-faint); line-height: 1.35; }
.mtile__sub { font-size: 12px; color: var(--ink-faint); }

/* ---- A number with its own history under it ------------------------------
   The count is the answer and the change is the meaning, so they share a box:
   "1,204" alone says nothing about whether that is a good week. Four across on
   a wide screen, two on a laptop, one on a phone — a row of eight-point digits
   nobody can read is worse than scrolling. */
.mtiles--stat { grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }
.mtile--stat { gap: 3px; }
.mstat { font-size: 12px; line-height: 1.35; margin-top: 2px; }
.mstat__sub { font-style: normal; color: var(--ink-faint); }
/* UP IS NOT ALWAYS GREEN AND DOWN IS NOT ALWAYS RED — but on every number that
   uses these it is, because they all count arrivals. The colour is a second
   channel behind the sign, not a replacement for it: the + and − carry the
   meaning for anybody who cannot separate the two. */
.mstat__d { font-weight: 800; }
/* ---- A QUIET WEEK IS NOT AN ERROR ---------------------------------------

   "No red in the UI - red only for real errors", asked for more than once, and
   this was the loudest thing left breaking it: --destructive is the alarm red,
   and it was painting "-67 (-64%)" on the front page of the admin every time
   traffic dipped. Nothing is wrong when fewer people visit on a Tuesday. It is
   a fact, and the reader is the person who most needs to read it calmly.

   SUNSET RATHER THAN GREY, because the direction still has to carry: with
   --ink-faint here, "down" and "flat" would be the same colour and the arrow
   would be doing all the work. The palette's own note on --sunset says what it
   is for - "unmistakably not a warning, still muted enough to sit beside the
   navy and the sage". Sage up, sunset down, grey flat. */
.mstat__d--up { color: var(--seaglass-ink); }
.mstat__d--down { color: var(--sunset-ink); }
.mstat__d--flat { color: var(--ink-faint); }
.mstat__vs { color: var(--ink-faint); }

/* Two questions side by side, stacking before either gets too narrow to read
   its own four boxes. */
.msplit { display: grid; gap: var(--sp-5); margin-top: var(--sp-3); }
@media (min-width: 900px) { .msplit { grid-template-columns: 1fr 1fr; } }

/* A worklist tile with something in it earns the eye; one at zero deliberately
   does not. Zero is the target on this row, so it reads as calm rather than as
   an empty statistic. */
/* SAGE, NOT RED. Every tile on the worklist was drawn in --destructive, so
   "274 events nobody has checked" - a queue doing exactly what a queue does -
   arrived looking like an outage. Red is for something that has gone wrong,
   and none of these have. */
a.mtile--todo { border-color: var(--seaglass); }
a.mtile--todo .mtile__n { color: var(--seaglass-ink); }
a.mtile--todo:hover { background: var(--surface-2); }
a.mtile--clear { opacity: .55; }
a.mtile--clear:hover { opacity: 1; }

.mgrid { display: grid; gap: var(--sp-5); margin-top: var(--sp-5);
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
.mhead { font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  color: var(--ink-faint); margin: 0 0 var(--sp-3); }

.mbars { display: flex; flex-direction: column; gap: 7px; }
.mbar { display: grid; grid-template-columns: minmax(64px, 34%) 1fr auto; align-items: center;
  gap: var(--sp-2); font-size: 13px; }
.mbar__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink-faint); }
.mbar__track { background: var(--line); border-radius: 999px; height: 10px; overflow: hidden; }
.mbar__fill { display: block; height: 100%; background: var(--primary); border-radius: 999px; }
.mbar__n { font-weight: 700; font-variant-numeric: tabular-nums; }
.mbar__n em { font-style: normal; font-weight: 400; color: var(--ink-faint); font-size: 12px; }

/* WHAT PEOPLE OPENED. Full width under the two-column grid, because a listing
   title is a sentence and the paths above it are a word. The label column is
   wider and darker for the same reason: here the label IS the answer, where on
   a day-by-day chart it is only an axis. */
.mlistings { margin-top: var(--sp-6); }
.mlistings .mbar { grid-template-columns: minmax(140px, 46%) 1fr auto; }
.mlistings .mbar__label { color: var(--ink); font-weight: 600; }
@media (max-width: 640px) {
  /* The bar is the first thing to give up its room on a narrow screen; the
     title and the number are the two things that have to stay readable. */
  .mlistings .mbar { grid-template-columns: minmax(0, 1fr) 34px auto; }
}

/* Period on period. Direction is in the WORD ("up on…"), not only the colour
   and the triangle — a green chip means nothing in a monochrome screenshot,
   to a colour-blind reader, or in forced-colors mode. */
.varn {
  display: inline-flex; align-items: baseline; gap: 3px;
  font-size: 11.5px; font-weight: 600; white-space: nowrap;
}
.varn__dir { font-size: 9px; line-height: 1; }
.varn--up   { color: var(--signal-green, #2E7D5B); }
.varn--down { color: var(--destructive); }
.varn--flat, .varn--new { color: var(--ink-faint); font-weight: 400; }
.varn--new svg { vertical-align: -1px; }
.period-total {
  margin-top: var(--sp-3); display: flex; align-items: baseline; gap: 8px;
  flex-wrap: wrap; font-size: 14px; color: var(--ink-soft);
}
.period-total strong { color: var(--ink); font-size: 17px; font-variant-numeric: tabular-nums; }

/* ---- Money over time, as columns ----------------------------------------
   Vertical because time runs left to right; the categorical charts beside it
   stay horizontal because their labels are long text. See columns() in
   finances.js. */
/* THE HEIGHT CHAIN IS EXPLICIT ON PURPOSE. A column's fill is sized as a
   PERCENTAGE of the plot, and a percentage height only resolves against a
   parent whose own height is definite. Sizing the plot with `flex: 1` left it
   indeterminate, and the fills rendered hundreds of thousands of pixels tall.
   So the two heights are variables and every box derives from them. */
.mcols {
  --plot-h: 148px;   /* the drawing area */
  --x-h: 18px;       /* the month labels under it */
  display: grid; grid-template-columns: auto 1fr; gap: 10px; margin-top: var(--sp-4);
}
/* The axis carries the values no column is labelled with, so it is the thing
   that makes selective labelling safe. */
.mcols__y {
  display: flex; flex-direction: column; justify-content: space-between;
  height: var(--plot-h);
  font-size: 11px; color: var(--ink-faint); font-variant-numeric: tabular-nums;
  text-align: right;
}
.mcols__plot { position: relative; }
/* Hairline, solid, one step off the surface — recessive on purpose. Never
   dashed: a dashed grid competes with the data for attention. */
.mcols__grid {
  position: absolute; inset: 0 0 var(--x-h) 0; pointer-events: none;
  background-image: linear-gradient(var(--line) 1px, transparent 1px);
  background-size: 100% 50%;
  border-bottom: 1px solid var(--line-strong);
}
.mcols__bars { display: flex; align-items: flex-end; gap: 2px; }
.mcol {
  flex: 1 1 0; min-width: 0; position: relative;
  display: flex; flex-direction: column; align-items: center;
  border-radius: var(--radius-sm, 8px);
}
/* Definite height, so the fill's percentage below has something real to be a
   percentage OF. */
.mcol__plot {
  height: var(--plot-h); width: 100%;
  display: flex; align-items: flex-end; justify-content: center; position: relative;
}
/* Capped at 24px so a lone column does not become a slab; the leftover width
   in the band is deliberate air. Square at the baseline, rounded at the data
   end — the end is data, the base is the axis. */
.mcol__fill {
  display: block; width: 100%; max-width: 24px; background: var(--primary);
  border-radius: 4px 4px 0 0; min-height: 2px;
}
/* A month that banked nothing draws a flat baseline tick, never a short bar:
   a sliver reads as a small amount, and zero is not a small amount. */
.mcol__fill.is-zero { height: 2px !important; background: var(--line-strong); border-radius: 0; }
.mcol__cap {
  position: absolute; bottom: 100%; margin-bottom: 4px; white-space: nowrap;
  font-size: 11.5px; font-weight: 700; color: var(--ink);   /* ink, never the series colour */
  font-variant-numeric: tabular-nums;
}
.mcol__x {
  font-size: 10.5px; color: var(--ink-faint); height: var(--x-h); line-height: var(--x-h);
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Hover and keyboard both, because a chart you can only read with a mouse is a
   chart half the readers cannot use. */
.mcol__tip {
  position: absolute; bottom: calc(100% - 14px); left: 50%; transform: translateX(-50%);
  display: grid; gap: 1px; padding: 6px 9px; border-radius: var(--radius-sm, 8px);
  background: var(--ink); color: #fff; font-size: 12px; line-height: 1.35;
  white-space: nowrap; text-align: center; pointer-events: none;
  z-index: 5; font-variant-numeric: tabular-nums;
  /* `display: none`, NOT `visibility: hidden`. A hidden-but-laid-out tooltip is
     still a nowrap box centred on a 24px column, so it hangs off the left of the
     first column and the right of the last — and the PAGE grows a horizontal
     scrollbar for a thing nobody can see. Taken out of layout entirely, there is
     nothing to overflow. */
  display: none;
}
.mcol__tip strong { font-weight: 700; }
.mcol__tip em { font-style: normal; opacity: .75; font-size: 11px; }
.mcol:hover .mcol__tip, .mcol:focus-visible .mcol__tip { display: grid; }
.mcol:hover .mcol__fill { background: var(--primary-ink); }
.mcol:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

.mcol-table { margin-top: var(--sp-3); font-size: 13px; }
.mcol-table summary { cursor: pointer; color: var(--ink-soft); }
.mcol-table table { width: 100%; border-collapse: collapse; margin-top: var(--sp-2); }
.mcol-table th, .mcol-table td { text-align: left; padding: 5px 8px; border-bottom: 1px solid var(--line); }
.mcol-table td { font-variant-numeric: tabular-nums; }
.mcol-table thead th { color: var(--ink-faint); font-weight: 600; font-size: 12px; }

/* Twelve months on a phone would be 20px a column with unreadable labels, so
   the plot scrolls sideways inside its own box rather than the page doing it. */
@media (max-width: 560px) {
  .mcols__plot { overflow-x: auto; }
  .mcols__bars { min-width: 420px; }
  .mcols__grid { right: auto; width: 100%; min-width: 420px; }
}

/* The typed-name gate on an irreversible action. Set apart from the prose
   above it so it reads as a thing to do, not another sentence to skim. */
.confirm-type { margin-top: var(--sp-4); }
.confirm-type label { font-size: 14px; }
.confirm-type input { margin-top: var(--sp-2); }
.settings-danger { border-color: var(--destructive); }
.settings-danger .settings-head svg { color: var(--destructive); }
.settings-danger-btn { color: var(--destructive); border-color: var(--destructive); }
.settings-danger-btn:hover { background: var(--destructive); color: #fff; }
/* TWO COLUMNS ON ANYTHING WIDER THAN A PHONE.

   This was a single column, which suited the three or four options these lists
   used to hold and became silly at nine towns: a tall ladder of checkboxes down
   the left with half the form empty beside it, pushing everything below it off
   the screen.

   A GRID, not columns on a flex container. Both `column-count` and flex-column
   flow items top-to-bottom-then-across, which reads wrongly for an alphabetical
   list — the eye expects A and B on the first row, not A on the left and F on
   the right. Grid fills left-to-right, so reading order and visual order agree.

   Shared by every checkbox list on the site (towns, interests, audiences, sync
   defaults), so this is one rule rather than eight. Single column below 560px,
   where two would leave the labels too cramped to read. */
/* `.checks.checks--stack`, not `.checks--stack`. The base `.checks` rule sets
   display:flex and is declared LATER in this file at equal specificity, so a
   single-class selector here loses on `display` and the grid template is
   silently ignored — the same trap `.modal.modal--wide` had to solve. */
/* The second offer on the closing screen — a cheaper plan, under the discount.
   Quieter than the one above it: two boxes shouting equally would make the
   screen feel like a wall of pleading rather than two honest options. */
.cancel-offer--alt { background: transparent; }

/* The two-factor QR. Bounded and centred: Supabase returns an SVG, which will
   otherwise happily scale to the width of the card and look like a poster. */
.mfa-qr { display: block; width: 180px; margin: var(--sp-4) 0;
  background: #fff; padding: var(--sp-2); border: 1px solid var(--line);
  border-radius: var(--radius-sm); }
/* The SVG arrives with its own width/height in the markup; this makes it fit
   the box instead of printing at whatever size Supabase chose. */
.mfa-qr svg { display: block; width: 100%; height: auto; }
/* The typed key, for setting this up on the same device you are reading it on.
   Monospace and breakable — it is long, and a key that overflows its card is a
   key nobody can copy. */
.mfa-secret code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 13px; letter-spacing: .06em; word-break: break-all;
  background: var(--surface-2); padding: 3px 7px; border-radius: 5px; }

/* ---- Brand kit page -----------------------------------------------------
   A gallery of one drawing wearing different hats. Cards are wide enough that
   the 160px art is not the biggest thing on them — the LABEL is what somebody
   is reading, since they already know what the logo looks like and are here to
   find out which file to use. */
.bk-grid { display: grid; gap: var(--sp-4); margin-top: var(--sp-4);
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); }
.bk-card { position: relative; margin: 0; padding: var(--sp-4);
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); }
.bk-card--pick { border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-tint); }
.bk-card__flag { position: absolute; top: -10px; left: var(--sp-4);
  background: var(--primary); color: #fff; font-size: 11px; font-weight: 700;
  padding: 2px 8px; border-radius: 999px; }
/* A CHEQUERBOARD UNDER THE ART, so a transparent file is visibly transparent
   rather than looking as though it has a white background it does not have —
   the difference matters the moment somebody drops it on a dark header. */
.bk-card__art { display: grid; place-items: center; padding: var(--sp-3); border-radius: var(--radius-sm);
  background-color: #fff;
  background-image:
    linear-gradient(45deg, #EFE9DD 25%, transparent 25%, transparent 75%, #EFE9DD 75%),
    linear-gradient(45deg, #EFE9DD 25%, transparent 25%, transparent 75%, #EFE9DD 75%);
  background-size: 16px 16px; background-position: 0 0, 8px 8px; }
.bk-card__art img { display: block; width: 160px; height: 160px; }
.bk-card figcaption { margin-top: var(--sp-3); display: flex; flex-direction: column; gap: 3px; }
.bk-card figcaption strong { font-size: 15px; }
.bk-card figcaption em { font-style: normal; font-size: 13px; color: var(--ink-faint); line-height: 1.45; }
.bk-card__acts { display: flex; flex-wrap: wrap; gap: 5px; margin-top: var(--sp-3); }
.bk-card__acts .btn { min-width: 0; }

/* A town we do not cover yet. It is a footnote on the label, not a second
   label: <em> inherited the label's weight AND added italics, so it arrived
   bold-italic and shouted louder than the town name it was qualifying.
   Lowercase, in parentheses, muted, and a shade smaller — enough to answer
   "why can I not use this yet" for anyone who looks, and quiet enough that
   nobody scanning the list has to read it nine times. */
.check__soon { font-weight: 400; font-style: normal; font-size: 13px; color: var(--ink-faint); }

/* A town that cannot be bought yet, and the home town that cannot be
   unticked. Both are disabled, so both are dimmed — but neither is hidden:
   the closed ones are what make per-town pricing make sense, and the home one
   is what they are already paying for. */
.check.is-off { opacity: .55; cursor: default; }
.check input:disabled + span { cursor: default; }
.hl-towns { margin-top: var(--sp-4); }
.hl-towns .checks { margin-top: var(--sp-2); }
/* Sits under the total, explaining a figure that would otherwise look like
   the ladder had changed its mind. */
.dash-buy__towns { display: block; font-size: 12px; font-weight: 400; color: var(--ink-faint); }

.checks.checks--stack {
  display: grid; grid-template-columns: 1fr;
  gap: var(--sp-2) var(--sp-4); align-items: start;
}
@media (min-width: 560px) {
  .checks.checks--stack { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.dash-card {
  margin-top: var(--sp-5); background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow); padding: var(--sp-5);
}
.dash-card__title {
  font-family: var(--font-display); font-size: 20px; font-weight: 600; color: var(--ink);
  display: flex; align-items: center; gap: 9px;
}
.dash-card__title svg { color: var(--primary); }
.dash-card__text { margin-top: var(--sp-2); color: var(--ink-soft); font-size: 15px; line-height: 1.55; }
.dash-actions { margin-top: var(--sp-4); display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; }
.dash-soon { font-size: 12.5px; font-weight: 700; color: var(--ink-soft); background: var(--surface-2); padding: 4px 10px; border-radius: var(--radius-pill); }

.dash-buy-grid { display: grid; grid-template-columns: 1fr; gap: var(--sp-4); margin-top: var(--sp-4); }
@media (min-width: 720px) { .dash-buy-grid { grid-template-columns: 1fr 1fr; } }
.dash-buy {
  border: 1px solid var(--line-strong); border-radius: var(--radius); padding: var(--sp-4);
  background: var(--surface-2); display: flex; flex-direction: column;
}
.dash-buy__title { font-family: var(--font-display); font-size: 16px; font-weight: 600; color: var(--ink); display: flex; align-items: center; gap: 7px; }
.dash-buy__title svg { color: var(--seaglass-ink); }
.dash-buy__text { margin: 6px 0 0; font-size: 13.5px; color: var(--ink-soft); line-height: 1.5; }
.dash-buy__typedesc { margin: 6px 0 0; font-size: 12.5px; color: var(--ink-faint); line-height: 1.45; }
/* The Highlight builder, folded away until asked for. What shows above it is
   the offer; what hides inside is the three-step setup. */
.dash-buy__more { margin-top: var(--sp-4); border-top: 1px solid var(--line-strong); }
.dash-buy__summary {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  padding: var(--sp-3) 0 0; cursor: pointer; list-style: none;
  font-size: 14.5px; font-weight: 600; color: var(--primary);
}
.dash-buy__summary::-webkit-details-marker { display: none; }   /* Safari's own triangle */
.dash-buy__summary:hover { color: var(--primary-ink); }
.dash-buy__summary:focus-visible { outline: 3px solid var(--focus); outline-offset: 3px; border-radius: var(--radius-sm); }
.dash-buy__summarylbl { display: inline-flex; align-items: center; gap: 7px; }
/* The starting price stays visible while collapsed — the one number someone
   deciding whether to open this actually wants. */
.dash-buy__from { margin-left: auto; font-weight: 700; color: var(--ink); }
/* The way out at the bottom of the builder. Quiet on purpose: it sits under
   the Next button and must not compete with it — the person who wants this is
   leaving, not buying. */
.dash-buy__collapse {
  display: flex; align-items: center; gap: 5px; margin: var(--sp-3) 0 0 auto;
  padding: 4px 2px; background: none; border: 0; cursor: pointer;
  font-size: 12.5px; font-weight: 600; color: var(--ink-faint);
}
.dash-buy__collapse:hover { color: var(--ink); }
.dash-buy__collapse svg { transform: rotate(180deg); }
.dash-buy__caret { flex: none; transition: transform .18s; color: var(--ink-faint); }
.dash-buy__more[open] .dash-buy__caret { transform: rotate(180deg); }
.dash-buy__morebody { padding-top: var(--sp-2); }
@media (prefers-reduced-motion: reduce) { .dash-buy__caret { transition: none; } }

.dash-buy__fields { margin-top: var(--sp-3); display: grid; gap: var(--sp-3); }
.dash-buy__foot { margin-top: var(--sp-4); display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); flex-wrap: wrap; }
.dash-buy__price { font-family: var(--font-display); font-size: 18px; font-weight: 600; color: var(--ink); }
.dash-save {
  display: inline-block; margin-left: 5px; vertical-align: middle; font-family: var(--font-body);
  font-size: 11.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .03em;
  color: #fff; background: var(--seaglass-ink); border-radius: var(--radius-pill); padding: 2px 9px;
}
/* Featured "1st free" perk banner + FREE price */
.dash-free {
  display: flex; align-items: flex-start; gap: 10px; margin-top: var(--sp-3);
  background: var(--primary-tint); border: 1px solid var(--primary); border-radius: var(--radius); padding: var(--sp-3) var(--sp-4);
}
.dash-free > svg { color: var(--primary); flex: none; margin-top: 1px; }
.dash-free__body { display: flex; flex-direction: column; gap: 3px; }
.dash-free__head { font-size: 14.5px; font-weight: 700; color: var(--ink); line-height: 1.35; }
.dash-free__free { color: var(--primary); font-weight: 800; text-transform: uppercase; }
.dash-free__sub { font-size: 12.5px; color: var(--ink-soft); line-height: 1.4; }
/* Struck-through list price beside what this plan actually pays. */
.dash-price-was {
  font-family: var(--font-body); font-size: 14px; font-weight: 600;
  color: var(--ink-faint); text-decoration: line-through; margin-right: 7px;
}
/* Weekender submission: spot grid + guideline note */
.wk-guide { margin-top: var(--sp-3); }
.wk-spots { display: grid; grid-template-columns: 1fr; gap: 6px; margin-top: var(--sp-4); }
@media (min-width: 560px) { .wk-spots { grid-template-columns: 1fr 1fr; } }
.wk-spot {
  display: flex; align-items: center; gap: 10px; margin: 0; padding: 9px 14px;
  border: 1px solid var(--line-strong); border-radius: var(--radius-sm); background: var(--surface);
}
.wk-spot__num { font-family: var(--font-display); font-weight: 700; color: var(--ink); min-width: 32px; }
.wk-spot__status { flex: 1; font-size: 13px; color: var(--ink-soft); display: inline-flex; align-items: center; gap: 5px; }
.wk-spot__status svg { color: var(--ink-faint); flex: none; }
.wk-spot__price { font-weight: 700; color: var(--ink); font-family: var(--font-display); }
.wk-spot--open { cursor: pointer; }
.wk-spot--open input { accent-color: var(--accent); flex: none; }
.wk-spot--open:hover { border-color: var(--accent); }
.wk-spot--open:has(input:checked) { border-color: var(--accent); background: var(--accent-tint); box-shadow: inset 0 0 0 1px var(--accent); }
.wk-spot--open:has(input:checked) .wk-spot__status { color: var(--accent); font-weight: 700; }
.wk-spot--reserved, .wk-spot--taken { opacity: .65; background: var(--surface-2); }
.wk-spot--reserved .wk-spot__num, .wk-spot--taken .wk-spot__num { color: var(--ink-faint); }
.dash-buy__done { display: flex; gap: 10px; align-items: flex-start; }
.dash-buy__done svg { color: var(--primary); flex: none; margin-top: 2px; }
.dash-buy__done p { margin: 0; font-size: 14px; color: var(--ink-soft); line-height: 1.5; }
/* The Highlight of the Week builder — sea glass, matching the highlight cards. */
.dash-buy--main { background: var(--seaglass-tint); border-color: var(--seaglass); }
.dash-buy__badge {
  display: inline-block; margin-left: 8px; font-size: 10px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .05em; color: #fff; background: var(--seaglass-ink); border-radius: var(--radius-pill);
  padding: 2px 8px; vertical-align: middle;
}
.dash-buy__note {
  margin: var(--sp-3) 0 0; display: flex; gap: 6px; align-items: flex-start; font-size: 12.5px;
  color: var(--ink-soft); background: var(--surface); border: 1px dashed var(--line-strong);
  border-radius: var(--radius-sm); padding: 8px 10px; line-height: 1.45;
}
.dash-buy__note svg { color: var(--seaglass-ink); flex: none; margin-top: 1px; }

/* A single soft line under a card's opening sentence. Unboxed on purpose: the
   dashed .dash-buy__note above reads as a callout, and an offer nobody asked
   for has not earned a box on a page somebody opened to do something else. */
.dash-card__fine {
  margin: 6px 0 0; display: flex; gap: 6px; align-items: flex-start;
  font-size: 13px; line-height: 1.5; color: var(--ink-soft);
}
.dash-card__fine svg { color: var(--seaglass-ink); flex: none; margin-top: 2px; }
.dash-card__fine a { color: var(--primary-ink); text-decoration: underline; text-underline-offset: 2px; }

/* ---- Highlight preview: the tile and the page behind it ------------------
   Two views of one record, toggled with radios rather than script — the
   preview modal takes a string of HTML and offers nowhere to wire a handler,
   and :checked does this correctly for a keyboard and a screen reader for
   free. See showPreview() in highlight.js. */
.hlpv__tabs { display: flex; gap: 6px; margin-bottom: var(--sp-4); flex-wrap: wrap; }
/* The radios live on .hlpv itself so `~` can reach both the labels and the
   panes; they are hidden but still focusable and still announced. */
.hlpv > input[name="hlpv"] { position: absolute; opacity: 0; width: 0; height: 0; }
.hlpv__tabs label {
  display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
  padding: 7px 12px; border: 1px solid var(--line-strong); border-radius: 999px;
  font-size: 13px; font-weight: 600; color: var(--ink-soft); background: var(--surface);
}
.hlpv__tabs label:hover { border-color: var(--primary); }
/* Reached from the input by `~`, then matched to its own label by `for`. */
#hlpv-tile:checked ~ .hlpv__tabs label[for="hlpv-tile"],
#hlpv-page:checked ~ .hlpv__tabs label[for="hlpv-page"] {
  background: var(--primary); border-color: var(--primary); color: #fff;
}
/* Focus has to be visible even though the input itself is not. */
#hlpv-tile:focus-visible ~ .hlpv__tabs label[for="hlpv-tile"],
#hlpv-page:focus-visible ~ .hlpv__tabs label[for="hlpv-page"] {
  outline: 2px solid var(--primary); outline-offset: 2px;
}
.hlpv__pane { display: none; }
#hlpv-tile:checked ~ .hlpv__pane--tile,
#hlpv-page:checked ~ .hlpv__pane--page { display: block; }
/* A preview is a picture of a page, not the page. Its links must not lead
   anywhere — a tile that navigated to featured.html?id=__preview__ would land
   on "nothing highlighted right now" from inside a modal about to take money. */
.hlpv a { pointer-events: none; cursor: default; }
.hlpv__detail {
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface); padding: var(--sp-4);
}
.hlpv__detail .featured-detail__title { margin-top: var(--sp-2); }
.hlpv .preview-pane__cap { margin-top: var(--sp-4); }

/* ---- Coupon code box ---------------------------------------------------- */
.coupon {
  margin-top: var(--sp-4); padding: var(--sp-3) var(--sp-4);
  background: var(--surface-2); border-radius: var(--radius-sm);
}
.coupon > label {
  display: block; font-size: 12px; font-weight: 700; letter-spacing: .05em;
  text-transform: uppercase; color: var(--ink-faint); margin-bottom: 6px;
}
.coupon__row { display: flex; gap: var(--sp-2); align-items: stretch; }
.coupon__row input {
  flex: 1; min-width: 0; padding: 9px 12px; font: inherit; font-size: 14.5px;
  letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink); background: var(--surface);
  border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
}
.coupon__row input:disabled { color: var(--ink-faint); background: var(--surface-2); }
.coupon__row .btn { flex: none; }
.coupon__msg { margin: var(--sp-2) 0 0; font-size: 12.5px; line-height: 1.55; }
.coupon__msg:empty { display: none; }
.coupon__msg.is-bad { color: var(--destructive); }
.coupon__msg.is-good { display: flex; gap: 6px; align-items: flex-start; color: var(--seaglass-ink); }
.coupon__msg.is-good svg { flex: none; margin-top: 2px; }
/* Restated total once a code lands — the struck-through original stays visible
   so the saving is checkable rather than just asserted. */
.modal-total, .signup-charge {
  display: flex; gap: 7px; align-items: baseline; margin: var(--sp-2) 0 0;
  font-size: 14px; color: var(--ink);
}
.modal-total s, .signup-charge s { color: var(--ink-faint); }
.signup-charge { align-items: flex-start; margin-top: var(--sp-3); color: var(--ink-soft); }
.signup-charge svg { flex: none; margin-top: 3px; color: var(--ink-faint); }

/* ---- Recurring promotion posts ------------------------------------------ */
.recur {
  margin: var(--sp-4) 0; padding: var(--sp-3) var(--sp-4);
  background: var(--surface-2); border-radius: var(--radius-sm);
}
.recur__on, .recur__over { display: flex; gap: 10px; align-items: flex-start; cursor: pointer; }
.recur__on input, .recur__over input { margin-top: 3px; flex: none; }
.recur__on em, .recur__over em {
  display: block; margin-top: 3px; font-style: normal; font-size: 12.5px;
  line-height: 1.55; color: var(--ink-soft);
}
/* THE TITLE SAYS IT ON A PHONE. "Repeat this post automatically" carried three
   lines underneath explaining what repeating a post automatically is. The
   owner: "that title is already very, very self explanatory, so no need to
   have the additional text underneath." Kept on a desktop, where the sentence
   about each repeat using one post is worth the room it costs. */
@media (max-width: 620px) {
  .recur__on em, .recur__over em { display: none; }
}

/* "This night, or all twelve?" — borrows .recur's tinted block because it is
   the same subject and sits in the same form. Tinted seaglass rather than the
   plain surface: this one appears only when an edit could reach posts that are
   already live, and it should not read as another optional setting. */
.series-scope {
  background: var(--seaglass-tint); border: 1px solid var(--seaglass);
}
.series-scope__head { margin: 0 0 var(--sp-3); font-size: 13.5px; line-height: 1.55; }
.series-scope .check { display: flex; gap: 10px; align-items: flex-start; cursor: pointer; }
.series-scope .check + .check { margin-top: var(--sp-2); }
.series-scope .check input { margin-top: 3px; flex: none; }
.series-scope .check em {
  display: block; margin-top: 3px; font-style: normal; font-size: 12.5px;
  line-height: 1.55; color: var(--ink-soft);
}
.recur__body { margin-top: var(--sp-3); padding-top: var(--sp-3); border-top: 1px solid var(--line); }
.recur__dates {
  display: flex; gap: 7px; align-items: flex-start; margin: var(--sp-3) 0 0;
  font-size: 13px; line-height: 1.6; color: var(--ink-soft);
}
.recur__dates svg { flex: none; margin-top: 3px; color: var(--seaglass-ink); }

/* ---- WHICH REPEAT, exactly -----------------------------------------------
   The dropdown names a shape ("Every week"); these say which one. Only the row
   belonging to the chosen shape is shown — a weekday picker under "Every 3
   days" is a control that does nothing, and a form full of those teaches
   people that the controls do not matter.

   `.field--tight` is the small half of an uneven pair: "Repeat every [week]"
   and "Which one [3rd]" are each a single short answer sitting beside a much
   wider one, and an even 1fr 1fr split leaves them stranded in white space. */
.field-row .field--tight { max-width: 190px; }
.recur__params { margin-top: var(--sp-2); }
.recur__params .field-row[hidden] { display: none; }
/* THE DAY PICKER NEEDS THE WHOLE WIDTH ON A PHONE. `.field-row` is two equal
   columns at every size, so seven 44px buttons were being asked to fit in half
   of a 375px screen and wrapped onto three lines. A row of days that reads as
   a row is the entire reason it is not a dropdown. */

/* SEVEN BOXES, NOT A SEVEN-ITEM MULTI-SELECT. "Tuesdays and Thursdays" is one
   glance as a row of days and three fiddly interactions as a list box. Sized to
   the tap target rather than to the text, so a thumb on a phone lands on the
   day it was aiming at. */
.daypick { display: flex; flex-wrap: wrap; gap: 6px; }
.daypick__d { position: relative; cursor: pointer; }
.daypick__d input {
  position: absolute; inset: 0; width: 100%; height: 100%;
  margin: 0; opacity: 0; cursor: pointer;
}
.daypick__d span {
  display: flex; align-items: center; justify-content: center;
  /* 44 SQUARE, not 44 wide and 40 tall. The width was sized to the tap target
     and the height was left at whatever looked right, which is half a job: a
     thumb misses a short button as easily as a narrow one, and seven of these
     sit side by side. */
  min-width: 44px; min-height: 44px; padding: 0 6px;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--ink-soft);
  font-size: 13px; font-weight: 600; user-select: none;
  transition: background .12s, border-color .12s, color .12s;
}
.daypick__d input:checked + span {
  background: var(--primary); border-color: var(--primary); color: #fff;
}
.daypick__d input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (hover: hover) {
  .daypick__d input:not(:checked):hover + span { border-color: var(--ink-faint); color: var(--ink); }
}

/* AFTER the .daypick rules above, not before them. Written earlier in the file
   it lost to `.daypick { gap: 6px }` on identical specificity, and the phone
   layout it exists for silently did not happen. */
@media (max-width: 560px) {
  .recur__params .field-row { grid-template-columns: 1fr; }
  .field-row .field--tight { max-width: none; }
  /* ONE PIXEL, AND IT COSTS A WHOLE ROW. Seven 44px buttons with 6px between
     them come to 344px, and a 375px phone leaves 343px inside its padding — so
     Saturday wrapped onto a line of its own and the week stopped looking like a
     week. Five fits, with the tap targets untouched. */
  .daypick { gap: 5px; }
}

/* THE RULE, IN WORDS, under the controls that built it. Four controls that
   combine into one schedule are four chances to be sure you said something you
   did not. Written from the same canonical rule the DATES come from, so it
   cannot flatter the form: if it reads wrong, the schedule IS wrong. */
.recur__says {
  margin: var(--sp-3) 0 0;
  font-size: 14px; font-weight: 600; color: var(--ink);
}
.recur__says:empty { display: none; }

/* How a repeat is actually paid for. Tinted rather than plain text: this is the
   sentence that stops a "repeat forever" being a surprise later. */
.recur__note {
  display: flex; gap: 8px; align-items: flex-start;
  margin-top: var(--sp-3); padding: 10px 12px;
  background: var(--accent-tint); border-radius: var(--radius-sm);
  font-size: 14px; line-height: 1.6; color: var(--ink-soft);
}
.recur__note svg { flex: none; margin-top: 2px; color: var(--accent); }
.recur__note strong { color: var(--ink); }
.recur__note em { font-style: italic; color: var(--ink); }
/* The "until when" option you are not using: still readable, plainly inactive.
   Paired with `disabled` on the control in promote.js, so it cannot be typed
   into or tabbed to either. */
.field.is-off { opacity: .55; }
.field.is-off label { color: var(--ink-faint); }
.field.is-off select, .field.is-off input { cursor: not-allowed; background: var(--surface-2); }
/* The "or just upgrade" answer, sitting under whichever overage message
   applies. Its own block so the recommendation reads as a separate thought
   from the charge it is an alternative to. */
.recur__rec {
  display: flex; gap: 7px; align-items: flex-start;
  margin-top: 9px; padding-top: 9px; border-top: 1px solid var(--line);
  font-size: 12.5px; line-height: 1.55;
}
.recur__rec svg { flex: none; margin-top: 2px; color: var(--primary); }
.recur__rec strong { color: var(--ink); }
.recur__rec a { font-weight: 600; }
/* The allowance question is a money decision — set it apart from the plumbing. */
.recur__over {
  margin-top: var(--sp-3); padding: var(--sp-3);
  background: var(--surface); border-radius: var(--radius-sm);
  border-left: 3px solid var(--accent);
}
/* Plans with no overage tier: a statement of the ceiling, not a choice. */
.recur__cap {
  display: flex; gap: 7px; align-items: flex-start;
  margin: var(--sp-3) 0 0; padding: var(--sp-3);
  background: var(--surface); border-radius: var(--radius-sm);
  border-left: 3px solid var(--line-strong);
  font-size: 12.5px; line-height: 1.6; color: var(--ink-soft);
}
.recur__cap svg { flex: none; margin-top: 3px; color: var(--ink-faint); }

/* Repeating schedules on the dashboard */
.dash-sched { margin-top: var(--sp-4); padding-top: var(--sp-4); border-top: 1px solid var(--line); }
.dash-sched__title {
  display: flex; align-items: center; gap: 6px; margin: 0 0 var(--sp-3);
  font-family: var(--font-body); font-size: 12px; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase; color: var(--ink-faint);
}
.dash-sched__title svg { color: var(--seaglass-ink); }
/* The collapsible variant. As a <summary> the heading is a control, so it needs
   a pointer, a hover state and a real focus ring — a heading that opens
   something must not look identical to one that does not. */
.dash-sched--fold > summary {
  cursor: pointer; list-style: none; margin-bottom: 0;
  padding: var(--sp-2) 0; border-radius: var(--radius-sm);
}
.dash-sched--fold > summary::-webkit-details-marker { display: none; }
/* Our own chevron, so the disclosure state reads the same in every browser. */
.dash-sched--fold > summary::before {
  content: ''; flex: none; width: 7px; height: 7px; margin-right: 2px;
  border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: rotate(-45deg); transition: transform .15s;
}
.dash-sched--fold[open] > summary::before { transform: rotate(45deg); }
.dash-sched--fold > summary:hover { color: var(--ink); }
.dash-sched--fold > summary:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
/* Open: restore the gap the heading's own margin used to provide. */
.dash-sched--fold[open] > summary { margin-bottom: var(--sp-2); }
.dash-sched__row {
  display: flex; gap: var(--sp-3); align-items: center; justify-content: space-between;
  padding: var(--sp-2) 0; border-bottom: 1px solid var(--line);
}
.dash-sched__row:last-of-type { border-bottom: 0; }
.dash-sched__name { font-size: 14.5px; font-weight: 600; color: var(--ink); min-width: 0; }
.dash-sched__name em {
  display: block; margin-top: 2px; font-style: normal; font-size: 12.5px;
  font-weight: 400; color: var(--ink-faint);
}
.dash-sched__note { margin: var(--sp-3) 0 0; font-size: 12px; line-height: 1.6; color: var(--ink-faint); }

/* "Draft — not published", on the row rather than in the section heading.

   The heading says it too, but a row here looks exactly like a row in the post
   history immediately below it, and somebody scrolling past the heading would
   have every reason to read it as something that is on the site. The cost of
   saying it twice is nothing; the cost of being wrong about it is a member who
   thinks they have advertised an event they have not. */
.dash-sched__tag {
  display: inline-block; margin-left: 6px;
  font-style: normal; font-size: 11px; font-weight: 700;
  letter-spacing: .04em; text-transform: uppercase;
  color: var(--ink-faint); background: var(--surface-2);
  border: 1px solid var(--line); border-radius: 999px; padding: 1px 8px;
  vertical-align: 1px;
}
/* The drafts block is the same furniture as the schedules and history blocks,
   drawn as a dashed box rather than divided off by a rule — unfinished, and it
   should look it at a glance rather than on reading the heading.

   Every side is stated. `border-style: dashed` on its own was enough to make
   all four appear, because border-width's initial value is `medium` and only
   renders as nothing while the style is `none` — so a one-word rule produced a
   3px box nobody chose, hugging the text on the left. */
.dash-sched--drafts {
  border: 1px dashed var(--line);
  border-radius: var(--radius-sm);
  padding: var(--sp-4);
}

/* Smart Draft, offered where a scan cannot help. Sits under the card's own
   actions as a quieter second thought, not a competing headline. */
.fi-smart { margin-top: var(--sp-4); padding-top: var(--sp-3); border-top: 1px solid var(--line); }
.fi-smart__lede {
  display: flex; align-items: flex-start; gap: 8px; margin: 0 0 var(--sp-3);
  font-size: 14px; line-height: 1.5; color: var(--ink-soft);
}
.fi-smart__lede svg { flex: none; margin-top: 2px; color: var(--accent); }

/* The on-demand scan, on the dashboard. A quiet strip rather than a card with
   a heading: most days there is nothing to do here and it should look like it,
   sitting under the busier cards rather than competing with them. */
.dash-scannow { padding-top: var(--sp-3); padding-bottom: var(--sp-3); }
.dash-scannow__line {
  display: flex; align-items: flex-start; gap: 8px; margin: 0 0 var(--sp-3);
  font-size: 14px; line-height: 1.5; color: var(--ink-soft);
}
.dash-scannow__line svg { flex: none; margin-top: 2px; color: var(--accent); }
/* Same strip in Settings, under the sources it scans. */
.set-scannow { margin-top: var(--sp-3); padding-top: var(--sp-3); border-top: 1px solid var(--line); }

/* A CONTROL NAMED IN RUNNING TEXT, drawn as a small picture of itself.

   "Press Create a post" in bold reads as emphasis rather than as the name of a
   thing on the screen. At a glance this is recognisably the button the reader
   is being sent to look for, which is the whole job.

   Deliberately NOT interactive: no hover, no pointer, and it is a span. The
   real control is inches away and a second one here would be two doors to the
   same room. Slightly smaller and lighter than the real button so it reads as
   a reference to it rather than a competitor. */
.btn-name {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2px 9px; border-radius: var(--radius-pill);
  background: var(--ink); color: var(--paper);
  font-size: 12.5px; font-weight: 700; line-height: 1.5;
  white-space: nowrap; vertical-align: 1px;
}
.btn-name svg { flex: none; }

/* The title and its window pills on one line.

   `flex-wrap` rather than `nowrap`: on a phone, or behind a long title, the
   pills drop below instead of squeezing the name to nothing. That is the
   layout they used to have all the time, kept as the fallback it should always
   have been. `min-width: 0` on both so a long unbroken title can shrink rather
   than pushing the status and buttons off the row. */
.dash-sched__head {
  display: flex; flex-wrap: wrap; align-items: center; gap: 5px 8px; min-width: 0;
}
.dash-sched__title { min-width: 0; overflow-wrap: anywhere; }

/* The grace countdown, in the seaglass tint rather than a warning colour —
   "you can still undo this for free" is good news. */
.dash-sched__grace {
  display: inline-flex; align-items: center; gap: 4px; margin-top: 4px;
  font-style: normal; font-size: 11.5px; font-weight: 600; color: var(--seaglass-ink);
  background: var(--seaglass-tint); border-radius: 999px; padding: 2px 8px;
}
.dash-sched__grace svg { flex: none; }
/* Two windows can be open at once (date, then refund) — keep them apart. */
.dash-sched__grace + .dash-sched__grace { margin-left: 5px; }
/* Beside the title the flex `gap` already spaces them, and the top margin was
   there to clear the date line above. Both would double up here. */
.dash-sched__head .dash-sched__grace { margin-top: 0; }
.dash-sched__head .dash-sched__grace + .dash-sched__grace { margin-left: 0; }
/* A window that has closed. Same pill, drained of colour, so "you still can"
   and "you no longer can" are told apart at a glance rather than read. */
.dash-sched__grace.is-spent {
  color: var(--ink-faint); background: var(--surface-2); border: 1px solid var(--line);
  padding: 1px 7px;
}
.dash-sched__grace.is-spent svg { color: var(--ink-faint); }

/* ---- THE ONE CHIP THAT IS A BUTTON ---------------------------------------
   Tickets on a history row, and the way to that listing's orders. It wears
   the same pill as the notes beside it so the row stays one line of chips,
   and it is coloured as a link and lifts on hover so the one you can press is
   not disguised as the four you cannot. */
button.dash-sched__grace { border: 0; font: inherit; font-size: 11.5px; font-weight: 600; cursor: pointer; }
.dash-sched__tix { color: var(--primary); background: var(--primary-tint); }
.dash-sched__tix:hover { filter: brightness(.97); text-decoration: underline; }
.dash-sched__tix:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }

/* ---- AND THE WHOLE CARD FOLDS (owner, 15 Sep 2026) ------------------------
   "Collapse the Tickets box and leave only this top visible." Shut, the card
   IS its heading; open, everything sits under it as before. The chevron is on
   the right so the heading reads as a heading and not as a control with a
   label, and the whole summary row is the target - a 16px arrow is not
   something to ask a thumb to find. */
.dash-tickets__fold > summary {
  display: flex; align-items: center; gap: var(--sp-2);
  cursor: pointer; list-style: none;
}
.dash-tickets__fold > summary::-webkit-details-marker { display: none; }
/* The chevron is the site's own, not a glyph of this card's invention: the
   same chevronD icon and the same 180-degree flip every NL.oneLine fold uses
   (.join-more, above). Only the rule is repeated, because .join-more also
   carries a border, a background and its own padding - which inside a card
   that already has all three would draw a second box. */
.dash-tickets__fold > summary > svg:last-child {
  margin-left: auto; flex: none; color: var(--ink-faint); transition: transform .18s;
}
.dash-tickets__fold[open] > summary > svg:last-child { transform: rotate(180deg); }
.dash-tickets__fold > summary:hover > svg:last-child { color: var(--ink); }
.dash-tickets__fold > summary:focus-visible { outline: 3px solid var(--focus); outline-offset: 3px; border-radius: var(--radius-sm); }
/* The title carries the card's bottom margin when it is the only thing shown;
   open, the body below it needs that space back rather than twice over. */
.dash-tickets__fold > summary .dash-card__title { margin: 0; }
.dash-tickets__fold[open] > summary { margin-bottom: var(--sp-3); }

/* HAS ANYBODY SEEN IT. The one pill on the row that reports rather than warns,
   so it is outlined instead of filled and never drains: what happened does not
   expire the way an editing window does. First in the notes line, because on a
   row of four pills it is the only one that is news. */
.dash-sched__seen {
  background: transparent; border: 1px solid var(--seaglass);
  color: var(--seaglass-ink); font-weight: 700;
}
.dash-sched__seen strong { font-weight: 800; }
/* Nothing counted yet on a post still to come. Present, and deliberately the
   quietest thing on the row - it is a reassurance, not a number. */
.dash-sched__seen.is-quiet {
  border-color: var(--line); color: var(--ink-faint); font-weight: 500;
}
.dash-sched__seen.is-quiet svg { color: var(--ink-faint); }

/* ---- THE LITTLE "i" ------------------------------------------------------

   Same bubble as the scan desk's tooltips, on a control a member can actually
   reach: it answers to hover, to keyboard focus AND to a tap, because a
   tooltip that only knows about hover does not exist on a phone.

   `is-open` is the tap state, set by wire() in dashboard.js. */
.info-tip {
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; margin-left: 5px; padding: 0; vertical-align: 1px;
  border: 1px solid currentColor; border-radius: 50%;
  background: none; color: inherit; opacity: .6;
  font-family: var(--font-body); font-size: 11px; font-weight: 700; font-style: normal;
  line-height: 1; cursor: pointer;
}
.info-tip:hover, .info-tip:focus-visible, .info-tip.is-open { opacity: 1; }
.info-tip:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.info-tip::after {
  content: attr(data-tip);
  position: absolute; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%);
  /* Sized to the words, never one character per line. */
  width: max-content; max-width: 15rem;
  padding: 7px 10px; border-radius: var(--radius-sm);
  background: var(--ink); color: #fff;
  font-size: 12px; font-weight: 500; line-height: 1.4; text-align: center; white-space: normal;
  /* display:none rather than only invisible: a hidden box still counts toward
     the scroll area, and this one sits inside a card that would then scroll. */
  display: none; pointer-events: none; z-index: 20;
}
.info-tip:hover::after, .info-tip:focus-visible::after, .info-tip.is-open::after { display: block; }
@media (max-width: 480px) {
  /* NARROWER, STILL CENTRED. Pinning it to the button's right edge was the
     first attempt and it was worse: the tip sits mid-sentence, so anchoring
     right threw a 277px bubble off the LEFT of a 375px screen. Centred on the
     button it spans about 51px to 291px and fits with room either side; the
     cap is only here to stop a longer sentence growing past that. */
  .info-tip::after { max-width: 62vw; }
}

/* The account's total, ON THE HEADING (owner, 16 Sep 2026). It was a tinted
   banner of its own under the heading; now it is the tail of the heading's
   line, in sentence case where the heading is small caps, so it reads as the
   news and not as a second label. Wraps under the heading on a phone. */
.dash-sched__title { flex-wrap: wrap; row-gap: 2px; }
.dash-sched__reach {
  display: inline-flex; align-items: center; gap: 5px; margin-left: 2px;
  font-size: 12.5px; font-weight: 500; letter-spacing: 0; text-transform: none;
  color: var(--seaglass-ink);
}
.dash-sched__reach svg { flex: none; }
.dash-sched__reach strong { font-weight: 700; }
.dash-sched__reach .info-tip { margin-left: 4px; }

/* A finished post: nothing here does anything any more. */
.dash-sched__row.is-over .dash-sched__name { color: var(--ink-soft); }
.btn.is-disabled, .btn:disabled {
  opacity: .45; cursor: not-allowed; pointer-events: none;
  box-shadow: none; background: var(--surface-2);
}
/* `is-disabled` is on an <span> standing in for a link, so it needs the
   inline-flex the .btn rules give a real button. */
span.btn.is-disabled { display: inline-flex; align-items: center; }

/* Row actions. flex-none so a long title cannot squeeze them, and they wrap
   below the row on a phone rather than crushing the name to two characters. */
.dash-sched__acts { display: flex; flex: none; gap: 6px; align-items: center; }
.dash-sched__acts .btn { white-space: nowrap; }

/* ---- A post in the history: one tidy row -------------------------------
   Title, date, notes down the left; status and buttons in one cluster top
   right, so every row has the same shape whatever it says. */
.dash-sched__row--post { align-items: flex-start; padding: 12px 0; gap: var(--sp-4); }
.dash-sched__row--post .dash-sched__name { flex: 1 1 auto; }
.dash-sched__post {
  display: block; font-size: 15px; font-weight: 600; line-height: 1.3; color: var(--ink);
}
.dash-sched__row--post .dash-sched__name em { margin-top: 3px; }
.dash-sched__notes { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 7px; }
.dash-sched__notes .dash-sched__grace { margin: 0; }
.dash-sched__side { display: flex; flex: none; align-items: center; gap: 8px; margin-top: 1px; }
@media (max-width: 560px) {
  .dash-sched__row--post { flex-wrap: wrap; }
  .dash-sched__side { flex: 1 1 100%; justify-content: space-between; margin-top: 4px; flex-wrap: wrap; row-gap: 6px; }
  /* Three buttons and a status chip do not fit a phone in one line; let the
     buttons wrap under the chip rather than run off the card (16 Sep 2026). */
  .dash-sched__side .dash-sched__acts { flex: 1 1 auto; flex-wrap: wrap; min-width: 0; justify-content: flex-end; }
}
@media (max-width: 560px) {
  .dash-sched__row { flex-wrap: wrap; }
  .dash-sched__name { flex: 1 1 100%; }
  .dash-sched__acts { margin-left: auto; }
}

/* ---- Owner tools (admin.html) ------------------------------------------
   Not part of the resident-facing site. Deliberately reuses the existing form,
   dashboard and modal components rather than inventing a back-office look, so
   posting here feels the same as posting as an organization — which is the
   whole point of the page. */

/* Which tab the chosen category files an event under, said before you post
   rather than discovered afterwards. */
.admin-realm { display: block; margin-top: 4px; color: var(--seaglass-ink); font-weight: 600; }

/* The row currently loaded in the form above. */
.dash-sched__row.is-editing {
  background: var(--seaglass-tint); border-radius: var(--radius-sm);
  padding-left: var(--sp-3); padding-right: var(--sp-3);
}

.admin-steps { margin: var(--sp-3) 0 var(--sp-4); padding-left: 1.35em; }
.admin-steps li { margin-top: 6px; font-size: 14.5px; line-height: 1.6; color: var(--ink-soft); }

/* The long version, for the one job on this site that is genuinely
   step-by-step. More air between steps, because they are followed one at a
   time with a browser tab in between. */
.admin-steps--full { counter-reset: step; list-style: none; padding-left: 0; }
.admin-steps--full li {
  position: relative; margin-top: var(--sp-4); padding-left: 40px;
  font-size: 15px; line-height: 1.6; color: var(--ink);
}
.admin-steps--full li::before {
  counter-increment: step; content: counter(step);
  position: absolute; left: 0; top: -1px;
  width: 28px; height: 28px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 14px; font-weight: 700;
  color: var(--on-primary); background: var(--primary);
}
.admin-steps__sub {
  display: block; margin-top: 4px; font-size: 14px; line-height: 1.6; color: var(--ink-soft);
}
.admin-steps--full .btn { margin-top: 8px; }
kbd {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .85em; font-weight: 600; color: var(--ink);
  background: var(--surface); border: 1px solid var(--line-strong);
  border-bottom-width: 2px; border-radius: 5px; padding: 1px 6px; white-space: nowrap;
}

/* The private invitation's own line on its page: the same calm sea-glass as
   every other reassurance, never the warning red. */
.event-private {
  display: flex; align-items: flex-start; gap: 11px;
  margin: 0 0 var(--sp-4); padding: 12px var(--sp-4);
  background: var(--seaglass-tint); border: 1px solid var(--seaglass);
  border-radius: var(--radius); color: var(--ink-soft);
  font-size: 14.5px; line-height: 1.5;
}
.event-private svg { flex: none; margin-top: 2px; color: var(--seaglass-ink); }
.event-private strong { color: var(--ink); }
/* "Updated Fri, Sep 4." Lighter than the private line above it: a fact, not
   a reassurance, so no box, just the clock and the sentence. */
.event-updated {
  display: flex; align-items: flex-start; gap: 8px;
  margin: -6px 0 var(--sp-4); padding: 0 4px;
  font-size: 13.5px; line-height: 1.5; color: var(--ink-soft);
}
.event-updated svg { flex: none; margin-top: 2px; color: var(--primary); }
.event-updated strong { color: var(--ink); }

/* "You cannot break this." Calm, not a warning. */
.admin-reassure {
  margin-top: var(--sp-4); padding: var(--sp-4);
  background: var(--seaglass-tint); border-radius: var(--radius-sm);
}
.admin-reassure__head {
  display: flex; align-items: center; gap: 7px; margin: 0;
  font-size: 14.5px; font-weight: 700; color: var(--ink);
}
.admin-reassure__head svg { flex: none; color: var(--seaglass-ink); }
.admin-reassure__text {
  margin: 6px 0 0; font-size: 14px; line-height: 1.6; color: var(--ink-soft);
}

/* The generated block. Scrolls inside itself — a long batch must never make
   the page scroll sideways. */
.admin-code { position: relative; }
.admin-code pre {
  margin: 0; padding: var(--sp-4); padding-right: 96px;
  max-height: 380px; overflow: auto;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--radius-sm);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12.5px; line-height: 1.65; color: var(--ink);
  white-space: pre; tab-size: 2;
}
.admin-code__copy { position: absolute; top: var(--sp-3); right: var(--sp-3); z-index: 1; }

.admin-idline { margin-top: var(--sp-3); font-size: 13px; color: var(--ink-faint); }
.admin-idline code, .admin-steps code, .signup-info__text code, .dash-sched__note code,
.modal__text code, .form-note code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .92em; background: var(--surface-2); border: 1px solid var(--line);
  border-radius: 5px; padding: 1px 5px;
}

/* Owner tools: the first question on the page. */
.admin-mode { display: grid; gap: var(--sp-3); margin: var(--sp-4) 0; }
@media (min-width: 620px) { .admin-mode { grid-template-columns: 1fr 1fr; } }
.admin-mode__opt {
  position: relative; display: block; cursor: pointer;
  padding: var(--sp-4); background: var(--surface);
  border: 2px solid var(--line); border-radius: var(--radius);
  transition: border-color .15s, background .15s;
}
.admin-mode__opt:hover { border-color: var(--seaglass); }
.admin-mode__opt.is-on { border-color: var(--primary); background: var(--primary-tint); }
.admin-mode input { position: absolute; width: 1px; height: 1px; opacity: 0; margin: -1px; }
.admin-mode__lbl { display: block; font-weight: 700; font-size: 15.5px; color: var(--ink); }
.admin-mode__hint {
  display: block; margin-top: 4px; font-size: 13.5px; line-height: 1.55; color: var(--ink-soft);
}

/* "You started this and did not finish." Offered, never done silently. */
.draft-banner {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3);
  justify-content: space-between;
  margin-top: var(--sp-4); padding: var(--sp-3) var(--sp-4);
  background: var(--seaglass-tint); border: 1px solid var(--seaglass);
  border-radius: var(--radius-sm);
}
.draft-banner__t {
  display: flex; align-items: center; gap: 7px; margin: 0;
  font-size: 14px; line-height: 1.5; color: var(--ink);
}
.draft-banner__t svg { flex: none; color: var(--seaglass-ink); }
.draft-banner__acts { display: flex; gap: 6px; }

/* ---- Save progress / Clear form -----------------------------------------
   Sits with the form's own submit row, not floating above it: these act on
   the form and belong where the other decisions about the form are made.

   Clear is NOT styled as a danger button. It is guarded by a confirm dialog
   and it destroys nothing anybody has published — dressing it in red next to
   Publish would make the more common action look like the risky one. It gets
   a red hover instead, which arrives at the moment of aiming. */
.draft-acts {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2);
  margin-top: var(--sp-3);
}
.draft-acts__clear:hover, .draft-acts__clear:focus-visible {
  color: var(--destructive); border-color: currentColor;
}
.draft-acts__note {
  /* Takes the rest of the row and wraps inside it. Without the min-width a
     flex item refuses to shrink below its longest word and pushes the
     buttons off the edge on a narrow screen. */
  flex: 1 1 12rem; min-width: 0;
  font-size: 13px; line-height: 1.5; color: var(--ink-faint);
}
.draft-acts__note.is-ok { color: var(--seaglass-ink); }
.draft-acts__note.is-warn { color: var(--ink-soft); }

/* The quiet "saved" note under a form that autosaves. */
.autosave-note {
  display: flex; align-items: center; gap: 6px;
  margin: var(--sp-3) 0 0; font-size: 13px; color: var(--ink-faint);
}
.autosave-note svg { flex: none; }
/* Hidden until the first save actually happens: a form that claims to have
   saved before anything was typed is claiming something untrue. */
.autosave-note { visibility: hidden; }
.autosave-note.is-on { visibility: visible; }

/* Which mode the owner tools are in: publishing, or not. */
.db-state {
  display: flex; align-items: flex-start; gap: 8px;
  margin: var(--sp-4) 0 0; padding: 10px var(--sp-4);
  font-size: 14px; line-height: 1.55; color: var(--ink-soft);
  background: var(--surface-2); border-radius: var(--radius-sm);
}
.db-state svg { flex: none; margin-top: 2px; color: var(--ink-faint); }
.db-state.is-on { background: var(--seaglass-tint); color: var(--ink); }
.db-state.is-on svg { color: var(--seaglass-ink); }

.admin-publish { border-color: var(--primary); }

/* The owner's index of every page. Grouped rows of small links. */
.admin-pages { margin-top: var(--sp-4); }
.admin-pages__cap {
  margin: 0 0 6px; font-size: 12px; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--ink-faint);
}
.admin-pages__row { display: flex; flex-wrap: wrap; gap: 6px; }

/* One paste-ready block per destination. */
.admin-block { margin-top: var(--sp-4); }
.admin-block__cap {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  margin: 0 0 6px; font-size: 13px; font-weight: 700; color: var(--ink);
}
.admin-block__cap svg { flex: none; color: var(--seaglass-ink); }
.admin-block__cap em { font-style: normal; font-weight: 400; color: var(--ink-faint); }

/* Listed by us, not by an organization. A 5px dot in the corner: findable if
   you are looking for it, invisible if you are not.

   Was 7px with a 2px ring, which read as a status light — the eye landed on it
   before the title. The mark only has to be PRESENT: its job is to keep the
   site honest about who listed something, and nobody scanning for a Saturday
   needs that competing with the event itself. */
.owner-mark {
  position: absolute; right: 8px; bottom: 8px; z-index: 2;
  width: 5px; height: 5px; border-radius: 50%;
  background: var(--seaglass); box-shadow: 0 0 0 1.5px var(--surface);
  opacity: .8;
}
.event-card { position: relative; }

/* A member's own words about their own site, on the client page. Quoted rather
   than restyled as ours: the owner needs to see at a glance that this is the
   venue talking, because some of it is a job for a person and not for a
   parser. */
.syn-note {
  margin-top: var(--sp-4); padding: var(--sp-3) var(--sp-4);
  border-left: 3px solid var(--seaglass); border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  background: var(--surface-2);
}
.syn-note__cap {
  display: flex; align-items: center; gap: 6px; margin: 0 0 4px;
  font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  color: var(--ink-faint);
}
.syn-note__cap svg { flex: none; }
.syn-note__text { margin: 0; font-size: 14px; line-height: 1.5; white-space: pre-wrap; }

/* ---- Stock artwork ------------------------------------------------------
   The generated SVG fills its box the way an <img> would, so a stock picture
   and an uploaded one are interchangeable everywhere a photo appears. */
.stock-art { display: block; width: 100%; height: 100%; }
.stock-art svg { display: block; width: 100%; height: 100%; object-fit: cover; }

/* The bank under the file input: a scrolling row of pick-one tiles.

   `min-width: 0` is load-bearing. `.field` is a grid, and a grid item defaults
   to `min-width: auto`, which means it refuses to shrink below its content.
   Without it the row is as wide as fifteen tiles (about 2100px) and drags the
   whole page sideways at every screen size, instead of scrolling inside
   itself. */
.stock-bank { margin-top: var(--sp-3); min-width: 0; max-width: 100%; }
.stock-bank__head, .stock-bank__wait {
  display: flex; align-items: flex-start; gap: 7px; margin: 0 0 var(--sp-3);
  font-size: 13.5px; line-height: 1.55; color: var(--ink-soft);
}
.stock-bank__head svg, .stock-bank__wait svg { flex: none; margin-top: 2px; color: var(--seaglass-ink); }
.stock-bank__wait { color: var(--ink-faint); }

/* Scrolls inside itself. A row of tiles must never widen the form. */
.stock-bank__row {
  display: flex; gap: var(--sp-3); overflow-x: auto; padding-bottom: 6px;
  scroll-snap-type: x proximity;
}
/* SAYING THAT IT SCROLLS, WHICH IT ALREADY DID SILENTLY.

   Reported: "the pictures seem to be going over the screen and keep going to
   the right... it's going over the frame." Nothing was actually overflowing —
   this row has scrolled inside itself since it was built — but a tile sliced
   off square at the card's edge looks like a layout fault, not an invitation.
   The owner's own two suggestions were smaller tiles or showing a couple with
   some hint of more. This is both.

   132px to 108px fits three across a 375px form instead of two and a sliver,
   and the fade turns the cut edge into the usual "there is more this way".
   Masked rather than a gradient overlay because the row scrolls: an overlay
   would need its own positioning context and would sit over the tiles when
   they moved under it. */
@media (max-width: 620px) {
  /* `.stock-bank` PREFIXED FOR WEIGHT, not for tidiness. This block sits above
     the `.stock-tile` rule that sets 132px, and a media query adds no
     specificity of its own — so a bare `.stock-tile` here loses to the plain
     one below and the tiles stayed exactly as they were. */
  .stock-bank .stock-tile { width: 108px; }
  .stock-bank .stock-tile__box { height: 70px; }
  .stock-bank__row {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 32px), transparent 100%);
    mask-image: linear-gradient(to right, #000 calc(100% - 32px), transparent 100%);
  }
}
/* `position: relative` is load-bearing too. The radio is visually hidden with
   `position: absolute`, and without a positioned parent its containing block
   is the page, not the tile. That puts it outside the scrolling row's clip, so
   every off-screen tile's input extended the document and the whole page
   scrolled sideways. */
.stock-tile { position: relative; flex: none; width: 132px; cursor: pointer; scroll-snap-align: start; }
.stock-tile input { position: absolute; width: 1px; height: 1px; opacity: 0; margin: -1px; }
.stock-tile__box {
  display: block; height: 84px; overflow: hidden;
  border: 2px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface-2); transition: border-color .15s, box-shadow .15s;
}
/* The generated SVG carries a viewBox but no width, so its intrinsic size is
   800px. Left alone every tile is 800 wide and the page goes with it. */
.stock-tile__box svg { display: block; width: 100%; height: 100%; }
.stock-tile--none .stock-tile__box {
  display: flex; align-items: center; justify-content: center; color: var(--ink-faint);
}
.stock-tile--none .stock-tile__box svg { width: 20px; height: 20px; }
.stock-tile__cap {
  display: block; margin-top: 5px; font-size: 12px; line-height: 1.35; color: var(--ink-soft);
}
.stock-tile__cap em {
  display: block; font-style: normal; font-size: 10.5px; font-weight: 700;
  letter-spacing: .04em; text-transform: uppercase; color: var(--seaglass-ink);
}
.stock-tile:hover .stock-tile__box { border-color: var(--seaglass); }
.stock-tile input:checked + .stock-tile__box {
  border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-tint);
}
.stock-tile input:focus-visible + .stock-tile__box { outline: 3px solid var(--focus); outline-offset: 2px; }
.stock-tile input:checked ~ .stock-tile__cap { color: var(--ink); font-weight: 600; }

/* Closing an account: the reason list, and the offer that answers it. */
.cancel-reasons { display: flex; flex-direction: column; gap: 2px; margin-top: var(--sp-3); }
.cancel-reasons .check { padding: 7px 0; font-size: 14.5px; line-height: 1.45; }

.cancel-offer {
  margin-top: var(--sp-4); padding: var(--sp-4);
  background: var(--seaglass-tint); border: 1px solid var(--seaglass);
  border-radius: var(--radius-sm);
}
.cancel-offer__head {
  display: flex; align-items: center; gap: 7px; margin: 0;
  font-weight: 700; font-size: 14.5px; color: var(--ink);
}
.cancel-offer__head svg { flex: none; color: var(--seaglass-ink); }
.cancel-offer__text {
  margin: 6px 0 0; font-size: 14px; line-height: 1.6; color: var(--ink-soft);
}
.cancel-offer .dash-actions { margin-top: var(--sp-3); }

/* Small print that belongs on the form, not in the terms page: the windows
   you get after posting. Quiet, but present at the moment you decide.

   A WRAPPER, NOT A PANEL. This used to be a grey flex row holding a sentence.
   Both places that use it now hold a FOLD instead, and a fold inside a flex
   row is sized to its own content - so "Editing and cancelling a post" came
   out as a fat grey box half the width of the form, sitting inside a second
   grey box. The bar has to run the full width of the field above it, and it
   brings its own surface, so this carries spacing and nothing else. */
.form-foot { margin: var(--sp-4) 0 0; }
.form-foot .oneline { margin: 0; }
.form-foot .oneline__more { margin-top: 0; }

/* Where you stand on this period's allowance. */
.dash-usage {
  display: flex; align-items: flex-start; gap: 8px;
  margin: var(--sp-3) 0 0; padding: 10px var(--sp-3);
  font-size: 13.5px; line-height: 1.55; color: var(--ink-soft);
  background: var(--seaglass-tint); border-radius: var(--radius-sm);
}
.dash-usage svg { flex: none; margin-top: 2px; color: var(--seaglass-ink); }
.dash-usage strong { color: var(--ink); }
/* Used up is not an error — it is just a fact about the month. Slate blue,
   never the alarm colour. */
.dash-usage.is-spent { background: var(--primary-tint); }
.dash-usage.is-spent svg { color: var(--primary-ink); }
.dash-sched__count {
  font-size: 11px; font-weight: 700; color: var(--primary-ink); background: var(--primary-tint);
  border-radius: var(--radius-pill); padding: 1px 7px; letter-spacing: 0;
}
/* Post history status — the label carries the meaning, colour only reinforces it. */
.dash-status {
  flex: none; font-size: 11.5px; font-weight: 700; letter-spacing: .02em;
  border-radius: var(--radius-pill); padding: 2px 9px; white-space: nowrap;
  color: var(--ink-soft); background: var(--surface-2);
}
.dash-status.is-live { color: var(--primary-ink); background: var(--primary-tint); }
.dash-status.is-repeating { color: var(--seaglass-ink); background: var(--seaglass-tint); }
/* A Highlight's three live states. Scheduled borrows the seaglass tint that
   already means "arranged, not happening yet"; a draft is deliberately the
   quietest thing in the list, because it is the one that has cost nothing. */
.dash-status.is-sched { color: var(--seaglass-ink); background: var(--seaglass-tint); }
.dash-status.is-draft { color: var(--ink-faint); background: var(--surface-2); }
.dash-status.is-off { color: var(--ink-faint); background: var(--surface-2); text-decoration: line-through; }
/* A REPEAT THAT HAS STOPPED ITSELF. Sunset rather than coral, for the reason
   given where the token is defined: nothing has gone wrong and nothing is
   broken — the schedule reached the monthly allowance and is waiting, exactly
   as the posting form said it would. Painting that as an error would send
   people to support over a system working correctly. It still has to catch the
   eye, because it is the one row on the page that is not doing anything. */
.dash-status.is-over { color: var(--sunset); background: var(--sunset-tint); }
@media (max-width: 520px) {
  .dash-sched__row { flex-wrap: wrap; }
}

/* ---- Highlight availability calendar ------------------------------------ */
.dash-buy__cal { margin-top: var(--sp-4); }
.dash-buy__callabel {
  display: block; font-size: 12px; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--ink-faint); margin-bottom: var(--sp-2);
}
/* ---- TWO QUESTIONS IN ONE EXPLAINER ----------------------------------------
   "We post for you" and "Or post something yourself" are different questions
   with different answers, and running them together is what made the posting
   half unfindable. The heading is what separates them; a blank line would not. */
.dash-guide__head {
  margin: var(--sp-4) 0 var(--sp-2); font-size: 0.8rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em; color: var(--ink-faint);
}
.dash-guide__head:first-child { margin-top: 0; }

/* ---- "WE NEED YOUR INPUT" --------------------------------------------------
   Drawn ONLY when something is waiting — there is no empty state and no "all
   clear" card, deliberately. A panel that is always there reading zero is a
   panel nobody sees on the week it reads three.

   So when it IS there it should not look like the cards around it: amber, not
   the page's own paper. Sunset rather than the destructive red, which would
   read as "something is broken" when the honest message is "we would rather ask
   than guess". */
.dash-eye {
  background: var(--sunset-tint);
  border-color: var(--sunset);
  border-left: 4px solid var(--sunset);
}
.dash-eye .dash-card__title { display: flex; align-items: center; gap: 8px; }
.dash-eye__tag {
  background: var(--sunset); color: #fff; border-radius: 999px;
  font-size: 0.78rem; font-weight: 700; padding: 1px 9px; line-height: 1.6;
}
.dash-eye__list { list-style: none; margin: var(--sp-3) 0 0; padding: 0; }
.dash-eye__row {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-3); flex-wrap: wrap;
  padding: var(--sp-2) 0; border-top: 1px solid var(--line);
}
.dash-eye__what { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.dash-eye__what em { font-style: normal; font-size: 0.86rem; color: var(--ink-soft); }
.dash-eye__acts { display: flex; gap: 6px; flex: none; }

/* ---- THE ORGANIZATION'S LOGO, BESIDE ITS NAME ------------------------------
   Drawn only when there is one — see orgLogoHTML — so the heading keeps its own
   alignment on an account that has not uploaded a mark. `contain`, because a
   logo is somebody's identity and cropping it to fill a square is not ours to
   do. */
.dash-idhead { display: flex; align-items: center; gap: var(--sp-3); }
.dash-idhead .featured-detail__title { margin: 0; }
.dash-idhead__logo {
  width: 52px; height: 52px; flex: none; padding: 4px;
  object-fit: contain; background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
}

/* ---- CHOOSE HOW TO START A POST --------------------------------------------
   One form with an optional head start, not two rival boxes. See chooserHTML. */
.ppick { text-align: left; }
.ppick__lede { margin: 0 0 var(--sp-3); color: var(--ink-soft); }
/* EVERY ONE OF THESE IS PHONE-ONLY, AND THAT IS THE POINT.

   A run of the owner's notes from their own iPhone, all the same shape:
   secondary text under a control that already names itself.

     the Smart Draft lede    sits above buttons reading Smart Draft and I'll
                             write it myself
     the visibility cards    "Open to everyone" and "Private, by invitation"
                             say what they are

   "These changes are all for the mobile web and app versions, not the actual
   website for desktops." So none of this deletes anything: a desktop shows
   every word it showed before, and the same markup serves both.

   WHAT IS DELIBERATELY NOT HERE: `.field__hint` as a blanket rule. Most of
   those on the posting form are not decoration — "Still changeable for another
   6 hours, then it is fixed", "This uses two of your posts", the charge note.
   Hiding hints wholesale would take the terms and the prices off the phone and
   leave them on the laptop, which is the opposite of the point. */
@media (max-width: 620px) {
  .ppick__lede--default { display: none; }
  .pickcards--visibility .pickcard__sub { display: none; }
}
.ppick__row { display: flex; flex-wrap: wrap; gap: var(--sp-3); }
/* FILLED, SO THEY READ AS BUTTONS. Two outlined boxes on the page's own paper
   look like panels of text you are meant to read, not two things to press.
   `--primary-tint` is the navy washed out to a haze — the same fill the
   selected mode switch uses, so "this is pressable" looks the same across the
   admin pages. */
.ppick__card {
  flex: 1 1 240px; min-width: 0; text-align: left; cursor: pointer;
  display: flex; flex-direction: column; gap: 6px;
  background: var(--primary-tint); border: 1px solid var(--primary-tint-2);
  border-radius: var(--radius-sm); padding: var(--sp-4);
  font: inherit; color: var(--ink);
  transition: background .15s ease, border-color .15s ease, transform .15s ease;
}
.ppick__card:hover {
  background: var(--primary-tint-2); border-color: var(--primary); transform: translateY(-1px);
}
.ppick__card:active { transform: none; }
.ppick__card:focus-visible { outline: 2px solid var(--focus, currentColor); outline-offset: 2px; }
.ppick__card strong { font-size: 1.02rem; }
.ppick__card em { font-style: normal; color: var(--ink-soft); font-size: 0.9rem; }
.ppick__icon { color: var(--primary); }
.ppick__from {
  display: flex; gap: 8px; align-items: flex-start;
  margin: 0 0 var(--sp-2); padding: var(--sp-2) var(--sp-3);
  border-left: 3px solid var(--accent, var(--line)); color: var(--ink-soft);
}
.ppick__back {
  background: none; border: 0; margin: 0 0 var(--sp-3); padding: 0; cursor: pointer;
  font: inherit; font-size: 0.88rem; color: var(--ink-soft);
  display: inline-flex; align-items: center; gap: 6px;
}
.ppick__back:hover { color: var(--primary); text-decoration: underline; }
.ppick__back:focus-visible { outline: 2px solid var(--focus, currentColor); outline-offset: 2px; }

/* ---- A SMALL FOLD FOR A RARELY-NEEDED QUESTION -----------------------------
   "Is it somewhere else?" is optional and nearly always no, so it reads as a
   quiet line rather than as two more fields to fill in. See NL.elsewhereFold. */
.fold-min { border-top: 1px solid var(--line); margin: var(--sp-3) 0; padding-top: var(--sp-2); }
.fold-min__sum {
  cursor: pointer; list-style: none; display: flex; align-items: center; gap: 6px;
  font-size: 0.9rem; color: var(--ink-soft); padding: 2px 0;
}
.fold-min__sum::-webkit-details-marker { display: none; }
.fold-min__sum::after {
  content: 'A'; margin-left: auto; transition: transform .15s ease; opacity: .6;
}
.fold-min[open] .fold-min__sum::after { transform: rotate(90deg); }
.fold-min__sum:hover { color: var(--ink); }
.fold-min__sum:focus-visible { outline: 2px solid var(--focus, currentColor); outline-offset: 2px; }
.fold-min__body { padding-top: var(--sp-2); }
.fold-min__body .field { margin-bottom: var(--sp-2); }

/* ---- A CALENDAR PER TOWN ---------------------------------------------------
   A Highlight is sold per town and each town now picks its own dates, so the
   boards sit side by side and wrap to a column when there is not room for two.
   `max-width` on .avail keeps a single board the size it has always been. */
.avail-towns { display: flex; flex-wrap: wrap; gap: var(--sp-3); }
.avail-town { flex: 1 1 300px; min-width: 0; }
.avail-town__name {
  margin: 0 0 var(--sp-1); font-weight: 600; font-size: 0.95rem;
  display: flex; align-items: baseline; gap: var(--sp-1); flex-wrap: wrap;
}
.avail-town__tag {
  font-style: normal; font-weight: 400; font-size: 0.8rem; color: var(--ink-faint);
}
/* Two boards share the row, so neither may hold the old single-board width. */
.avail-towns--many .avail { max-width: none; }

.avail {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: var(--sp-3); max-width: 340px;
}
.avail__head { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--sp-2); }
.avail__month { font-weight: 700; font-size: 14px; }
.avail__nav {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; border-radius: 50%; border: 1px solid var(--line);
  background: none; color: var(--ink-soft); cursor: pointer;
}
.avail__nav:hover:not(:disabled) { background: var(--surface-2); color: var(--ink); }
.avail__nav:disabled { opacity: .35; cursor: default; }
.avail__dow, .avail__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.avail__dow span { text-align: center; font-size: 11px; font-weight: 700; color: var(--ink-faint); padding-bottom: 4px; }
.avail-cell {
  aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
  border: 1px solid transparent; border-radius: var(--radius-sm);
  background: none; font: inherit; font-size: 13px; color: var(--ink); cursor: pointer;
}
.avail-cell--pad { cursor: default; }
.avail-cell--free:hover { background: var(--seaglass-tint); border-color: var(--seaglass); }
.avail-cell--past, .avail-cell--nofit, .avail-cell--taken { cursor: not-allowed; }
.avail-cell--past { color: var(--line-strong); }
/* Booked days read as struck-through, so "not available" survives being
   printed, screenshotted, or looked at by someone who can't see the gray. */
.avail-cell--taken { color: var(--ink-soft); background: var(--surface-2); text-decoration: line-through; }
.avail-cell--nofit { color: var(--ink-faint); opacity: .55; }
.avail-cell.is-inrun { background: var(--seaglass-tint); color: var(--seaglass-ink); }
.avail-cell.is-selected {
  background: var(--primary); border-color: var(--primary);
  color: var(--on-primary); font-weight: 700;
}
.avail__key {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px;
  margin: var(--sp-3) 0 0; font-size: 11.5px; color: var(--ink-faint);
}
.avail-dot { width: 9px; height: 9px; border-radius: 3px; display: inline-block; margin-left: 2px; }
.avail-dot--free { background: var(--seaglass-tint); border: 1px solid var(--seaglass); }
.avail-dot--taken { background: var(--surface-2); border: 1px solid var(--line-strong); }
.avail-dot--nofit { background: var(--line); opacity: .55; }

.dash-buy__runs { margin: var(--sp-3) 0 0; display: flex; gap: 6px; align-items: center; font-size: 13px; color: var(--ink-soft); }
.dash-buy__runs svg { color: var(--seaglass-ink); flex: none; }
.dash-buy__runs.is-warn { color: var(--destructive); }
.dash-buy__runs.is-warn svg { color: var(--destructive); }
/* Analytics & metrics (Featured) */
.metric-period {
  display: inline-block; margin-left: 6px; font-size: 12px; font-weight: 700; color: var(--ink-faint);
  background: var(--surface-2); border-radius: var(--radius-pill); padding: 2px 10px; vertical-align: middle;
}
.dash-metrics { margin-top: var(--sp-4); display: grid; grid-template-columns: 1fr; gap: var(--sp-3); }
@media (min-width: 560px) { .dash-metrics { grid-template-columns: 1fr 1fr; } }
@media (min-width: 860px) { .dash-metrics { grid-template-columns: 1fr 1fr 1fr; } }
.metric {
  display: flex; flex-direction: column; gap: 2px; padding: var(--sp-4);
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--radius);
}
.metric__icon { color: var(--accent); }
.metric__val { font-family: var(--font-display); font-size: 28px; font-weight: 700; color: var(--ink); line-height: 1.1; margin-top: 4px; }
.metric__label { font-size: 14px; font-weight: 700; color: var(--ink); }
.metric__sub { font-size: 12.5px; color: var(--ink-faint); line-height: 1.4; }

/* label above a select/input built as <label class="field"><span>…</span>… */
.field > span { font-size: 13px; font-weight: 700; color: var(--ink); }

/* ---- Edit listing page -------------------------------------------------- */
.listing-intro { color: var(--ink-soft); font-size: 16px; line-height: 1.6; margin: var(--sp-3) 0 0; }
.listing-remove { margin-top: var(--sp-4); }
.listing-remove .signup-subhead { color: var(--ink); }

/* A field fixed by the account (e.g. which business you post as) — shaded and
   read-only so it is obviously informative rather than editable. */
/* Matches the `[type=...]` attribute too, so it outranks the generic
   `.field input[type="text"]` rule defined further down. */
.field.field--locked input[type="text"],
.field.field--locked input[type="date"] {
  background: var(--surface-2); color: var(--ink-soft); cursor: default;
  border-color: var(--line); font-weight: 600;
}
.field.field--locked input[type="text"]:focus,
.field.field--locked input[type="date"]:focus {
  outline: none; box-shadow: none; border-color: var(--line);
}

/* Guidance / "how to make this good" panels. Sea glass, not red — this is
   encouragement, and red reads as a warning or a correction. */
.signup-info {
  margin-top: var(--sp-5); padding: var(--sp-4) var(--sp-5);
  background: var(--seaglass-tint); border: 1px solid var(--seaglass); border-radius: var(--radius);
}
.signup-info__head {
  margin: 0; display: inline-flex; align-items: center; gap: 7px; font-family: var(--font-display);
  font-size: 16px; font-weight: 600; color: var(--seaglass-ink);
}
.signup-info__head svg { color: var(--seaglass-ink); }
.signup-info__text { margin: 8px 0 0; font-size: 15px; line-height: 1.6; color: var(--ink-soft); }

/* ---- Post something: who it publishes as --------------------------------
   One row: the label, the Us / A client buttons, and the name box that only
   exists once "A client" is pressed. Wraps on a phone so the box drops to its
   own line rather than shrinking to a slit. */
.ad-client__row { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.ad-client__lbl {
  display: inline-flex; align-items: center; gap: 7px; font-family: var(--font-display);
  font-size: 16px; font-weight: 600; color: var(--seaglass-ink);
}
.ad-client__lbl svg { color: var(--seaglass-ink); }
.ad-client__name { width: auto; flex: 1 1 220px; min-width: 200px; }
.ad-client__chosen { margin-top: var(--sp-3); }
.ad-client__who { display: flex; align-items: center; gap: var(--sp-3); }
.ad-client__who .org-row__meta { display: block; }

/* Starter signup: factual (non-pushy) upsell note */
.signup-upsell {
  margin-top: var(--sp-5); display: flex; align-items: center; gap: 7px;
  font-size: 13.5px; color: var(--ink-faint);
}
.signup-upsell svg { color: var(--ink-faint); flex: none; }
.signup-upsell a { font-weight: 600; }

/* ---- About page --------------------------------------------------------- */
/* 660px at 17px was a textbook 78-character measure — correct in isolation,
   but it read as a thin ribbon against the empty page either side. Bigger
   type is the fix rather than longer lines: at 19px the column grows to
   820px while the measure stays ~82 characters, so it fills more of the
   screen AND is easier to read. Widening at 17px would have pushed past 95
   characters, which is where prose starts to get hard to track. */
/* Widened and CENTRED. A left-aligned 820px column inside a 1130px page is
   the same lopsided void the owner objected to on the calendar; centring it
   makes the whitespace read as a margin rather than as a mistake, and 1100
   lets it fill most screens outright. */
.about { max-width: 1100px; margin-inline: auto; }
.about__kicker {
  font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .09em;
  color: var(--accent);
}
.about h1 { font-size: clamp(28px, 6vw, 40px); letter-spacing: -.02em; margin-top: var(--sp-3); }
.about__lead {
  font-family: var(--font-display); font-size: clamp(19px, 3.6vw, 23px); line-height: 1.5;
  color: var(--ink); margin-top: var(--sp-5);
}
.about p { font-size: 19px; line-height: 1.7; color: var(--ink-soft); margin-top: var(--sp-5); }
.about em { color: var(--ink); font-style: italic; }
.about__promise { margin-top: var(--sp-6); }
.about__sign {
  font-family: var(--font-display); font-size: 19px; font-style: italic; line-height: 1.5;
  color: var(--ink); margin-top: var(--sp-6);
}
.about__by { font-weight: 600; color: var(--ink-soft); margin-top: var(--sp-2) !important; font-size: 15.5px; }
/* THE ONE LINE ON ABOUT THAT ASKS FOR SOMETHING. Display face and accent, so
   it reads as an invitation in the page's own voice rather than as an ad
   dropped at the foot of it; underlined only on hover, because a sentence this
   short is obviously a link once the cursor is anywhere near it.
   (.about .detail-actions used to be here; the page's single button came out at
   the owner's ask on 17 Sep 2026 and nothing on About is a button now.) */
.about__ad { margin-top: var(--sp-6); }
.about__ad a {
  font-family: var(--font-display); font-size: 19px; font-weight: 600;
  color: var(--accent); text-decoration: none;
}
.about__ad a:hover, .about__ad a:focus-visible { text-decoration: underline; }

/* ---- EVERY HOST ACTION, IN ORDER OF WHAT IT COSTS -----------------------

   Three rows at the top of the panel: change it quietly, change it and tell
   everybody, call it off. Each is a button with the CONSEQUENCE beside it -
   "Nobody is emailed" rather than "Free", because that is the thing being
   decided and it is what stops the free option reading as the lesser one.

   The button keeps a fixed width so the three notes start on one line: a
   ragged left edge down a column of three makes them read as unrelated
   controls rather than as three answers to one question. Below 520px they
   stack, note under button, because 150px of button plus a sentence is not a
   line that fits on a phone. */
.acts { margin: 0 0 var(--sp-3); }
/* THE TWO BUTTONS ARE ONE COLUMN, CENTRED (owner, 4 Sep 2026: "center these
   buttons better, their placement is poor"). They share a width so the
   column has one edge, the label sits in the middle of each rather than
   pushed to the left of a wide pill, and the note beside them is centred on
   the button rather than hung off its baseline. */
.acts__row {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) 0; border-bottom: 1px solid var(--line);
}
.acts__row:last-child { border-bottom: 0; }
.acts__btn { flex: 0 0 auto; width: 200px; justify-content: center; }
.acts__note { font-size: 12.5px; color: var(--ink-faint); }
/* The price sits inside the note rather than on the button: a button with a
   figure on it reads as a checkout, and this one opens a dialog. */
.acts__price { font-weight: 700; color: var(--ink-soft); white-space: nowrap; }
.acts__off { margin: 0; font-size: 13px; color: var(--ink-faint); }

@media (max-width: 520px) {
  .acts__row { flex-direction: column; align-items: stretch; gap: 4px; }
  .acts__btn { width: auto; min-width: 0; justify-content: center; }
}

/* ---- THE BUNDLE, DRAWN AS ONE THING -------------------------------------

   ASKED FOR: "make it obvious that those are a bundle... make it seem and feel
   and visually be like a package. You get that package, and you could do any
   and all of those things for $7.99."

   What was actually wrong was not the styling: "Change and tell everyone" sat
   in the row of free actions as a fourth sibling with a price stuck on it, so
   reading down the column gave four separate decisions, one of which cost
   money. Moving it inside the package is most of the fix; this only has to
   draw the box around it.

   A SURFACE AND A BORDER, not a colour. The panel it sits in is already the
   card tint, so a tinted box inside a tinted box is a smudge - the border and
   the paler ground are what make it read as one object lifted out of the
   list. Nothing here is louder than the buy button, which is the one thing on
   the page that should be. */
.pkg {
  margin-top: var(--sp-3); padding: var(--sp-3) var(--sp-4) var(--sp-4);
  border: 1px solid var(--line-strong); border-radius: 12px;
  background: var(--surface);
}
/* Bought: the border takes the sage that means "this is on", and nothing else
   changes. A second treatment for the paid state would make it a different
   component to read. */
.pkg--on { border-color: var(--seaglass); }

.pkg__head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--sp-3); margin: 0;
}
.pkg__name { font-weight: 800; font-size: 14px; display: inline-flex;
  align-items: center; gap: 6px; }
.pkg__price {
  font-weight: 700; font-size: 12.5px; white-space: nowrap;
  padding: 2px 9px; border-radius: 999px;
  background: var(--primary-tint); color: var(--primary);
}
.pkg--on .pkg__price { background: var(--seaglass-tint); color: var(--seaglass-ink); }
.pkg__sub { margin: 4px 0 var(--sp-3); font-size: 12.5px; color: var(--ink-faint); }

.pkg__list { list-style: none; margin: 0; padding: 0; }
/* One row per thing the payment buys, whatever kind of thing it is: a
   capability with nothing to set, an action with a button, an email with a
   tick and a day. The tick column keeps them aligned so the list reads as one
   set rather than as three kinds of row. */
.pkg__item {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) 0; border-top: 1px solid var(--line);
  font-size: 13.5px;
}
.pkg__item:first-child { border-top: 0; }
.pkg__tick { flex: none; color: var(--seaglass-ink); display: inline-flex; }
.pkg__text { flex: 1 1 auto; }
/* "Booked. Goes out Tue, Sep 8." under the nudge and the reminder once the
   package is paid for: the arrangement, in sea-glass, so the tick and the
   days picker beside it read as adjusting something already in the queue. */
.pkg__booked {
  display: flex; align-items: center; gap: 5px; margin-top: 3px;
  font-size: 12px; font-weight: 600; color: var(--seaglass-ink);
}
.pkg__booked svg { flex: none; }
.pkg__booked.is-off { color: var(--ink-faint); font-weight: 500; }
/* "Send it now?" asked in the status line, with its two answers beside it. */
.sched__save .btn { margin-left: 8px; vertical-align: middle; }
.pkg__ctl { flex: none; display: inline-flex; align-items: center; gap: 6px; }

/* "on" beside the box rather than a sentence: the item's own text already says
   what it does, so the tick only has to say whether it is happening. */
.pkg__on { display: inline-flex; align-items: center; gap: 5px; margin: 0;
  font-size: 12.5px; color: var(--ink-faint); }
.pkg__onlab { text-transform: uppercase; letter-spacing: .04em; font-weight: 700;
  font-size: 11px; }

.pkg__buy { margin: var(--sp-3) 0 0; }
.pkg__foot { margin: 8px 0 0; font-size: 12px; color: var(--ink-faint); }

@media (max-width: 560px) {
  /* ---- THE TICK BELONGS TO THE SENTENCE, NOT TO A ROW OF ITS OWN ----------

     REPORTED, with the column circled: "these look oddly placed in the mobile
     web version, please fix so the check is on the same line as the text."

     AND IT WAS ONE DECLARATION. `flex: 1 1 100%` on the text was meant to stop
     the control squeezing it to a word per row, and it did - by pushing the
     text onto a new line, which left the tick alone on the line above it. Four
     items each opening with a floating check mark read as a list of ticks with
     captions underneath.

     WHAT WAS ACTUALLY WANTED IS THE CONTROL DROPPING, so the control is what
     takes the full row. `1 1 0` on the text lets it shrink and wrap inside
     itself beside the tick instead of jumping below it.

     ALIGNED TO THE TOP, because the text now runs to two or three lines on a
     phone and a centred tick against a three-line sentence floats in the
     middle of nothing. `2px` is the optical nudge onto the first line's
     baseline. */
  .pkg__item { flex-wrap: wrap; align-items: flex-start; }
  .pkg__tick { margin-top: 2px; }
  .pkg__text { flex: 1 1 0; min-width: 0; }
  .pkg__ctl { flex: 1 1 100%; margin-left: 22px; }
}

/* ---- WHAT WE WILL SEND, ON THE ORGANISER'S OWN PAGE ---------------------

   Two ticks and two day pickers, shown to the host whether or not the upgrade
   has been bought - the point is that the purchase is a thing you have picked
   rather than a thing you are told about afterwards.

   It sits inside .upsell, which already carries the card colour, so this adds
   the rows and nothing else: no second panel, no second border, no impression
   of a separate product. That is the same argument that collapsed two priced
   buttons into one. */
.sched { margin-top: var(--sp-3); }
.sched__head { margin: 0 0 var(--sp-2); font-size: 13.5px; }
.sched__note { display: block; font-size: 12.5px; color: var(--ink-faint); margin-top: 2px; }

.sched__row {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2) var(--sp-3);
  padding: var(--sp-2) 0; border-top: 1px solid var(--line);
}
.sched__check { flex: 1 1 240px; margin: 0; align-items: flex-start; }
.sched__blurb { display: block; font-style: normal; font-size: 12.5px;
  color: var(--ink-faint); margin-top: 1px; }

/* The picker sits on the same line on a laptop and drops under the tick on a
   phone, which is what `flex-wrap` plus these two bases do. Never hidden when
   the tick is clear - see wireSchedule - only disabled, so the answer to "how
   many days" keeps its place on the page. */
.sched__days { display: inline-flex; align-items: center; gap: 6px; flex: 0 0 auto; }
.sched__dayslab { font-size: 12.5px; color: var(--ink-faint); }
.sched__sel { font-size: 13px; padding: 5px 8px; }
.sched__sel:disabled { opacity: .5; }

.sched__save { margin: var(--sp-2) 0 0; font-size: 12.5px; color: var(--ink-faint);
  min-height: 1em; }
/* The one place a failure is louder than the palette's usual quiet: a setting
   that did not save is a promise about somebody else's inbox. */
.sched__save.is-bad { color: var(--destructive); }

/* ---- Legal (Privacy / Terms) content pages ------------------------------ */
/* Same reasoning as .about. Legal pages are the longest prose on the site,
   so they keep a measure — but a generous, centred one. */
.legal { max-width: 1100px; margin-inline: auto; }
.legal h1 { font-size: clamp(28px, 6vw, 38px); letter-spacing: -.02em; }
.legal .legal-updated { color: var(--ink-faint); font-size: 14px; margin-top: var(--sp-2); font-weight: 600; }
.legal h2 { font-size: 20px; margin-top: var(--sp-6); }
.legal p, .legal li { color: var(--ink-soft); font-size: 16px; line-height: 1.65; }
.legal p { margin-top: var(--sp-3); }
.legal ul { margin-top: var(--sp-3); padding-left: 20px; }
.legal li { margin-top: var(--sp-2); }
.legal .legal-note {
  margin-top: var(--sp-5); padding: var(--sp-4); background: var(--surface-2);
  border-radius: var(--radius); font-size: 14px; color: var(--ink-soft);
}

/* ---- Footer business CTA -------------------------------------------------
   Deliberately quiet: no card, no shadow — it sits in the footer as one plain
   line so residents skim past it, but a business owner can still spot and
   click it. */
.site-footer__biz { margin-bottom: 0; }
.biz-cta {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  padding: var(--sp-3) 0;
}
.biz-cta__t {
  margin: 0; font-size: 14px; font-weight: 400; color: var(--ink-soft);
}
/* The shop icon IS the link to the plans page. Given a circle and a hover fill
   so it reads as a button rather than decoration. */
.biz-cta__go {
  margin-left: auto; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; border-radius: var(--radius-pill);
  background: var(--surface); border: 1px solid var(--line-strong); color: var(--primary);
  transition: background .15s, border-color .15s, color .15s, transform .06s;
}
.biz-cta__go:hover {
  background: var(--primary); border-color: var(--primary); color: var(--on-primary);
  text-decoration: none;
}
.biz-cta__go:active { transform: translateY(1px); }
.biz-cta__go:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
@media (max-width: 520px) {
  .biz-cta__go { margin-left: 0; }
}

/* ---- Newsletter page ---------------------------------------------------- */
.news-card {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: var(--sp-6); margin-top: var(--sp-5);
}
.news-benefits { list-style: none; margin: var(--sp-5) 0 0; padding: 0; display: grid; gap: var(--sp-3); }
.news-benefits li { display: flex; gap: 10px; align-items: flex-start; color: var(--ink-soft); font-size: 15px; }
.news-benefits svg { color: var(--primary); flex: none; margin-top: 2px; }

/* minmax(0, …) RATHER THAN THE IMPLICIT auto TRACK. An auto track is floored
   at its content's min-content width, and two children here have a large one
   that cannot shrink: the file input, whose button and "no file chosen" are
   drawn by the browser, and the number input, which is sized by its own
   default. On a 320px screen the track grew to 265px inside a 222px form and
   the photo picker and the stock strip hung off the right of the page. The
   same fix .field-row already carries. */
.field { display: grid; grid-template-columns: minmax(0, 1fr); gap: 6px; margin-top: var(--sp-4); }
/* `display: grid` above beats the browser's own `[hidden] { display: none }`,
   so every conditionally-shown field on the site was visible whether or not it
   was meant to be: the "how many times" box, the write-in category, the
   write-in trade, the one-off venue. The DOM said hidden and the screen said
   otherwise, which is why it survived testing that checked the property. */
.field[hidden] { display: none; }

/* THE SAME TRAP, EVERYWHERE. Any author rule that sets `display` outranks the
   browser's own `[hidden] { display: none }`, so every element on this site
   that is both styled and conditionally shown was visible whether or not it
   was meant to be. Fixing them one class at a time meant finding them one bug
   at a time; `!important` here is the rare case where it is the right tool,
   because "hidden" is not a suggestion. */
[hidden] { display: none !important; }
.field > label { font-size: 13px; font-weight: 700; color: var(--ink); }
/* Both selectors, one definition. The bare class covers hints used outside a
   `.field`; the child selector exists purely to out-specify the generic
   `.field > span` rule below (0-2-0 beats 0-1-1) — that was winning, so every
   hint inside a field rendered as bold ink, visually identical to the label
   above it and impossible to skim past. */
/* SAME CLASS, TWO TAGS, TWO LAYOUTS. 77 of these are <span> and 46 are <p>,
   and the reset zeroes h1/h2/h3 but never p — so the <p> half carried a browser
   1em bottom margin the <span> half never had, and the stylesheet gave no clue
   why one hint pushed the next thing down and an identical one did not.
   Spacing here is set by whatever comes next, as everywhere else in this file,
   so the bottom margin is stated rather than left to the browser. */
.field__hint,
.field > .field__hint {
  font-size: 12.5px; font-weight: 500; color: var(--ink-faint);
  margin: var(--sp-2) 0 0;
}
/* THE PULL-UP BELONGS TO INPUTS, NOT TO EVERY HINT. -2px is right against the
   bottom of a text box, which already carries its own visual gap. It was on the
   bare class too, so a hint sitting under anything else — a row of buttons, a
   preview, a paragraph — was dragged 2px INTO whatever came before it. On the
   logo card that put the file-size line on top of the Upload button. */
.field > .field__hint { margin-top: -2px; }
/* A hint can name the one post type a field belongs to; that word carries the
   meaning, so let it stand out against the faint hint colour. */
.field__hint strong { font-weight: 700; color: var(--ink-soft); }
/* THE VENUE'S OWN ADDRESS, ON ITS OWN LINE. The Address field carries a fixed
   hint saying where the street shows, and fillVenueAddress appends a second
   sentence to it whenever the chosen venue supplied the street itself. Both
   are inline, so on a phone the two ran together into one three-line paragraph
   in which neither could be picked out. A block puts the venue's note under
   the rule rather than trailing off the end of it. */
#ad-addr-note { display: block; margin-top: 2px; }
/* Every text-entry type a form on this site uses. `date`, `time`, `url` and
   `tel` were missing, so those fields fell back to the browser's own 21px
   control and sat next to 48px styled ones — the Date and Start time on the
   promote form, the phone number on signup and settings. `number` stays out on purpose: the only number inputs are
   money, and they are styled by `.price-input` with a $ prefix. */
.field input[type="email"], .field input[type="text"], .field input[type="password"],
.field input[type="date"], .field input[type="time"], .field input[type="url"],
.field input[type="tel"],
.field textarea, .field select {
  font: inherit; color: var(--ink); background: var(--surface);
  border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  padding: 12px 14px; min-height: 48px; width: 100%;
}
.field textarea { min-height: 88px; resize: vertical; }

/* THE DATE FIELD STOPS READING AS A SLAB ON A PHONE.

   "The button for the date is very, very long. Oddly big compared to other
   buttons. And I am wondering if for dates there isn't a better button that
   folks can pick, something that resembles a calendar."

   It is not actually bigger: it takes the same padding, height and width:100%
   as every other input in this form. Three things make it look wrong anyway,
   and all three are fixed here.

     IT IS THE WIDTH OF THE FORM for a value eleven characters long, which is
     what makes a text field read as a button. Capped, so it is sized like the
     thing it holds.

     iOS CENTRES THE VALUE in a date input where every neighbouring field is
     left-aligned, so it looks like a label in a box rather than something
     typed. Stated explicitly; it is a no-op in browsers that already left-
     align, and the one line that matters on an iPhone.

     AND NOTHING SAID "DATE". A calendar mark answers the question the owner
     was really asking. Drawn in the background rather than by changing the
     control, so iOS keeps its own native wheel picker, which is already the
     best date picker on that phone and is not something to replace. */
@media (max-width: 620px) {
  .field input[type="date"] {
    max-width: 15rem; text-align: left; padding-left: 42px;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%234A5768' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2'/%3E%3Cpath d='M16 2v4M8 2v4M3 10h18'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: left 13px center;
  }
}
.field input:focus, .field textarea:focus, .field select:focus {
  outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-tint);
}
/* The Highlight headline's character counter. It shipped with no rule at all,
   so "48/65 characters" rendered at full body size in full body colour — as
   loud as the headline it was counting — and `.over` changed nothing, which
   made the one state worth noticing the one state that looked identical. */
.wordcount {
  display: block; margin-top: 4px;
  font-size: 12.5px; font-weight: 500; color: var(--ink-faint);
}
.wordcount.over { color: var(--destructive); font-weight: 700; }

.field__err { font-size: 13px; color: var(--destructive); font-weight: 600; display: none; }
.field.is-invalid input, .field.is-invalid select, .field.is-invalid textarea { border-color: var(--destructive); }
.field.is-invalid .field__err { display: block; }

/* MATCHED. The quiet half of the live password check: a green edge and a tick
   so "they are the same" is visible without a sentence claiming it. Success
   should be reassuring at a glance, not another line of text to read.

   Colour is not carrying this on its own — the ✓ is a real character, so it
   survives a colour-blind reader and a monochrome screenshot alike. */
/* `--seaglass-ink` is this site's success colour — the one .syn-tag--ok and
   .syn-banner--ok already use. An invented `--success` would have resolved to
   nothing and fallen back silently, which is how ten `var(--muted)` uses ended
   up rendering at full ink earlier in this project. */
.field.is-ok input { border-color: var(--seaglass-ink); }
.field.is-ok::after {
  content: '✓ Match';
  display: block; margin-top: 4px;
  font-size: 13px; font-weight: 600; color: var(--seaglass-ink);
}

/* Password field with show/hide toggle */
.pw-wrap { position: relative; }
.pw-wrap input { padding-right: 46px; }
.pw-toggle {
  position: absolute; top: 0; right: 0; height: 100%; width: 44px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; background: transparent; color: var(--ink-faint); border-radius: var(--radius-sm);
}
.pw-toggle:hover { color: var(--ink); }
.pw-toggle:focus-visible { outline: 3px solid var(--focus); outline-offset: -2px; }

/* Segmented radio (frequency / plan-ish choices) */
/* The newsletter waiting list. Says up front that there is nothing to receive
   yet — burying that under a sign-up form would be a small deception. */
.news-soon {
  display: flex; gap: var(--sp-3); align-items: flex-start;
  padding: var(--sp-4); margin-bottom: var(--sp-4);
  /* Light blue, not the coral tint. A red-family notice reads as a warning,
     and this is an invitation. */
  background: var(--primary-tint); border: 1px solid var(--primary);
  border-radius: var(--radius);
  font-size: 15px; line-height: 1.6; color: var(--ink-soft);
}
.news-soon > svg { flex: none; color: var(--primary); }
.news-soon strong { color: var(--ink); }
/* Sub-heading inside the interests block, one per section. */
.nl-group {
  /* Was --sp-4 and read as attached to the paragraph above it. A
     subheading needs more space before it than after, or it looks like
     a caption for what it follows rather than a title for what it
     introduces. */
  /* An explicit value, not a --sp token. --sp-6 turned out to be 16px, so
     reaching for "the big one" silently produced the same gap it already had.
     A subheading needs more space BEFORE it than after, or it reads as a
     caption for the paragraph it follows rather than a title for the one it
     introduces. */
  margin: 44px 0 var(--sp-2);
  font-size: 12px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink-faint);
}
.nl-group:first-of-type { margin-top: var(--sp-3); }

.seg { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
/* Two equal choices side by side (neighbor / organization). */
.seg--2 label { flex: 1 1 220px; }
.seg label {
  flex: 1; min-width: 140px; border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  padding: 12px 14px; cursor: pointer; display: flex; flex-direction: column; gap: 2px; transition: border-color .15s, background .15s;
}
.seg label:hover { background: var(--surface-2); }
.seg input { position: absolute; opacity: 0; width: 0; height: 0; }
.seg label:has(input:checked) { border-color: var(--primary); background: var(--primary-tint); box-shadow: inset 0 0 0 1px var(--primary); }
.seg label:has(input:focus-visible) { outline: 3px solid var(--focus); outline-offset: 2px; }
.seg__t { font-weight: 700; font-size: 15px; color: var(--ink); }
.seg__d { font-size: 13px; color: var(--ink-faint); }
/* Disabled "coming soon" frequency option */
.seg label.seg__soon { opacity: .6; cursor: not-allowed; background: var(--surface-2); }
.seg label.seg__soon:hover { background: var(--surface-2); }
.seg label.seg__soon input { cursor: not-allowed; }
.seg__badge {
  display: inline-block; margin-left: 6px; font-size: 10.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .04em; color: var(--ink-faint); background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--radius-pill); padding: 1px 7px; vertical-align: middle;
}

/* ---- Per-page selector + pager (Events list) ---------------------------- */
.perpage {
  display: inline-flex; align-items: center; gap: 6px; font-size: 13px;
  color: var(--ink-faint); font-weight: 600; white-space: nowrap;
}
.perpage select {
  font: inherit; font-size: 14px; font-weight: 600; color: var(--ink);
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm); padding: 7px 26px 7px 10px; min-height: 40px;
  cursor: pointer; appearance: none; -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%238A8271' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 8px center;
}
.perpage select:hover { background-color: var(--surface-2); }
.perpage select:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }

.pager {
  margin-top: var(--sp-6); padding-top: var(--sp-4); border-top: 1px solid var(--line);
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4); flex-wrap: wrap;
}
.pager__info { margin: 0; font-size: 13.5px; color: var(--ink-faint); font-weight: 600; }
.pager__info strong { color: var(--ink); }
.pager__ctrls { display: flex; align-items: center; gap: 4px; margin-left: auto; }
.pager__btn, .pager__num {
  min-width: 40px; height: 40px; padding: 0 6px; border-radius: var(--radius-sm);
  border: 1px solid var(--line-strong); background: var(--surface); color: var(--ink);
  font: inherit; font-weight: 700; font-size: 14px;
  display: inline-flex; align-items: center; justify-content: center; transition: background .15s, border-color .15s;
}
.pager__btn:hover:not(:disabled), .pager__num:hover:not(.is-current) { background: var(--surface-2); }
.pager__btn:disabled { opacity: .4; cursor: not-allowed; }
.pager__num.is-current { background: var(--primary); border-color: var(--primary); color: var(--on-primary); cursor: default; }
.pager__gap { min-width: 24px; text-align: center; color: var(--ink-faint); font-weight: 700; }

@media (max-width: 460px) {
  .pager { justify-content: center; }
  .pager__ctrls { margin-left: 0; }
}

/* ---- THE PER-PAGE PICKER ON THE PAGER'S LINE (owner, 17 Sep 2026) --------

   "The per page button looks like a lost button in a misplaced spot. Please
   try to keep it in the same line, perhaps as the first item on that line in
   the left? We may also need to make it a different color to make it not look
   like the other page buttons."

   On a phone the toolbar it used to sit in is sent below the results, so the
   picker ended up alone on a line under the pager, right-aligned, 40px tall
   and bordered exactly like a page number. NL.placePerPage moves the node into
   the slot at the head of the pager; this decides how the row then breaks.

   FIRST LINE: THE PICKER AND THE COUNT, at the two ends of it. Measured at
   343px: the picker is about 111px with "Show" dropped, the count about 150px,
   so they share a line with room to spare and nothing wraps mid-phrase.

   SECOND LINE: THE PAGE BUTTONS, the full width and centred. `flex: 1 0 100%`
   is what forces the break rather than leaving it to chance - at 343px the
   buttons come to ~244px, which would otherwise try to squeeze up beside the
   count on a wider phone and break the pair apart at some width nobody tested.

   A DIFFERENT GROUND, WHICH IS THE REST OF THE ASK. A page button is
   `--surface` inside a `--line-strong` border, and the current one is solid
   navy; this is a warm tint with a hairline, so it reads as the quiet dropdown
   it is. Smaller, too: it is a setting, not a destination. */
.pager__pp { flex: none; }
.pager__pp:empty { display: none; }
@media (max-width: 620px) {
  .pager { justify-content: space-between; row-gap: var(--sp-3); }
  .pager__ctrls { flex: 1 0 100%; justify-content: center; margin-left: 0; }
  .pager__pp .perpage { font-size: 12.5px; }
  .pager__pp .perpage select {
    min-height: 36px; font-size: 13.5px; padding: 6px 24px 6px 9px;
    background-color: var(--surface-2); border-color: var(--line); color: var(--ink-soft);
  }
}

/* Inline checkboxes (e.g. newsletter towns) */
.checks { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4); }
.check { display: inline-flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 600; color: var(--ink); cursor: pointer; min-height: 44px; }
.check input { width: 18px; height: 18px; accent-color: var(--primary); }

.form-actions { margin-top: var(--sp-5); }
.form-note { font-size: 13px; color: var(--ink-faint); margin: var(--sp-3) 0 0; }
/* Billing small print — set apart from the ordinary note so it is read, not
   skimmed past, but still quiet. */
.form-note--prepay {
  display: flex; gap: 8px; align-items: flex-start; line-height: 1.55;
  margin-top: var(--sp-4); padding: var(--sp-3) var(--sp-4);
  background: var(--surface-2); border-radius: var(--radius-sm);
  border-left: 3px solid var(--line-strong); color: var(--ink-soft);
}
.form-note--prepay svg { flex: none; margin-top: 2px; color: var(--ink-faint); }
.form-note--prepay strong { color: var(--ink); }
/* Under the wide plan grid the same note would run to ~110 characters a line;
   hold it to a readable measure so it gets read rather than skipped. */
#business .form-note--prepay { margin: var(--sp-6) 0 0; }

/* Promote-an-event: char counter, file input, image previews */
.char-count { display: block; text-align: right; font-size: 12px; color: var(--ink-faint); margin-top: 4px; }
.char-count.over { color: var(--destructive); font-weight: 700; }
.field input[type="file"] {
  font-size: 14px; color: var(--ink-soft); cursor: pointer;
  padding: var(--sp-2) 0; border: none; background: none;
}
.field input[type="file"]::file-selector-button {
  font: inherit; font-weight: 600; cursor: pointer;
  color: var(--ink); background: var(--surface-2, #fff); border: 1px solid var(--line, #e2ddd3);
  border-radius: var(--radius-sm, 8px); padding: 8px 14px; margin-right: var(--sp-3);
}
.field input[type="file"]::file-selector-button:hover { border-color: var(--primary); color: var(--primary); }
.img-previews { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3); margin-top: var(--sp-3); }
.img-previews img { width: 88px; height: 88px; object-fit: cover; border-radius: var(--radius-sm, 8px); border: 1px solid var(--line, #e2ddd3); }
/* Stock artwork sits in these rows too — the fix-an-event form shows whatever
   the listing is using now, and that is a stock tile more often than not. It
   is an SVG asking for 100% of a parent with no height, so it needs one. */
.img-previews .stock-art { width: 88px; height: 88px; flex: none; border-radius: var(--radius-sm, 8px); overflow: hidden; }
/* The note is a whole line under the picture, not a word beside it: at 88px a
   thumbnail cannot tell you whether it is the venue's poster or our clip art,
   which is the one thing this row exists to say. */
.img-previews .img-note { flex-basis: 100%; margin: 0; display: flex; align-items: center; gap: 6px; }
.img-note { font-size: 12.5px; color: var(--ink-faint); font-weight: 600; }
/* ---- AMOUNT, WHEN THE TICKETS HAVE ALREADY ANSWERED IT --------------------
   Shown in place of the presets and the boxes rather than over them, so there
   is one answer on screen and not two (assets/promote.js, syncDerivedPrice).
   The figure is set in the ink the page uses for an answer; the sentence
   after it says where the figure came from and is deliberately quieter. */
.price-derived {
  margin-top: var(--sp-2); padding: 10px 12px;
  border: 1px solid var(--line); border-radius: 10px; background: var(--paper);
  font-size: 13.5px; color: var(--ink-soft);
}
.price-derived strong { font-size: 16px; color: var(--ink); margin-right: 6px; }

/* Custom price amount input with a $ prefix */
/* Quick answers for the common cases, in place of the old Price dropdown. */
.price-presets { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-2); }
.price-preset {
  padding: 9px 14px; min-height: 40px;
  background: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--radius-pill);
  font-size: 14px; font-weight: 600; color: var(--ink);
  transition: background .15s, border-color .15s, color .15s;
}
.price-preset:hover { border-color: var(--primary); color: var(--primary); }
.price-preset.is-on { background: var(--primary); border-color: var(--primary); color: var(--on-primary); }
.price-preset:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }

.price-input { position: relative; }
.price-input__cur { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); color: var(--ink-soft); font-weight: 600; pointer-events: none; }
/* width:100% because a number input carries its own intrinsic width, and
   min-width:0 because without it the flex item below cannot shrink to the
   170px it asks for. */
.price-input input { padding-left: 28px; min-height: 52px; font-size: 16px;
  width: 100%; min-width: 0; }
.price-format { margin-top: var(--sp-2); }
.price-format label { min-width: 0; flex: 1; text-align: center; }
.price-amounts { display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-3); flex-wrap: wrap; }
.price-amounts .price-input { flex: 0 1 170px; min-width: 0; }
/* Exact / From / Range share one row three ways. Below this the 14px of side
   padding leaves 41px for a word that needs 44, and "Range" loses its last
   letters. */
@media (max-width: 380px) {
  .price-format.seg label { padding-left: 6px; padding-right: 6px; }
}
.price-dash { font-weight: 700; color: var(--ink-faint); }
.ev-allday { margin-top: var(--sp-3); }
.field-row.is-disabled { opacity: .5; }
/* THE SHARED BUTTON-ROW CLASS, despite the name.
   `.form-actions` on its own is a block with a top margin, so two buttons
   inside it are separated only by the text space between the tags: a gap
   sideways and none at all once a narrow screen wraps them, which reads as
   two buttons stuck together. Add this alongside it on any row holding more
   than one button. Used by promote, highlight, weekender-build,
   newsletter-build and invite. Do not restyle bare `.form-actions` to get
   this behaviour: it has 48 call sites, most of them a single button. */
.promote-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3); }
.promote-actions .form-note { width: 100%; margin-top: 0; }
/* Preview-of-post card */
.promote-preview { margin-top: var(--sp-5); }
.promote-preview__label {
  display: inline-flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 700;
  color: var(--ink-faint); text-transform: uppercase; letter-spacing: .05em; margin-bottom: var(--sp-2);
}
/* Split preview: list-card banner on top, full detail page below (stacked).
   Broken out of the narrow form column so it uses the full page width. */
.preview-split { display: grid; grid-template-columns: 1fr; gap: var(--sp-6); }
@media (min-width: 900px) {
  .promote-preview { width: min(100vw - 2 * var(--sp-5), 1120px); margin-left: 50%; transform: translateX(-50%); }
}
.preview-pane { min-width: 0; }
.preview-pane__cap {
  display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 700;
  color: var(--ink-faint); text-transform: uppercase; letter-spacing: .04em; margin-bottom: var(--sp-2);
}
.preview-pane__cap svg { color: var(--ink-faint); flex: none; }
.preview-frame {
  background: var(--surface-2); border: 1px dashed var(--line-strong);
  border-radius: var(--radius); padding: var(--sp-4);
}
/* The picture, shown under the tile so it can be judged rather than believed.
   Capped by HEIGHT, not width: flyers are portrait and posters are landscape,
   and a width cap turns the first into a column half a screen tall. `contain`
   over `cover` for the same reason — this view exists to check the picture, so
   cropping the date off the bottom of a flyer would defeat it. */
.preview-shot {
  margin-top: var(--sp-3); background: var(--surface-2);
  border: 1px dashed var(--line-strong); border-radius: var(--radius);
  padding: var(--sp-3); display: flex; justify-content: center;
}
.preview-shot img {
  max-height: 230px; max-width: 100%; width: auto; height: auto;
  object-fit: contain; border-radius: var(--radius-sm); display: block;
}
/* Stock art is an SVG with no intrinsic size, and `.stock-art` asks for 100%
   of a parent that has no height here — so it needs one given to it. Its own
   aspect ratio is the tile's, which is what it is drawn for. */
.preview-shot .stock-art {
  width: 260px; max-width: 100%; height: 150px;
  border-radius: var(--radius-sm); overflow: hidden;
}
.event-card--static { cursor: default; }
.event-card--static:hover { transform: none; }
/* ---- THE LIVE TILE (promote, highlight, admin-post) -----------------------
   The Board form's "How it will look in the list", on every form that makes
   a post: the card as it will sit in the list, above the buttons, redrawn as
   you type. See NL.wireLiveTile. Inert on purpose — the card is the real
   component, links and buttons included, and none of them may go anywhere
   from here. */
/* IN A TINTED BOX, like the Board form's (owner, 16 Sep 2026: "add this blue
   background to the live draft tile view"). The wash marks it off from the
   fields around it as a picture of the post rather than one more thing to
   fill in. */
.live-tile {
  margin: var(--sp-6) 0 var(--sp-2); padding: var(--sp-4);
  background: var(--primary-tint); border-radius: var(--radius);
}
.live-tile__t {
  font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
  color: var(--primary); margin-bottom: 12px;
}
.live-tile__card { pointer-events: none; }
.live-tile__card .event-card, .live-tile__card .hl-tile, .live-tile__card .board-card { margin-bottom: 0; }
.live-tile__empty { margin: 0; font-size: 14px; color: var(--ink-faint); }
/* The popup's compact form: the tile, the cost, and a way to the full page. */
.preview-more { margin: var(--sp-3) 0 0; }
/* Cost confirmation + final submit, shown inside the preview */
.preview-confirm {
  margin-top: var(--sp-5); display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4);
  flex-wrap: wrap; background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--radius); padding: var(--sp-4) var(--sp-5);
}
.preview-confirm__cost { display: flex; align-items: center; gap: 9px; margin: 0; font-size: 15px; color: var(--ink-soft); line-height: 1.45; }
.preview-confirm__cost svg { color: var(--accent); flex: none; }
.preview-confirm__cost strong { color: var(--ink); }
/* Full-page preview scaled to fit the pane */
.detail--preview h1 { font-size: clamp(20px, 4vw, 26px); margin-top: var(--sp-2); }
.detail--preview .detail-when { margin-top: var(--sp-3); font-size: 15px; }
.detail--preview .detail-facts { margin-top: var(--sp-4); }
.detail--preview .fact { padding: var(--sp-3); }
.detail--preview .detail-desc { margin-top: var(--sp-4); font-size: 15px; }
.detail--preview .detail-actions { margin-top: var(--sp-4); }
/* The one photo on a post, shown WHOLE. `cover` was cropping the top and
   bottom off anything wider than 2.9:1, which is most phone photos held
   sideways — the poster framed the shot, so cutting it is our mistake, not
   theirs. `contain` keeps the whole frame; the cap only stops a portrait photo
   from taking over the page, and the background fills the letterbox on the rare
   image tall enough to hit it. */
/* Spaced ABOVE by default: our stock artwork is the last thing on a detail page
   (see event.js), so the gap belongs between it and the tags it follows. A real
   photo leads the page instead and gets `--lead` below, which turns that round. */
.detail-imgs {
  margin-top: var(--sp-5); border-radius: var(--radius-sm, 8px); overflow: hidden;
  background: var(--surface-2);
}
/* A PHOTO THE POSTER ACTUALLY SENT, at the top of the page. Nothing above it,
   so no top margin; the space goes underneath, between it and the category.

   Capped shorter than the stock block's 460px on purpose. This one sits above
   the date, and the whole reason the picture used to be last was that a tall
   image pushed the facts off the screen — 360px keeps a phone showing the
   title and the day without scrolling, which is the constraint that argument
   was really about. */
.detail-imgs--lead { margin-top: 0; margin-bottom: var(--sp-5); }

/* THE PHOTO CREDIT, in the corner of the picture it belongs to.

   `position: relative` on the block and absolute here, so the credit sits ON
   the image rather than under it - a line of text below a photograph reads as
   a caption about the event, which is the wrong thing entirely.

   A SCRIM RATHER THAN A COLOUR. White on a dark wash works over any
   photograph; white alone disappears on a pale flyer and dark alone
   disappears on a night shot, and we do not get to choose what a venue
   publishes. Small and low-contrast enough to stay out of the way, still over
   4.5:1 against its own backing. */
.detail-imgs { position: relative; }
.img-credit {
  position: absolute; right: 8px; bottom: 8px; max-width: calc(100% - 16px);
  padding: 3px 8px; border-radius: var(--radius-sm);
  background: rgba(23, 30, 38, .66); color: #fff;
  font-size: 11.5px; line-height: 1.4; letter-spacing: .01em;
  text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
a.img-credit:hover { background: rgba(23, 30, 38, .82); text-decoration: underline; color: #fff; }
a.img-credit:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
.detail-imgs img { display: block; width: 100%; height: auto; max-height: 460px; object-fit: contain; }
/* AFTER `.detail-imgs img`, not before it. The two selectors weigh exactly the
   same — one class and one element each — so the only thing separating them is
   which comes second, and written above it this rule lost silently and the
   lead image kept the 460px cap.

   `contain`, repeated here rather than left to be inherited, as a reminder:
   NOT `cover`. Half of these are flyers with the date printed on them, and
   cropping to fill a box is how you cut the date off. Letterboxing against
   --surface-2 is the plainer option and it is the right one. */
.detail-imgs--lead img { max-height: 360px; object-fit: contain; }
.detail-imgs--multi { display: grid; grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); gap: 4px; }
/* Thumbnails in the legacy multi-photo grid stay cropped on purpose: they are
   an index, not the photo itself, and a uniform grid needs uniform tiles. */
.detail-imgs--multi img { max-height: 130px; object-fit: cover; }
.detail-imgs--empty {
  display: flex; align-items: center; justify-content: center; gap: 8px; padding: var(--sp-5);
  background: var(--surface); border: 1px dashed var(--line-strong); color: var(--ink-faint);
  font-size: 13px; font-weight: 600;
}
.detail-imgs--empty svg { color: var(--ink-faint); }

/* ---- Build-your-Highlight page ------------------------------------------
   Deliberately distinct from the ordinary post form — this is the paid
   placement, so the card is tinted and led by the product name. */
/* Sunset, not the coral-rose accent: a whole card outlined in --accent reads as
   an error, and this is the one thing on the page somebody is paying for. */
.news-card--hl { border-color: var(--sunset); border-top-width: 3px; padding-top: var(--sp-4); }
.hl-form-head { padding-bottom: var(--sp-4); border-bottom: 1px solid var(--line); margin-bottom: var(--sp-2); }
.hl-form-head__badge {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--font-body); font-size: 12px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em;
}
/* Matches the border, or the star and the card disagree about the colour. */
.hl-form-head__badge svg { color: var(--sunset); }
.hl-form-head__note { margin: 8px 0 0; font-size: 14px; color: var(--ink-soft); line-height: 1.55; }

/* The placement they just bought — a neutral receipt, not an alert. */
.hl-summary {
  display: flex; align-items: center; gap: 7px; margin-top: var(--sp-3); width: fit-content;
  font-size: 14px; font-weight: 600; color: var(--ink-soft);
  background: var(--surface-2); border: 1px solid var(--line-strong); border-radius: var(--radius-pill); padding: 8px 14px;
}
.hl-summary svg { color: var(--seaglass-ink); flex: none; }
.featured-detail__title svg { color: var(--seaglass-ink); vertical-align: -3px; }
.hl-uploads { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-top: var(--sp-3); }
.hl-thumb {
  position: relative; width: 128px; margin: 0; border: 2px solid var(--line);
  border-radius: var(--radius-sm); overflow: hidden; background: var(--surface);
}
.hl-thumb img { display: block; width: 100%; height: 92px; object-fit: cover; }
.hl-thumb.is-main { border-color: var(--seaglass); box-shadow: 0 0 0 1px var(--seaglass); }
.hl-thumb__badge {
  position: absolute; top: 6px; left: 6px; font-size: 10px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .04em; color: #fff; background: var(--seaglass-ink); border-radius: var(--radius-pill); padding: 2px 7px;
}
.hl-thumb__pick { display: flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 600; color: var(--ink-soft); padding: 6px 8px; cursor: pointer; }
.hl-thumb__pick input { accent-color: var(--seaglass-ink); }
/* Highlight preview card */
.hl-card { display: flex; flex-direction: column; max-width: 440px; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow); }
.hl-card__img img { display: block; width: 100%; height: 210px; object-fit: cover; }
.hl-card__img--empty { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; height: 160px; background: var(--accent-tint); color: var(--ink-faint); font-size: 13px; font-weight: 600; }
.hl-card__img--empty svg { color: var(--seaglass-ink); }
.hl-card__body { padding: var(--sp-4) var(--sp-5); }
.hl-card__label { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: var(--accent); }
.hl-card__title { font-family: var(--font-display); font-size: 20px; font-weight: 600; color: var(--ink); margin-top: 6px; line-height: 1.2; }
.hl-card__blurb { margin-top: var(--sp-2); font-size: 15px; color: var(--ink-soft); line-height: 1.55; }
.hl-card__more { display: inline-flex; align-items: center; gap: 4px; margin-top: var(--sp-3); font-size: 13px; font-weight: 700; color: var(--primary); }

/* Success panel */
.success {
  text-align: center; padding: var(--sp-7) var(--sp-4);
  background: var(--primary-tint); border: 1px solid var(--primary); border-radius: var(--radius); margin-top: var(--sp-5);
}
.success svg { color: var(--primary); }
.success h2 { font-size: 24px; margin-top: var(--sp-3); }
.success p { color: var(--ink-soft); margin-top: var(--sp-2); }
.success__aside {
  margin-top: var(--sp-4); padding-top: var(--sp-3);
  border-top: 1px solid var(--line); font-size: 13px;
}

.contact-email {
  display: flex; gap: 8px; align-items: center; margin: var(--sp-3) 0 0;
  font-size: 14px; color: var(--ink-soft);
}
.contact-email svg { flex: none; color: var(--ink-faint); }

/* Contact form: the per-topic pointer, shown once a subject is chosen. */
.contact-hint {
  display: flex; gap: 8px; align-items: flex-start;
  margin: calc(var(--sp-2) * -1) 0 var(--sp-4); padding: var(--sp-3);
  background: var(--seaglass-tint); border-radius: var(--radius-sm);
  font-size: 13px; line-height: 1.6; color: var(--seaglass-ink);
}
.contact-hint svg { flex: none; margin-top: 3px; }

/* The page being reported, carried in from the Report button. Styled apart
   from .contact-hint because it is a FACT about this message rather than
   advice about the form — and it has to be obviously checkable, since somebody
   who navigated before pressing Report needs to notice it names the wrong one. */
.contact-ref {
  display: flex; gap: 8px; align-items: flex-start;
  margin: calc(var(--sp-2) * -1) 0 var(--sp-4); padding: var(--sp-3);
  background: var(--surface-2); border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  font-size: 13px; line-height: 1.6; color: var(--ink-soft);
}
.contact-ref svg { flex: none; margin-top: 3px; color: var(--ink-faint); }
.contact-ref a { font-weight: 600; word-break: break-all; }

#contact textarea { min-height: 150px; }

/* Modal / popup dialog */
.modal-overlay {
  position: fixed; inset: 0; z-index: 200; display: flex; align-items: center; justify-content: center;
  padding: var(--sp-4); background: rgba(18, 28, 33, .55);
}
/* 520, up from 460. "Make the window larger if you must" - and it must: the
   date-and-two-times row is the widest thing any dialog here holds, and at 460
   it had to drop to two columns even with auto-fit above. This fits all three
   on a laptop and still reads as a dialog rather than a page. */
.modal {
  background: var(--surface); border-radius: var(--radius); box-shadow: 0 20px 60px rgba(0, 0, 0, .3);
  width: 100%; max-width: 520px; max-height: 90vh; overflow: auto; padding: var(--sp-5);
}
.modal__title { font-family: var(--font-display); font-size: 22px; font-weight: 600; color: var(--ink); }
/* The X in a circle, top right, drawn only when a dialog asks for it (16 Sep 2026). */
.modal { position: relative; }
.modal__x {
  position: absolute; top: 12px; right: 12px; width: 32px; height: 32px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--line-strong); border-radius: 50%; background: var(--surface);
  color: var(--ink-soft); cursor: pointer; padding: 0;
}
.modal__x:hover { color: var(--ink); background: var(--surface-2); }
.modal__x:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
.modal__text { margin: var(--sp-2) 0 0; color: var(--ink-soft); font-size: 15px; line-height: 1.55; }
/* A list inside a modal, for the one place a paragraph would be worse: what
   closing an account actually takes with it. Four short lines are read; the
   same four inside a sentence are skimmed, and this is not a sentence to skim. */
.modal__list {
  margin: var(--sp-2) 0 0;
  padding-left: 1.25em;
  color: var(--ink-soft);
  font-size: 15px;
  line-height: 1.55;
}
.modal__list li { margin-top: 4px; }
.modal .field { margin-top: var(--sp-4); }
.modal__actions {
  margin-top: var(--sp-5); display: flex; justify-content: flex-end;
  /* WRAPS, AND KEEPS ITS GAP WHEN IT DOES. Two answers do not fit side by side
     on a 360px phone, and without this the second one is pushed off the edge
     instead of dropping under the first. `--sp-2` is the gap between them on
     one line; the row gap is bigger, because two stacked buttons touching is
     what was reported ("the two buttons are very close together"). */
  flex-wrap: wrap; gap: var(--sp-2); row-gap: 10px;
}
/* THE WAY OUT, UNDER THE ANSWERS RATHER THAN AMONG THEM. See NL.simpleModal:
   "Not now" in a button the same size as the offer made the row too crowded to
   hold "Sign in", which is why signing in ended up as a word in a grey
   sentence. It is a link now, and the row is for answers. */
.modal__skip { margin-top: 12px; text-align: center; }
.modal__skip button, .modal__skip a {
  background: none; border: 0; padding: 6px 10px; min-height: 36px;
  font: inherit; font-size: 14px; font-weight: 600; color: var(--ink-soft);
  text-decoration: underline; text-underline-offset: 3px; cursor: pointer;
}
.modal__skip button:hover, .modal__skip a:hover { color: var(--ink); }
/* Two real answers on a phone: each takes the row rather than being squeezed. */
/* STACKED, PRIMARY ON TOP. Side by side, the convention is the primary on the
   right; stacked, it is the one at the top - `column-reverse` put "Sign in"
   above "Create my free account", which reads as the recommendation. */
@media (max-width: 420px) {
  .modal__actions { flex-direction: column; align-items: stretch; }
  .modal__actions .btn { justify-content: center; }
}

/* A ROW OF BUTTONS THAT SURVIVES WRAPPING. Two inline-blocks and a literal
   space read fine until the row is too narrow to hold both: the space collapses
   at the line break and the buttons end up touching. Reported on a phone, of
   the welcome screen after signing in. */
.btn-row {
  margin-top: var(--sp-4); display: flex; flex-wrap: wrap;
  gap: var(--sp-3); align-items: center;
}
/* Centred inside a success card, and its own top margin restated: `.success p`
   is a class AND an element, so it outranks the bare `.btn-row` above and was
   quietly pulling the row up to --sp-2. */
.success .btn-row { justify-content: center; margin-top: var(--sp-4); }

/* Preview popup: head and foot stay put, only the preview itself scrolls, so the
   cost line and Submit are always in view. */
.modal--preview {
  max-width: 980px; max-height: 92vh; padding: 0;
  display: flex; flex-direction: column;
}
.modal__head {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--line); flex: none;
}
.modal__head .modal__title { font-size: 19px; flex: 1; }
.modal__close {
  flex: none; width: 34px; height: 34px; display: inline-flex; align-items: center; justify-content: center;
  border: 0; background: transparent; color: var(--ink-faint); border-radius: var(--radius-pill);
}
.modal__close:hover { background: var(--surface-2); color: var(--ink); }
.modal__body {
  flex: 1; overflow-y: auto; padding: var(--sp-5); background: var(--paper);
}
.modal__foot {
  flex: none; padding: var(--sp-4) var(--sp-5); border-top: 1px solid var(--line);
  background: var(--surface); display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap;
}
.modal__foot .modal__actions { margin-top: 0; margin-left: auto; }
.modal-cost {
  margin: 0; display: flex; align-items: flex-start; gap: 8px; flex: 1; min-width: 240px;
  font-size: 14px; color: var(--ink-soft); line-height: 1.5;
}
.modal-cost svg { color: var(--seaglass-ink); flex: none; margin-top: 2px; }
.modal-cost strong { color: var(--ink); }
@media (max-width: 560px) {
  .modal__foot .modal__actions { margin-left: 0; width: 100%; }
}

/* ---- Business / membership page ----------------------------------------- */
.promo-banner {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  background: var(--accent-tint); border: 1px solid var(--accent); border-radius: var(--radius);
  padding: var(--sp-4) var(--sp-5); margin-top: var(--sp-5);
}
.promo-banner svg { color: var(--accent); flex: none; }
.promo-banner strong { color: var(--accent); }
.promo-banner code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace; font-weight: 700; letter-spacing: .05em;
  background: var(--surface); border: 1px dashed var(--accent); border-radius: var(--radius-sm);
  padding: 2px 10px; color: var(--accent);
}

.plan-grid { display: grid; grid-template-columns: 1fr; gap: var(--sp-4); margin-top: var(--sp-5); }
.plan {
  position: relative; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  padding: var(--sp-5); box-shadow: var(--shadow); display: flex; flex-direction: column;
}
.plan.is-popular { border-color: var(--primary); box-shadow: 0 0 0 1px var(--primary), var(--shadow); }
.plan__badge {
  position: absolute; top: -11px; left: var(--sp-5); background: var(--primary); color: #fff;
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
  padding: 3px 10px; border-radius: var(--radius-pill);
}
/* Standing annual offer. Sits under the CTA at the bottom-right of the card —
   the top corners are already taken by the "Most popular" badge. */
.plan__annual {
  align-self: flex-end; margin-top: var(--sp-3);
  display: inline-flex; align-items: center; gap: 5px;
  background: var(--seaglass-tint); color: var(--seaglass-ink);
  border: 1px solid var(--seaglass); border-radius: var(--radius-pill);
  font-size: 11px; font-weight: 600; letter-spacing: .01em; padding: 4px 10px;
  transition: background .15s, border-color .15s, transform .06s;
}
.plan__annual strong { font-weight: 800; }
.plan__annual:hover { background: var(--seaglass); color: #fff; border-color: var(--seaglass-ink); }
.plan__annual:active { transform: translateY(1px); }
.plan__annual:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }

.plan__name { font-family: var(--font-display); font-size: 22px; font-weight: 600; }
.plan__tag { font-size: 14px; color: var(--ink-faint); margin-top: 2px; }
.plan__price { margin-top: var(--sp-3); font-family: var(--font-display); }
.plan__amt { font-size: 34px; font-weight: 600; color: var(--ink); }
.plan__unit { font-size: 14px; color: var(--ink-faint); font-family: var(--font-body); font-weight: 600; }
.plan__was { font-size: 16px; color: var(--ink-faint); text-decoration: line-through; margin-left: 6px; font-family: var(--font-body); }
.plan__save { display: inline-block; margin-top: 6px; font-size: 12.5px; font-weight: 700; color: var(--primary-ink); background: var(--primary-tint); padding: 2px 9px; border-radius: var(--radius-pill); }
.plan__offer {
  display: flex; align-items: center; gap: 7px; margin-top: var(--sp-3);
  font-size: 13.5px; font-weight: 700; color: var(--accent-ink, var(--accent));
  background: var(--accent-tint); border: 1px solid var(--accent); border-radius: var(--radius-sm, 8px);
  padding: 8px 11px; line-height: 1.35;
}
.plan__offer svg { color: var(--accent); flex: none; }
.plan__feats { list-style: none; margin: var(--sp-4) 0; padding: 0; display: grid; gap: 10px; flex: 1; }
.plan__feats li { display: flex; gap: 9px; align-items: flex-start; font-size: 14.5px; color: var(--ink-soft); }
.plan__feats svg { color: var(--primary); flex: none; margin-top: 2px; }
/* ---- Sync health (owner) --------------------------------------------------
   A triage list. Colour carries the verdict before the words do, because the
   question being asked is "who needs me?" and the answer should be countable
   from across the room. */
.syn-banner {
  display: flex; align-items: center; gap: 11px;
  padding: var(--sp-4); border-radius: var(--radius); margin-bottom: var(--sp-5);
  font-size: 16px;
}
.syn-banner--bad { background: var(--accent-tint); color: var(--ink); }
.syn-banner--ok  { background: var(--seaglass-tint); color: var(--seaglass-ink); }
.syn-banner svg { flex: none; }

.syn-count {
  display: inline-block; min-width: 22px; padding: 1px 7px; border-radius: var(--radius-pill);
  background: var(--surface-2); color: var(--ink-soft);
  font-family: var(--font-body); font-size: 13px; font-weight: 700; vertical-align: 2px;
}
/* Numbered steps in the owner's own instructions. The number is a marker to
   count down, not decoration — this is a procedure. */
.syn-step { display: flex; gap: 11px; margin: 0 0 var(--sp-4); font-size: 14.5px; line-height: 1.55; color: var(--ink-soft); }
.syn-step strong { color: var(--ink); }
.syn-step__n {
  flex: none; width: 24px; height: 24px; border-radius: 50%;
  background: var(--primary); color: var(--on-primary);
  font-size: 13px; font-weight: 700; line-height: 24px; text-align: center;
}
.syn-src { padding: var(--sp-3) 0; border-top: 1px solid var(--line); }
.syn-src:first-of-type { border-top: 0; }
.syn-src__url { margin: 0; font-size: 14px; word-break: break-all; }
.syn-conf__row { margin: 6px 0 0; display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: baseline; }
.syn-conf {
  font-size: 11.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  padding: 2px 8px; border-radius: var(--radius-pill); white-space: nowrap;
}
.syn-conf--high   { background: var(--seaglass-tint); color: var(--seaglass-ink); }
.syn-conf--medium { background: var(--primary-tint); color: var(--primary-ink); }
.syn-conf--low,
.syn-conf--none   { background: var(--accent-tint); color: var(--accent); }
.syn-conf__what { flex: 1 1 200px; font-size: 13px; color: var(--ink-faint); line-height: 1.45; }

.syn-row { padding: var(--sp-3) 0; border-top: 1px solid var(--line); }
.syn-row:first-of-type { border-top: 0; }
.syn-row__main { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; }
.syn-row__name { font-weight: 600; color: var(--ink); }
.syn-row__why { margin: 4px 0 0; font-size: 13.5px; color: var(--ink-soft); line-height: 1.45; }
.syn-row__meta { margin: 3px 0 0; font-size: 12.5px; color: var(--ink-faint); }
.syn-row__meta a { color: inherit; }
/* HOW LONG IT HAS BEEN WRONG, darker than the rest of the meta line it sits in.
   It shares a row with "last visit 5 hours ago" and has to win: one is their
   problem and the other is our schedule. */
.syn-row__since { color: var(--ink); font-weight: 700; }
/* Already chased. Quiet on purpose — it is a receipt, not an alert. */
.syn-row__handled {
  display: flex; align-items: flex-start; gap: 6px;
  margin: 6px 0 0; font-size: 12.5px; line-height: 1.45; color: var(--seaglass-ink);
}
.syn-row__handled svg { flex: none; margin-top: 2px; }
.syn-banner__done { font-style: normal; opacity: 0.75; }

.syn-tag {
  font-size: 11.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  padding: 2px 8px; border-radius: var(--radius-pill); white-space: nowrap;
}
/* Zero events is the loudest thing on the page on purpose — it is the failure
   that never reports itself. */
.syn-tag--zero        { background: var(--destructive); color: #fff; }
.syn-tag--blocked,
.syn-tag--unreachable,
.syn-tag--other       { background: var(--accent-tint); color: var(--accent); }
.syn-tag--manual      { background: var(--primary-tint); color: var(--primary-ink); }
.syn-tag--unscanned   { background: var(--surface-2); color: var(--ink-faint); }
.syn-tag--ok          { background: var(--seaglass-tint); color: var(--seaglass-ink); }

/* ---- Prospects ------------------------------------------------------------
   Businesses being tested before they are clients. Deliberately plainer than
   the member rows above: this is a working list, not a status board. */
.syn-src__actions {
  margin: var(--sp-3) 0 0; display: flex; flex-wrap: wrap;
  gap: 8px 14px; align-items: center;
}
.syn-toggle {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 13px; color: var(--ink-soft); cursor: pointer;
}
.syn-toggle input { accent-color: var(--primary); width: 15px; height: 15px; }
/* A handful of their real events, to show them on a call. Capped in JS at five
   — the point is recognition, not a full listing. */
.syn-sample { margin: var(--sp-2) 0 0; padding: 0; list-style: none; font-size: 13.5px; color: var(--ink-soft); }
.syn-sample li { margin: 0; }
.syn-sample .syn-row__meta { display: inline; margin: 0; }
/* Each sampled event opens the real card. A button rather than a link because
   it opens a preview in place — there is no page to navigate to. */
.syn-sample__btn {
  display: flex; align-items: baseline; gap: 8px; width: 100%;
  background: none; border: 0; border-radius: var(--radius-sm);
  padding: 5px 8px; margin-left: -8px;
  font: inherit; color: inherit; text-align: left; cursor: pointer;
}
.syn-sample__btn:hover { background: var(--surface-2); }
.syn-sample__see {
  margin-left: auto; flex: none; display: inline-flex; align-items: center; gap: 4px;
  font-size: 12px; font-weight: 600; color: var(--primary); opacity: 0;
}
.syn-sample__btn:hover .syn-sample__see,
.syn-sample__btn:focus-visible .syn-sample__see { opacity: 1; }
/* Touch has no hover, so it would never appear at all. */
@media (hover: none) { .syn-sample__see { opacity: 1; } }

/* In tonight's queue, or not. A stateful button rather than a checkbox: what
   the owner needs at a glance days later is whether this prospect IS scheduled,
   not whether they once ticked something. Off is quiet and outlined; on is
   filled and unmistakable from across the page. */
.syn-queue {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 7px 13px; border-radius: var(--radius-pill);
  border: 1px solid var(--line-strong); background: var(--surface);
  font: inherit; font-size: 13px; font-weight: 600; color: var(--ink-soft);
  cursor: pointer; transition: background .12s ease, border-color .12s ease, color .12s ease;
}
.syn-queue svg { flex: none; }
.syn-queue:hover { border-color: var(--primary); color: var(--primary-ink); }
.syn-queue.is-on {
  background: var(--seaglass-tint); border-color: var(--seaglass);
  color: var(--seaglass-ink);
}
.syn-queue.is-on:hover { border-color: var(--seaglass-ink); }
.syn-queue:disabled { opacity: .6; cursor: default; }
.syn-queue:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---- What a prospect is pursued FOR, changeable on the spot ---------------

   Smaller than .syn-queue on purpose, and sitting on its own line under the
   actions rather than among them. The buttons above DO things — visit, read a
   menu, make them a client. These state a fact and let you correct it, which
   is a quieter job and should not compete for the same attention.

   Deliberately NOT checkboxes. A tick box asks "is this on?" and then needs a
   Save to mean anything; a chip that is either lit or not answers the question
   by looking like the answer, and one click is the whole interaction.
--------------------------------------------------------------------------- */
.syn-sections {
  display: flex; flex-wrap: wrap; align-items: center; gap: 7px;
  margin: var(--sp-3) 0 0;
}
.syn-sections__lbl {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12.5px; font-weight: 700; color: var(--ink-faint);
  letter-spacing: .02em; text-transform: uppercase; margin-right: 2px;
}
.syn-sections__lbl svg { flex: none; }
.syn-sec {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 4px 10px; border-radius: var(--radius-pill);
  border: 1px dashed var(--line-strong); background: transparent;
  font: inherit; font-size: 12.5px; font-weight: 600; color: var(--ink-faint);
  cursor: pointer; transition: background .12s ease, border-color .12s ease, color .12s ease;
}
.syn-sec svg { flex: none; }
.syn-sec:hover { border-color: var(--primary); color: var(--primary-ink); }
/* Solid when it is true, dashed when it is an offer. The border style carries
   the state as well as the colour does, so it survives being printed, being
   looked at by somebody who cannot separate the two greens, and being seen at
   a glance from across a desk. */
.syn-sec.is-on {
  border-style: solid; background: var(--seaglass-tint);
  border-color: var(--seaglass); color: var(--seaglass-ink);
}
.syn-sec.is-on:hover { border-color: var(--seaglass-ink); }
.syn-sec:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---- What we made of a prospect's events ----------------------------------
   A table, not a list, because the useful question is no longer "could we read
   the page" but "did we file it right" — and that needs the tab and the
   category side by side with the name, where they can be compared down a
   column. Collapsed by default: this page lists every prospect at once, and
   several open tables would bury whatever brought you here.
--------------------------------------------------------------------------- */
/* THE SCAN DESK GETS THE WHOLE WINDOW. Nine columns of live controls need
   more room than the 1040px every other admin page reads comfortably at, and
   the fix-a-hundred-rows day is this page's whole job. :has() scopes the
   widening to the one wrap that holds #sync, so no other page moves; browsers
   without :has() simply keep the standard width. */
/* max() around it so a narrow window never gets LESS than the standard wide
   width — 96vw of a small window would be less, and the wrap's own gutters
   already handle small windows. */
.wrap--wide:has(> #sync) { max-width: max(var(--maxw-wide), min(96vw, 1560px)); }

.syn-table-wrap { margin-top: var(--sp-3); }
.syn-table-wrap > summary {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 8px; margin-left: -8px; border-radius: var(--radius-sm);
  font-size: 13px; font-weight: 600; color: var(--primary);
  cursor: pointer; list-style: none;
}
.syn-table-wrap > summary::-webkit-details-marker { display: none; }
.syn-table-wrap > summary::after { content: '▾'; font-size: 11px; opacity: .7; }
.syn-table-wrap[open] > summary::after { content: '▴'; }
.syn-table-wrap > summary:hover { background: var(--surface-2); }
.syn-table-wrap > summary svg { flex: none; }

/* The table scrolls inside its own box rather than pushing the page sideways —
   five columns do not fit a phone and the page must not scroll horizontally.

   AND VERTICALLY, at about ten rows. This page lists every prospect at once, so
   a venue publishing forty events would otherwise push every other venue off
   the screen — and the answer is not to show fewer of their events, it is to
   give each venue the same amount of room. 10 rows is enough to judge a source
   at a glance and short enough that four prospects still fit on a laptop.

   `max-height` rather than `height`: a venue with three events gets a box three
   rows tall, not a box with a hole in it. */
/* Taller since the inline-edit pass: rows carry real controls now, so 25rem
   showed barely four of them. Capped against the viewport so a laptop still
   keeps the batch bar and the actions in reach. */
.syn-table-scroll { overflow: auto; margin-top: var(--sp-2); max-height: min(38rem, 64vh); overscroll-behavior: contain; }

/* The header has to survive the scroll or, ten rows down, nothing has a name. */
.syn-table thead th { position: sticky; top: 0; z-index: 1; background: var(--surface); }

/* A visible edge, so a table that continues below the fold looks like it
   continues rather than like it ended. */
.syn-table-scroll { border: 1px solid var(--line); border-radius: var(--radius); padding: 0 var(--sp-3); }
.syn-table { width: 100%; border-collapse: collapse; font-size: 13px; }
/* NOT `nowrap`, and that is what made the headings collide.

   `table-layout: fixed` sets the column width from the CSS; `nowrap` then
   forbids the HEADING from wrapping to fit it. So "Who it's for" simply ran
   out of its own cell and over the top of "Repeats", and the row read
   "WHO IT FOREPEATS STATUS". A heading on two short lines is fine; a heading
   printed across its neighbour is not. */
.syn-table th {
  /* 6px left, matching the inline controls' own padding, so a heading sits
     flush over the text in its column instead of a half-step to the left. */
  text-align: left; padding: 6px 10px 6px 6px; white-space: normal;
  font-size: 11.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  color: var(--ink-faint); border-bottom: 1px solid var(--line-strong);
}
/* MIDDLE, not top. Rows here are uneven by design — the category carries a
   second line saying where it came from, and a long title wraps — so top
   alignment left every short cell floating at the ceiling of a tall row and
   nothing on a shared line across the row. */
.syn-table td {
  padding: 10px 9px 10px 0; border-bottom: 1px solid var(--line);
  vertical-align: middle;
}
.syn-table tr:last-child td { border-bottom: 0; }
/* 7rem, not 8.5. The floor here is the last thing standing between a
   seven-column table and its own card, and a title wraps gracefully where the
   actions column cannot. */
/* No min-width here: `table-layout: fixed` ignores min-width on a cell, so the
   7rem floor this used to carry did nothing at all while looking like a
   guarantee. The floor that works is on the table — see .syn-table min-width. */
.syn-t__name { font-weight: 600; color: var(--ink); }

/* THE DATE WRAPS. `nowrap` here held the widest column in the table open at
   the full width of "Wednesday, September 12, 2026", which pushed the actions
   column off the right edge — so the Publish button, the one thing on the row
   you are there to press, could only be reached by scrolling sideways. Two
   short lines cost nothing; a hidden primary action costs the whole table. */
.syn-t__when { color: var(--ink-soft); min-width: 6.5rem; }
/* The hours, under the day. Darker than the date on purpose: two events on the
   same evening are told apart by this line, so it is the one being scanned. */
.syn-t__time { display: block; margin-top: 2px; font-size: 12.5px; font-weight: 600; color: var(--ink); }
.syn-t__time.syn-t__none { font-weight: 400; }

/* A category pill is one label and must not break inside itself: "Kids’
   activity" split over two lines read as two separate things. Scoped to this
   table rather than added to `.tag`, which is used all over the site and is
   allowed to wrap where the space is genuinely tight. */
.syn-table .tag { white-space: nowrap; }
.syn-t__tab {
  display: inline-block; padding: 2px 8px; border-radius: var(--radius-pill);
  background: var(--primary-tint); color: var(--primary-ink);
  font-size: 12px; font-weight: 600; white-space: nowrap;
}
/* Where the category came from, under it — the difference between "the model
   thought so" and "the title literally says Live Music". */
.syn-t__from { display: block; margin-top: 3px; font-size: 11.5px; color: var(--ink-faint); }
.syn-t__none { color: var(--ink-faint); font-style: italic; }
/* The second category on a scan row. Quiet and on its own line, like the
   provenance under it: the point is that setting one is VISIBLE from the
   collapsed row, not that it competes with the category it sits beneath. */
.syn-t__also {
  display: block; margin-top: 3px; font-size: 11.5px;
  color: var(--ink-soft); font-weight: 600;
}
.syn-t__aud {
  display: inline-block; padding: 2px 8px; border-radius: var(--radius-pill);
  background: var(--surface-2); border: 1px solid var(--line);
  font-size: 12px; font-weight: 600; white-space: nowrap; cursor: help;
}
/* A wider dialog for the ones that are really forms. The default is right for
   a question and two buttons and cramped for anything you have to fill in.

   TWO CLASSES on purpose. `.modal--invite` also sets a max-width and is defined
   further down this file, so at equal specificity it won this outright and the
   dialog stayed narrow no matter what was asked for. Doubling up on `.modal`
   settles it by weight rather than by whoever happens to be last. */
.modal.modal--wide { max-width: 680px; }

/* THE AUDIENCE CHECKBOXES, which used to scroll sideways.

   `.checks--stack` is `flex-direction: column`, and this had a `max-height` on
   it — a column flex that cannot grow downwards wraps into NEW COLUMNS instead,
   so seven boxes became a horizontally scrolling strip with half of "Adults
   (30-45)" cut off. The height cap was the bug, not the layout.

   A plain wrapping grid instead: as many columns as fit, all seven visible, no
   scrolling in either direction. */
.ed-auds {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 6px var(--sp-4); padding: 2px; max-height: none; overflow: visible;
}
.ed-auds .check { margin: 0; white-space: nowrap; }
/* WHERE A ROW HAS GOT TO. Colour-coded because the whole value is scanning a
   long table for what has not been dealt with — grey reads as untouched, amber
   as in hand, green as done, red as waiting on somebody else. A select rather
   than a cycling button: four states is too many to click through, and a
   dropdown says what the options ARE without being opened. */
.syn-status {
  appearance: none; -webkit-appearance: none;
  padding: 3px 22px 3px 9px; border-radius: var(--radius-pill);
  border: 1px solid var(--line-strong); background-color: var(--surface);
  font: inherit; font-size: 12px; font-weight: 600; color: var(--ink-soft);
  cursor: pointer; max-width: 100%;
  background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%236B7280' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 8px center;
}
.syn-status:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.syn-status:disabled { opacity: .55; cursor: default; }
/* Untouched is deliberately the quietest — a table of them should read as a
   pile of work, not a wall of alarm. */
.syn-status--unchecked { background-color: var(--surface-2); color: var(--ink-faint); }
.syn-status--reviewing { background-color: #FBF0DA; border-color: #D9B872; color: #6B4E14; }
.syn-status--cleared   { background-color: var(--seaglass-tint); border-color: var(--seaglass); color: var(--seaglass-ink); }
.syn-status--sentback  { background-color: #F7E2DE; border-color: #D79A8E; color: #7A2E1E; }

/* Three buttons on one line, right-aligned. They fit now that the date column
   wraps instead of holding itself open at "Wednesday, September 12, 2026" —
   that column, not these buttons, was what pushed the table past its card.

   Allowed to wrap only when the viewport genuinely cannot hold them, because
   wrapping them by default stacks three buttons per row and triples the height
   of every row to solve a problem that no longer exists. */
/* LEFT AS A TABLE CELL, deliberately. `display: flex` here looked like the
   modern answer and was the wrong one: it takes the cell out of the table's
   column sizing, so the buttons stopped reporting a natural width and wrapped
   even at 1280px with room to spare.

   As an ordinary cell the column asks for all three buttons on one line and
   gets it whenever the table can afford it, and wraps only when it genuinely
   cannot — which is the behaviour `white-space: nowrap` was forbidding, and
   the reason the whole table scrolled sideways instead. */
/* THIS COLUMN IS THE ONE THAT MAY NOT SHRINK, and that is the whole trick.
   Every column here can wrap, so the browser shrinks them all in proportion and
   the buttons never get the ~200px they need — they broke onto two lines even
   at 1280px with the table not overflowing at all. `nowrap` makes this cell's
   minimum equal its natural width, so the table hands it over and takes the
   space out of the prose columns, which can afford to wrap.

   This is safe now only because the date column above no longer does the same
   thing. Two immovable columns is what pushed the table off its card in the
   first place; one is what keeps the actions reachable without scrolling. */
/* The heading follows its buttons to the right edge; a left-aligned word
   floating a hundred pixels from the controls it names read as misaligned. */
/* THE HEADING AND THE BUTTONS UNDER IT LINE UP NOW. The th declared 4px of
   right padding and the td inherited the table's 9px, so "ACTIONS" sat five
   pixels right of the cluster it names - part of the "oddly placed" report.
   One number for both, and the cell gets five pixels of width back, which is
   what lets the controls have a real gap between them. */
.syn-table th.syn-c-go,
.syn-table td.syn-t__go { text-align: right; padding-right: 4px; }
.syn-t__go { text-align: right; white-space: nowrap; }
.syn-t__go .btn + .btn { margin-left: 6px; }
/* Below this there is genuinely not enough room for seven columns and three
   buttons, and a wrapped button is still a button you can press — a scrollbar
   hides it. Releasing the nowrap is the only thing that changes. */
@media (max-width: 880px) {
  .syn-t__go { white-space: normal; line-height: 2.1; }
}
.syn-t__stale {
  display: flex; align-items: flex-start; gap: 6px;
  margin-top: var(--sp-3); padding-top: var(--sp-2);
  border-top: 1px dashed var(--line);
}
.syn-t__stale svg { flex: none; margin-top: 2px; }

.syn-add {
  margin-top: var(--sp-4); padding-top: var(--sp-4);
  border-top: 1px dashed var(--line);
  display: grid; gap: var(--sp-3);
}
.syn-add .field { margin: 0; }
.syn-add .btn { justify-self: start; }
@media (min-width: 620px) {
  .syn-add { grid-template-columns: 1fr 1fr; align-items: end; }
  .syn-add .field:nth-child(3),
  .syn-add .btn { grid-column: 1 / -1; }
}
/* A scan that could not be run says so where the button is, not in an alert. */
.form-note--bad { color: var(--destructive); font-weight: 600; }

/* ---- Opened from the admin ------------------------------------------------
   A slim bar above everything on a page reached from the owner tools. It has
   to sit above the site header, because the point is that it belongs to the
   admin rather than to the page you are looking at. */
.admin-bar {
  background: var(--primary); color: var(--on-primary);
  font-size: 13.5px; position: relative; z-index: 60;
}
.admin-bar__inner {
  max-width: var(--maxw-wide); margin: 0 auto; padding: 8px var(--sp-4);
  display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap;
}
.admin-bar__back {
  color: inherit; font-weight: 700; text-decoration: none;
  display: inline-flex; align-items: center; gap: 5px; white-space: nowrap;
}
.admin-bar__back:hover { text-decoration: underline; }
.admin-bar__note {
  display: inline-flex; align-items: center; gap: 6px;
  opacity: .8; font-size: 12.5px;
  /* Takes the slack so Exit is pinned to the right edge. */
  margin-right: auto;
}
.admin-bar__exit {
  flex: none; background: transparent; color: inherit; cursor: pointer;
  border: 1px solid rgba(255, 255, 255, .45); border-radius: var(--radius-pill);
  font: inherit; font-size: 12.5px; font-weight: 600; padding: 3px 12px;
}
.admin-bar__exit:hover { background: rgba(255, 255, 255, .14); }

/* THE "YOU ARE LOOKING AT SOMEBODY ELSE'S ACCOUNT" BAR.

   Same furniture as the admin bar, three deliberate differences:

   STICKY, because it has to be true wherever you are on the page and not only
   at the top of it. The rest of this project learned that lesson the hard way:
   a message the owner has scrolled past is a message that was never shown.

   THE NOTE STAYS AT EVERY WIDTH and at full strength. `.admin-bar__note` is
   dimmed and hidden under 560px, which is right for "you came from the admin"
   and wrong for this — here the note IS the bar, and losing it on a phone
   leaves somebody typing into a form that is going to refuse them with no idea
   why.

   DESTRUCTIVE RED, not the brand colour, so it never reads as ordinary
   furniture. */
.admin-bar--viewing {
  background: var(--destructive); color: #fff;
  position: sticky; top: 0; z-index: 200;
}
/* LOOK-ONLY IS THE SAFE STATE, so it does not get the alarm colour. Red now
   means "changes you make here are real and land on somebody else's listings",
   which is the default. A bar that shouts equally in both states teaches the
   reader to stop reading it, and then it is not saying anything at all. */
.admin-bar--viewing.admin-bar--looking { background: var(--ink, #22303c); }
.admin-bar--viewing .admin-bar__note { opacity: 1; font-size: 13px; }
.admin-bar--viewing .admin-bar__exit { border-color: rgba(255, 255, 255, .7); }
@media (max-width: 560px) {
  .admin-bar--viewing .admin-bar__note { display: inline-flex; }
  .admin-bar--viewing .admin-bar__inner { gap: var(--sp-2); padding: 7px var(--sp-3); }
}
@media (max-width: 560px) {
  .admin-bar__note { display: none; }
  /* With the note gone there is nothing left to push Exit over. */
  .admin-bar__back { margin-right: auto; }
}

/* ---- The owner's areas ----------------------------------------------------
   Three jobs with different rhythms — checking pages, posting, and running the
   paid tier — so they get equal weight and the current one is obvious. Cards
   rather than tabs because each needs a line saying what it is: "Sync health"
   told the owner nothing on its own. */
/* ---- Owner tabs ----------------------------------------------------------
   One line of tabs across the top of every owner page, and the whole of the
   choice on the hub. Replaces a three-column grid of seven cards that was a
   page in its own right — fine as the hub's only content, furniture when
   repeated above the page you were already on. */
.owner-tabs {
  display: flex; gap: 2px; margin: var(--sp-4) 0 var(--sp-5);
  border-bottom: 1px solid var(--line);
  /* Sideways rather than wrapping. Seven tabs on a phone would otherwise
     become three ragged rows and stop reading as one control.

     THE SCROLLBAR IS HIDDEN, NOT THE SCROLLING. `scrollbar-width: thin` drew a
     track down the right-hand side of the strip that looked like a control and
     was not one — on a desktop the tabs fit and it had nothing to do. Scrolling
     still works where it is needed, which is a phone, where the bar was never
     drawn anyway. */
  overflow-x: auto; overflow-y: hidden;
  scrollbar-width: none; -ms-overflow-style: none;
  -webkit-overflow-scrolling: touch;
}
.owner-tabs::-webkit-scrollbar { width: 0; height: 0; display: none; }
/* THE ICON GOES ABOVE THE WORDS, and that is what fixes this.

   Side by side, a two-word name wrapped and left the icon floating against a
   ragged block of text at three different line counts across the strip — "Post
   / an / event" beside "Clients / & / scans" beside a single-line "All tools".
   It read as broken, because a row of controls that are the same kind of thing
   should be the same shape.

   Stacked and centred, every tab is one column of equal width: icon, then the
   name on at most two balanced lines. `text-wrap: balance` is what stops the
   two-liners breaking as five words and one; where it is unsupported the name
   simply wraps as before, which is the old behaviour rather than a broken
   one. */
.owner-tab {
  display: inline-flex; flex-direction: column; align-items: center; gap: 5px;
  flex: 1 1 0; min-width: 92px; max-width: 132px;
  padding: 10px 8px 9px; color: var(--ink-soft); text-decoration: none;
  font-size: 14px; font-weight: 600;
  white-space: normal; text-align: center; line-height: 1.25;
  text-wrap: balance;
  border-bottom: 2px solid transparent; margin-bottom: -1px;
  transition: color .12s ease, border-color .12s ease;
}
.owner-tab svg { flex: none; color: var(--ink-faint); }
/* EVERY LABEL RESERVES TWO LINES, whether it needs them or not.

   Only one name wraps. Left to size themselves the other seven sat a line
   shorter, and the two obvious fixes each broke something: top-aligning left
   the short labels hanging with a gap beneath, and centring the whole stack
   put the icons at two different heights — which is the more visible fault,
   because eight icons in a row are read as a row.

   Reserving the space fixes both at once. The icons stay level, every label
   occupies the same block, and a one-line name is centred inside it. */
.owner-tab > span {
  display: flex; align-items: center; justify-content: center;
  min-height: 2.5em;
}
.owner-tab:hover { color: var(--ink); text-decoration: none; border-bottom-color: var(--line-strong); }
.owner-tab:hover svg { color: var(--ink-soft); }
/* Where you are, not where you can go. The underline carries it, and the
   weight change carries it again for anyone who cannot see the colour. */
.owner-tab.is-active {
  color: var(--primary); border-bottom-color: var(--primary); font-weight: 700;
}
.owner-tab.is-active svg { color: var(--primary); }
.owner-tab:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
/* THE BACK ROOM READS AS A DOOR, NOT A JOB. Advanced is pushed to the far end
   and drawn fainter than the working tabs, so the strip reads as "the five
   things I do" plus one door for everything else. It regains full colour on
   hover and when you are standing on it — muted is not hidden. */
.owner-tab--adv { margin-left: auto; flex: 0 1 auto; color: var(--ink-faint); font-weight: 500; }
.owner-tab--adv svg { opacity: .7; }
.owner-tab--adv:hover, .owner-tab--adv.is-active { font-weight: 600; }
.owner-tab--adv:hover svg, .owner-tab--adv.is-active svg { opacity: 1; }

/* The panel a member sees instead of their dashboard until they have added a
   payment method. Reassuring rather than punitive on purpose — their listing is
   still working, and the first line says so before anything is asked for. */
.billing-gate { border-color: var(--primary); box-shadow: inset 0 0 0 1px var(--primary); }
.billing-gate .dash-card__title svg { color: var(--primary); }

/* ---- A member's own listings ----------------------------------------------
   One row per event, with what it is on the left and what you can do on the
   right. A cancelled one stays in the list rather than disappearing: it is how
   somebody sees that the cancellation took, and how they undo it.
--------------------------------------------------------------------------- */
.mye-list { display: grid; gap: var(--sp-2); margin-top: var(--sp-3); }
.mye {
  display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface);
}
.mye__main { flex: 1 1 260px; min-width: 0; }
.mye__title { margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 7px; font-size: 15px; }
.mye__when { margin: 3px 0 0; font-size: 13.5px; font-weight: 600; color: var(--ink-soft); }
.mye__meta { margin: 4px 0 0; font-size: 12.5px; color: var(--ink-faint); display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.mye__note { margin: 5px 0 0; font-size: 12.5px; color: var(--ink-faint); display: flex; align-items: center; gap: 5px; }
/* The "we posted this for you" line. Seaglass, not grey: it is reassurance
   ("already handled, costs you nothing"), and it must not read like the
   cancellation note beside it. */
.mye__note--ours { color: var(--seaglass-ink); }
.mye__note--ours svg { flex: none; }

/* ---- The attach-listings dialog (admin-orgs) ------------------------------
   Rows are labels so the whole line is the checkbox's target. */
.adopt-list { border: 1px solid var(--line); border-radius: var(--radius); max-height: 17rem; overflow: auto; overscroll-behavior: contain; }
.adopt-row {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; border-bottom: 1px solid var(--line);
  cursor: pointer; margin: 0;
}
.adopt-row:last-child { border-bottom: 0; }
.adopt-row:hover { background: var(--surface-2); }
.adopt-row__main { flex: 1; min-width: 0; }
.adopt-row__main strong { display: block; font-size: 13.5px; color: var(--ink); }
.adopt-row__main em { display: block; font-style: normal; font-size: 12px; color: var(--ink-faint); }
.adopt-row .syn-tag { flex: none; }
.adopt-rest { margin-top: var(--sp-3); }
.adopt-rest > summary { cursor: pointer; font-size: 13px; font-weight: 600; color: var(--ink-soft); }
.adopt-foot { display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-3); }
.adopt-foot__done { font-size: 12.5px; color: var(--seaglass-ink); display: inline-flex; align-items: center; gap: 4px; }
.mye__go { display: flex; flex-wrap: wrap; gap: 6px; margin-left: auto; }
/* Struck through rather than greyed to nothing: a member has to be able to read
   which date they called off. */
.mye.is-off { background: var(--surface-2); border-style: dashed; }
.mye.is-off .mye__title strong,
.mye.is-off .mye__when { text-decoration: line-through; color: var(--ink-faint); }

/* ---- The organization roster ----------------------------------------------
   A row per client: who they are on the left, what you can do on the right.
   Deliberately not a table — the actions belong beside each name rather than in
   a column, and a table would make the closed list look like data to read
   instead of a shelf to take things off.
--------------------------------------------------------------------------- */
.org-list { display: grid; gap: var(--sp-2); margin-top: var(--sp-3); }
.org-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface);
}
.org-row__main { flex: 1 1 240px; min-width: 0; }
.org-row__name { margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 7px; font-size: 15px; }
.org-row__meta {
  margin: 3px 0 0; font-size: 12.5px; color: var(--ink-faint);
  overflow-wrap: anywhere;
}
.org-row__go { display: flex; flex-wrap: wrap; gap: 6px; margin-left: auto; }
/* THE MANAGE FOLD. A full-width strip inside the row (flex-basis 100% forces
   the wrap), visually a drawer: everything that used to crowd the row lives
   here, shut by default. Dashed top edge so it reads as part of its row, not
   as the next one. */
.org-row__more {
  flex: 1 1 100%; display: flex; flex-wrap: wrap; gap: 6px;
  padding-top: var(--sp-3); margin-top: 2px;
  border-top: 1px dashed var(--line);
}
.org-row [data-manage].is-on { background: var(--surface-2); }
/* Closed is quieter, not hidden: you still need to find them to reopen. */
.org-row.is-closed { background: var(--surface-2); border-style: dashed; }
.org-row.is-closed .org-row__name strong { color: var(--ink-faint); }

/* ---- The hub's three doors ------------------------------------------------
   The same three areas as .owner-nav, but this page's ENTIRE job is the choice,
   so the choice gets the room: bigger targets, the icon on its own line, and a
   chevron to say each one goes somewhere. On the other owner pages the nav is a
   way back; here it is the content. */
.owner-doors__lede { margin-bottom: var(--sp-5); }

.owner-doors { display: grid; gap: var(--sp-4); margin-bottom: var(--sp-6); }
@media (min-width: 760px) { .owner-doors { grid-template-columns: repeat(3, 1fr); } }

.owner-door-card {
  position: relative; display: block;
  padding: var(--sp-5); border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface); color: var(--ink); text-decoration: none;
  transition: border-color .12s ease, box-shadow .12s ease, transform .12s ease;
}
.owner-door-card:hover {
  border-color: var(--primary); box-shadow: var(--shadow-hover);
  transform: translateY(-1px); text-decoration: none;
}
.owner-door-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.owner-door-card__icon {
  display: flex; align-items: center; justify-content: center;
  width: 46px; height: 46px; margin-bottom: var(--sp-3);
  border-radius: var(--radius); background: var(--primary-tint); color: var(--primary);
}
.owner-door-card strong { display: block; font-family: var(--font-display); font-size: 18px; }
.owner-door-card em {
  display: block; margin-top: 5px; font-style: normal;
  font-size: 13.5px; color: var(--ink-faint); line-height: 1.45;
}
.owner-door-card__go {
  position: absolute; top: var(--sp-5); right: var(--sp-5);
  color: var(--ink-faint);
}
.owner-door-card:hover .owner-door-card__go { color: var(--primary); }

/* ---- Administrator-only warning ------------------------------------------
   The first thing on the admin gate. Loud on purpose: whoever is reading it
   either belongs here and will never read it again, or does not and should
   know inside a second. */
.admin-warn {
  display: flex; align-items: flex-start; gap: 11px;
  margin: var(--sp-4) 0 var(--sp-5);
  padding: var(--sp-4); border-radius: var(--radius);
  background: var(--accent-tint); color: var(--accent);
  border: 1px solid var(--accent);
  font-size: 14.5px; line-height: 1.5;
}
.admin-warn svg { flex: none; margin-top: 1px; }
.admin-warn a { color: inherit; font-weight: 700; }

/* ---- Who is signed in, on the admin --------------------------------------- */
/* A STATUS LIGHT IN THE BAR, NOT A PANEL IN THE PAGE. This was a full-width
   tinted strip at 14px between the tabs and the content, saying the one thing
   the owner already knew. It keeps its tint — that colour is what makes it
   readable at a glance as "you are in the account that can publish" — but it is
   now a chip in the empty right-hand end of the tabs bar, which costs the page
   no height at all. Discreet, and still impossible to miss if you look for it.

   layout.js moves the node here; the eight owner pages still draw it where they
   always did. */
.admin-who {
  display: inline-flex; align-items: center; gap: 7px; flex: none;
  margin: 0; padding: 4px 10px; border-radius: var(--radius-pill);
  background: var(--seaglass-tint); color: var(--seaglass-ink);
  font-size: 12px; line-height: 1.3; white-space: nowrap;
}
.admin-who strong { font-weight: 800; }
.admin-who svg { flex: none; }
/* The email is the part that can be long, and the part you least often need:
   the tint and the word "owner" already answer "am I in the right account?". */
.admin-who .admin-who__email { color: var(--ink-faint); }
.admin-who .admin-who__note { display: none; }
.nav-owner { display: inline-flex; align-items: center; gap: 10px; flex: none; }
/* It lives in .header-tools now, which is a flex row with a gap, and on every
   public page this span is empty. Without this it would still spend its gap,
   pushing Sign in leftwards for no visible reason. */
.nav-owner:empty { display: none; }
/* The kicker that used to sit above the page heading, moved up beside the chip
   and kept in the accent — it is the one word on this bar that says you are
   somewhere the public is not. Same treatment as .about__kicker, which is what
   it replaces. */
.nav-owner__tag {
  font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .08em;
  color: var(--accent); white-space: nowrap;
}
/* IT IS THE WAY BACK TO THE HUB, so it has to answer a pointer - but this bar
   already carries an email, a Sign out and a Sign in, and a fifth underlined
   thing in the corner would read as clutter. Underlined only on hover and on
   keyboard focus: no weight at rest, unmistakable the moment it is aimed at. */
a.nav-owner__tag { text-decoration: none; }
a.nav-owner__tag:hover { text-decoration: underline; }
a.nav-owner__tag:focus-visible {
  text-decoration: underline;
  outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 3px;
}
@media (max-width: 1100px) {
  /* The chip is the part that carries information; this is a caption for it. */
  .nav-owner__tag { display: none; }
}
@media (max-width: 900px) {
  .admin-who .admin-who__email { display: none; }
}
.admin-who svg { flex: none; }
/* No database to sign in to — a different fact, so it should not wear the
   colour that means "you are authenticated". */
.admin-who--none { background: var(--accent-tint); color: var(--accent); }

/* ---- "Something needs you" (member dashboard) -----------------------------
   The one loud thing on an otherwise calm page, and it renders only when there
   is a real question to answer. Bordered rather than filled: the message is
   "look at this", not "something is wrong" — nothing here is broken, and the
   copy says so. */
.dash-alert {
  border: 2px solid var(--accent);
  background: var(--accent-tint);
}
.dash-alert .dash-card__title { color: var(--accent); }
.dash-alert__list { list-style: none; margin: var(--sp-4) 0 0; padding: 0; }
.dash-alert__item {
  padding: var(--sp-4) 0; border-top: 1px solid rgba(0, 0, 0, .09);
}
.dash-alert__item:first-child { border-top: 0; }
.dash-alert__what { margin: 0; font-size: 15.5px; color: var(--ink); }
.dash-alert__when { font-size: 13px; color: var(--ink-faint); font-weight: 500; }
.dash-alert__ask { margin: 4px 0 0; font-size: 14px; color: var(--ink-soft); line-height: 1.5; }
.dash-alert__actions {
  margin: var(--sp-3) 0 0; display: flex; flex-wrap: wrap; gap: 8px;
}
/* The ghost button sits on a tinted panel here, so it needs its own surface to
   stay legible. */
.dash-alert__actions .btn--ghost { background: var(--surface); }

/* ---- Event source (settings) ----------------------------------------------
   Each source carries a state line under it. Colour-coded because the question
   being asked is "is this working?" and the answer should land before the
   sentence is read. */
.src + .src { margin-top: var(--sp-4); padding-top: var(--sp-4); border-top: 1px solid var(--line); }
.src__state {
  display: flex; align-items: flex-start; gap: 7px;
  margin: 6px 0 0; font-size: 13.5px; line-height: 1.45;
}
.src__state svg { flex: none; margin-top: 2px; }
.src__state.is-ok      { color: var(--seaglass-ink); }
.src__state.is-pending { color: var(--ink-faint); }
.src__state.is-bad     { color: var(--destructive); }
.src [data-src-del] { margin-top: 8px; }

/* ---- The signup scan ------------------------------------------------------
   What we found on their website, shown before the account is made. Sits in the
   ordinary `.signup-info` panel because it is guidance, not an alarm — even the
   failures here are "we need pointing at the right page", never "you have done
   something wrong". */
.signup-scan.is-loading .signup-info__head { color: var(--ink-soft); }
.scan-list { list-style: none; margin: var(--sp-3) 0; padding: 0; display: grid; gap: 6px; }
.scan-ev {
  display: grid; grid-template-columns: 84px 1fr; gap: 2px 10px;
  font-size: 14.5px; align-items: baseline;
}
.scan-ev__when { color: var(--ink-faint); font-variant-numeric: tabular-nums; white-space: nowrap; }
.scan-ev__t { color: var(--ink); font-weight: 600; }
/* The venue drops under the title rather than beside it: on a phone these are
   the two longest strings on the page and side by side they both wrap. */
.scan-ev__at { grid-column: 2; color: var(--ink-faint); font-size: 13px; }
.scan-more { color: var(--ink-faint); font-weight: 400; }
.scan-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: var(--sp-4); }
.signup-scan .field { margin-top: var(--sp-4); }

/* Who a plan actually suits. Quieter than the feature list and set apart by a
   rule, so it reads as a note from us rather than as a benefit that lost its
   tick — which is exactly how it read while it was a bullet. */
.plan__foot {
  margin: 0 0 var(--sp-4); padding-top: var(--sp-3);
  border-top: 1px solid var(--line);
  font-size: 13px; line-height: 1.5; color: var(--ink-faint);
}
/* The cancel-anytime line under a paid plan's Choose button. Centered under
   the button it reassures about, quiet on purpose — it answers a hesitation,
   it does not sell. */
.plan__cancel {
  margin: var(--sp-2) 0 0; text-align: center;
  font-size: 12.5px; line-height: 1.5; color: var(--ink-faint);
}
/* The one line a plan is really sold on. Lifted just enough to be read first
   without turning into a banner: full-strength ink instead of the softer body
   colour, a half-step more weight and size, and a wash behind it to give the
   eye something to catch on the way down the list. Kept to ONE line per card —
   the effect is entirely relative to the quieter lines around it, so a second
   one would cost both of them their prominence.

   Deliberately the PRIMARY blue rather than the accent: the accent is the
   site's alert-ish coral, and a warm wash on a plan card read as a warning
   sitting where the good news is meant to be. The blue is the same family as
   the buttons and links, so the band reads as part of the product. */
.plan__feats li.is-key {
  color: var(--ink);
  font-weight: 600;
  font-size: 15px;
  background: var(--primary-tint);
  border-radius: var(--radius-sm, 8px);
  padding: 9px 11px;
  /* Pulled out towards the card's own padding so it reads as a band across the
     card rather than an indented block sitting inside the list. */
  margin-inline: -11px;
}
/* One step deeper than the ordinary check marks, so the icon is distinguishable
   from them without introducing a second hue. */
.plan__feats li.is-key svg { color: var(--primary-ink); }

/* ---- The add-on, offered on the signup form ------------------------------
   THE SAME OBJECT THE PLAN CARD SHOWS, so somebody who chose Full Shenanigans
   on the plans page meets it again here and recognises it: the navy-tint band
   of `.plan__feats li.is-key`, the sparkle instead of a check, and the very
   same `.plan__addon` tag element.

   IT IS A TICKBOX, WHICH THE PLAN CARD'S VERSION IS NOT, and that is the only
   real difference: this one can be chosen. So it takes the band and the icon
   but keeps a 44px hit area and a pointer, and the whole row is the label. */
.addon-field { margin-top: var(--sp-3); }
.addon-field__row {
  display: flex; align-items: flex-start; gap: 11px;
  background: var(--primary-tint); border-radius: var(--radius-sm, 8px);
  padding: 12px 13px; cursor: pointer; min-height: 44px;
}
.addon-field__row input { margin-top: 3px; flex: none; }
.addon-field__body { display: block; }
.addon-field__head {
  display: flex; align-items: center; flex-wrap: wrap; gap: 7px;
  font-size: 15px; font-weight: 600; color: var(--ink);
}
.addon-field__head svg { color: var(--primary-ink); flex: none; }
.addon-field__price { font-weight: 700; color: var(--primary-ink); }
.addon-field__blurb {
  display: block; margin-top: 4px;
  font-size: 13.5px; line-height: 1.45; color: var(--ink-soft);
}
/* What happens past the monthly allowance. Quieter than a feature — it is a
   condition, not a selling point — but it sits above the button so nobody
   chooses a plan without seeing it. min-height keeps the three buttons on one
   line when the notes wrap to different depths. */
.plan__over {
  font-size: 13px; line-height: 1.45; color: var(--ink-faint);
  border-top: 1px dashed var(--line); padding-top: 10px;
  margin: 0 0 var(--sp-3); min-height: 40px;
}
/* ---- Pay-per-post promotion: list price painted out, tag at the corner ----
   The tag is absolutely positioned but the space for it is RESERVED by
   padding-top on .paint__was, so it never overlaps the line of text above when
   the price sits inside a sentence. */
/* padding-top reserves the tag's row, so it never reaches into the line of text
   above even when the price sits mid-sentence. 19px, not the tag's 14px height:
   rotating the tag makes its bounding box ~19px tall, and 14 let it dip onto
   the prices. */
.paint {
  position: relative; display: inline-flex; align-items: baseline; gap: 8px;
  padding-top: 19px; white-space: nowrap;
}
/* --ink-soft, not --ink-faint: the old price is meant to stay legible under the
   paint, so it should not also be the palest text on the page. */
.paint__was { position: relative; display: inline-block; color: var(--ink-soft); font-weight: 600; }
.paint__stroke {
  position: absolute; left: -10%; top: 0; width: 120%; height: 100%;
  color: var(--paint); pointer-events: none; overflow: visible;
}
.paint__tag {
  position: absolute; top: 0; right: -5px;
  padding: 2px 5px; background: var(--paint); color: #fff;
  font-family: var(--font-body);
  font-size: 9px; font-weight: 800; line-height: 1.15; letter-spacing: .03em;
  text-transform: uppercase; white-space: nowrap;
  /* uneven corners + a slight tilt: cut and stuck on by hand, not a chip */
  border-radius: 4px 8px 3px 7px; transform: rotate(-6deg);
  box-shadow: 0 1px 2px rgba(38, 48, 58, .18);
}
.paint__now { font-weight: 800; color: var(--ink); }
/* Visually hidden, still read aloud. */
.paint__sr {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ---- Plan promotion: struck list price above the price they actually pay --
   Same green brush as the pay-per-post promotion, scaled for the big price on
   a plan card. The promotional figure keeps the large .plan__amt type — it is
   the number they are charged, so it is the number that should be loudest. */
.plan__promo {
  display: flex; align-items: center; gap: 9px;
  margin-bottom: 2px; position: relative;
}
.plan__was {
  position: relative; display: inline-block;
  font-family: var(--font-display); font-size: 20px; font-weight: 600; color: var(--ink-soft);
}
.plan__was .paint__stroke {
  position: absolute; left: -8%; top: 0; width: 116%; height: 100%;
  color: var(--paint); pointer-events: none; overflow: visible;
}
.plan__off {
  padding: 3px 8px; background: var(--paint); color: #fff;
  font-size: 10.5px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase;
  border-radius: 4px 8px 3px 7px; transform: rotate(-4deg); white-space: nowrap;
  box-shadow: 0 1px 2px rgba(38, 48, 58, .18);
}
/* A real perk that happens to be a limited-time offer, as opposed to {soon},
   which is something not switched on yet. Green, like every other promotion. */
.plan__ltd {
  display: inline-block; vertical-align: 1px; margin-left: 6px;
  padding: 2px 7px; border-radius: var(--radius-pill);
  background: var(--paint); color: #fff;
  font-size: 10px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase;
}
/* ---- "+ ADD-ON", on the feature line you pay extra for --------------------

   IT SITS INCHES FROM `.plan__soon` AND MEANS THE OPPOSITE. Coming soon is an
   absence - nothing here yet, nothing to do, nothing to pay. This is a
   presence with a price on it. A member scanning a card should be able to tell
   those two apart without reading either, so they are drawn apart three ways
   and not one:

     COLOUR   SOLID NAVY. --sunset was tried first, on the reasoning that the
              palette keeps it for paid placements - and the owner read it as
              red: "dont make the 'add-on' red... use another color." They are
              right that it is the wrong register. Sunset sits one step from
              --accent, whose own token note admits it "reads as RED at scale",
              and red on the one line that costs extra makes an upsell look
              like a warning.

              Navy is the palette's colour for "actions and anything that
              matters", so it says important without saying alarm - and against
              Coming soon's pale warm grey a solid dark fill is the largest
              separation available here without leaving the brand. Filled
              rather than tinted for that reason: --primary-tint is a cool grey
              and would have sat too close to the warm grey beside it at 9.5px.
              White on --primary is about 12:1.

     SHAPE    a squared 4px corner against Coming soon's full pill. A pill is a
              status; a rectangular tag is a label attached to something. The
              difference survives at 9.5px, where a hue difference alone may
              not.

     A GLYPH  the "+". Colour is doing real work here and roughly one man in
              twelve would see less of it than intended, so the tag must still
              read as an addition in greyscale. The plus is what does that, and
              it is the fastest way to say "on top of" without a word.

   THE PLUS IS DECORATIVE, hence ::before rather than markup: the word "Add-on"
   is the content, and a screen reader saying "plus add-on" would be repeating
   in noise what the sentence beside it already says plainly. */
.plan__addon {
  display: inline-block; vertical-align: 1px; margin-left: 6px;
  padding: 2px 7px 2px 6px; border-radius: 4px;
  background: var(--primary); color: #fff;
  border: 1px solid var(--primary);
  font-size: 9.5px; font-weight: 800; letter-spacing: .05em; text-transform: uppercase;
  white-space: nowrap;
}
.plan__addon::before {
  content: '+';
  margin-right: 3px; font-weight: 900;
  /* Slightly larger than the letters it precedes, so it reads as a sign
     rather than as the first character of the word. */
  font-size: 11px; line-height: 1; vertical-align: -.5px;
}
/* ---- The inaugural offer strip, above the plan cards --------------------
   Sits between the section heading and the grid, so the argument for deciding
   this year is made before the prices rather than in a footnote under them.

   Uses the paint accent that every other promotion on this site uses — a
   fourth colour for a fourth kind of good news is how a page starts to look
   like a clearance sale. */
/* THE SAME OFFER, FOLDED, ON A MEMBER'S OWN DASHBOARD.

   The bar wears the banner's sage so the two read as one thing at two sizes,
   and so it is legible as good news while shut - which is the only state most
   people will ever see it in. Sage rather than the site's warm coral: this is
   an offer, not a warning.

   The banner INSIDE loses its own margin and its left rule. The rule marks the
   block out from the page; in here the bar above it is already doing that, and
   two nested left borders read as an error state. */
.inaug-fold {
  margin: 0 0 var(--sp-4);
  border: 1px solid var(--seaglass); border-radius: var(--radius);
  background: var(--seaglass-tint);
}
.inaug-fold__sum {
  display: flex; align-items: center; gap: 8px; cursor: pointer; list-style: none;
  padding: 10px var(--sp-4);
  font-size: 14px; font-weight: 700; color: var(--seaglass-ink);
}
.inaug-fold__sum::-webkit-details-marker { display: none; }
.inaug-fold__sum svg { flex: none; }
/* The chevron to the far end, and it turns when the fold opens — the same
   affordance every other <details> on this site uses. */
.inaug-fold__sum .inaug-fold__chev { margin-left: auto; transition: transform .18s; }
.inaug-fold[open] .inaug-fold__sum .inaug-fold__chev { transform: rotate(180deg); }
.inaug-fold__sum:focus-visible { outline: 3px solid var(--focus); outline-offset: -3px; border-radius: var(--radius); }
.inaug-fold__body { padding: 0 var(--sp-3) var(--sp-3); }
.inaug-fold__body .inaug {
  margin: 0; border-left-width: 1px; background: var(--surface);
}
@media (prefers-reduced-motion: reduce) {
  .inaug-fold__sum .inaug-fold__chev { transition: none; }
}

.inaug {
  /* Pale sage, the tint this site already uses for good news — see
     --seaglass-tint. The offer is the best thing on the page and was wearing
     the same surface as everything else. */
  margin: 0 0 var(--sp-4); padding: var(--sp-3) var(--sp-4);
  background: var(--seaglass-tint); border: 1px solid var(--seaglass);
  border-left: 4px solid var(--seaglass-ink); border-radius: var(--radius);
}
.inaug__tag {
  display: flex; align-items: center; gap: 6px; margin: 0 0 2px;
  font-size: 11px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase;
  color: var(--seaglass-ink);
}
.inaug__tag svg { flex: none; }
.inaug__lede { margin: 0; font-size: 15px; line-height: 1.45; color: var(--ink); }
/* The two rates side by side, wrapping to a column on a phone rather than
   shrinking — a price is the last thing on a page worth making small. */
.inaug__rates {
  display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-5);
  margin: var(--sp-2) 0 0;
}
.inaug__rate {
  display: flex; align-items: baseline; gap: 6px;
  font-family: var(--font-display); font-size: 20px; font-weight: 700; color: var(--ink);
}
.inaug__rate strong {
  font-family: var(--font-body); font-size: 12.5px; font-weight: 700;
  color: var(--ink-soft); text-transform: uppercase; letter-spacing: .04em;
}
/* The cents live INSIDE this, so `sup.cents` behaves as it does site-wide:
   raised, small, tight to the dollars. Straight in the flex row they were a
   flex item and did neither. */
.inaug__now { white-space: nowrap; }
/* A rate that is a link keeps its colours and gains a hairline underline
   under the name, so the row reads as a row of doors rather than of prose. */
.inaug__rate--link { text-decoration: none; color: var(--ink); border-radius: 6px; }
.inaug__rate--link strong { text-decoration: underline; text-decoration-color: var(--line-strong, var(--line)); text-underline-offset: 3px; }
.inaug__rate--link:hover strong, .inaug__rate--link:focus-visible strong { color: var(--accent); text-decoration-color: currentColor; }
.inaug__links { margin: var(--sp-2) 0 0; font-size: 14px; }
.inaug__links a { color: var(--ink); font-weight: 600; }
.inaug__links svg { vertical-align: -2px; }
.inaug__was {
  font-size: 14px; font-weight: 600; color: var(--ink-faint);
  text-decoration: line-through; white-space: nowrap;
}
/* ---- NOW -> NEW, ON ONE LINE, WITH ROOM BETWEEN THE WORDS -----------------
   Owner, 10 Sep 2026, from the app on their phone, changing plans: "the text
   is way too close together - NowBasicFree - and the arrow looks misplaced;
   put them on the same line and separate the words." The row was built of
   spans that had no rule at all, so they touched, and the arrow, a block on
   its own, broke the line. */
.planswap {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2) var(--sp-3);
  margin: var(--sp-3) 0 var(--sp-2);
}
.planswap__side { display: inline-flex; align-items: baseline; gap: 7px; white-space: nowrap; }
.planswap__label {
  font-size: 12.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  color: var(--ink-soft);
}
.planswap__name { font-family: var(--font-display); font-size: 20px; font-weight: 700; color: var(--ink); }
.planswap__price { color: var(--ink-soft); }
.planswap__side--was .planswap__name { color: var(--ink-soft); }
.planswap__arrow { font-size: 20px; color: var(--ink-soft); }

.inaug__after {
  margin: var(--sp-2) 0 0; font-size: 12.5px; line-height: 1.45; color: var(--ink-soft);
}
/* THE SENTENCE UNDER THE RATES, and the promise inside it (owner, 9 Sep
   2026): "we ask for no card to start one" is the most important thing on the
   page, so it breaks onto its own line in the display face at heading size,
   in the navy that means "this matters" everywhere else on the site. */
.inaug__lede--below { margin-top: var(--sp-3); }
.inaug__nocard {
  display: block; margin-top: 6px;
  font-family: var(--font-display); font-size: 26px; font-weight: 700; line-height: 1.15;
  letter-spacing: -.01em; color: var(--primary-ink);
}
@media (min-width: 720px) { .inaug__nocard { font-size: 30px; } }
/* Inside a member's closed fold it is a reminder, not a headline. */
.inaug-fold__body .inaug__nocard { font-size: 20px; }
/* ---- Pause, offered inside the close-account dialog ----------------------
   Reads as an alternative to leaving rather than a plea after the decision, so
   it sits above the two Close buttons and is styled as a card rather than as
   more actions in the same row. */
.settings-pause {
  margin: var(--sp-4) 0 0; padding: var(--sp-4);
  background: var(--surface-2); border: 1px solid var(--line-strong);
  border-left: 4px solid var(--primary); border-radius: var(--radius);
}
.settings-pause__head {
  display: flex; align-items: center; gap: 7px; margin: 0 0 var(--sp-2);
  font-size: 15px; line-height: 1.4;
}
.settings-pause__head svg { flex: none; color: var(--primary); }
.settings-pause__text {
  margin: 0; font-size: 13.5px; line-height: 1.55; color: var(--ink-soft);
}
/* One row of short buttons; wraps rather than shrinking on a phone. */
.settings-pause__btns {
  display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-3);
}
/* The fee, said once and large enough to be the thing the eye lands on: the
   whole offer is "keep everything for this much", so the figure is the offer. */
.settings-pause__price {
  display: inline-flex; align-items: baseline; gap: 4px; margin-left: 6px;
  font-weight: 700; color: var(--ink);
}
/* What a pause keeps and what it stops, as two short lists side by side on a
   wide screen and one after the other on a phone. */
.pause-facts {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--sp-3); margin: var(--sp-3) 0 0;
}
.pause-facts__col { margin: 0; padding: var(--sp-3); background: var(--surface-2);
  border: 1px solid var(--line); border-radius: var(--radius-sm); }
.pause-facts__head { margin: 0 0 6px; font-size: 13px; font-weight: 700;
  letter-spacing: .02em; text-transform: uppercase; color: var(--ink-soft); }
.pause-facts__col ul { margin: 0; padding-left: 18px; font-size: 14px; line-height: 1.55; }
.pause-facts__col li + li { margin-top: 3px; }
/* The paused state on the member's own dashboard. Informational, not a
   barrier — see NL.pausedNotice for why it does not replace the page. */
.paused-card { border-left: 4px solid var(--primary); }

/* The line above the two Close buttons, naming the refund. Sits between the
   modal body and its actions, so it is read before either is pressed — the
   figure is the whole difference between the two choices. */
.settings-cancel-note {
  margin: var(--sp-4) 0 0; padding-top: var(--sp-3);
  border-top: 1px solid var(--line); line-height: 1.5;
}
/* "On the site" is a button now, not a label — it sends the latest from the
   visit again, which is the only route from a scan to a listing already up.
   Styled as the tag it was, with the affordances a button needs. */
.syn-tag--btn {
  border: 0; cursor: pointer; font: inherit; font-size: 11px; font-weight: 700;
  display: inline-flex; align-items: center; gap: 4px;
}
.syn-tag--btn:hover { filter: brightness(0.94); }
.syn-tag--btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.plan .btn { width: 100%; }
.plan.is-selected { border-color: var(--primary); box-shadow: 0 0 0 2px var(--primary-tint), var(--shadow-hover); }

.promo-row { display: flex; gap: var(--sp-2); align-items: flex-end; flex-wrap: wrap; }
.promo-row .field { flex: 1; min-width: 180px; margin-top: 0; }
.promo-msg { font-size: 13.5px; font-weight: 600; margin-top: var(--sp-2); }
/* Same as .coupon__msg and .logo-say: an empty live region holds a margin it
   has no content to justify. */
.promo-msg:empty { display: none; }
.promo-msg.ok { color: var(--primary-ink); }
.promo-msg.bad { color: var(--destructive); }

@media (min-width: 720px) {
  .plan-grid { grid-template-columns: repeat(2, 1fr); align-items: start; }
  /* Fewer than 3 plans (member upgrading): center 1–2 cards */
  .plan-grid--few { display: flex; flex-wrap: wrap; justify-content: center; }
  .plan-grid--few .plan { flex: 1 1 300px; max-width: 380px; }
}
/* Let the row size itself to however many plans there are.

   This used to be a hard `repeat(4, 1fr)`, written when there were four tiers.
   There are three, so the row laid out four columns, filled three, and left a
   whole card's width of dead space on the right — and squeezed every card to
   243px to make room for a column that was never used. That is what made the
   pricing read as cramped: not too little page, too many columns.

   `auto-fit` collapses the tracks nothing lands in, so three cards share the
   full width and four still get their own row. The minimum is what decides how
   many fit; nothing here needs changing if a tier is added or dropped.

   Padding and gap are NOT tightened at this breakpoint any more. Squeezing the
   inside of a card is the wrong lever when the problem is the outside of it. */
@media (min-width: 1040px) {
  .plan-grid { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: var(--sp-4); }
}

/* ---- Featured business card (Weekender) --------------------------------- */
.featured-label {
  display: inline-flex; align-items: center; gap: 6px; font-size: 11px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .08em; color: var(--ink-faint);
  background: var(--surface-2); border: 1px solid var(--line); padding: 3px 10px; border-radius: var(--radius-pill);
}
.featured-label svg { color: var(--seaglass-ink); }

.featured-card {
  display: flex; flex-direction: column; overflow: hidden; color: inherit;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow); margin-top: var(--sp-5);
  transition: box-shadow .15s, border-color .15s, transform .06s;
}
.featured-card:hover { box-shadow: var(--shadow-hover); border-color: var(--line-strong); text-decoration: none; }
.featured-card:active { transform: translateY(1px); }
.featured-card:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
.featured-card__media { background: var(--surface-2); }
.featured-card__media img { width: 100%; height: 200px; object-fit: cover; display: block; }
.featured-card__body { padding: var(--sp-5); }
.featured-card__title {
  font-family: var(--font-display); font-size: 22px; font-weight: 600; letter-spacing: -.01em;
  color: var(--ink); margin-top: var(--sp-3);
}
.featured-card:hover .featured-card__title { color: var(--primary-ink); }
.featured-card__text {
  margin-top: var(--sp-2); color: var(--ink-soft); font-size: 15px; line-height: 1.55;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.featured-card__cta {
  display: inline-flex; align-items: center; gap: 6px; margin-top: var(--sp-3);
  color: var(--primary); font-weight: 700; font-size: 14px;
}

@media (min-width: 640px) {
  .featured-card { flex-direction: row; }
  .featured-card__media { flex: 0 0 42%; }
  .featured-card__media img { height: 100%; min-height: 230px; }
  .featured-card__body { flex: 1; align-self: center; }
}

/* ---- Featured detail page ----------------------------------------------- */
.featured-detail__title { font-size: clamp(26px, 6vw, 38px); letter-spacing: -.02em; margin-top: var(--sp-3); }
/* A small note after a page title - "(posts can be public or private)" on
   the post form (owner, 16 Sep 2026). Its own line on a phone. */
.featured-detail__sub {
  display: inline-block; margin-left: 8px; vertical-align: middle;
  font-family: var(--font-body); font-size: 14px; font-weight: 500; letter-spacing: 0; color: var(--ink-soft);
}
@media (max-width: 560px) { .featured-detail__sub { display: block; margin: 4px 0 0; } }
.featured-detail__biz { display: flex; align-items: center; gap: 6px; color: var(--ink-faint); font-weight: 600; font-size: 15.5px; margin-top: 8px; }
.featured-detail__biz svg { color: var(--ink-faint); flex: none; }
.featured-detail__hero { margin-top: var(--sp-5); border-radius: var(--radius); overflow: hidden; border: 1px solid var(--line); }
.featured-detail__hero img { width: 100%; display: block; }
.featured-gallery { margin-top: var(--sp-4); display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: var(--sp-3); }
.featured-gallery img { width: 100%; border-radius: var(--radius-sm); border: 1px solid var(--line); display: block; }

/* ---- Home (the front door) ---------------------------------------------- */
.home-hero { padding: var(--sp-7) 0 var(--sp-5); text-align: center; }
.home-title {
  font-family: var(--font-display); font-weight: 600;
  font-size: clamp(30px, 6.4vw, 50px); line-height: 1.08; letter-spacing: -.025em;
}
.home-sub { margin: var(--sp-3) 0 0; color: var(--ink-soft); font-size: 17px; }

/* ---- Home calendar -------------------------------------------------------
   One connected surface. The grid's 1px gaps sit on a line-coloured background
   so every cell shares a hairline with its neighbours — a real calendar, not a
   scatter of separate tiles. */
.cal {
  margin-top: var(--sp-6);
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden;
}
.cal__head {
  display: flex; align-items: baseline; gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5) var(--sp-3);
}
.cal__month {
  font-family: var(--font-display); font-size: 19px; font-weight: 600; color: var(--ink);
}
/* Week nav: visible, but quiet enough not to compete with the dates. */
.cal__nav { margin-left: auto; display: flex; gap: 4px; }
.cal__nav button {
  width: 30px; height: 30px; display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--ink-soft);
  transition: border-color .15s, color .15s, background .15s;
}
.cal__nav button:hover:not(:disabled) { border-color: var(--primary); color: var(--primary); background: var(--surface-2); }
.cal__nav button:disabled { opacity: .35; cursor: default; }
.cal__nav button:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
.cal__dow {
  display: grid; grid-template-columns: repeat(7, 1fr);
  border-bottom: 1px solid var(--line);
}
.cal__dow span {
  text-align: center; padding: 0 0 8px;
  font-size: 11px; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--ink-faint);
}
.cal__grid {
  display: grid; grid-template-columns: repeat(7, 1fr);
  gap: 1px; background: var(--line);          /* the gaps ARE the gridlines */
}
.cal-day {
  display: flex; flex-direction: column; align-items: center; justify-content: flex-start;
  /* The gap has to clear the pencil ring drawn round today's date, which spills
     well outside the digit on purpose. Tighten it and the ring cuts through the
     "1 event" line below. */
  gap: 15px;
  min-height: 88px; padding: 10px 4px;
  /* Days still to come read as live and tappable in the light blue used on
     the buttons; past days fall back to beige below. */
  background: var(--primary-tint); border: 0; border-radius: 0; color: var(--ink);
  transition: background .12s, color .12s;
}
/* A day with something on is a deeper step of the same blue. The count text
   underneath says WHAT is on; this is so the shape of the month reads at a
   glance, before you focus on any one cell. Beaten by is-today, is-selected,
   hover and is-past below, all of which are more specific or come later. */
.cal-day.has-items { background: var(--primary-tint-2); }
.cal-day__num { font-family: var(--font-display); font-size: 17px; font-weight: 600; line-height: 1; }
.cal-day__num { position: relative; }
/* ---- THE COUNT, IN A PILL ------------------------------------------------

   REPORTED: "everything is blue and it kinda blends together... put the number
   of things inside an oval shaped thing at a different colour, maybe the beige
   or maybe even the orange. But it has to be somewhat discrete."

   IT WAS NAVY TEXT ON A BLUE CELL - `--primary` on `--primary-tint-2` - which
   is legible and completely uniform, so a month of thirty cells read as one
   blue block and the counts had to be hunted for one at a time.

   PALE BLUE WITH NAVY NUMERALS, arrived at over three tries, and both
   rejections are worth recording so they are not walked back into:

     THE ACCENT BLUSH read as the wrong colour for the job - "looks kind of
     odd" - and fairly so. Rust is this site's shade for warmth and emphasis,
     and a count is neither.

     SOLID NAVY was too heavy. It works and it is perfectly legible, but thirty
     dark chips on a pale month is a lot of weight for a number whose whole job
     is to help somebody skim.

   So it is the lightest step that still separates: `--primary-tint`, one stop
   up from the `--primary-tint-2` the busy cells are painted, with the site's
   own navy for the digits and a hairline of the cell's colour to give the
   shape an edge. Same family, one value apart, which is what keeps it
   discrete while still reading as a chip rather than as loose text.

   ONE APPEARANCE ON EVERY CELL, which is the quiet advantage of going pale
   rather than dark. A navy pill had to invert itself on the navy states or
   vanish into them; a pale one sits on the cream of TODAY, the blue of a busy
   day and the navy of the selected day without changing at all - so it always
   reads as the same object, and there is one rule instead of three.

   CONTRAST: navy on `--primary-tint` is about 11:1, far past what small bold
   text needs.

   `:empty` IS WHY THE HEIGHT IS SPLIT IN TWO. Past days and days with nothing
   on render the span with no text inside, and an empty inline-flex box with
   padding would draw a small blank pill on two thirds of the month. So the
   pill styling belongs to the filled state and the empty one keeps only the
   13px that stops every row jumping when a month starts on a Saturday. */
.cal-day__n {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 17px; padding: 1px 7px;
  border-radius: var(--radius-pill);
  background: var(--primary-tint); color: var(--primary);
  border: 1px solid var(--primary-tint-2);
  font-size: 10.5px; font-weight: 700; line-height: 1.2; text-align: center;
  /* The cell is a centred flex column, so the pill has to be told to be only
     as wide as its own text rather than the full width of the day. */
  align-self: center; white-space: nowrap;
  transition: background .12s, color .12s;
}
/* The space between the number and the word, as a margin: the pill is a flex
   container and would eat a literal space between its two items. */
.cal-day__unit { margin-left: .3em; }
.cal-day__n:empty {
  display: block; min-height: 13px; padding: 0; background: none; border: 0;
}
/* Today sits with the past in beige, not with the days still to come: by the
   time you are planning, today is mostly spent. The pencil ring still marks
   it. Excluded when selected or hovered so those states can take over. */
.cal-day.is-today:not(.is-selected) { background: var(--paper); }

.cal-day:hover:not(:disabled) { background: var(--primary); color: var(--on-primary); }
/* ---- AND IT DOES NOT CHANGE ON THE NAVY CELLS ---------------------------

   Hover and selection paint the whole cell `--primary`, and these two rules
   used to fade the count to white-ish text - right when it was bare text on
   navy, wrong now that it sits inside a pale box.

   HELD RATHER THAN LEFT TO INHERIT. The old rules are still in this file and
   would otherwise win on order. Stating it is also the honest expression of
   the intent: this pill is meant to look identical wherever it lands, which is
   the whole reason a pale chip was chosen over a dark one. */
.cal-day:hover:not(:disabled) .cal-day__n,
.cal-day.is-selected .cal-day__n {
  background: var(--primary-tint); color: var(--primary);
  border-color: var(--primary-tint-2);
}
.cal-day:hover:not(:disabled) .cal-day__mark { color: #fff; opacity: .85; }
.cal-day:focus-visible { outline: 3px solid var(--focus); outline-offset: -3px; }
/* Today is ringed with a hand-drawn pencil loop rather than a crisp border —
   graphite, tilted, off-centre and free to spill outside the digit. Wide and
   flat: a round ring big enough to clear a two-digit date runs down into the
   count line, which is exactly what this shape avoids.

   The cell is LIFTED out of the static flow here. Every .cal-day paints its own
   background over the grid's gridline colour, so any part of the ring that
   spilled past today's cell was being painted over by the neighbouring cell and
   the day-of-week header — the ring looked broken exactly where it overshoots
   and closes. z-index puts today above its neighbours so the whole loop shows. */
.cal-day.is-today { position: relative; z-index: 2; }
.cal-day.is-today .cal-day__mark {
  position: absolute; left: 50%; top: 50%;
  width: 86px; height: 38px; transform: translate(-51%, -52%) rotate(-2.5deg);
  color: var(--ink-soft); opacity: .8; pointer-events: none; overflow: visible;
}
.cal-day.is-selected { background: var(--primary); color: var(--on-primary); }
/* (the selected state is handled with hover above, so the two cannot drift) */
/* On the selected (dark) cell the loop goes to near-solid white — a graphite
   line at 62% vanished against the slate fill. */
.cal-day.is-selected.is-today .cal-day__mark { color: #fff; opacity: .95; }
/* Gone by: warm beige, so the eye skips it. --ink-faint rather than
   --line-strong: the old grey on beige was too faint to read the date. */
.cal-day:disabled, .cal-day.is-past {
  background: var(--paper); color: var(--ink-faint); cursor: default;
}
/* The blank cells squaring the month off to full weeks — the month view shows
   one month only, so there is no date in them and nothing to hover. The hover
   rule above outweighs a bare class, hence the doubled-up selector. */
.cal-day.cal-day--pad, .cal-day.cal-day--pad:hover {
  background: var(--paper); cursor: default;
}

/* Says which calendar you are looking at. Tinted so it reads as a signpost
   rather than body copy — people arrive here hunting for their own list. */
.home-whose {
  display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-top: var(--sp-4); padding: 9px 14px;
  background: var(--seaglass-tint); border: 1px solid var(--seaglass);
  border-radius: var(--radius-pill);
  font-size: 14.5px; line-height: 1.55; color: var(--seaglass-ink);
}
.home-whose svg { flex: none; color: var(--destructive); fill: currentColor; }
.home-whose a { font-weight: 700; color: var(--primary); }

.home-day { margin-top: var(--sp-6); }
/* Each row says which section it belongs to, since a day mixes all three. */
.home-item + .home-item { margin-top: var(--sp-3); }
.home-item__realm {
  display: inline-block; margin-bottom: 5px;
  font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
  padding: 2px 8px; border-radius: var(--radius-pill);
}
.realm--events  { color: var(--primary);      background: var(--primary-tint); }
.realm--classes { color: var(--seaglass-ink); background: var(--seaglass-tint); }
.realm--dining  { color: var(--accent);       background: var(--accent-tint); }
.home-day .day-head { margin-bottom: var(--sp-3); }

.home-more {
  margin-top: var(--sp-7); padding-top: var(--sp-5); border-top: 1px solid var(--line);
  display: flex; flex-wrap: wrap; gap: var(--sp-5);
}
.home-more a {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 14.5px; font-weight: 600; color: var(--primary);
}
@media (max-width: 480px) {
  .cal-day { min-height: 70px; padding: 8px 2px; gap: 8px; }
  .cal-day__num { font-size: 15px; }
  /* NARROWER CELLS, TIGHTER PILL. At 375px a column is about 50px wide with
     2px of cell padding either side, so the pill's own padding is what decides
     whether "10 things" fits on one line or wraps into two and pushes the row
     taller. Measured at 375 and at 320. */
  /* NARROWER CELLS, SO THE WORD GOES AND THE NUMBER STAYS. A column is about
     50px wide here and "10 things" cannot sit on one line inside a pill at
     that size - it wrapped to two, which turned every oval into a tall box and
     added a row's worth of height to the month. The number is what the count
     is for; the word is what a phone can afford to lose, and the aria-label
     still says it in full. Measured at 375 and 320. */
  .cal-day__n { font-size: 10px; padding: 1px 6px; min-height: 16px; }
  .cal-day__unit { display: none; }
  /* The ring scales with the cell, or it would swallow a narrow phone column. */
  .cal-day.is-today .cal-day__mark { width: 52px; height: 30px; }
  .cal__head { padding: var(--sp-3) var(--sp-4) var(--sp-2); }
}

/* ---- Compact home highlight (same footprint as an event card, tinted) --- */
/* AIR ABOVE IT. A single Highlight is rendered by highlightMini, which is an
   ordinary event card — so it inherited an event card's spacing and sat flush
   against the toolbar directly above it, the List/Calendar buttons touching its
   border. Inside a list of cards the gap comes from the list; this one is not
   in a list, it is a paid tile sitting on its own between two other things. */
.event-card--highlight { background: var(--seaglass-tint); border-color: var(--seaglass); }
.toolbar + .event-card--highlight,
.toolbar + .highlight-row { margin-top: var(--sp-4); }
.event-card--highlight:hover { border-color: var(--seaglass-ink); box-shadow: var(--shadow-hover); }
.event-card--highlight .event-title { color: var(--ink); }
.event-card--highlight:hover .event-title { color: var(--seaglass-ink); }
.event-thumb {
  align-self: stretch; min-height: 64px; border-radius: var(--radius-sm); overflow: hidden;
  background: var(--surface); border: 1px solid rgba(110, 158, 147, .4);
}
.event-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.mini-highlight-label {
  display: inline-flex; align-items: center; gap: 5px; font-size: 10.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em; color: var(--seaglass-ink); margin-bottom: 4px;
}
.mini-highlight-label svg { color: var(--seaglass-ink); }

/* Two highlights side by side (Families / Bars & Restaurants); stacks on phones */
/* The paid tiles sit directly under the toolbar (result count, per-page, view
   toggle) and directly above the first day heading. Both need real air: with
   none, "Show 6 per page" read as a caption on the Highlight beneath it, and
   the tiles ran straight into the day list below. */
.highlight-row {
  display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3);
  margin: var(--sp-5) 0 var(--sp-6);
}
@media (max-width: 479px) { .highlight-row { grid-template-columns: 1fr; } }
/* One slot, one column. The empty-slot invitation uses this when the site sells
   a single Highlight per section, so the space on offer is the width of the
   space actually sold rather than half of it beside a gap. */
.highlight-row--one { grid-template-columns: 1fr; }

/* Compact highlight tile — image BESIDE the text, not above it. A full-width
   strip only 76px tall showed a thin middle slice of a 8:5 illustration, so you
   couldn't tell what it was. Beside the text the art keeps its own 8:5 shape
   (150x94), so the whole picture is visible — and the tile ends up shorter
   still, because its height is set by the copy rather than by the image. */
.hl-tile {
  display: flex; align-items: stretch; overflow: hidden; color: inherit;
  background: var(--seaglass-tint); border: 1px solid var(--seaglass); border-radius: var(--radius);
  box-shadow: var(--shadow); transition: box-shadow .15s, transform .06s;
}
.hl-tile:hover { box-shadow: var(--shadow-hover); text-decoration: none; }
.hl-tile:active { transform: translateY(1px); }
.hl-tile:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
.hl-tile__media {
  flex: 0 0 150px; display: flex; align-items: center; justify-content: center;
  background: var(--surface); border-right: 1px solid rgba(110, 158, 147, .4);
}
.hl-tile__media img { width: 150px; height: 94px; object-fit: cover; display: block; }
.hl-tile__body { padding: 10px var(--sp-4); flex: 1; min-width: 0; }
.hl-tile__title {
  font-family: var(--font-display); font-size: 16px; font-weight: 600; letter-spacing: -.01em;
  color: var(--ink); margin-top: 1px;
}
.hl-tile:hover .hl-tile__title { color: var(--seaglass-ink); }
.hl-tile .event-meta { margin-top: 3px; }
.hl-tile .event-desc { margin-top: 5px; font-size: 13.5px; line-height: 1.4; }
/* Two tiles share the row on tablets, so give the copy more of the width. */
@media (max-width: 760px) {
  .hl-tile__media { flex-basis: 116px; }
  .hl-tile__media img { width: 116px; height: 73px; }
}

/* ===========================================================================
   RESIDENT ACCOUNTS — the invite, the toast, and a resident's own calendar
   Residents browse and mark interest with no account at all; everything here
   is the offer that turns those taps into a calendar. Deliberately quieter
   than the business surfaces — this is a nudge, not a sales page.
   ========================================================================= */

/* ---- The invite modal --------------------------------------------------- */
.modal--invite { max-width: 480px; }
.invite-list { list-style: none; margin: var(--sp-4) 0 0; padding: 0; display: grid; gap: 10px; }
.invite-list li { display: flex; gap: 10px; align-items: flex-start; font-size: 14.5px; color: var(--ink-soft); }
.invite-list svg { color: var(--primary); flex: none; margin-top: 2px; }
.modal__text--quiet { margin-top: var(--sp-3); font-size: 14px; color: var(--ink-faint); }

/* ---- Toast --------------------------------------------------------------
   Bottom-centre on phones, bottom-left on desktop: never over the control they
   just tapped, never over the header. */
.toast {
  position: fixed; left: 50%; bottom: 18px; transform: translate(-50%, 12px);
  z-index: 90; display: flex; align-items: center; gap: 9px;
  /* width, NOT just max-width. A fixed box with `left: 50%` and no `right` is
     sized shrink-to-fit against what is left of the viewport from that edge —
     half the screen — so on a phone this was a 187px box wrapping "Added to
     your calendar" over three lines no matter how much room sat to its left.
     max-content asks for the sentence's real width and the max-width below
     still caps it. */
  width: max-content; max-width: min(92vw, 460px); padding: 11px 15px;
  background: var(--ink); color: var(--surface);
  border-radius: var(--radius); box-shadow: 0 8px 26px rgba(38, 48, 58, .3);
  font-size: 14.5px; font-weight: 600; line-height: 1.3;
  opacity: 0; pointer-events: none; transition: opacity .18s ease, transform .18s ease;
}
.toast.is-on { opacity: 1; transform: translate(-50%, 0); pointer-events: auto; }
.toast svg { flex: none; color: var(--surface); }
/* THE MARK IN THE CONFIRMATION, AND IT KEEPS ITS OWN COLOURS. The toast is
   painted in --ink, which is a hair off the navy the bird is drawn in, so the
   obvious move was the white knockout used on the slate button — and that
   costs the orange beak, which is the half of the mark people recognise.
   Asked for on 15 Sep 2026: blue bird, orange beak, everywhere it appears.
   So the logo sits on a small light tile instead, the ordinary answer for a
   logo on a dark field.

   Square-ish rather than round: the drawing is a gull ON a swag of lights and
   runs nearly corner to corner, so a circle clips the bulbs off the ends.

   The negative block margins .brand-inline carries are for spilling out of a
   button pill; a toast is one short line with 11px of room, so they come off
   and the toast grows the couple of pixels instead. */
.toast .brand-inline {
  margin: 0 1px; top: 0; padding: 2px;
  background: var(--surface); border-radius: 6px;
}
.toast a { color: #fff; font-weight: 800; text-decoration: underline; text-underline-offset: 3px; white-space: nowrap; }
.toast a:hover { color: #fff; }
@media (min-width: 720px) {
  .toast { left: 22px; transform: translate(0, 12px); }
  .toast.is-on { transform: translate(0, 0); }
}
@media (prefers-reduced-motion: reduce) { .toast { transition: opacity .18s ease; } }

/* ---- "What you have saved" panel (join page + signed-out calendar) ------ */
.join-saved {
  margin-top: var(--sp-5); padding: var(--sp-4) var(--sp-5);
  background: var(--primary-tint); border: 1px solid var(--primary);
  border-radius: var(--radius);
}
.join-saved--empty {
  display: flex; gap: 12px; align-items: flex-start;
  background: var(--surface-2); border-color: var(--line-strong);
}
.join-saved--empty > svg { color: var(--primary); flex: none; margin-top: 2px; }
.join-saved__head {
  display: flex; align-items: center; gap: 7px;
  font-family: var(--font-display); font-size: 17px; font-weight: 600; color: var(--primary-ink);
}
.join-saved--empty .join-saved__head { color: var(--ink); }
.join-saved__list { list-style: none; margin: var(--sp-3) 0 0; padding: 0; display: grid; gap: 8px; }
.join-saved__list li { display: flex; gap: 9px; align-items: flex-start; font-size: 14.5px; color: var(--ink); }
.join-saved__list svg { color: var(--primary); flex: none; margin-top: 3px; }
.join-saved__list em { display: block; font-style: normal; font-size: 13px; color: var(--ink-soft); margin-top: 1px; }
.join-saved__more { font-size: 13.5px; color: var(--ink-soft); padding-left: 23px; }
.join-saved__text { margin-top: var(--sp-3); font-size: 14px; color: var(--ink-soft); }
.join-alt { margin-top: var(--sp-5); text-align: center; font-size: 14.5px; color: var(--ink-soft); }

/* ---- A resident's calendar page ---------------------------------------- */
/* align-items:flex-START, not flex-end. Bottom-aligned, the buttons dropped to
   the foot of a four-line text block and floated on their own in the corner
   with ~330px of nothing between them and the copy. Top-aligned they pair with
   the heading, which is what they belong to. */
/* Account chrome on its own row, top right — the same shape as `.dash-top`
   on the org dashboard, so Settings lives in one corner across the whole
   site. */
.mycal-top {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-3); margin-bottom: 2px;
}
.mycal-top__acts { display: inline-flex; align-items: center; gap: 4px; }

/* NO LONGER A FLEX ROW, and that is the fix rather than a tidy-up. The
   heading and intro used to sit in one column of a two-column row with the
   buttons in the other, so the prose wrapped at roughly half the page and
   left a void beside it. Block layout, and the words use the width they are
   given. */
.mycal-head { margin-bottom: var(--sp-2); }
.mycal-title { font-family: var(--font-display); font-size: clamp(28px, 5vw, 40px); font-weight: 600; color: var(--ink); }
/* 76ch rather than 60: at 60 this sat at half the width of a 1008px header and
   broke into three short lines with a void beside them. */
/* The subtitle is fixed copy, so the one moving piece of information — what is
   actually next — gets its own quieter line underneath rather than being
   spliced into the sentence. */
/* The invitation button sits at the RIGHT end of its row. It is the one thing
   this page offers that nothing else on the site does, and the left edge is
   where the eye has already been reading the heading and the intro — putting
   it there made it the fourth thing in a column of text. On a phone the row is
   the full width anyway, so `flex-end` costs nothing and the button simply
   stretches. */
/* And the search sits under everything it is about — see mycalendar.js. */
/* The called-off notice on a listing's own page. Deliberately louder than the
   pill the cards carry: on a card it is one row among twenty and a quiet tag is
   proportionate, and here it is the whole reason the page is being read. */
.event-offbig {
  display: flex; align-items: flex-start; gap: 12px;
  padding: var(--sp-4); border-radius: var(--radius);
  margin-bottom: var(--sp-4);
  background: var(--accent-tint); border: 1px solid var(--accent);
  color: var(--ink);
}
.event-offbig svg { flex: none; margin-top: 1px; color: var(--accent); }
.event-offbig strong { display: block; font-size: 16px; }
.event-offbig em { display: block; font-style: normal; color: var(--ink-soft); font-size: 14px; margin-top: 2px; }
/* Postponed is not cancelled: it is still happening, so it does not get the
   colour that means "do not go". */
.event-offbig--postponed { background: var(--surface-2); border-color: var(--line-strong); }
.event-offbig--postponed svg { color: var(--ink-soft); }

/* ---- Answering an invitation ----------------------------------------------
   Sits between the description and the host's own strips, which is where a
   guest's eye lands after reading what the thing is. Deliberately quieter than
   the primary button above it: the answer matters, but "See the details" was
   the reason they opened the page. */
.rsvp {
  margin-top: var(--sp-5); padding: var(--sp-4);
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: var(--radius);
}
.rsvp__ask { margin: 0 0 var(--sp-3); font-weight: 700; color: var(--ink); }
.rsvp__row { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.rsvp__btn.is-on { font-weight: 700; }
/* Picked but not yet sent: outlined in the primary colour, not filled, so
   the difference between "chosen" and "sent" is visible at a glance. */
.rsvp__btn.is-picked { border-color: var(--primary); box-shadow: inset 0 0 0 1px var(--primary); color: var(--primary-ink); font-weight: 700; }
.rsvp__submit { margin: var(--sp-3) 0 0; }
.rsvp__note { margin: var(--sp-3) 0 0; font-size: 13px; color: var(--ink-faint); }
.rsvp__cal { margin: var(--sp-3) 0 0; }

/* ---- The host's guest list ------------------------------------------------
   The counts read first and the names second, because "how many" is the
   question and "who" is the follow-up. */
.guests { margin-top: var(--sp-4); }
.guests__loading { margin: 0; font-size: 14px; color: var(--ink-faint); }
.guests__head {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--sp-2) var(--sp-4); margin-bottom: var(--sp-3);
}
/* The headline. Big enough to read from the top of the page without hunting,
   which is the whole reason the names went behind a fold. */
.tally { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-5); margin: var(--sp-3) 0 var(--sp-4); }
.tally__bit { display: flex; align-items: baseline; gap: 7px; }
.tally__bit b {
  font-family: var(--font-display); font-size: 27px; font-weight: 600;
  line-height: 1; font-variant-numeric: tabular-nums; color: var(--ink);
}
.tally__bit span { font-size: 13.5px; color: var(--ink-faint); }
/* Nobody has replied yet is the number a host can still act on, so it is the
   one that is coloured. The other three are facts; this one is a job. */
.tally__bit--none b { color: var(--accent); }

.guests__fold { border-top: 1px solid var(--line); }
.guests__fold > summary {
  display: flex; align-items: center; justify-content: space-between;
  padding: 11px 2px; cursor: pointer; list-style: none;
  font-size: 14px; font-weight: 600; color: var(--ink-soft);
}
.guests__fold > summary::-webkit-details-marker { display: none; }
.guests__fold > summary:hover { color: var(--ink); }
.guests__fold > summary svg { transition: transform .15s; color: var(--ink-faint); }
.guests__fold[open] > summary svg { transform: rotate(180deg); }
/* A long guest list scrolls inside the fold rather than taking the page. */
.guests__fold .guests__list { max-height: 22rem; overflow-y: auto; }
.guests__list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--line); }
.guests__row {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--sp-3); padding: 10px 2px; border-bottom: 1px solid var(--line);
}
.guests__who { min-width: 0; }
.guests__mail { color: var(--ink-faint); font-size: 13.5px; overflow-wrap: anywhere; }
.guests__off { font-size: 12px; color: var(--ink-faint); font-style: italic; }
.guests__said {
  flex: 0 0 auto; font-size: 12px; font-weight: 700;
  letter-spacing: .04em; text-transform: uppercase;
  padding: 3px 9px; border-radius: var(--radius-pill);
}
.guests__said--yes   { background: #DCEBDC; color: #2A5A32; }
.guests__said--maybe { background: #F4E7CB; color: #7A5310; }
.guests__said--no    { background: #F1E0DD; color: #8C3129; }
.guests__said--none  { background: var(--surface-2); color: var(--ink-faint); }
.guests__add {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-2) var(--sp-3); margin: var(--sp-4) 0 0;
}
.guests__addnote { font-size: 13px; color: var(--ink-faint); }

/* ---- THE NUDGE OFFER, DIRECTLY UNDER THE TALLY IT IS ABOUT ----------------

   Same row shape as .guests__add so the two read as one family, and sitting
   against the "no answer" count rather than under the guest list, because the
   count is what sends somebody looking for it.

   TINTED, NOT BORDERED. It is an offer inside a panel that already has a
   border, and a second box inside the first reads as an alert. The tint is the
   same one .upsell uses, so the whole page has one colour for "this costs
   money" and never uses red for it. */
.guests__nudge {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-2) var(--sp-3);
  margin: var(--sp-3) 0 0;
  padding: var(--sp-3);
  border-radius: var(--radius-sm);
  /* --surface-2, the same tint .upsell uses below. Two invented tokens stood
     here first (--wash and --paper-2, neither of which this stylesheet
     declares), which is a transparent background that looks deliberate. */
  background: var(--surface-2);
}
.guests__nudge .guests__addnote strong { color: var(--ink); }

/* ---- The Big Occasion offer -----------------------------------------------
   At the top of the invitation form, before a name is typed. Quiet by default:
   it is an option, and the free version is the one most people should take.
   Ticking it is the only state change, so the border is the only thing that
   moves. */
.upsell {
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface-2); padding: var(--sp-4); margin-top: var(--sp-5);
  transition: border-color .15s, background .15s;
}
.upsell--on { border-color: var(--primary); background: var(--surface); }
/* THE FREE ANSWER, AT THE TOP AND NOT WHISPERED.

   The card used to open on a price and mention that any of this is free once,
   at the bottom, in the faintest style it had. So it read as a thing that
   costs money, which it is not. This line is now first, and it is styled as
   the panel's own good news — sage tint, readable sage ink, the same voice the
   site uses everywhere else for "this is fine" — while the price below it
   keeps the quiet treatment it always had.

   A RULE UNDER IT, not around it, so the two halves read as one card asking
   two questions rather than as two competing offers. */
.upsell__free {
  display: flex; gap: 9px; align-items: flex-start;
  margin: 0 0 var(--sp-4); padding: 0 0 var(--sp-4);
  border-bottom: 1px solid var(--line);
  font-size: 14.5px; line-height: 1.55; color: var(--ink-soft);
}
.upsell__free svg { flex: none; margin-top: 3px; color: var(--seaglass-ink); }
.upsell__free strong { color: var(--ink); }
/* ---- The paid half, folded away -------------------------------------------
   Asked for: make that part collapsible, and collapsed by default. Putting the
   free line on top fixed which fact you read first; it did not change that a
   price, four things you do not get and a tick box were still the bulk of the
   panel. Most people inviting a dozen neighbours are done at the line above.

   The same disclosure behaviour as .set-group and .smartdraft, so the site has
   one of these and not three. */
.upsell__more { margin: 0; }
.upsell__head {
  margin: 0; display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-2); cursor: pointer; list-style: none;
}
.upsell__head::-webkit-details-marker { display: none; }
.upsell__head:hover strong { color: var(--primary); }
.upsell__head:focus-visible {
  outline: 3px solid var(--focus); outline-offset: 3px; border-radius: var(--radius-sm);
}
.upsell__head .set-group__chev { margin-left: auto; }
.upsell__more[open] > .upsell__head .set-group__chev { transform: rotate(180deg); }
.upsell__body { margin-top: var(--sp-3); }
.upsell__price { color: var(--ink-faint); font-size: 14px; font-variant-numeric: tabular-nums; }
.upsell__sub { margin: var(--sp-2) 0 0; color: var(--ink-soft); font-size: 14px; }
.upsell__list { list-style: none; margin: var(--sp-3) 0 0; padding: 0; display: grid; gap: 5px; }
.upsell__list li { display: flex; align-items: baseline; gap: 7px; font-size: 14.5px; }
.upsell__list svg { color: var(--primary); flex: none; }
.upsell__pick { margin-top: var(--sp-4); }
.upsell__note { margin: var(--sp-3) 0 0; color: var(--ink-faint); font-size: 13px; }

/* ---- The invitations page -------------------------------------------------
   Rows, not cards. A host scans this for one thing (which of these needs
   chasing) and the counts are what they are scanning, so the counts get the
   right-hand edge and the same position on every row. */
.inv-head {
  display: flex; flex-wrap: wrap; align-items: flex-start;
  justify-content: space-between; gap: var(--sp-3) var(--sp-4);
  margin-bottom: var(--sp-5);
}
.inv-head .signup-title { margin-bottom: 4px; }
.inv-group {
  font-family: var(--font-display); font-size: 19px; font-weight: 600;
  display: flex; align-items: baseline; gap: var(--sp-2);
  margin: var(--sp-6) 0 var(--sp-3);
}
.inv-group__n {
  font-family: var(--font-body); font-size: 12px; font-weight: 600;
  color: var(--ink-faint); background: var(--surface-2);
  border-radius: var(--radius-pill); padding: 2px 9px;
}
.inv-rows { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-2); }
.inv-row { border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); }
.inv-row.is-off { opacity: .7; }
.inv-row__link {
  display: grid; gap: 4px var(--sp-4); padding: 14px var(--sp-4);
  grid-template-columns: 1fr auto auto; align-items: center;
  color: inherit; text-decoration: none;
}
.inv-row__link:hover { background: var(--surface-2); text-decoration: none; }
.inv-row__head { grid-column: 1; display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2); }
.inv-row__title { font-weight: 700; }
.inv-row__when {
  grid-column: 1; grid-row: 2; display: flex; flex-wrap: wrap; align-items: center;
  gap: 5px var(--sp-3); color: var(--ink-faint); font-size: 13.5px;
}
.inv-row__where { display: inline-flex; align-items: center; gap: 5px; }
.inv-row__counts {
  grid-column: 2; grid-row: 1 / span 2;
  display: flex; flex-wrap: wrap; gap: 5px var(--sp-3); justify-content: flex-end;
}
.inv-count { font-size: 13px; color: var(--ink-faint); white-space: nowrap; }
.inv-count b { font-variant-numeric: tabular-nums; }
.inv-count--yes b { color: var(--ink); }
.inv-count--none b { color: var(--accent); }
.inv-row__none { font-size: 13px; color: var(--ink-faint); }
.inv-row__go { grid-column: 3; grid-row: 1 / span 2; color: var(--ink-faint); display: flex; }
/* A count on a button, for "My invitations 3". */
.btn__n {
  margin-left: 6px; font-variant-numeric: tabular-nums;
  background: var(--surface-2); border-radius: var(--radius-pill);
  padding: 1px 7px; font-size: 12px;
}
@media (max-width: 620px) {
  .inv-row__link { grid-template-columns: 1fr auto; }
  .inv-row__counts { grid-column: 1; grid-row: 3; justify-content: flex-start; }
  .inv-row__go { grid-row: 1 / span 3; }
}

/* ---- Invitations you are hosting ------------------------------------------
   One row per invitation, the reply count on the row rather than a click away,
   because the count is the reason somebody comes back to it. */
.host-rows { list-style: none; margin: var(--sp-3) 0 0; padding: 0; display: grid; gap: var(--sp-2); }
/* A private invitation's row with its ticket buttons beside the link (16 Sep 2026). */
.host-row--inv { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2); }
.host-row--inv .host-row__link { flex: 1 1 240px; min-width: 0; }
.host-row__acts { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding-right: 12px; }
.host-row { border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); }
.host-row.is-off { opacity: .68; }
.host-row__link {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--sp-2) var(--sp-4); padding: 12px var(--sp-4);
  color: inherit; text-decoration: none;
}
.host-row__link:hover { background: var(--surface-2); text-decoration: none; }
.host-row__main { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2); min-width: 0; }
.host-row__when { color: var(--ink-faint); font-size: 13.5px; }
.host-row__rsvp {
  margin-left: auto; color: var(--ink-soft); font-size: 13.5px;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* Between the month and the day it belongs to. The gap above is bigger than
   the one below on purpose: the bar reads as the start of "what else is on",
   not as a footnote to the calendar it sits under. */
/* Search takes the room, export takes what it needs. Below 620 they stack and
   the export goes full width, because a button squeezed beside a search field
   on a phone is a button nobody can hit. */
.mycal-find {
  display: flex; align-items: center; gap: var(--sp-3);
  margin: var(--sp-5) 0 var(--sp-3);
}
.mycal-find__search { flex: 1 1 auto; min-width: 0; }
.mycal-find .search { margin: 0; }
.mycal-find__export { flex: 0 0 auto; white-space: nowrap; }
@media (max-width: 620px) {
  .mycal-find { flex-wrap: wrap; }
  .mycal-find__export { width: 100%; justify-content: center; }
}
.mycal-cta { margin-top: var(--sp-5); display: flex; gap: var(--sp-3); flex-wrap: wrap; }
.mycal-why { margin-top: var(--sp-7); padding-top: var(--sp-5); border-top: 1px solid var(--line); }

.mycal-day { margin-top: var(--sp-5); }
.mycal-day__head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-bottom: var(--sp-3); }
.mycal-day__title { font-family: var(--font-display); font-size: 21px; font-weight: 600; color: var(--ink); }
.mycal-day__date { font-size: 14px; color: var(--ink-faint); }
.mycal-empty { font-size: 15px; color: var(--ink-soft); padding: var(--sp-4) 0; }

.mycal-block { margin-top: var(--sp-7); }
.mycal-block__title {
  display: flex; align-items: center; gap: 8px;
  font-family: var(--font-display); font-size: 19px; font-weight: 600; color: var(--ink);
  padding-bottom: var(--sp-3); border-bottom: 1px solid var(--line);
}
.mycal-block__title svg { color: var(--primary); }
.mycal-block__sub { margin-top: var(--sp-3); font-size: 15px; line-height: 1.6; color: var(--ink-soft); }
/* Preview banner. Deliberately the loudest thing on the page — the calendar
   below it is example data, and nobody should be a scroll away from learning
   that. Accent tint rather than the usual seaglass, so it does not read as
   another saved-item surface. */
.mycal-preview {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  margin-bottom: var(--sp-5); padding: var(--sp-4);
  background: var(--accent-tint); border: 1px solid var(--accent);
  border-radius: var(--radius);
}
.mycal-preview > svg { flex: none; color: var(--accent); }
.mycal-preview__body { flex: 1; min-width: 220px; font-size: 15px; line-height: 1.6; color: var(--ink-soft); }
.mycal-preview__body strong { color: var(--ink); }
.mycal-block__acts { margin-top: var(--sp-4); }
.mycal-group { margin-top: var(--sp-4); }
.mycal-group__day {
  font-size: 12px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink-faint); margin-bottom: 8px;
}
.mycal-group__day span { font-weight: 600; letter-spacing: 0; text-transform: none; }

/* One saved thing. The two actions sit on the right and stay a comfortable
   tap target on a phone. */
/* A saved item on the resident's OWN page. Deliberately not the white card
   used on the listing pages: this is something they already chose, so it wears
   the same soft nacre tint a saved item takes on everywhere else. The point is
   that a glance tells you "these are mine" rather than "here is something new
   to consider". `position: relative` anchors the corner badge. */
.saved-row {
  position: relative;
  display: flex; gap: var(--sp-3); align-items: center;
  padding: var(--sp-3) var(--sp-4);
  background: var(--seaglass-tint); border: 1px solid var(--seaglass);
  border-radius: var(--radius);
}
.saved-row + .saved-row { margin-top: 8px; }
/* States the obvious on purpose — the whole page is favorites, and saying so
   on the item is what stops the missing "Add to my favorites" button reading
   as something gone wrong. */
.saved-row__fav {
  position: absolute; top: -8px; right: 12px;
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 10.5px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase;
  color: var(--seaglass-ink); background: var(--surface);
  border: 1px solid var(--seaglass); border-radius: var(--radius-pill);
  padding: 2px 9px;
}
/* Filled red heart. The badge is the one place on the site that says "this is
   already yours", and an outlined grey heart read as a button you had not
   pressed yet — the exact opposite. */
.saved-row__fav svg { color: var(--destructive); fill: currentColor; }
/* IT IS THE CONTROL NOW, not a badge that describes one. Sized as a target
   rather than as a label, and the empty heart is what "not in your favorites"
   looks like - the same pair the tiles have always used. */
.saved-row__fav {
  padding: 4px 7px; gap: 0; cursor: pointer;
  transition: border-color .15s, background .15s, transform .12s;
}
.saved-row__fav:hover { border-color: var(--destructive); transform: translateY(-1px); }
.saved-row__fav:not(.is-on) { border-color: var(--line-strong); }
.saved-row__fav:not(.is-on) svg { color: var(--ink-faint); fill: none; }
.saved-row__fav:not(.is-on):hover svg { color: var(--destructive); }
/* UNHEARTED, AND STILL ON THE SCREEN. Asked for: "it doesn't go away right
   away, unless I leave the screen and come back. So I have the chance to
   unheart it and heart it back." Faded enough to read as no longer yours,
   solid enough to find the heart again. */
.saved-row--dropped { opacity: .5; }
.saved-row--dropped .saved-row__fav { opacity: 1; }
/* THE ORDINARY CASE IS JUST THE HEART. Every row on this page is a favorite, so
   spelling it out per row shouts the page's own title back at the reader. The
   words live in `title` and come back on hover. */
.saved-row__fav--yes { padding: 3px 6px; gap: 0; }
/* The realm pill would vanish against the tinted row — flip it to the surface. */
.saved-row__realm { background: var(--surface); }
/* Room for the badge that overhangs the top edge. */
.mycal-group .saved-row:first-child, .mycal-day .saved-row:first-child { margin-top: 10px; }
.saved-row__when {
  flex: none; width: 52px; text-align: center; display: flex;
  flex-direction: column; align-items: center; justify-content: center; line-height: 1;
}
.saved-row__t { font-family: var(--font-display); font-size: 19px; font-weight: 600; color: var(--ink); }
.saved-row__ampm { font-size: 10.5px; font-weight: 800; letter-spacing: .05em; color: var(--ink-faint); margin-top: 2px; }
.saved-row__allday { font-size: 11px; font-weight: 700; color: var(--ink-faint); line-height: 1.25; }
.saved-row__body { flex: 1; min-width: 0; }
.saved-row__title {
  display: block; font-family: var(--font-display); font-size: 16.5px; font-weight: 600;
  color: var(--ink); text-decoration: none;
}
.saved-row__title:hover { color: var(--primary); text-decoration: underline; }
.saved-row__meta {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  margin-top: 3px; font-size: 13px; color: var(--ink-soft);
}
.saved-row__meta svg { color: var(--ink-faint); flex: none; }
.saved-row__realm {
  font-size: 11px; font-weight: 700; letter-spacing: .03em; text-transform: uppercase;
  color: var(--seaglass-ink); background: var(--seaglass-tint);
  border-radius: var(--radius-pill); padding: 2px 8px;
}
.saved-row__acts { flex: none; display: flex; gap: 6px; align-items: center; }
/* An icon at every width now, with its sentence in `title`. The label used to
   be spelled out here and shrunk to font-size:0 under 560px — which is a button
   that says "Export to my calendar app" on a laptop and nothing at all on a
   phone, and left this control three times the width of the two beside it. */
.saved-row__cal { background: var(--surface); }
.saved-row--past { opacity: .72; }
.saved-row--past .saved-row__when { width: 84px; }
/* The past list is over and done with — no tint, no badge. */
.saved-row--past { background: var(--surface); border-color: var(--line); }

.icon-btn {
  width: 34px; height: 34px; display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--ink-soft);
  transition: border-color .15s, color .15s, background .15s;
}
.icon-btn:hover { border-color: var(--primary); color: var(--primary); background: var(--surface-2); }
.icon-btn:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
.icon-btn--off:hover { border-color: var(--destructive); color: var(--destructive); background: var(--accent-tint); }
/* Adding something back is not a destructive act and should not borrow the red
   hover that says it is. */
.icon-btn--add:hover { border-color: var(--seaglass); color: var(--seaglass-ink); background: var(--seaglass-tint); }

.mycal-past__toggle {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: var(--sp-3) 0; background: none; border: 0; border-bottom: 1px solid var(--line);
  font-family: var(--font-display); font-size: 17px; font-weight: 600; color: var(--ink-soft);
}
.mycal-past__toggle:hover { color: var(--primary); }
.mycal-past__toggle svg:last-child { margin-left: auto; }
.mycal-block--account { padding-bottom: var(--sp-6); }

/* ---- Help / FAQ --------------------------------------------------------
   Native <details> rather than scripted accordions: it is keyboard-operable
   and findable by the browser's own in-page search with no JS at all. */
.help-jump { margin-top: var(--sp-5); display: flex; gap: var(--sp-2); flex-wrap: wrap; }
.help-block { margin-top: var(--sp-7); scroll-margin-top: 90px; }
.help-block__t { font-family: var(--font-display); font-size: clamp(22px, 3.4vw, 28px); font-weight: 600; color: var(--ink); margin-top: 2px; }
.help-block__s { margin-top: 6px; font-size: 15.5px; color: var(--ink-soft); }
.help-list { margin-top: var(--sp-4); border-top: 1px solid var(--line); }
.help-q { border-bottom: 1px solid var(--line); }
.help-q summary {
  display: flex; align-items: center; gap: 12px; cursor: pointer;
  padding: var(--sp-4) 0; list-style: none;
  font-family: var(--font-display); font-size: 17px; font-weight: 600; color: var(--ink);
}
.help-q summary::-webkit-details-marker { display: none; }
.help-q summary svg { margin-left: auto; flex: none; color: var(--ink-faint); transition: transform .18s; }
.help-q[open] summary svg { transform: rotate(180deg); }
.help-q summary:hover { color: var(--primary); }
.help-q summary:focus-visible { outline: 3px solid var(--focus); outline-offset: 3px; border-radius: var(--radius-sm); }
.help-a { padding: 0 0 var(--sp-4); font-size: 15px; line-height: 1.6; color: var(--ink-soft); }
.help-a code { font-size: 13.5px; background: var(--surface-2); border-radius: 4px; padding: 1px 5px; }
@media (prefers-reduced-motion: reduce) { .help-q summary svg { transition: none; } }

/* ===========================================================================
   THE LOGO'S LITTLE MOMENT — see assets/brand-skit.js.
   Everything happens inside the logo's own 40px box; the resting state is the
   logo exactly as drawn. Props sit at opacity 0 until `.is-playing` lands on
   the <svg>, and every animation resolves back to identity, so removing the
   class is always safe mid-flight.

   Timeline (all skits): glance + blink → two bobs → HOP, flipping scaleX
   about the box centre so the bird lands on the other side of the swag facing
   the other way (~2.1s) → the skit itself (~2s–6.2s). The flip lives on
   .bs-hop and holds for the whole skit; .bs-bird inside it carries the
   skit's own body motion, so the two transforms never share an element.
   ========================================================================= */
.brand-skit .bs-guitar, .brand-skit .bs-band, .brand-skit .bs-gi, .brand-skit .bs-n,
.brand-skit .bs-c, .brand-skit .bs-impact, .brand-skit .bs-table { opacity: 0; }
.brand-skit .bs-hop, .brand-skit .bs-head, .brand-skit .bs-eye, .brand-skit .bs-bird,
.brand-skit .bs-guitar, .brand-skit .bs-gi, .brand-skit .bs-kick,
.brand-skit .bs-glass, .brand-skit .bs-n, .brand-skit .bs-c, .brand-skit .bs-impact {
  transform-box: view-box;   /* px in transform-origin = viewBox units */
}
.brand-skit .bs-kick   { transform-origin: 7.6px 17.4px; }    /* the hip */
.brand-skit .bs-gi     { transform-origin: 9.4px 14.5px; }
.brand-skit .bs-impact { transform-origin: 19.5px 16px; }     /* where the foot lands */
.brand-skit .bs-hop    { transform-origin: 16px 16px; }       /* the box centre */
.brand-skit .bs-head   { transform-origin: 13.2px 11.5px; }   /* the neck */
.brand-skit .bs-eye    { transform-origin: 14.9px 8.2px; }
.brand-skit .bs-bird   { transform-origin: 9.4px 20.5px; }    /* the feet */
.brand-skit .bs-guitar { transform-origin: 13.2px 15.4px; }
.brand-skit .bs-glass  { transform-origin: 7.4px 13.5px; }   /* stem base */
.brand-skit .bs-c { transform-origin: 8px 7px; }
.brand-skit text {
  font: 700 6px/1 var(--font-body, sans-serif);
  fill: var(--primary); text-anchor: middle;
}
.brand-skit .bs-n { font-size: 4.6px; font-weight: 600; }

/* The frame around every skit: notice you, bob, hop across — perform — then
   crouch, hold a beat, and hop back home. ONE keyframe block spans the whole
   6.15s so the outbound and the return are a matched pair, and the animation
   ends where the static logo stands: when the JS strips the class a moment
   later, nothing visibly changes. Mid-hop the scaleX passes through 0 — the
   paper-thin frame IS the turn, both ways.

   Percent map (duration 6.15s, delay 1.05s):
     0–18%    out: two bobs, jump, flip, land   (~1.1s)
     18–82%   hold, facing left — the skit runs here
     82–100%  back: crouch, beat, jump, flip, settle (~1.1s) */
.brand-skit.is-playing .bs-head { animation: bsLook 1s ease-in-out .15s both; }
.brand-skit.is-playing .bs-eye  { animation: bsBlink .9s ease-in-out .25s both; }
.brand-skit.is-playing .bs-hop  { animation: bsHop 6.15s ease-in-out 1.05s both; }
@keyframes bsLook {
  0%, 100% { transform: rotate(0); }
  35%, 70% { transform: rotate(6deg); }   /* dips toward the viewer */
}
@keyframes bsBlink {
  0%, 24%, 55%, 79%, 100% { transform: scaleY(1); }
  12%, 67%                { transform: scaleY(.1); }
}
@keyframes bsHop {
  0%    { transform: translateY(0)      scaleX(1); }
  2.9%  { transform: translateY(1.7px)  scaleX(1); }    /* bob one */
  5.4%  { transform: translateY(.3px)   scaleX(1); }
  7.2%  { transform: translateY(1.6px)  scaleX(1); }    /* bob two loads the spring */
  9.8%  { transform: translateY(-3.8px) scaleX(.35); }  /* airborne, turning */
  11.1% { transform: translateY(-4.4px) scaleX(-.2); }  /* apex, edge-on */
  14%   { transform: translateY(-.6px)  scaleX(-1); }
  15.4% { transform: translateY(.8px)   scaleX(-1); }   /* landing squash */
  17.9%, 83% { transform: translateY(0) scaleX(-1); }   /* the skit plays here */
  86%   { transform: translateY(1.7px)  scaleX(-1); }   /* crouch... */
  88%   { transform: translateY(1.7px)  scaleX(-1); }   /* ...and a beat — intent, not cleanup */
  90.5% { transform: translateY(-3.8px) scaleX(-.3); }  /* the hop home */
  92%   { transform: translateY(-4.4px) scaleX(.25); }
  95%   { transform: translateY(-.5px)  scaleX(1); }
  96.5% { transform: translateY(.7px)   scaleX(1); }    /* settle */
  100%  { transform: translateY(0)      scaleX(1); }    /* exactly the logo */
}

/* ---- Events: the guitar ------------------------------------------------- */
@keyframes bsPopHold {
  0%       { opacity: 0; transform: scale(.4); }
  8%       { opacity: 1; transform: scale(1.08); }
  12%, 86% { opacity: 1; transform: scale(1); }
  100%     { opacity: 0; transform: scale(.7); }
}
.brand-skit.is-playing[data-skit="guitar"] .bs-guitar {
  animation: bsPopHold 3.9s ease 2s both;
}
.brand-skit.is-playing[data-skit="guitar"] .bs-bird {
  animation: bsRock 3s ease-in-out 2.35s both;
}
@keyframes bsRock {
  0%, 100% { transform: rotate(0); }
  12%      { transform: rotate(3deg); }
  30%      { transform: rotate(-3deg); }
  48%      { transform: rotate(3deg); }
  66%      { transform: rotate(-3deg); }
  84%      { transform: rotate(2deg); }
}
.brand-skit.is-playing[data-skit="guitar"] .bs-n1 { animation: bsNote 1.3s ease-out 2.7s both; }
.brand-skit.is-playing[data-skit="guitar"] .bs-n2 { animation: bsNote 1.3s ease-out 3.5s both; }
.brand-skit.is-playing[data-skit="guitar"] .bs-n3 { animation: bsNote 1.3s ease-out 4.3s both; }
@keyframes bsNote {
  0%   { opacity: 0; transform: translate(0, 2.5px); }
  18%  { opacity: 1; }
  100% { opacity: 0; transform: translate(-1.5px, -4px); }  /* drifts up-left, the way he faces */
}

/* ---- Classes: karate — one, two, THREE, kick --------------------------- */
/* The gi and headband appear together (he suits up), the counts tick 1-2-3
   with a small ready-crouch on each, and the kick fires ON three: leg snaps
   out, body counters back, impact flash at the foot, then a controlled recoil.
   A changing SILHOUETTE is what makes this legible at 40px — the old three
   bobs did not change the outline at all. */
.brand-skit.is-playing[data-skit="fitness"] .bs-gi,
.brand-skit.is-playing[data-skit="fitness"] .bs-band {
  animation: bsPopHold 3.9s ease 1.95s both;
}
.brand-skit.is-playing[data-skit="fitness"] .bs-bird {
  animation: bsStance 3.1s ease-in-out 2.3s both;
}
@keyframes bsStance {
  0%, 100% { transform: translateY(0) rotate(0); }
  6%   { transform: translateY(1.3px) rotate(0); }        /* ready — one */
  13%  { transform: translateY(0) rotate(0); }
  25%  { transform: translateY(1.3px) rotate(0); }        /* ready — two */
  32%  { transform: translateY(0) rotate(0); }
  44%  { transform: translateY(1.6px) rotate(0); }        /* coil — three */
  56%  { transform: translateY(-.8px) rotate(7deg); }     /* KICK: counter-lean */
  64%  { transform: translateY(-.8px) rotate(7deg); }
  78%  { transform: translateY(.4px) rotate(-2deg); }     /* recoil */
  88%  { transform: translateY(0) rotate(0); }
}
/* The kicking leg: tucked, then snapped horizontal, then set back down. */
.brand-skit.is-playing[data-skit="fitness"] .bs-kick {
  animation: bsKick 3.1s ease-in-out 2.3s both;
}
/* NEGATIVE angles. The leg lives inside .bs-bird, which sits inside the
   scaleX(-1) flip — so a positive rotation here lands the foot on the RIGHT of
   the screen while the impact flash (drawn outside the flip) is on the LEFT,
   and the kick misses its own target. Negated, the foot arrives where the
   bird is actually facing. */
@keyframes bsKick {
  0%, 44%, 100% { transform: rotate(0); }
  50%  { transform: rotate(-38deg); }    /* chamber — knee comes up */
  56%  { transform: rotate(-104deg); }   /* SNAP — leg out front, horizontal */
  64%  { transform: rotate(-100deg); }
  78%  { transform: rotate(-26deg); }    /* pulls it back in, under control */
  88%  { transform: rotate(0); }
}
.brand-skit.is-playing[data-skit="fitness"] .bs-c1 { animation: bsCount .72s ease 2.42s both; }
.brand-skit.is-playing[data-skit="fitness"] .bs-c2 { animation: bsCount .72s ease 3.02s both; }
.brand-skit.is-playing[data-skit="fitness"] .bs-c3 { animation: bsCount3 .95s ease 3.62s both; }
@keyframes bsCount {
  0%   { opacity: 0; transform: translateY(1.5px) scale(.6); }
  22%  { opacity: 1; transform: translateY(0) scale(1); }
  75%  { opacity: 1; }
  100% { opacity: 0; transform: translateY(-1px) scale(1); }
}
/* Three is the one that matters, so it lands harder and holds through the kick. */
@keyframes bsCount3 {
  0%   { opacity: 0; transform: translateY(1.5px) scale(.6); }
  18%  { opacity: 1; transform: translateY(0) scale(1.3); }
  30%  { transform: translateY(0) scale(1); }
  80%  { opacity: 1; }
  100% { opacity: 0; transform: translateY(-1px) scale(1); }
}
/* The flash sits where the foot arrives, and only while it is there. */
.brand-skit.is-playing[data-skit="fitness"] .bs-impact {
  animation: bsImpact .5s ease-out 4.05s both;
}
@keyframes bsImpact {
  0%   { opacity: 0; transform: scale(.45); }
  30%  { opacity: 1; transform: scale(1.1); }
  60%  { opacity: .9; transform: scale(1); }
  100% { opacity: 0; transform: scale(1.25); }
}

/* ---- Food & Drinks: table, wine, lean back ------------------------------ */
.brand-skit.is-playing[data-skit="wine"] .bs-table {
  animation: bsTable 4.2s ease 1.9s both;
}
@keyframes bsTable {
  0%        { opacity: 0; transform: translateX(-13px); }  /* from the left edge */
  6%        { opacity: 1; }
  14%, 86%  { opacity: 1; transform: translateX(0); }
  96%, 100% { opacity: 0; transform: translateX(-13px); }
}
.brand-skit.is-playing[data-skit="wine"] .bs-glass {
  animation: bsSip 2.4s ease-in-out 2.9s both;
}
/* The glass is taller now, so its rim swings further per degree — smaller
   translate, same destination: the rim meets the beak. */
@keyframes bsSip {
  0%, 68%, 100% { transform: translate(0, 0) rotate(0); }
  20%           { transform: translate(1.6px, -1.3px) rotate(38deg); }
  32%           { transform: translate(1.8px, -1.5px) rotate(44deg); }  /* the sip */
  48%           { transform: translate(1.6px, -1.3px) rotate(38deg); }
}
/* Head tips back to drink; `forwards` (not `both`) so its delay period does
   not stamp rotate(0) over the opening glance, which shares this property. */
.brand-skit.is-playing[data-skit="wine"] .bs-head {
  animation: bsLook 1s ease-in-out .15s both, bsHeadBack 2.4s ease-in-out 2.9s forwards;
}
@keyframes bsHeadBack {
  0%, 80%, 100% { transform: rotate(0); }
  20%, 55%      { transform: rotate(-8deg); }
}
.brand-skit.is-playing[data-skit="wine"] .bs-bird {
  animation: bsLean 2.6s ease-in-out 2.75s both;
}
@keyframes bsLean {
  0%, 100% { transform: rotate(0); }
  16%, 70% { transform: rotate(-3deg); }
}

/* Belt to the JS braces: if the script ever adds the class anyway, nothing moves. */
@media (prefers-reduced-motion: reduce) {
  .brand-skit .bs-hop, .brand-skit .bs-head, .brand-skit .bs-eye, .brand-skit .bs-bird,
  .brand-skit .bs-guitar, .brand-skit .bs-glass, .brand-skit .bs-table,
  .brand-skit .bs-n, .brand-skit .bs-c, .brand-skit .bs-band,
  .brand-skit .bs-gi, .brand-skit .bs-kick, .brand-skit .bs-impact {
    animation: none !important;
  }
}

/* ---- Coupons -------------------------------------------------------------
   A list you scan for one thing: which codes are live. The state word carries
   that, the left edge repeats it in colour, and a switched-off row is dimmed
   rather than hidden — an expired offer is still a record of what somebody
   was promised. */
.promo-list { list-style: none; margin: var(--sp-4) 0 var(--sp-3); padding: 0; display: grid; gap: 8px; }
.promo-row {
  display: grid; gap: 2px 14px; align-items: baseline;
  grid-template-columns: minmax(9rem, auto) 1fr auto auto auto;
  padding: 12px 14px; border: 1px solid var(--line);
  border-left: 4px solid var(--line-strong);
  border-radius: var(--radius); background: var(--surface); font-size: 13.5px;
}
.promo-row--on   { border-left-color: var(--seaglass-ink); }
.promo-row--soon { border-left-color: #E8B33F; }
.promo-row--off  { border-left-color: var(--ink-faint); opacity: .68; }
.promo-row__code { font-weight: 700; font-family: ui-monospace, Menlo, Consolas, monospace; }
.promo-row__code em, .promo-row__label em {
  display: block; font-style: normal; font-weight: 400; font-size: 12.5px;
  color: var(--ink-faint); font-family: var(--font-body, inherit);
}
.promo-row__note { font-style: italic !important; }
.promo-row__uses { color: var(--ink-soft); white-space: nowrap; font-variant-numeric: tabular-nums; }
.promo-row__state { font-weight: 600; white-space: nowrap; }
.promo-row--on .promo-row__state { color: var(--seaglass-ink); }
.promo-row--off .promo-row__state { color: var(--ink-faint); }
.promo-row__go { justify-self: end; }
/* Below 720px the columns stop being columns: five tracks in a phone width
   gives every one of them two words and a hyphen. */
@media (max-width: 720px) {
  .promo-row { grid-template-columns: 1fr auto; }
  .promo-row__label { grid-column: 1 / -1; }
}

/* ============================================================================
   My Calendar: the two views, and the controls that switch between them.
   ========================================================================== */

/* A day carrying one of YOUR favorites, while the grid is showing everything
   on. Sits in the corner rather than replacing the count, because the count is
   still the honest total of what is happening — the heart only says some of it
   is already yours. */
.cal-day__fav {
  position: absolute; top: 4px; right: 5px;
  color: var(--primary); line-height: 0; pointer-events: none;
}
.cal-day { position: relative; }
.cal-day.has-fav { background: var(--primary-tint); }

/* ---- The quiet calendar (the resident dashboard) ------------------------
   On the public pages nearly every day has something on, so the blue on days
   still to come reads as "live". On a list of favorites nearly every day has
   nothing on, and the same blue painted twenty-six empty cells as heavily as
   the three that mattered. Empty days go to plain paper; only a day with a
   plan on it takes the tint, so the shape of the week reads at a glance. */
.cal--quiet .cal-day { background: var(--surface); }
.cal--quiet .cal-day.is-today:not(.is-selected):not(.has-items) { background: var(--surface); }
.cal--quiet .cal-day.has-items { background: var(--primary-tint-2); }
.cal--quiet .cal-day.has-fav { background: var(--primary-tint); }
.cal--quiet .cal-day.is-selected { background: var(--primary); color: var(--on-primary); }
.cal--quiet .cal-day:disabled, .cal--quiet .cal-day.is-past,
.cal--quiet .cal-day.cal-day--pad, .cal--quiet .cal-day.cal-day--pad:hover { background: var(--paper); }
/* Week or month, in the card's own header beside the arrows. */
/* `.cal__nav button` above fixes every button in the bar to a 30px square,
   which is right for an arrow and clips a word; hence the doubled selector. */
.cal__nav .cal__span, .cal__span {
  width: auto; height: 30px; padding: 0 12px; margin-right: 4px;
  border: 1px solid var(--line); border-radius: 999px;
  background: var(--surface); color: var(--ink-soft);
  font: inherit; font-size: 12.5px; font-weight: 600; white-space: nowrap; cursor: pointer;
}
.cal__nav .cal__span:hover { border-color: var(--primary); color: var(--primary); background: var(--surface-2); }
.cal__span:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
/* A tapped day with nothing on it: one line under the grid, not a section. */
.mycal-daynote {
  display: flex; align-items: flex-start; gap: 8px;
  margin: var(--sp-3) 0 0; font-size: 14px; line-height: 1.5; color: var(--ink-soft);
}
.mycal-daynote svg { flex: none; margin-top: 3px; color: var(--primary); }
/* The group a calendar tap landed on: a wash of the same tint that fades. */
@keyframes mycal-lit { from { background: var(--primary-tint); } to { background: transparent; } }
.mycal-group.is-lit { animation: mycal-lit 1.6s ease-out; border-radius: var(--radius); }
.mycal-group { scroll-margin-top: 12px; }
/* Export beside the heading of the list it exports. */
.mycal-block__title { flex-wrap: wrap; }
.mycal-export { margin-left: auto; font-family: var(--font-body, inherit); }
/* The same red, filled heart the rows carry, standing in for the word
   "favorites" (owner, 4 Sep 2026: "send all [red heart] to my personal
   calendar"). aria-label on the span says the word for a screen reader. */
.mycal-export__heart { display: inline-flex; margin: 0 2px; vertical-align: -2px; }
.mycal-export__heart svg { color: var(--destructive); fill: currentColor; }
.mct-nav { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.mct-nav__btns { flex: 1 1 auto; }
.mct-nav .mycal-export { margin-left: auto; }

/* ---- The view switcher --------------------------------------------------
   A segmented control: both destinations visible at once, current one plainly
   pressed. A dropdown would hide half the page's capability behind a click,
   and nobody goes looking for a view they do not know exists. */
/* ONE BAR HOLDING BOTH GROUPS.

   These are two different questions — WHICH VIEW (calendar or table) and WHAT
   GOES IN IT (only favorites, or everything) — and they act on the same thing,
   so they belong in one control rather than as two loose clusters floating in
   the page. The shared tinted bar says "these work together"; the space
   between the groups says "these are two questions". Both readings at once,
   which is what neither the touching version nor the far-apart version gave.

   The tinted surface moved OUT of `.mcv__tabs` and onto `.mcv` — nesting one
   tinted pill-group inside another drew a box inside a box. */
/* ---- Two tiles on one row: a pick for you, and hosting ------------------
   The old hosting band's material (a shade of the paper, one hairline) split
   into two equal cards, so the pick costs no new space. Buttons sit on the
   bottom edge of both, whatever the text above them runs to, so the row reads
   as one row and not two cards of different heights. Alone, a tile takes the
   whole row: the pick is allowed to be missing, hosting is not. */
/* TWO THIRDS AND A THIRD, not halves. Equal halves gave hosting, which has
   one sentence and two buttons, the same room as the pick, which has five
   lines, and the difference showed as a block of empty beige. Narrower,
   hosting's buttons take two rows and its sentence wraps, and the two tiles
   end up the same height by having the same amount in them. */
.tiles {
  display: grid; grid-template-columns: 2fr 1fr; gap: var(--sp-3);
  margin: var(--sp-4) 0 var(--sp-2);
}
.tile {
  display: flex; flex-direction: column; gap: 5px; min-width: 0;
  padding: 12px var(--sp-4) var(--sp-3);
  border-radius: var(--radius); background: var(--surface-2);
  border: 1px solid var(--line);
}
.tile:only-child { grid-column: 1 / -1; }
.tile__kicker {
  display: flex; align-items: center; gap: 6px; margin: 0 0 2px;
  font-size: 11.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--primary);
}
.tile__kicker svg { flex: none; }
/* PINNED TO THE TILE'S CORNER, not the end of the header line. The header
   sits in the text column, and that column is narrower or wider depending
   on the drawing beside it, so a count at the end of the line moved from
   pick to pick. The tile's corner does not move. */
.tile--pick { position: relative; }
.tile__count {
  position: absolute; top: 12px; right: var(--sp-4); margin: 0;
  font-weight: 600; letter-spacing: 0; text-transform: none;
  color: var(--ink-faint);
  display: inline-flex; align-items: center; gap: 8px;
}
/* The round arrow beside the count. Small and quiet, the same grey as the
   count until it is pointed at; a 26px circle is still a comfortable tap. */
.tile__refresh {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--surface); color: var(--ink-soft);
  cursor: pointer; padding: 0;
}
.tile__refresh:hover { color: var(--primary); border-color: var(--line-strong); }
.tile__refresh:disabled { opacity: .5; cursor: default; }
/* ---- The working tile ----------------------------------------------------
   Placeholder lines the shape of the pick to come, with a light sweeping
   across them, and a spark turning beside the line that says what is being
   done. Motion is the message here (something is happening), so under
   reduced-motion it all holds still and the changing line does the work. */
.tile__skel {
  display: block; height: 13px; border-radius: 6px;
  background: linear-gradient(90deg, var(--line) 0%, var(--surface) 45%, var(--line) 90%);
  background-size: 220% 100%;
  animation: tile-shimmer 1.6s ease-in-out infinite;
}
.tile__skel--title { height: 19px; width: 62%; margin-top: 3px; }
.tile__skel--meta { width: 48%; }
.tile__skel--why { width: 86%; margin-top: 3px; }
@keyframes tile-shimmer { from { background-position: 120% 0; } to { background-position: -120% 0; } }
.tile__busy {
  display: flex; align-items: center; gap: 8px;
  margin: auto 0 0; padding-top: var(--sp-2);
  font-size: 13px; color: var(--ink-soft);
}
.tile__busy svg { flex: none; color: var(--primary); animation: tile-spin 2.2s linear infinite; }
.tile__busy span { transition: opacity .28s; }
.tile__busy span.is-fading { opacity: 0; }
@keyframes tile-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .tile__skel, .tile__busy svg { animation: none; }
}
.tile__say { margin: 0; font-size: 14px; line-height: 1.5; color: var(--ink-soft); }
.tile__say a { font-weight: 600; color: var(--primary); }
.tile__title {
  font-family: var(--font-display); font-size: 17px; font-weight: 600; line-height: 1.25;
  color: var(--ink); text-decoration: none;
}
.tile__title:hover { color: var(--primary); }
.tile__meta { margin: 0; font-size: 13px; color: var(--ink-faint); }
.tile__why { margin: 1px 0 0; font-size: 13.5px; line-height: 1.5; color: var(--ink); }
.tile__acts { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: auto; padding-top: 6px; }
.tile__acts .btn svg { vertical-align: -3px; }
.tile__acts .btn { white-space: nowrap; }
.tile__nudge { margin: 0; font-size: 12.5px; line-height: 1.5; color: var(--ink-faint); }
/* The pick with its drawing: words on the left, the drawing in the space
   they leave on the right, centred on the tile's height. Smaller than on a
   card (54): a gentle thing in a corner, not the subject. Two categories sit
   side by side and step down again. */
/* BOTH TILES, not only the pick. The rule was written when the pick was the
   only one with a drawing beside its words; hosting has one now, and a
   selector naming one tile would have left the other stacking its art under
   the buttons. */
.tile.has-art { flex-direction: row; align-items: stretch; gap: 12px; }
.tile.has-art .tile__body { display: flex; flex-direction: column; gap: 5px; flex: 1 1 auto; min-width: 0; }
.tile__art { display: flex; align-items: center; flex: none; padding: 4px 2px 0 0; }
.tile__art .cat-mark svg { width: 50px; height: 50px; }
.tile__art .cat-marks--two .cat-mark svg { width: 38px; height: 38px; }
@media (max-width: 420px) {
  .tile__art .cat-mark svg { width: 42px; height: 42px; }
  .tile__art .cat-marks--two .cat-mark svg { width: 32px; height: 32px; }
}
.tile__nudge a { color: var(--primary); font-weight: 600; }
/* ---- The scan desk's town cards (8 Sep 2026) ------------------------------ */
/* HALF THE CARD (owner, 17 Sep 2026: "reduce the size of each of these town
   boxes to half"). Twenty-five towns sit on this board now rather than ten -
   every coming-soon one as well - and at the old size, six to a row, that is
   four rows of cards before you reach the training log. Same four lines in
   each, set smaller and packed tighter: about half the area, nine or ten to a
   row on a desktop screen, and still a comfortable tap on a phone.

   The card is a link to a town, so it is shrunk by type and padding rather
   than by dropping a line: the count, the work waiting and the last visit are
   the whole reason to look at the board. */
.syn-towns { grid-template-columns: repeat(auto-fill, minmax(152px, 1fr)); gap: var(--sp-2); }
.tile--town { text-decoration: none; color: inherit; transition: border-color .15s, box-shadow .15s; }
.tile--town:hover { border-color: var(--line-strong); box-shadow: var(--shadow); text-decoration: none; }
.tile--town { padding: 8px 10px 9px; gap: 2px; }
/* The name wraps under the pin rather than running out of the card: "New
   Fairfield" and "Bridgeport" are wider than the ids this was sized for. */
.tile--town .tile__kicker { flex-wrap: wrap; margin: 0 0 1px; gap: 4px; font-size: 10px; min-width: 0; }
.tile--town .tile__kicker svg { width: 11px; height: 11px; }
.tile--town .check__soon { font-size: 9.5px; letter-spacing: 0; }
.tile--town .tile__title { font-size: 13.5px; }
.tile--town .tile__meta { font-size: 11px; line-height: 1.45; }
.tile--town .tile__nudge { font-size: 10.5px; line-height: 1.4; }
.tile--town.is-empty .tile__title { color: var(--ink-faint); font-weight: 500; }
/* THE PHONE RULE BELOW STAYS OUT OF `.tiles`' WAY BY NAMING BOTH CLASSES.
   `.tiles { grid-template-columns: 1fr }` under 720px is further down this
   file, and at equal specificity the later rule wins - so a one-class
   `.syn-towns` here would be beaten and the board would be twenty-six
   full-width bars to scroll past on a phone. Two to a row instead: 128 leaves
   room for a second column and a gap at 375px, where 152 does not. */
@media (max-width: 720px) {
  .tiles.syn-towns { grid-template-columns: repeat(auto-fill, minmax(128px, 1fr)); }
}
.syn-towns__line { margin: 0 0 var(--sp-3); font-size: 15px; color: var(--ink-soft); }
.syn-towns__line strong { color: var(--ink); }
/* ---- THE SECTION IS A BAR, AND IT FOLDS (owner, 8 Sep 2026) ---------------

   It was an <h3> in the page's own ink at seventeen point, sitting directly on
   the background between two seven-column tables. The owner could not find it,
   which is fair: nothing about it said "this heading owns everything below me
   and not the table above".

   So it is a filled bar with a rule down its left edge, and the whole bar is
   the button. Weight comes from the fill and the edge rather than from a
   larger typeface, because three of these stacked at twenty point would be
   shouting - and the count and the "N drafts waiting" have to fit on the same
   line without competing with the name. */
.syn-fold { margin: var(--sp-5) 0 0; border-radius: var(--radius-sm); }
.syn-fold + .syn-fold { margin-top: var(--sp-4); }
.syn-fold__head {
  display: flex; align-items: center; gap: 10px; margin: 0;
  padding: 11px var(--sp-4); font-size: 16px; font-weight: 700;
  background: var(--surface-2); border-left: 4px solid var(--line-strong);
  border-radius: var(--radius-sm); cursor: pointer; list-style: none;
  transition: background .15s, border-color .15s;
}
.syn-fold__head::-webkit-details-marker { display: none; }
.syn-fold__head:hover { background: var(--line); }
.syn-fold__head:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.syn-fold__head svg { flex: none; color: var(--ink-soft); }
.syn-fold__name { flex: none; }
/* The summary line takes the slack, so the chevron stays pinned right however
   long or short the sentence is. */
.syn-fold__line {
  flex: 1 1 auto; min-width: 0; text-align: right;
  font-size: 13.5px; font-weight: 400; color: var(--ink-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Work waiting is sage, never red - it is a to-do, not a fault. */
.syn-fold__line.is-work { color: var(--seaglass-ink); }
.syn-fold__line strong { font-weight: 700; }
.syn-fold__head > svg:last-child { transition: transform .18s; color: var(--ink-faint); }
.syn-fold[open] > .syn-fold__head { border-color: var(--primary); border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
.syn-fold[open] > .syn-fold__head > svg:last-child { transform: rotate(180deg); }
.syn-fold__body { padding-top: var(--sp-3); }
@media (max-width: 640px) {
  /* On a phone the count and the chevron are what has to survive; the summary
     sentence is repeated inside the fold by the table itself. */
  .syn-fold__line { display: none; }
  .syn-fold__name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
}
.syn-core.is-on { color: var(--accent); }
/* The teaching panel and its log. */
.syn-teach__result { margin-top: var(--sp-3); padding: var(--sp-3) var(--sp-4); background: var(--surface-2); border-radius: var(--radius-sm); }
.syn-teach__result p { margin: 0; }
.syn-teach__result .dash-actions { margin-top: var(--sp-2); }
.syn-teach__logs { list-style: none; margin: var(--sp-3) 0 0; padding: 0; }
.syn-teach__log { padding: 8px 0; border-top: 1px solid var(--line); font-size: 14px; line-height: 1.5; }
.tile[aria-busy="true"] .tile__say { color: var(--ink-faint); }
@media (max-width: 720px) {
  .tiles { grid-template-columns: 1fr; }
}

/* ---- THE SAME TILES ON A PHONE (owner, 6 Sep 2026, with screenshots) ------

   Two things were wrong. The drawing sat in a column of its own down the
   whole right side, so every line of text wrapped early with clear space
   beside it: "Sat, Sep 12 · Norwalk Oyster Festival ·" and then "Norwalk" on
   a line by itself. And the little back arrow wrapped onto the same line as
   "Add to my favorites" while "Next pick" fell underneath, so back and next
   were not together.

   So on a narrow screen the drawing gives up its column: it is pinned to the
   tile's bottom corner, the text runs the full width above it, and only the
   button rows leave room for it. The favorites button takes a whole row, so
   the two arrows land together on the row beneath, back on the left. */
@media (max-width: 640px) {
  .tile.has-art { display: flex; flex-direction: column; position: relative; }
  .tile.has-art .tile__art { position: absolute; right: var(--sp-4); bottom: var(--sp-3); padding: 0; }
  .tile.has-art .tile__acts, .tile.has-art .tile__nudge { padding-right: 64px; }
  .tile--pick .tile__acts [data-pick-add] { flex: 1 1 100%; }
}

/* CLOSER TO THE CALENDAR THAN TO THE TILES (owner, 4 Sep 2026: "those two
   go together"). The bar governs the grid under it, so the gap above is the
   section gap and the gap below is a hairline's worth. The calendar card
   below carries a top margin of its own on the browse pages; on this page
   it is taken off (see #mycal below) so the 8px here is the whole gap. */
.mcv {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-3);
  margin: 28px 0 8px;
  padding: 3px; border-radius: 999px;
  background: var(--surface-2); border: 1px solid var(--line);
}
#mycal .mcv { margin: 28px 0 8px; }
#mycal #mc-cal .cal { margin-top: 0; }
.mcv__tabs { display: inline-flex; gap: 2px; }
/* ON A PHONE THERE IS ONE VIEW. Owner, 10 Sep 2026, from the app on their
   own iPhone: "remove these two buttons on the mobile versions". The table
   needs the width of a desk; on a phone the week is the calendar, and a pair
   of tabs offering a choice that does not fit is only clutter above it. The
   view state defaults to the calendar and is never stored, so hiding the
   switch cannot strand anyone in the table. */
@media (max-width: 620px) {
  .mcv__tabs { display: none; }
}
.mcv__tab {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 8px 15px; border: 0; border-radius: 999px;
  background: transparent; color: var(--ink-soft);
  font: inherit; font-size: 14px; font-weight: 600; cursor: pointer;
  white-space: nowrap;
}
.mcv__tab:hover { color: var(--ink); }
/* The pressed one is a raised white pill — the same "this is where you are"
   language the rest of the site uses for the current tab. */
.mcv__tab.is-on {
  background: var(--surface); color: var(--ink);
  box-shadow: 0 1px 2px rgba(0,0,0,.10);
}
.mcv__tab svg { flex: none; }

/* PUSHED TO THE FAR RIGHT, away from the view tabs.

   These are two different questions — WHICH VIEW (calendar or table) and WHAT
   IS IN IT (only favorites, or everything) — and sitting them side by side
   made four pills in a row that read as one set of four choices. Separated,
   the grouping does the explaining.

   `margin-left: auto` inside the flex row, so the gap is whatever space is
   going rather than a fixed value that would collapse awkwardly at some
   widths. When the row wraps on a narrow screen the auto margin does nothing
   and they simply stack, which is the right answer there. */
.mcv__sub { display: inline-flex; flex-wrap: wrap; gap: 6px; margin-left: auto; }
/* ONE LABEL, TWO LENGTHS, CHOSEN BY THE SCREEN. Both are in the markup so a
   resize is correct immediately; only one is ever visible, and the hidden one
   is display:none rather than clipped so a screen reader reads it once. */
.mcv__short { display: none; }
.mcv__pill {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 13px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--surface);
  color: var(--ink-soft); font: inherit; font-size: 13px; font-weight: 600;
  cursor: pointer; white-space: nowrap;
}

/* A STADIUM THAT WRAPS IS NOT A STADIUM.

   `.mcv` is a 999px-radius pill holding two tabs and two filter chips, and it
   is right at a width where all four fit on one line. On a phone they do not,
   so the row wrapped — and a two-row box with fully round ends puts its
   corner buttons outside the curve. Reported as the buttons "coming out of the
   round oval circle... they're just misplaced".

   The container was never the problem; the SHAPE was a promise that its
   contents would stay on one line, and on a phone they cannot. So below 620px
   it stops promising: an ordinary rounded rectangle with real padding, the
   tabs on their own full-width row, the chips on theirs. Two tidy rows that
   look deliberate instead of one pill that has burst.

   The chips keep their natural widths rather than being stretched to halves.
   "Every event & activity" is wider than "Only my favorites" and they carry
   `white-space: nowrap`, so forcing equal columns would push one of them out
   of its own border — the same mistake as the invitation row above. */
@media (max-width: 620px) {
  .mcv {
    border-radius: var(--radius);
    padding: var(--sp-2);
    gap: var(--sp-2);
  }
  .mcv__tabs { flex: 1 1 100%; }
  .mcv__tab { flex: 1; justify-content: center; }
  /* SIDE BY SIDE, HALF EACH. Reported: "the placement in the mobile web does
     not look good... make them fit side by side." They are two states of one
     choice, and stacked they read as two separate buttons. Equal widths say
     they are alternatives; the short label is what makes the pair fit. */
  .mcv__sub { margin-left: 0; flex: 1 1 100%; flex-wrap: nowrap; }
  .mcv__pill { flex: 1; justify-content: center; padding: 7px 10px; }
  .mcv__long { display: none; }
  .mcv__short { display: inline; }
}
.mcv__pill:hover { border-color: var(--line-strong); color: var(--ink); }
.mcv__pill.is-on {
  background: var(--primary); border-color: var(--primary); color: #fff;
}
.mcv__note {
  display: flex; align-items: flex-start; gap: 8px;
  margin: 0 0 var(--sp-3); padding: 10px 12px;
  border-radius: var(--radius-sm, 8px);
  background: var(--seaglass-tint); color: var(--ink-soft);
  font-size: 13.5px; line-height: 1.5;
}
.mcv__note svg { flex: none; margin-top: 2px; color: var(--seaglass-ink); }

/* A row in the day panel that is NOT one of theirs, shown only in "everything
   on" mode. Deliberately plainer than a saved row: no nacre tint, so the
   difference between "mine" and "on offer" is visible at a glance rather than
   only in the badge. */
.saved-row--other { background: var(--surface); }
/* WAS UNREADABLE: --ink-faint on a transparent background, inside a dashed
   border, at 10.5px uppercase. Three ways of saying "ignore me" stacked on a
   label that has to be read to do its job.

   The badge sits on the row's top edge, so it needs its own solid ground to
   sit on and ink dark enough to hold at this size. Ink-soft on the surface
   clears contrast comfortably; the dashed border stays, because "not yours
   yet" genuinely is the lighter state and the solid border belongs to the one
   that says it IS yours. */
.saved-row__fav--no {
  background: var(--surface); color: var(--ink-soft);
  border: 1px dashed var(--ink-faint);
}

/* ---- The activities table -----------------------------------------------
   The whole point is sorting, so the headings have to look pressable and the
   sorted one has to be obvious without reading it. */
.mct-block { padding-top: var(--sp-2); }
.mct__count {
  margin: 0 0 var(--sp-2); color: var(--ink-soft); font-size: 13.5px;
}
.mct__hint {
  display: block; margin-top: 4px; color: var(--ink-faint); font-size: 13px;
}
.mct__hint svg { vertical-align: -2px; margin-right: 4px; color: var(--seaglass-ink); }

/* IT FITS. IT DOES NOT SCROLL.

   The first version put the table in its own horizontal scroller, which keeps
   the PAGE from scrolling sideways but still asks the reader to drag a table
   around to read a row — and a row you cannot see all of is a row you cannot
   compare, which is the entire reason to be looking at a table instead of the
   calendar.

   So the table is laid out to fit whatever width it is given: `table-layout:
   fixed` with percentage columns, and text that wraps instead of pushing. The
   scroller stays only as a last resort for absurdly narrow windows — it should
   never actually engage. Anything that will not fit is dropped a breakpoint at
   a time below, not scrolled to. */
.mct-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* ---- "+6" YOU CAN OPEN ---------------------------------------------------

   The full audience list used to live in a `title`, which is a hover tooltip -
   nothing at all on the device most of this dashboard is read on. It is a
   <details> now, and the styling has one job: make the summary read as
   something you can press without turning a quiet table cell into a button.

   NO FLOATING PANEL, and the line above is why: this table scrolls
   horizontally, so an absolutely positioned popover would be clipped at the
   scroller's edge and unreachable on the last column. It opens downward inside
   the cell, where there is always room. */
.mct__audpop { display: inline-block; }
.mct__audpop > summary {
  cursor: pointer; list-style: none; display: inline-flex; align-items: baseline;
  gap: 4px; border-radius: 4px;
}
/* Safari draws its own triangle through ::-webkit-details-marker, which
   `list-style: none` does not reach. */
.mct__audpop > summary::-webkit-details-marker { display: none; }
/* The "+6" is the affordance, so it gets the pill: a whole cell that lights up
   on hover reads as a row action, and this is not one. */
.mct__audpop .mct__more {
  padding: 1px 6px; border-radius: 999px; background: var(--surface-2);
  font-weight: 600;
}
.mct__audpop > summary:hover .mct__more,
.mct__audpop[open] > summary .mct__more { background: var(--primary-tint); color: var(--primary); }
.mct__audpop > summary:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.mct__audlist {
  margin: var(--sp-2) 0 0; padding: var(--sp-2) var(--sp-3);
  list-style: none; border: 1px solid var(--line); border-radius: 8px;
  background: var(--surface); font-size: 12.5px; color: var(--ink-soft);
  /* It sits inside a table cell that may be narrow; one audience per line is
     what makes seven of them readable rather than a wrapped run-on. */
  min-width: 150px;
}
.mct__audlist li { padding: 1px 0; white-space: nowrap; }
.mct {
  width: 100%; border-collapse: collapse; font-size: 14px;
  table-layout: fixed;
}
/* Long titles and venue names wrap rather than forcing the column open. */
.mct td { overflow-wrap: anywhere; }
/* A PILL IN A FIXED COLUMN HAS TO WRAP. syn-tag is nowrap by default, which is
   right in a row of its own and wrong inside a 12% table cell: "CLASSES &
   WORKSHOPS" ran straight out of the Category column and printed on top of the
   Subcategory text beside it. table-layout is fixed, so the column cannot grow
   to rescue it — the label has to give. */
.mct td .syn-tag { white-space: normal; overflow-wrap: anywhere; }

/* THE MARK ITSELF, in the "fix one that came out wrong" table. The whole point
   of that panel is that a wrong logo is obvious at a glance and a venue's NAME
   is not, so the picture has to be big enough to recognise and small enough
   that forty of them are one scan rather than a scroll.

   White behind it and `contain`, for the same reasons the listing cards give
   (.org-mark--logo): a transparent PNG on a tinted ground picks up a colour its
   designer never chose, and a cropped logo is a different logo. */
.logo-chip {
  width: 40px; height: 40px; object-fit: contain;
  background: #fff; border: 1px solid var(--line);
  border-radius: 8px; padding: 2px;
}

/* "Have we got a picture for this venue?", answered under the field where the
   name is typed. Lives in a hint slot, so it inherits the hint's size and
   colour and does not turn a quiet line into a second heading — the MARK is
   what carries the answer, and the sentence only explains it. */
.venue-mark { display: flex; align-items: center; gap: 10px; margin-top: 2px; }
.venue-mark:empty { display: none; }
.venue-mark__chip { flex: none; line-height: 0; }
.venue-mark__say { min-width: 0; }

/* ---- The sync tables: prospects and clients ------------------------------
   Same component as the resident calendar table above, different columns. Its
   own widths because these rows are names and verdicts rather than events. */
.mct--sync { table-layout: auto; }
/* 30%, up from 28%, and it is the whole point of stacking the buttons: the
   name is the column somebody reads down, and it was the one being squeezed. */
/* 26% now, down from 30%: the Town and Drafts columns joined the table on
   3 Sep 2026 and the name column paid a little for them. Still the widest
   column by far. */
.mct--sync .mct__c-org { width: 26%; }
/* "EventsScheduled". The Events column holds "12" or an em dash, so under
   table-layout:auto it shrinks to the width of its own heading — and being
   right-aligned, that heading then sat flush against the left-aligned one
   beside it with nothing between them. The gap has to come from the cell,
   because the content is too narrow to provide any. */
.mct--sync .mct__num {
  text-align: right; font-variant-numeric: tabular-nums;
  min-width: 5em; padding-right: var(--sp-4);
}
/* ---- THE ACTIONS STACK, AND THE NAME GETS ITS COLUMN BACK ------------------

   REPORTED, with the prospects table in front of them: business names wrapping
   mid-word — "Ridgefiel / d Library", "Westpor / t Library" — and the whole
   thing reading badly.

   THREE BUTTONS SIDE BY SIDE WERE THE CAUSE. "Visit again", "Copy+paste again"
   and "Remove" in one nowrap row is about 340px the table cannot use, on a
   column whose content is three short labels. Everything else was squeezed to
   pay for it, and .mct td carries `overflow-wrap: anywhere` — so when the name
   column got too narrow it did what it was told and broke words between
   letters.

   The owner's own fix, and it is the right one: stack them. The column is now
   as wide as the widest single button instead of the sum of three, which hands
   roughly 200px back to the columns that hold sentences.

   A FIXED WIDTH, NOT THE `width: 1%` SHRINK IDIOM. That trick only works on a
   table whose own width is auto: this one is 100%, so the browser hands the
   surplus back out across the columns and a 1% request came back as 280px —
   measured, after trying it. Naming the width is the only way to keep it.

   168px is "Copy+paste again" plus its padding, which is the widest label
   here. Whatever is left goes to the columns that hold sentences. */
.mct--sync .mct__actions {
  text-align: right; white-space: nowrap; width: 168px;
}
.mct--sync .mct__actions > * {
  display: flex; width: 100%; justify-content: center;
}
.mct--sync .mct__actions > * + * { margin-top: 6px; }

/* AND THE NAME STOPS BREAKING BETWEEN LETTERS. `anywhere` is right for a
   column holding a URL, which has no spaces to break at and would otherwise
   force the table wider than the screen. It is wrong for a business name,
   which has spaces and should use them. `break-word` keeps the safety net —
   a single unbreakable word still breaks rather than overflowing — and takes
   it only as a last resort. */
.mct--sync .mct__c-org { overflow-wrap: break-word; }
/* The address underneath is the case `anywhere` was for: one long token. */
.mct--sync .mct__c-org em { overflow-wrap: anywhere; }
/* ---- Sortable headings ---------------------------------------------------
   The heading IS the button, so the whole cell is the target rather than a
   word inside it. It has to keep looking like a heading: a control that
   announces itself with a border would make six of them shout across the top
   of a table whose job is to be scanned. */
/* Clients or prospects — the first choice on the page, so it sits above the
   section tabs and is spaced away from them. Two questions stacked without a
   gap read as one row of five buttons. */
.syn-views { margin-bottom: var(--sp-3); }
.syn-views .syn-count { margin-left: 6px; }

/* ---- The worklist chips --------------------------------------------------
   These replaced ten stacked sections. They have to read as a filter rather
   than as navigation — smaller than a tab, quieter until chosen — because the
   thing below them is one list, not four pages. */
.syn-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: var(--sp-3); }
.syn-chip {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 7px 13px; border-radius: var(--radius-pill);
  border: 1px solid var(--line); background: var(--surface);
  font-size: 13.5px; font-weight: 700; color: var(--ink-soft);
  cursor: pointer; transition: border-color .15s, color .15s, background .15s;
}
.syn-chip:hover { border-color: var(--primary); color: var(--primary); }
.syn-chip:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
.syn-chip.is-on { background: var(--primary); border-color: var(--primary); color: #fff; }
/* The count is the point of the chip, so it stays legible in both states. */
.syn-chip__n {
  font-size: 12px; font-weight: 800; padding: 1px 7px; border-radius: var(--radius-pill);
  background: var(--surface-2); color: var(--ink-soft);
}
.syn-chip.is-on .syn-chip__n { background: rgba(255, 255, 255, .22); color: #fff; }

/* ---- Find, and how tightly to pack -------------------------------------- */
.syn-find { display: flex; align-items: center; gap: 8px; margin: var(--sp-3) 0; flex-wrap: wrap; }
.syn-find__box {
  display: flex; align-items: center; gap: 8px; flex: 1 1 260px; min-width: 0;
  padding: 8px 12px; border: 1px solid var(--line); border-radius: var(--radius-pill);
  background: var(--surface); color: var(--ink-faint);
}
.syn-find__box:focus-within { border-color: var(--primary); color: var(--primary); }
.syn-find__box svg { flex: none; }
.syn-find__box input {
  flex: 1; min-width: 0; border: 0; background: none; font: inherit; color: var(--ink);
  padding: 0; outline: none;
}
/* Safari draws its own clear button on type=search, which sits on top of ours. */
.syn-find__box input::-webkit-search-decoration,
.syn-find__box input::-webkit-search-cancel-button { -webkit-appearance: none; }
.syn-find__n { font-size: 13px; font-weight: 700; color: var(--ink-faint); }
.syn-find__dense { margin-left: auto; }
/* The shortlist toggle sits with Compact at the right-hand end, and takes the
   margin when it is there: two controls over how the list is SHOWN, kept apart
   from the box that decides what is IN it. */
.syn-find__stars { margin-left: auto; }
.syn-find__stars + .syn-find__dense { margin-left: 0; }
.syn-find__stars.is-on {
  border-color: var(--accent); color: var(--accent); background: var(--accent-tint);
}
.syn-find__stars.is-on svg { fill: currentColor; }

/* COMPACT IS FOR COMPARING, not for reading one row. It trades the second line
   and the padding for seeing three times as many venues at once, which is the
   right shape when the question is "which of these is worst". */
.syn-dense .mct--sync td { padding-top: 7px; padding-bottom: 7px; }
.syn-dense .mct--sync .mct__sub { display: none; }
.syn-dense .mct--sync .mct__link em { display: none; }
.syn-dense .syn-row__since { font-size: 12.5px; }
/* WHAT TO DO, not what is wrong — so it reads ahead of the explanation under
   it rather than as more of the same. Kept in compact mode when the `why` is
   dropped: the instruction is the half worth having. */
.syn-do { color: var(--ink); font-weight: 700; font-style: normal; }
.syn-dense .mct--sync .syn-do { display: block; }

/* ---- Selecting rows ------------------------------------------------------
   Its own narrow column, away from the name, because the row itself opens the
   venue and a control that means something else must not look like part of
   it. The label is the target so the 14px box is not the hit area. */
.mct--sync .mct__pick { width: 40px; padding-right: 0; min-width: 0; }
.syn-pick { display: inline-flex; align-items: center; justify-content: center;
  padding: 8px; margin: -8px; cursor: pointer; }
.syn-pick input { width: 15px; height: 15px; cursor: pointer; accent-color: var(--primary); }
.mct--sync tbody tr.is-picked { background: var(--seaglass-tint); }
.mct--sync tbody tr.is-picked:hover { background: var(--seaglass-tint); }

/* The bar only exists while something is selected, so it can afford to be
   loud — it is the answer to "now what?" and it disappears when answered. */
.syn-bulk {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: var(--sp-3) 0; padding: 10px var(--sp-4);
  background: var(--seaglass-tint); border: 1px solid var(--seaglass);
  border-radius: var(--radius-sm);
}
.syn-bulk__n { font-weight: 800; font-size: 13.5px; color: var(--seaglass-ink); margin-right: 4px; }

/* ---- Draft a post from something you already have ------------------------
   Collapsed by default and quieter than the form it fills: it is a shortcut
   into the ordinary way of posting, not a competing way to post. */
/* PADDED AGAIN. This was `padding: 0` because the drafter was a <details>
   whose summary and body each supplied their own — so when it became a plain
   <section> with a heading and a paragraph directly inside, nothing padded
   them and the text sat flush against the border and over it. */
.syn-drafter { padding: var(--sp-5); }
.syn-drafter__sum {
  display: flex; align-items: center; gap: 12px; cursor: pointer;
  padding: var(--sp-4); list-style: none;
}
.syn-drafter__sum::-webkit-details-marker { display: none; }
.syn-drafter__sum > span { flex: 1; display: flex; flex-direction: column; gap: 2px; }
.syn-drafter__sum em { font-style: normal; font-size: 13.5px; color: var(--ink-faint); }
.syn-drafter__sum svg:first-child { color: var(--seaglass); flex: none; }
.syn-drafter__sum svg:last-child { flex: none; color: var(--ink-faint); transition: transform .15s; }
.syn-drafter[open] .syn-drafter__sum svg:last-child { transform: rotate(180deg); }
.syn-drafter__body { padding: 0; }   /* the card pads it now — see .syn-drafter */
.syn-drafter__body .field { margin-top: var(--sp-3); }

/* ---- What the post will look like ----------------------------------------
   Two things a person cannot check by reading the form: the picture that came
   with the source, and whether the venue's mark was found. Shown at the size
   they matter at — big enough to tell one photograph from another, small
   enough not to become the page. */
.drpv { display: grid; gap: var(--sp-4); margin-top: var(--sp-4); }
@media (min-width: 620px) { .drpv { grid-template-columns: 2fr 1fr; } }
.drpv__cell { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.drpv__h { font-size: 12px; font-weight: 800; text-transform: uppercase;
  letter-spacing: .05em; color: var(--ink-faint); }
.drpv__frame {
  display: flex; align-items: center; justify-content: center;
  min-height: 120px; padding: 8px;
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: var(--radius-sm); overflow: hidden;
}
.drpv__frame--mark { min-height: 96px; }
.drpv__img { max-width: 100%; max-height: 160px; border-radius: var(--radius-sm); display: block; }
/* A URL that does not load is a picture that will not appear on the site
   either, so it has to look like an absence rather than like a broken page. */
.drpv__img.is-bad { display: none; }
.drpv__none { color: var(--ink-faint); }
.drpv__note { font-size: 12.5px; line-height: 1.45; color: var(--ink-faint); }
.drpv__frame--mark .org-mark { width: 64px; height: 64px; border-radius: 14px; font-size: 21px; }

.syn-sort {
  display: inline-flex; align-items: center; gap: 5px;
  background: none; border: 0; padding: 0;
  font: inherit; color: inherit; text-align: inherit; cursor: pointer;
}
.syn-sort:hover { color: var(--primary); }
.syn-sort:focus-visible { outline: 3px solid var(--focus); outline-offset: 3px; border-radius: 3px; }
/* The idle arrow is a hint, not a claim — it says "this can sort", so it sits
   back until the column is actually the one in use. */
.syn-sort__ar { opacity: .35; font-size: 11px; }
.syn-sort.is-on { color: var(--primary); }
.syn-sort.is-on .syn-sort__ar { opacity: 1; }
/* Each column needs room for its heading plus an arrow, or the six headings
   run together into "BusinessHow longResultPublished" — which is what happened
   the moment "Last sync" became the wider "How long". */
.mct--sync th { white-space: nowrap; padding-right: var(--sp-3); }
/* The competitor list fits the page (owner, 10 Sep 2026: "I do not like
   having to scroll to the sides"): five columns, cells that wrap, buttons
   that stack. */
.mct--comp { table-layout: fixed; width: 100%; }
.mct--comp th, .mct--comp td { white-space: normal; overflow-wrap: anywhere; vertical-align: top; }
.mct--comp .mct__c-org { width: 30%; }
.mct--comp .mct__actions { display: flex; flex-wrap: wrap; gap: 4px; }
/* The listing of ours a competitor's row matched, under its "Already up" tag. */
.syn-covered { display: block; margin-top: 3px; font-size: 11.5px; color: var(--ink-faint); }
/* The one line between the rows to review and the rows already on the site. */
.syn-t__divider td {
  padding: 10px 8px 6px; font-size: 12.5px; font-weight: 700; color: var(--ink-soft);
  background: var(--surface-2); border-top: 2px solid var(--line-strong);
}
.syn-t__divider td svg { vertical-align: -2px; color: var(--ok, #1E7A44); }
/* A STATUS THAT BREAKS MID-WORD IS NOT A STATUS. "NO SOURCE ON FILE" was
   wrapping to "NO SOUR / CE", because the pill was free to break anywhere and
   the column was the narrowest thing competing for space. The pill keeps its
   words; the column is given enough room to hold one. */
/* OUT-SPECIFYING `.mct td .syn-tag`, which sets overflow-wrap:anywhere and was
   written to stop a long tag overflowing a narrow cell. On these tables it does
   something worse than overflow: it breaks INSIDE the word, so "WEEKLY" renders
   as "WEEK / LY" and "NOT SCHEDULED" as "NOT / SCHED / ULED". A status anybody
   has to reassemble is not a status. The tag stays whole; the columns below are
   given enough room to hold one, which is the actual fix. */
.mct.mct--sync td .syn-tag { white-space: nowrap; overflow-wrap: normal; }
/* Result, and the two the prospect table puts beside it. Both tables share this
   class and their middle columns line up: 2 is when, 3 is the verdict, 5 is
   whether it is on a schedule. */
.mct--sync td:nth-child(2) { min-width: 8em; }
.mct--sync td:nth-child(3) { min-width: 11em; }
.mct--sync td:nth-child(5) { min-width: 8em; }
/* 20em WAS THE FLOOR THAT SQUEEZED EVERYTHING ELSE, and it was right while the
   buttons sat side by side: three of them in a nowrap row genuinely need that
   much, and "whatever is left over" would have crushed them.

   They are stacked now (see .mct__actions above), so the column needs the
   width of ONE button rather than three, and the ~130px that buys goes back to
   the name column — which is where it was needed, because names were breaking
   between letters to pay for this.

   Still a floor rather than a fixed width: `width` alone loses here. This
   table is width:100% under table-layout:auto, so the browser hands surplus
   back out across the columns and a plain `width` is a suggestion it can
   ignore. min-width it cannot. */
.mct--sync .mct__actions { min-width: 11em; }
/* The row is the target, so it has to look like one. */
/* The "why there is no table" block on a prospect's own page. Bordered rather
   than tinted: it is an explanation, not a warning, and most prospects are in
   this state — a red panel on every unread business would read as an alarm
   going off all day. */
.syn-empty {
  margin: var(--sp-4) 0; padding: var(--sp-4);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface-2);
}
.syn-empty__head {
  display: flex; align-items: center; gap: 7px;
  margin: 0 0 var(--sp-2); font-weight: 700; font-size: 14.5px;
}
.syn-empty__text { margin: 0 0 var(--sp-2); font-size: 13.5px; line-height: 1.6; }
.syn-empty__next { color: var(--ink); }
.syn-empty__err {
  margin: var(--sp-2) 0; padding: var(--sp-2) var(--sp-3);
  border-radius: var(--radius-sm); background: var(--surface);
  font-family: var(--mono, monospace); font-size: 12px; color: var(--danger, var(--ink-soft));
  overflow-x: auto;
}
.syn-empty .syn-row__meta { margin: var(--sp-2) 0 0; }
/* Standing in for the rows, so it gets the width of all seven columns and no
   border of its own — the table already has one. */
/* SET ASIDE, STILL READABLE. Faded rather than hidden or struck through: the
   row has to stay legible enough to reconsider, and a line through seven
   columns of real data reads as deleted, which it is not. The controls keep
   full contrast — the ignore toggle above all, since pressing it again is the
   whole way back. */
.syn-t__row--ignored > td { opacity: .45; }
.syn-t__row--ignored > td.syn-t__go { opacity: 1; }
/* AND THE BUTTON THAT UNDOES IT. The row greys to say it is set aside, but
   the way back must not be the least legible thing on it. Exempted on the
   CELL, not the button: opacity on a parent cannot be undone by a child, which
   is why .syn-t__go is done this way too. */
.syn-t__row--ignored > td.syn-c-skip { opacity: 1; }
.syn-t__row--ignored .syn-t__name { text-decoration: none; font-style: italic; }
/* Pressed. Same treatment the nightly-queue toggle uses, so a button that is
   holding a state looks the same everywhere on this page. */
.syn-act.is-on {
  background: var(--seaglass-tint); border-color: var(--seaglass); color: var(--seaglass-ink);
}

.syn-t__empty-row td { padding: 0; }
.syn-t__empty-row .syn-empty { margin: 0; border: 0; border-radius: 0; background: transparent; }

/* "Scan everything stale in this tab", above the table it acts on. Wraps rather
   than truncates: the note beside it is what defines "recently", and that is
   the difference between a button somebody trusts and one they are afraid to
   press. */
.syn-scanall {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2) var(--sp-3);
  margin: var(--sp-4) 0 var(--sp-3);
}
.syn-scanall__note {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 12.5px; line-height: 1.5; color: var(--ink-faint);
}
.syn-scanall__note svg { flex: none; color: var(--seaglass-ink); }

.mct--sync tbody tr { cursor: pointer; }
.mct--sync tbody tr:hover { background: var(--surface-2); }
.mct--sync tbody tr:focus-within { background: var(--surface-2); }
.mct__link { display: block; color: var(--ink); text-decoration: none; }
.mct__link strong { display: block; font-size: 15px; }
.mct__link em {
  display: block; font-style: normal; font-size: 12.5px;
  color: var(--ink-faint); margin-top: 1px;
}
.mct--sync tbody tr:hover .mct__link strong { color: var(--primary); }
.mct__sub {
  display: block; margin-top: 3px; font-style: normal;
  font-size: 12px; color: var(--ink-faint); line-height: 1.35;
}
.mct__c-org { width: 25%; }
.mct__c-date { width: 13%; }
.mct__c-cat { width: 12%; }
.mct__c-sub { width: 11%; }
.mct__c-aud { width: 13%; }
/* The widest column, and deliberately: its control reads "Add to my
   favorites", which is the label that makes the column self-explaining. */
.mct__c-fav { width: 26%; }
.mct th, .mct td {
  padding: 10px 12px; text-align: left; vertical-align: top;
  border-bottom: 1px solid var(--line);
}
.mct thead th {
  padding: 0; border-bottom: 2px solid var(--line-strong);
  background: var(--surface-2); white-space: nowrap;
  position: sticky; top: 0; z-index: 1;
}
.mct__h button {
  display: flex; align-items: center; gap: 5px; width: 100%;
  padding: 11px 12px; border: 0; background: transparent;
  font: inherit; font-size: 12.5px; font-weight: 700;
  letter-spacing: .03em; text-transform: uppercase;
  color: var(--ink-soft); cursor: pointer; text-align: left;
}
.mct__h button:hover { color: var(--ink); background: var(--surface); }
.mct__h.is-sorted button { color: var(--ink); }
.mct__arrow { display: inline-flex; line-height: 0; }
/* Only chevronD exists in the icon set, so ascending is the same glyph turned
   over. One icon, two meanings, no third state to keep in sync. */
.mct__arrow.is-asc { transform: rotate(180deg); }

.mct tbody tr:hover { background: var(--surface-2); }
/* One of theirs. A left edge rather than a fill, so it survives the hover
   background and does not fight the category chips for attention. */
.mct tbody tr.is-mine { box-shadow: inset 3px 0 0 var(--primary); }

.mct__org a { font-weight: 600; color: var(--ink); text-decoration: none; }
.mct__org a:hover { text-decoration: underline; }
.mct__by {
  display: block; margin-top: 2px; color: var(--ink-faint); font-size: 12.5px;
}
.mct__when { font-variant-numeric: tabular-nums; }
.mct__time { display: block; color: var(--ink-faint); font-size: 12.5px; }
.mct__aud { color: var(--ink-soft); }
.mct__more { color: var(--ink-faint); font-size: 12.5px; }
.mct__none { color: var(--ink-faint); font-style: italic; }

.mct__favcell { }
.mct__fav {
  display: inline-flex; align-items: center; gap: 6px;
  max-width: 100%; padding: 6px 11px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--surface);
  color: var(--ink-soft); font: inherit; font-size: 12.5px; font-weight: 600;
  cursor: pointer; text-align: left;
}
.mct__fav svg { flex: none; }
.mct__fav:hover { border-color: var(--line-strong); color: var(--ink); }
.mct__fav.is-on {
  background: var(--primary-tint); border-color: var(--primary);
  color: var(--primary);
}
/* A RED, FILLED HEART WHEN IT IS SAVED. The pill already changed colour, but
   the heart inherited the navy along with the rest of the label — so the one
   glyph on the page that means "saved" looked identical in both states except
   for its shape, which is the hardest difference to spot in a column of forty
   rows. Red and filled is what a heart means everywhere else on the site; see
   .saved-row__fav svg, which has always done this. */
.mct__fav.is-on svg { color: var(--destructive); fill: currentColor; }

/* On a phone the table stays a table and scrolls — collapsing it into stacked
   cards would throw away the column alignment, which is the only reason to be
   looking at a table rather than the calendar. The first column is pinned so
   you never lose track of which row you are reading across. */
/* NARROW SCREENS DROP COLUMNS RATHER THAN SCROLLING.

   Six columns cannot be read at 375px however hard they are squeezed, so the
   least load-bearing go first and what they held moves under the title, where
   there is room for it. The reader still sees a whole row — which is the only
   thing that makes a table worth reading — and never drags it sideways.

   Subcategory goes first: it is the finest distinction and the one most often
   empty. Audience follows. Category earns its place longest because Events vs
   Classes vs Food & Drinks is the split people actually scan for. */
@media (max-width: 860px) {
  .mct { font-size: 13.5px; }
  .mct th, .mct td { padding: 9px 8px; }
  .mct__c-org { width: 36%; }
  .mct__c-date { width: 18%; }
  .mct__c-cat { width: 17%; }
  .mct__c-sub { display: none; }
  .mct__c-aud { width: 15%; }
  .mct__c-fav { width: 14%; }
  /* Scoped away from the scan desk's prospect list, which shares .mct but
     not this column order — unscoped, this hid the prospect table's FOUND
     cell under the resident table's Subcategory heading and left every cell
     after it reading under the wrong column. The sync list has its own
     narrow-window plan below. */
  .mct:not(.mct--sync) tbody td:nth-child(4) { display: none; }
}
@media (max-width: 620px) {
  .mct__c-org { width: 46%; }
  .mct__c-date { width: 22%; }
  .mct__c-cat { width: 17%; }
  .mct__c-aud { display: none; }
  .mct__c-fav { width: 15%; }
  .mct:not(.mct--sync) tbody td:nth-child(5) { display: none; }
  /* What the dropped columns held, folded under the title so nothing is
     actually lost — just re-laid-out. */
  .mct__sub-inline { display: block; }
  /* The favourite control loses its label and keeps its icon; the heading
     above it already says what the column is. */
  .mct__fav span { display: none; }
  .mct__fav { padding: 7px 9px; }

  /* THE HEADINGS HAVE TO BE ALLOWED TO WRAP, and this is what was cutting the
     table off.

     `.mct thead th` carries `white-space: nowrap`, which is right on a wide
     screen and impossible on a narrow one. The table is `table-layout: fixed`,
     so a column is exactly the width it is declared — 22% for the favourites
     column on a phone, about 75px. "IN MY FAVORITES" at 12.5px uppercase with
     letter-spacing is roughly 130. It cannot wrap, so it runs out of its cell
     and drags the table's width with it, and the table lives in `.mct-scroll`
     with `overflow-x: auto` — so the whole thing simply ended sliced down the
     right-hand edge with "DATE & TIM" and "IN MY FA" showing.

     Reported as "the text is just on top of each other and cutting off to the
     right. That's not okay."

     Two short lines in a heading cell is ordinary. A table you have to drag
     sideways to read the headings of is not. */
  .mct thead th { white-space: normal; }
  .mct__h button { padding: 9px 8px; letter-spacing: .02em; }
}
@media (max-width: 430px) {
  .mct { font-size: 13px; }
  .mct th, .mct td { padding: 8px 6px; }
  .mct__c-org { width: 52%; }
  .mct__c-date { width: 26%; }
  .mct__c-cat { display: none; }
  .mct__c-fav { width: 22%; }
  .mct:not(.mct--sync) tbody td:nth-child(3) { display: none; }
  /* AND THE HEADINGS COME DOWN A SIZE, because letting them wrap is not enough
     this far in. On a 320px screen the favourites column is 63px and the word
     FAVORITES alone wanted 75 with its padding and letter-spacing — measured —
     so the heading still pushed the table 12px past its own scroll box and the
     right-hand column stayed sliced. Wrapping cannot help a single word that
     does not fit.

     11px, no letter-spacing, tighter padding: the same words, inside the
     column, no drag. Uppercase headings are the one place on this site where
     losing a little tracking costs nothing. */
  .mct__h button { font-size: 11px; letter-spacing: 0; padding: 8px 5px; }
  .mcv { gap: var(--sp-2); }
  .mcv__tab { padding: 8px 12px; font-size: 13.5px; }
  /* Stacked, the auto margin would right-align the filter pair under a
     left-aligned pair of tabs. Both flush left when they wrap. */
  .mcv__sub { margin-left: 0; }
}
/* Hidden until a breakpoint above turns it on. */
.mct__sub-inline { display: none; color: var(--ink-faint); font-size: 12.5px; margin-top: 2px; }

/* ---- The prospect list on a narrow window ---------------------------------
   Its own plan, header and body paired BY CLASS (mct__c-when / mct__c-found on
   both th and td) so a hidden heading always hides its own cell. What goes is
   what the Result column already says: the verdict pill repeats the found
   count in words, and "last read" matters less than "what happened". The
   Result column, its reason line, and the Scheduled toggle stay to the end —
   they are the row's whole point. */
/* The Found count became the Drafts cell (mct__c-work), which is the row's
   work and stays. Last read goes first now, then the town: the Drafts cell
   already says what is left and the town is one press away on the row. */
@media (max-width: 860px) {
  .mct--sync .mct__c-found { display: none; }
  .mct--sync .mct__c-when { display: none; }
}
@media (max-width: 620px) {
  .mct--sync .mct__c-town { display: none; }
}

/* ---- Paging the future ---------------------------------------------------
   Names the dates rather than a page number: "Oct 5 – Nov 30" is something a
   reader can hold against their own plans, and "page 3" is not. */
.mct-nav { margin: 0 0 var(--sp-2); }
.mct-nav--foot { margin: var(--sp-2) 0 0; }
.mct-nav__btns {
  display: flex; align-items: center; justify-content: center;
  flex-wrap: wrap; gap: 8px;
}
.mct-nav__span {
  flex: 1; min-width: 150px; text-align: center;
  font-family: var(--font-display); font-size: 15px; font-weight: 600;
  color: var(--ink);
}
.mct-nav .btn[disabled] { opacity: .45; cursor: default; }
.mct-nav__btns .btn svg { vertical-align: -2px; }

/* On a phone the two buttons take a row and the range sits above them, so
   neither gets squeezed to an unreadable width. */
@media (max-width: 520px) {
  .mct-nav__btns { justify-content: space-between; }
  .mct-nav__span { order: -1; flex: 1 0 100%; min-width: 0; margin-bottom: 4px; }
}

/* ============================================================================
   One organization's page — their details, and everything they run.
   ========================================================================== */
.org-head { margin-bottom: var(--sp-3); }
.org-head__name {
  font-family: var(--font-display); font-size: clamp(28px, 6vw, 40px);
  letter-spacing: -.02em; margin: var(--sp-1) 0 var(--sp-2); color: var(--ink);
}
.org-head__meta {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px;
  margin: 0 0 var(--sp-2); color: var(--ink-soft); font-size: 14px;
}
.org-head__meta svg { vertical-align: -2px; margin-right: 3px; }
.org-head__type {
  padding: 3px 10px; border-radius: 999px;
  background: var(--surface-2); font-weight: 600; font-size: 13px;
}
.org-head__thin {
  margin: var(--sp-2) 0 0; padding: 10px 12px;
  border-radius: var(--radius-sm, 8px); background: var(--seaglass-tint);
  color: var(--ink-soft); font-size: 13.5px;
}
.org-head__thin svg { vertical-align: -2px; color: var(--seaglass-ink); }

/* The facts, each one a thing you can act on: the address opens a map, the
   phone dials, the website leaves. A row that only displays text would be a
   row that makes the reader retype it. */
.org-facts { display: flex; flex-wrap: wrap; gap: 8px; }
.org-fact {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 9px 13px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--surface);
  color: var(--ink); font-size: 13.5px; font-weight: 600; text-decoration: none;
}
.org-fact:hover { border-color: var(--line-strong); background: var(--surface-2); }
.org-fact svg { flex: none; color: var(--ink-faint); }

.org-count { margin: 0 0 var(--sp-2); color: var(--ink-faint); font-size: 13.5px; }
.org-sec { margin-bottom: var(--sp-4); }
.org-sec__title {
  display: flex; align-items: center; gap: 10px;
  font-family: var(--font-display); font-size: clamp(20px, 4vw, 25px);
  margin: 0 0 var(--sp-2); color: var(--ink);
}
.org-sec__n {
  padding: 2px 9px; border-radius: 999px;
  background: var(--surface-2); color: var(--ink-soft);
  font-family: var(--font-body, inherit); font-size: 13px; font-weight: 700;
}
.org-sec__body { display: grid; gap: 10px; }

/* A recurring thing, folded into one row. THE ROW THAT MAKES THIS PAGE
   READABLE: a bar with a nightly happy hour publishes one listing a night, and
   unfolded that is thirty identical cards with everything else buried under
   them — the opposite of what somebody came here to see. */
.org-run {
  display: block; padding: 14px 16px; border-radius: var(--radius);
  border: 1px solid var(--line); background: var(--surface);
  text-decoration: none; color: inherit;
}
.org-run:hover { border-color: var(--line-strong); background: var(--surface-2); }
.org-run__rep {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 9px; margin-bottom: 6px; border-radius: 999px;
  background: var(--primary-tint); color: var(--primary);
  font-size: 12px; font-weight: 700;
}
.org-run__title {
  display: block; font-family: var(--font-display);
  font-size: 17px; font-weight: 600; color: var(--ink);
}
.org-run__meta { display: block; margin-top: 3px; color: var(--ink-soft); font-size: 13.5px; }
.org-run__all { display: block; margin-top: 2px; color: var(--ink-faint); font-size: 12.5px; }

.org-back { margin-top: var(--sp-4); }
.org-back a { color: var(--ink-soft); text-decoration: none; font-size: 14px; }
.org-back a:hover { color: var(--ink); text-decoration: underline; }
.org-back svg { vertical-align: -2px; }

/* The organization's name, wherever it is a link. Inherits its surroundings so
   it does not shout — it is a name first and a link second. */
.dir-item__name, .mct__by { text-decoration: none; }
.dir-item__name:hover, .mct__by:hover { text-decoration: underline; }
.mct__by { display: block; margin-top: 2px; color: var(--ink-faint); font-size: 12.5px; }

/* ============================================================================
   The scan results table, laid out to FIT rather than to scroll.

   Same rule the resident's activities table follows, and for the same reason:
   a row you cannot see all of is a row you cannot judge, and judging rows is
   the entire job of this screen. `table-layout: fixed` with percentage columns
   means the widest venue name in the world cannot push Publish off the edge —
   which is the failure this table has now had twice.
   ========================================================================== */
.syn-table { table-layout: fixed; }
/* A FLOOR, so a squeezed table scrolls instead of collapsing. .syn-table-scroll
   above is already `overflow: auto`, so this costs nothing when there is room
   and is the difference between a narrow column and an unreadable one. */
/* 900 now, up from 860: the cells hold real controls since the inline-edit
   pass — a date box cannot be squeezed the way a printed date could — and
   below this every input is too small to use. The table is better scrolled
   than crushed; .syn-table-scroll above is `overflow: auto` for exactly this. */
.syn-table { min-width: 900px; }
/* `break-word`, NOT `anywhere`, and the difference is exactly the bug above.

   `anywhere` lets the browser break between any two characters — which also
   tells table layout that this column's minimum useful width is ONE CHARACTER,
   so a column starved of space collapses to nothing instead of refusing to.
   `break-word` still rescues a genuinely unbreakable string (a long URL) but
   reports an honest minimum, so a title wraps between words like prose. */
.syn-table td { overflow-wrap: break-word; }
/* THE ACTIONS COLUMN IS SIZED IN PIXELS, not per cent, and that is the whole
   trick. Five 30px buttons plus their margins need about 169px whatever the
   window is doing — a percentage gives 128px at one width and 104px at
   another, and at 104px the four icons wrap onto two rows. A control group is
   a fixed size; only the text columns should stretch.

   The event name takes no width at all, so it absorbs whatever is left after
   the others have taken their share. It is the column that benefits most from
   extra room and suffers least from having less. */
/* ---- THE BUDGET. EVERY COLUMN'S WIDTH IS HERE, AND IT HAS TO ADD UP. ----

   REPORTED: every event title rendered as a vertical tower of single letters,
   one character per line, 525px tall.

   The cause was arithmetic, not styling. Town (9%) and Repeats (10%) were
   added to this table later and their widths were written a thousand lines
   below, next to the prospect-table rules — so nothing subtracted them from
   the columns already here. The percentages reached 85%, the Actions column
   takes a further 140px (15.3% of a 917px table), and that is 100.3%. The
   event name is the column with no width of its own, so it absorbs what is
   left — and what was left was nothing. Zero pixels, with `overflow-wrap:
   anywhere` below breaking the title at every character to fit.

   TWO RULES FOR WHOEVER CHANGES THIS NEXT:

     1. Every `.syn-c-*` width lives in this block. Not near the markup, not
        beside a related table. One block, one total, visible at a glance.
     2. The percentages must total well under 100% MINUS the Actions column,
        because Actions is 140 PIXELS and its share grows as the table gets
        narrower — 15% at 917px, 20% at 700px. 64% leaves the name about a
        fifth of the table at every width the admin is actually used at.

   netlify/functions/test/scan-table-columns.test.js checks both. */
/* The tick column: one checkbox, fixed like the actions column and for the
   same reason — a control is a size, not a share. */
.syn-c-pick   { width: 30px; }
.syn-c-when   { width: 12%; }
.syn-c-town   { width:  8%; }
/* Tab and Category are ONE COLUMN since the inline-edit pass — the tab follows
   the category and never disagrees with it, so `.syn-c-tab` is gone and its
   width went back into the pot. */
.syn-c-cat    { width: 13%; }
.syn-c-aud    { width:  9%; }
.syn-c-rep    { width:  7%; }
/* Wide enough for the longest thing the select can say ("Not checked yet").
   A <select> cannot wrap and cannot ellipsise — it just clips, so a column too
   narrow for its own options renders "Not che" and looks broken. */
.syn-c-status { width: 12%; }
/* SIX CONTROLS AT 32px EACH. The margin came down from 4px to 2px when the
   sixth button was found to be wrapping on any window narrower than a wide
   desktop - see the note on `.syn-act`. That freed twelve pixels, and they are
   given back here rather than pocketed: 6 x 32 is 192, the cell's own 9px
   right padding rides on top, and 210 leaves nine pixels of slack instead of
   the three that were never really a layout.

   THE TWELVE PIXELS WENT TO THE AUDIENCE HEADING. `.syn-c-aud` needed one more
   per cent so that "Audience" fits on one line, and the budget test refused it
   until this column gave the room up - which is exactly the trade it is there
   to force. Count the buttons in resultsTable() before changing this number. */
/* FIVE CONTROLS NOW, not six: Set aside moved out to the first column of the
   row, where the decision it records is actually made. 5 x 32 is 160 and the
   cell's own 9px right padding rides on top, so 169 is the floor.

   174 RATHER THAN 178, AND THE FOUR PIXELS WENT TO THE NEW COLUMN. The
   stylesheet's own note says it: take width off the others before adding any.
   The Event column is the one this table is read by and it absorbs whatever
   is left, so a new column that pays for itself out of slack costs it
   nothing, while one that does not squeezes every title on the page. */
.syn-c-go     { width: 174px; }

/* ---- SET ASIDE, THE FIRST COLUMN -----------------------------------------
   One 32px control plus a little air. Narrow on purpose: it is a margin mark,
   not a column of data, and every pixel here comes off the Event column.
   Paid for out of the Actions column above and the six pixels the sixth
   button gave back when it moved here. */
.syn-c-skip   { width: 34px; }
/* NO RIGHT PADDING ON THIS ONE. Every other cell in the table carries 9px of
   it, which here left a 34px column with 25px of content box and a 34px
   button spilling into the gap. Zeroed rather than widening the column,
   because the width had to be bought off the Actions column and the Event
   column pays for anything else. The gap to the tick beside it is that
   column's own padding. */
.syn-table th.syn-c-skip,
.syn-table td.syn-c-skip { padding-right: 0; }
/* 30px, NOT 34, AND CENTRED IN THE CELL.

   `.icon-btn` is 34px and this column is 34px wide with its right padding
   zeroed, so the button filled the cell edge to edge and sat flush against the
   tick beside it - the "too close to each other" half of the report. At 30px,
   centred, there are two clear pixels each side and the same 30px as every
   other control on the row, which is what it is: one of the row's controls,
   moved to the front.

   The left padding goes too. Every other cell in this table has none (the
   padding is on the right), and the pair of them made a 34px column with 25px
   of usable box. */
.syn-table td.syn-c-skip, .syn-table th.syn-c-skip { padding-left: 0; text-align: center; }
.syn-skip { width: 30px; height: 30px; }
/* The tick, centred in its own column for the same reason: a 15px checkbox
   left-aligned in a 30px cell reads as attached to the button before it. */
.syn-table td.syn-c-pick, .syn-table th.syn-c-pick { padding-right: 0; text-align: center; }

/* PRESSED, in the same seaglass the other state-holding toggles on this page
   use, so a button that is holding a state looks the same everywhere. Never
   red: this is a decision, not an error. */
.syn-skip.is-on {
  background: var(--seaglass-tint); border-color: var(--seaglass); color: var(--seaglass-ink);
}
/* .syn-c-name has no width ON PURPOSE — it takes whatever the others leave,
   which is the point of keeping their total honest. */

/* ---- THE SIMPLE VIEW, which is the default -------------------------------
   Four columns: event, when, status, actions. The four hidden here — town,
   category, audience, repeats — are summarised in words under the title
   (.syn-t__sum) and edited through the unfold, so nothing is lost, only
   parked. Hiding is CLASS-based on th and td alike, same rule as the
   responsive hiding below: nth-child would break the moment the pick column
   comes or goes. */
.syn-table--simple .syn-c-town,
.syn-table--simple .syn-c-cat,
.syn-table--simple .syn-c-aud,
.syn-table--simple .syn-c-rep { display: none; }

/* ---- AND NO BUTTON GOES WITH THEM ANY MORE -------------------------------

   THE FIRST VERSION HID THREE: ignore, the edit form and the source link, on
   the reasoning that "open, look and put-it-up is the whole simple loop". All
   three turned out to be wrong, in two rounds.

     THE SOURCE LINK is not a power tool. It is the only control on the row
     that shows what the scanner decided FROM, and it is wanted at exactly the
     moment the simple view is being used: a title reads oddly, and the
     question is what the page actually said.

     THE EDIT BUTTON opens the form, which is where a row gets FIXED. The
     simple view shows four columns precisely because the other four are edited
     through a form; hiding the button that opens that form took the way out of
     the view along with the columns.

     AND SET ASIDE was the third. It is not in the Actions column at all now -
     it is the first cell of the row, drawn in every view, because deciding a
     row is not work is the first thing done to it and was the one control the
     four-column view could not reach.

   So the simple view hides four COLUMNS and no buttons. The rule that used to
   live here is gone rather than emptied; if a control ever needs hiding again,
   the note about specificity is worth having back:

     three classes deep on purpose, because the tooltip rule further down
     (`.syn-act[data-tip]`) ties two classes and is declared later, so at equal
     specificity a hidden button came back as inline-block. */
/* With four columns the shares above are far too thin — When gets room for
   its two inputs on one line, Status keeps its select whole, and the name
   still absorbs the rest. */
.syn-table--simple .syn-c-when   { width: 21%; }
.syn-table--simple .syn-c-status { width: 17%; }
/* The same five as the full view now - chevron, edit, preview, source,
   publish - so this override has nothing left to say and simply matches. Kept
   as a declaration rather than deleted, because the two views have had
   different action counts twice and this is where that difference belongs. */
.syn-table--simple .syn-c-go     { width: 174px; }
/* The summary line only exists in the simple view; the full spread already
   shows these facts as columns. */
.syn-table:not(.syn-table--simple) .syn-t__sum { display: none; }
.syn-t__sum {
  display: inline-flex; align-items: center; gap: 5px; max-width: 100%;
  margin-top: 3px; padding: 1px 6px 2px; border: 0; border-radius: 6px;
  background: transparent; font-size: 12px; color: var(--ink-soft);
  text-align: left; cursor: pointer;
}
.syn-t__sum:hover { background: var(--surface-2); color: var(--ink); }
/* The row's own blocker, in words. SUNSET, not red and not --accent: red is
   for real errors and accent reads as red at scale — a missing town is
   ordinary work, and this line appears on many rows of a fresh scan. */
.syn-t__sum--need { color: var(--sunset); font-weight: 600; }
.syn-t__sum--need:hover { background: var(--sunset-tint, var(--surface-2)); color: var(--sunset); }
/* The view switch sits above the table, right-aligned, out of the data. */
.syn-tablebar { display: flex; justify-content: flex-end; margin: var(--sp-2) 0 0; }

/* ---- The quick scan box at the top of the desk --------------------------- */
.syn-quick {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  margin: var(--sp-4) 0 var(--sp-5); padding: var(--sp-3) var(--sp-4);
  background: var(--seaglass-tint); border: 1px solid var(--seaglass);
  border-radius: var(--radius);
}
.syn-quick__lbl {
  display: inline-flex; align-items: center; gap: 7px;
  font-family: var(--font-display); font-size: 16px; font-weight: 600;
  color: var(--seaglass-ink); white-space: nowrap;
}
.syn-quick__lbl svg { color: var(--seaglass-ink); }
.syn-quick__in { width: auto; flex: 1 1 260px; min-width: 220px; }
.syn-quick__hint { flex-basis: 100%; font-size: 12.5px; color: var(--ink-soft); }

/* Five icon buttons, kept on one line and given room to breathe. Wraps to two
   rows rather than overflowing if the window gets truly small. */
.syn-t__go {
  text-align: right; white-space: normal;
  display: table-cell;            /* NEVER flex: a flex <td> leaves table layout
                                     and the column widths above stop applying */
}
/* THE ROW OF CONTROLS, AS A FLEX ROW (owner, 8 Sep 2026, with a screenshot).

   Spacing used to be a 1px margin plus whatever whitespace the HTML happened
   to carry, on a line box with `line-height: 1.9` - so no two rows sat quite
   the same and the chevron's gap badge, which hangs outside its button by
   design, landed on the pencil next to it.

   A `gap` is the spacing now, and it is between the controls only: no margins,
   no whitespace, nothing to accumulate. `justify-content: flex-end` keeps the
   cluster pinned right whether the row has three controls or five.

   THE WIDE TAGS GET THEIR OWN LINE. "On the site" and "Already up?" are pills,
   not 30px squares, and either of them beside four icons is wider than the
   column - so instead of wrapping mid-cluster they take a full row of their
   own underneath, right-aligned under the icons. Same information, one shape
   per line, and every row in the table now has one of two predictable shapes.

   The cell itself stays a table-cell: making a <td> flex takes it out of table
   layout and every column width above stops applying. */
.syn-t__acts {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end;
  /* 4px, and the arithmetic is checked by scan-table-columns.test.js: five
     30px controls plus four gaps is 166px in a column that gives 170, and a
     sixth would need 200 and be refused - which is the headroom that keeps
     Duplicate on the unfolded row instead of in here. */
  gap: 4px;
}
.syn-t__acts > .syn-tag { flex: 0 0 100%; justify-content: flex-end; text-align: right; }
/* A link tag ("Up already") is not a flex box of its own, so it needs the
   right-hand alignment stated as a block rather than as content alignment. */
.syn-t__acts > a.syn-tag { display: block; margin-left: auto; width: max-content; }
.syn-act {
  display: inline-flex; align-items: center; justify-content: center;
  /* NO MARGIN. The flex `gap` on .syn-t__acts is the spacing now, and a margin
     on top of it would be two spacings fighting - which is how the old cluster
     came to look uneven. Five controls at 30px plus four 5px gaps is 170px in
     a 174px column. */
  width: 30px; height: 30px; padding: 0; margin: 0;
  /* MIDDLE, NOT BASELINE. Five of these are <button> and one - the source
     link - is an <a>, and an inline-flex box takes its baseline from its
     content, which is not the same thing for the two elements. So the source
     button sat a hair off the line the others were on. It is the only one that
     looked wrong and it was the only one that was a link. */
  vertical-align: middle;
  border-radius: 8px; border: 1px solid var(--line);
  background: var(--surface); color: var(--ink-soft); cursor: pointer;
}
/* The 1.9 line-height that used to sit here was a way of spacing inline-block
   buttons that had wrapped. .syn-t__acts wraps them properly now, and leaving
   it in only added a few pixels of dead height to every row in the table. */
.syn-act:hover { border-color: var(--line-strong); color: var(--ink); background: var(--surface-2); }
.syn-act[disabled] { opacity: .4; cursor: default; }
/* Publish is the one that changes the world, so it is the one that looks it. */
.syn-act--go { color: var(--primary); border-color: var(--primary-tint-2, var(--line)); }
.syn-act--go:hover { background: var(--primary-tint); color: var(--primary); }

/* Which batch of a long scan you are on. */
.syn-batch {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  flex-wrap: wrap; margin: var(--sp-2) 0;
}
.syn-batch__at {
  font-variant-numeric: tabular-nums; color: var(--ink-soft);
  font-size: 13px; font-weight: 600;
}
.syn-batch .btn[disabled] { opacity: .45; cursor: default; }

/* ============================================================================
   The scan table's inline fields — the fix-it-where-it-stands pass.

   The cells ARE the form now, so the controls have to read as values first and
   fields second: a table of thirty obvious input boxes is a wall of chrome.
   Borders are near-invisible until the pointer arrives, and the focused field
   gets the full treatment. Every control saves on change — see saveInline in
   sync.js — and flashes its edge green for a moment so "did that take?" never
   needs asking.
   ========================================================================== */
.syn-in {
  font: inherit; font-size: 12.5px; color: var(--ink);
  width: 100%; max-width: 100%; min-width: 0;
  padding: 3px 6px; border-radius: 7px;
  border: 1px solid transparent; background: transparent;
}
.syn-in:hover { border-color: var(--line); background: var(--surface); }
.syn-in:focus, .syn-in:focus-visible {
  outline: none; border-color: var(--primary);
  background: var(--surface); box-shadow: 0 0 0 2px var(--primary-tint);
}
.syn-in--name { font-weight: 600; }
.syn-in--date, .syn-in--time { display: block; }
.syn-in--time { margin-top: 2px; font-size: 12px; color: var(--ink-soft); }
.syn-in--sel { cursor: pointer; }
.syn-in--ta { resize: vertical; min-height: 44px; }
/* The saved flash. An animation, not a class left on, so the same field can
   flash again on its next save without anything cleaning up after it. */
@keyframes syn-saved { from { box-shadow: 0 0 0 2px var(--seaglass); border-color: var(--seaglass); } }
.syn-in.is-saved, .syn-aud.is-saved input + span { animation: syn-saved 1.2s ease-out; }
.syn-in.is-bad { border-color: var(--destructive); }

/* A FIELD THAT IS EMPTY AND SHOULD NOT BE. The brand's attention colour, not
   the error red — nothing is broken, something is missing. One class for the
   controls, one for the little word, so the collapsed row and the unfolded
   one flag a gap the same way. */
.syn-in--gap { border-color: var(--accent); background: var(--accent-tint); }
.syn-in--gap:hover { border-color: var(--accent); }
.syn-gap {
  font-style: normal; font-size: 11px; font-weight: 700;
  padding: 1px 6px; border-radius: var(--radius-pill);
  background: var(--accent-tint); color: var(--accent);
  white-space: nowrap;
}
/* How many fields are still empty, on the unfold button. */
.syn-chev { position: relative; }
.syn-chev svg { transition: transform .15s ease; }
.syn-chev.is-open svg { transform: rotate(90deg); }
.syn-gapn {
  position: absolute; top: -5px; right: -4px; z-index: 1;
  box-shadow: 0 0 0 1.5px var(--surface);
  min-width: 15px; height: 15px; padding: 0 3px;
  /* SUNSET, NOT --accent. At this size the accent coral reads as a red error
     badge — "the red 1" is how it was reported — and an empty optional field
     is ordinary work, not an error. Same reasoning as .syn-t__sum--need. */
  border-radius: var(--radius-pill); background: var(--sunset); color: #fff;
  font-size: 10px; font-weight: 700; line-height: 15px; text-align: center;
}

/* The first words of the description, under the title. */
.syn-t__desc {
  display: block; margin-top: 2px; padding-left: 6px;
  font-size: 11.5px; font-weight: 400; color: var(--ink-faint);
}

/* ---- The unfolded row ---------------------------------------------------- */
.syn-exp > td {
  background: var(--surface-2);
  border-bottom: 1px solid var(--line-strong);
  padding: var(--sp-3);
}
.syn-exp__grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: var(--sp-2) var(--sp-3);
}
.syn-exp__f--wide { grid-column: 1 / -1; }
.syn-exp__lbl {
  display: flex; align-items: center; gap: 6px; margin-bottom: 2px;
  font-size: 10.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .04em; color: var(--ink-faint);
}
.syn-exp .syn-in { background: var(--surface); border-color: var(--line); }
.syn-exp .syn-in:focus { border-color: var(--primary); }
.syn-exp__auds { display: flex; flex-wrap: wrap; gap: 5px; }
/* An audience is a pill that is on or off; the checkbox itself is hidden and
   the pill wears the state. */
.syn-aud { cursor: pointer; }
.syn-aud input { position: absolute; opacity: 0; pointer-events: none; }
.syn-aud span {
  display: inline-block; padding: 3px 10px; border-radius: var(--radius-pill);
  border: 1px solid var(--line-strong); background: var(--surface);
  font-size: 12px; font-weight: 600; color: var(--ink-soft);
}
.syn-aud.is-on span, .syn-aud input:checked + span {
  background: var(--primary); border-color: var(--primary); color: #fff;
}
.syn-aud input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 1px; }
.syn-exp__note { font-size: 12.5px; color: var(--ink-soft); }
.syn-exp__note svg { vertical-align: -2px; }
/* A link that reads as prose, for "open the full form" inside a sentence. */
.btn-link {
  border: 0; padding: 0; background: none; font: inherit;
  color: var(--primary); font-weight: 600; text-decoration: underline;
  cursor: pointer;
}
.btn-link:hover { color: var(--ink); }

/* ---- The batch bar and the ticks ----------------------------------------- */
/* The gap grew from 12px to 16px across. The checkbox label has no border and
   the Fill buttons do, so at 12px the label's last word sat right on the
   button's edge and the two read as one control. */
.syn-evbulk {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px var(--sp-4);
  margin: var(--sp-2) 0; padding: 7px 10px;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface-2); font-size: 13px;
}
.syn-evbulk__all { margin: 0; font-weight: 600; }
.syn-evbulk__all input[disabled] + span { color: var(--ink-faint); }
.syn-evbulk__n {
  font-weight: 700; color: var(--primary);
  font-variant-numeric: tabular-nums;
}
/* The hint takes whatever is left and gives it back when there is not enough,
   so it never squeezes the controls it is explaining onto two lines. */
.syn-evbulk__hint { flex: 1 1 240px; color: var(--ink-faint); font-size: 12.5px; }
.syn-pick { width: 15px; height: 15px; accent-color: var(--primary); cursor: pointer; }
/* The audience pill may not run over its neighbours: "Young Adults (18-29)"
   is wider than the column and was printing across Repeats. Ellipsised, with
   the full list already on the title attribute for hover. */
.syn-table .syn-t__aud { max-width: 100%; overflow: hidden; text-overflow: ellipsis; vertical-align: bottom; }
/* The row that cannot be ticked says why on hover — same tooltip the action
   icons use. */
.syn-pick-no { color: var(--ink-faint); opacity: .6; }

/* ---- The prospect list's reason line -------------------------------------- */
/* Under the verdict pill: what went wrong and the move that fixes it, so the
   list stops being forty clicks of "why?". */
.syn-why {
  display: block; margin-top: 4px; max-width: 34ch;
  font-size: 12px; line-height: 1.45; color: var(--ink-soft);
}
.syn-why strong { color: var(--ink); }
/* The button the reason recommends, picked out just enough to be found. */
.btn.syn-fix { border-color: var(--primary); color: var(--primary); }
.btn.syn-fix:hover { background: var(--primary-tint); }

/* Narrow admin windows drop the softest columns rather than scrolling. The
   actions and the event name are the two that can never go. */
/* BY CLASS NOW, NOT nth-child. The nth-child numbers were off by one once
   already (Town was inserted as column 3 and nothing that counts columns was
   updated, so the header hid one column while the body hid its neighbour) —
   and the tick column, which exists only on publishable tables, would make a
   fixed number wrong on half the tables at once. So the body cells that can
   be dropped carry the SAME class as their heading, and one rule hides both.
   scan-table-columns.test.js checks the pairing. */
@media (max-width: 1100px) {
  .syn-c-aud { display: none; }
  .syn-c-cat { width: 15%; }
}
@media (max-width: 960px) {
  .syn-c-rep { display: none; }
  .syn-c-when { width: 13%; }
  .syn-c-cat { width: 16%; }
  .syn-c-status { width: 12%; }
}

/* ============================================================================
   Signup: the "we'll take your list instead" branch.

   Given its own tint so the two optional boxes read as a pair and as a
   DIFFERENT kind of thing from the required fields around them — one is "fill
   this in", these are "only if this is you". The form was one flat colour, so
   an optional aside looked exactly like a field somebody had skipped.
   ========================================================================== */
.sendlist {
  margin: calc(var(--sp-2) * -1) 0 var(--sp-3);
  padding: 12px 14px;
  border: 1px solid var(--seaglass);
  border-top: 0;
  border-radius: 0 0 var(--radius) var(--radius);
  background: var(--seaglass-tint);
}
.sendlist__next {
  display: flex; align-items: flex-start; gap: 8px;
  margin: 0; color: var(--ink-soft); font-size: 13.5px; line-height: 1.55;
}
.sendlist__next svg { flex: none; margin-top: 2px; color: var(--seaglass-ink); }
.sendlist__next strong { color: var(--ink); }

/* The optional "point us somewhere else" panel, tinted to match, so the two
   optional things on this form look like each other rather than like fields. */
.signup-hint {
  border: 1px solid var(--seaglass);
  border-radius: var(--radius);
  background: var(--seaglass-tint);
}
.signup-hint > summary { color: var(--ink); }
.signup-hint > summary svg { color: var(--seaglass-ink); }

/* The website field, once they have said they have no calendar to scan. Faded
   rather than disabled: it is still usable if they have a site AND want to
   send a list, which is a normal combination. */
.field.is-dimmed { opacity: .6; }
.field.is-dimmed:focus-within { opacity: 1; }

/* ---- The one optional box: chevron, and the tick that lives inside it ------
   A <details> whose summary is only text reads as a heading, and people do not
   click headings. The chevron is the cheapest possible statement that this
   opens, and rotating it on open is the cheapest possible statement that it
   did. */
.signup-hint > summary {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 12px 14px; cursor: pointer; list-style: none;
}
.signup-hint > summary::-webkit-details-marker { display: none; }
.signup-hint > summary > span:first-of-type { flex: 1; }
.signup-hint__chev {
  flex: none; margin-left: auto; align-self: center;
  display: inline-flex; line-height: 0; color: var(--seaglass-ink);
  transition: transform .18s ease;
}
.signup-hint[open] > .signup-hint__chev,
.signup-hint[open] > summary .signup-hint__chev { transform: rotate(180deg); }
.signup-hint > summary:hover .signup-hint__chev { color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
  .signup-hint__chev { transition: none; }
}

/* The tick sits inside the box now, so it takes the surface colour rather than
   the box tint — otherwise a tinted card on a tinted box disappears. */
.signup-hint .check--card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm, 8px);
  margin-bottom: var(--sp-2);
}
.signup-hint .sendlist {
  margin: calc(var(--sp-2) * -1) 0 var(--sp-2);
  border-radius: 0 0 var(--radius-sm, 8px) var(--radius-sm, 8px);
  background: var(--surface);
}

/* A subheading inside the About article. The page had only h1 and body copy,
   so the brand story needed a way to announce itself as a new subject without
   competing with the headline above it. */
.about__h2 {
  font-family: var(--font-display);
  font-size: clamp(21px, 3.6vw, 26px);
  font-weight: 600; color: var(--ink);
  margin: var(--sp-4) 0 var(--sp-2);
}

/* ============================================================================
   The About page gull.

   THE SEQUENCE, 14 seconds:
     look at you -> look away -> a crab walks up and stops -> the bird picks it
     up -> it turns back to you WITH the crab -> drops it -> the crab walks off.

   ONE SHARED DURATION across six animations, so the beak closes exactly as the
   crab arrives without anything co-ordinating at runtime. Change it in one rule
   only and the bird bites at nothing.

   TRANSFORM-BOX: FILL-BOX ON EVERY MOVING PART, or transform-origin resolves
   against the whole viewBox and "rotate about the head" becomes "rotate about
   the corner of the drawing".

   Nothing uses linear timing. Real heads accelerate and settle, and that is the
   difference between a bird and a diagram.
   ========================================================================== */
.about__h2 { display: flex; align-items: center; gap: 12px; }
/* One step below the section heading, for the two account kinds. Not another
   .about__h2: those are the page's sections, and "Neighbor" and "Organization"
   are two halves of one of them. */
.about__h3 {
  font-family: var(--font-display);
  font-size: clamp(17px, 2.4vw, 19px);
  font-weight: 600; color: var(--ink);
  margin: var(--sp-5) 0 var(--sp-2);
}
/* ---- "How to use the site", the one collapsible on this page --------------
   The page's prose scale is built for a full-width reading column. Inside a
   bordered box at two thirds the width it reads long, and `.about p`'s big
   top margin doubles up with the heading's own — so the answers sit tight
   under the question they belong to instead of floating between two of them.
   Nothing else about the page's typography changes. */
/* "Logo captured", on the scan desk's bulk bar. A thumbnail and a sentence at
   the moment the drafts are on screen, rather than a trip to the venue page to
   find out. A logo URL that 404s between the scan and the desk shows as a
   broken image on purpose - see the note beside the img in sync.js. */
.syn-logo { display: inline-flex; align-items: center; gap: 7px; }
.syn-logo__img {
  width: 22px; height: 22px; object-fit: contain;
  border-radius: 5px; background: var(--surface); border: 1px solid var(--line);
}
.syn-logo__txt { font-size: 12.5px; font-weight: 600; color: var(--seaglass-ink); }

/* The provenance line in the footer of the browsing pages. The quietest thing
   there on purpose: a standing fact about where listings come from, not an
   announcement, and it must not compete with the nav beneath it.

   NO max-width. It had one, and against a right-aligned footer that turned a
   single sentence into two ragged lines - which is exactly what made it read
   as a notice rather than a footnote. One line wherever there is room for one,
   and it wraps on a phone like everything else does. */
.site-footer__src {
  margin: 0 0 var(--sp-3);
  font-size: 12.5px; line-height: 1.55; color: var(--ink-faint);
}
.site-footer__src a { color: var(--ink-soft); text-decoration: underline; }
.site-footer__src a:hover { color: var(--primary); }

/* ---- One sentence, and the rest behind a fold ----------------------------
   NL.oneLine. The lead is the thing that has to be read, so it carries the
   page's ordinary prose weight; the fold under it is deliberately quieter and
   tighter than .join-more is elsewhere, because here it sits inside a form
   rather than being the content of a page. */
/* THE ADVICE BLOCKS GO ON A PHONE. NOT THE POLICY ONES.

   Asked for: "all these little extra texts that are kinda collapsed, I don't
   think we need to show them on the app or on the web version of the iPhone.
   They're just extra text." Pointed at the posting form, where a tip line and
   a "Why that works" fold sat above the form somebody came to fill in.

   `:has(.oneline__lead)` IS THE WHOLE CARE HERE, because NL.oneLine renders
   two different things and only one of them is advice.

     WITH a lead    a sentence of guidance plus the reasoning behind it.
                    "Post a thing, not the place", "Get featured in the
                    newsletter". Nice to have, and this is what goes.

     WITHOUT a lead a bare fold, and every one of those is POLICY: how many
                    hours you have to change a date, that you can cancel a post
                    whenever you need to, what an automatic update does to your
                    edits. Hiding those would mean the terms of the thing are
                    readable on a laptop and not on a phone. They stay.

   Fourteen of these across six files, so the distinction had to be structural
   rather than a list of pages. */
@media (max-width: 620px) {
  .oneline:has(.oneline__lead) { display: none; }
}

.oneline { margin: var(--sp-4) 0; }
.oneline__lead { margin: 0; font-size: 15.5px; line-height: 1.5; color: var(--ink); }
.oneline__lead svg { vertical-align: -3px; color: var(--primary); }
.oneline__more { margin-top: var(--sp-2); background: transparent; border-color: var(--line); }
.oneline__more summary { padding: 8px var(--sp-3); }
.oneline__more summary strong { font-family: inherit; font-size: 13.5px; font-weight: 600; color: var(--ink-soft); }
.oneline__more .join-more__body { padding: 0 var(--sp-3) var(--sp-3); }
.oneline__more .join-more__body p { margin: 0; font-size: 14px; line-height: 1.6; color: var(--ink-soft); }

.about__how { margin-top: var(--sp-6); }
.about__how .join-more__body p { font-size: 17px; margin-top: var(--sp-2); }
/* Whatever comes first, since the panel now opens on a lead paragraph rather
   than a heading. */
.about__how .join-more__body > :first-child { margin-top: 0; }
.gull { flex: none; overflow: visible; }

.gull__head,
.gull__eye,
.gull__pupil,
.gull__beak-lo,
.gull__crab,
.gull__crab-held { transform-box: fill-box; }

/* Head. Pivots at the neck end so it turns where it joins the body rather than
   spinning in place. Negative is up-and-toward-you; positive is down. */
.gull__head {
  transform-origin: 8% 62%;
  animation: gull-head 14s ease-in-out infinite;
}
/* ---- THE GRAB, WHICH USED TO BE THREE THINGS IN THE WRONG ORDER ----------

   REPORTED: "the bird kind of leans down, and then all of a sudden the crab
   appears in its mouth... the piece that's missing is actually lowering its
   beak, grabbing the crab, and then continuing."

   Exactly right, and the timeline said so. The head reached the bottom at 52%
   and STARTED LIFTING AGAIN AT 56%; the crab swapped at 58%, by which time the
   head was already on its way up; and the beak did not close until 59%. So the
   bird leant, rose, a crab appeared inside an open beak, and the beak shut
   afterwards on nothing. Three beats played in the wrong order, which is why
   it read as a cut rather than a movement.

   THE ORDER NOW, and every number below is chosen to hold it:

     48%  a small dip - the bird notices
     53%  down onto the crab
     55%  settles the last degree, the way a head does at the end of a turn
     58%  THE BEAK CLOSES AND THE CRAB CHANGES HANDS, on the same frame
     60%  a tug: the head lifts a degree as the crab comes free
     66%  and only then the turn back to you

   THE SETTLE AND THE TUG ARE THE WHOLE TRICK. A head that arrives and stops
   dead is a diagram; one that overshoots by a degree and comes back has
   weight. A crab that is lifted without the head reacting weighs nothing. */
/* ---- IT ALSO HAS TO REACH, AND IT NEVER DID -----------------------------

   MEASURED, not guessed. With the head at its lowest the underside of the beak
   stopped 7.3px above the top of the crab, and the crab in the beak is drawn
   another 13.6px above where the crab on the sand sits. So the bird closed its
   beak in mid-air and the crab jumped about ten pixels upward to meet it. That
   jump is the "all of a sudden" as much as the ordering was.

   ROTATION ALONE CANNOT FIX IT. The head pivots at the neck, so turning it
   further swings the beak sideways rather than down: 16deg to 34deg closed the
   gap by less than four pixels, and 34deg looks like a broken bird.

   SO THE HEAD DROPS AS WELL AS TURNS, which is what a gull actually does - the
   whole head and neck come down, they do not merely tilt. Every keyframe here
   carries a translateY, including the zero ones: CSS interpolates transforms
   component by component only when the function lists match, and a keyframe
   with `rotate()` alone next to one with `translateY() rotate()` snaps instead
   of tweening.

   The other half of the meeting is in gull-crab, where the crab is lifted the
   rest of the way as the beak shuts. Neither number means anything without the
   other; change one and re-measure both. */
@keyframes gull-head {
  0%, 9%    { transform: translateY(0) rotate(0deg); }
  16%, 27%  { transform: translateY(0) rotate(-14deg); }    /* turns to look at you */
  34%, 45%  { transform: translateY(0) rotate(0deg); }      /* and back, crab walks up */
  48%       { transform: translateY(1px) rotate(4deg); }    /* notices it: a small dip */
  53%       { transform: translateY(4px) rotate(17deg); }   /* down onto it, overshoots */
  55%, 58%  { transform: translateY(5px) rotate(16deg); }   /* settles, HOLDS for the grab */
  60%       { transform: translateY(3px) rotate(13deg); }   /* the tug, crab coming free */
  66%, 76%  { transform: translateY(0) rotate(-14deg); }    /* back up to you, crab in beak */
  80%, 84%  { transform: translateY(4px) rotate(12deg); }   /* lowers to let it go */
  90%, 100% { transform: translateY(0) rotate(0deg); }
}

/* Blinks: idle, once while looking at you, once while holding the crab. */
.gull__eye {
  transform-origin: center;
  animation: gull-blink 14s ease-in-out infinite;
}
@keyframes gull-blink {
  0%, 5%, 8%, 21%, 24%, 67%, 70%, 92%, 95%, 100% { transform: scaleY(1); }
  6.5%, 22.5%, 68.5%, 93.5%                      { transform: scaleY(.12); }
}

/* A flat side-on bird cannot rotate into three-quarter view, so the pupil
   sliding forward and widening is what actually sells "it is looking at you". */
.gull__pupil {
  transform-origin: center;
  animation: gull-look 14s ease-in-out infinite;
}
@keyframes gull-look {
  0%, 13%   { transform: translateX(0) scale(1); }
  17%, 27%  { transform: translateX(-1.1px) scale(1.18); }
  33%, 58%  { transform: translateX(0) scale(1); }
  63%, 73%  { transform: translateX(-1.1px) scale(1.18); }
  79%, 100% { transform: translateX(0) scale(1); }
}

/* The beak: opens to take the crab, holds it shut while the head turns, opens
   again to drop it. */
.gull__beak-lo {
  transform-origin: 0% 0%;                 /* the hinge, where the halves meet */
  animation: gull-beak 14s ease-in-out infinite;
}
/* THE BEAK LEADS THE HEAD DOWN AND CLOSES BEFORE IT COMES UP. It used to open
   at 53% and still be open at 56% while the head was already rising, then shut
   at 59% - after the crab had appeared. Now it parts on the way down, is at its
   widest while the head is stationary over the crab, and SHUTS AT 58%, which is
   the same frame the crab changes hands. That single alignment is what turns a
   cut into a grab. */
@keyframes gull-beak {
  0%, 46%   { transform: rotate(0deg); }
  50%       { transform: rotate(7deg); }    /* parts on the way down */
  54%, 57%  { transform: rotate(20deg); }   /* wide, over the crab, head still */
  58%       { transform: rotate(2deg); }    /* SHUT on it - the grab */
  60%, 77%  { transform: rotate(3deg); }    /* carried */
  80%, 84%  { transform: rotate(16deg); }   /* opens, and it falls */
  88%, 100% { transform: rotate(0deg); }
}

/* THE CRAB ON THE GROUND. Walks in, stops under the beak, and hands over to the
   held crab at the moment of the grab. Comes back when dropped and walks off.
   The little y-steps are a scuttle — a crab that slides is a hovercraft. */
.gull__crab {
  transform-origin: center bottom;
  animation: gull-crab 14s ease-in-out infinite;
}
/* THE HANDOVER IS INSTANT, AND IT HAS TO BE. These keyframes are 0.1% apart -
   about a millisecond and a half, well under one frame at 60fps - because
   anything wider is a CROSSFADE, and a
   crab that fades out of the sand while another fades into the beak is the
   "all of a sudden" the owner saw. Worse, the old pair left 0.14s in which the
   ground crab had faded and the beak crab was not yet drawn: for two frames
   there was no crab on screen at all.

   AND IT LIFTS BEFORE IT GOES. One pixel, on the frame the beak shuts, so the
   crab is caught and raised rather than deleted. */
@keyframes gull-crab {
  0%, 33%     { transform: translate(19px, 0); opacity: 0; }
  36%         { transform: translate(19px, 0); opacity: 1; }   /* appears, off to the right */
  39%         { transform: translate(14px, -1px); opacity: 1; }
  42%         { transform: translate(9px, 0); opacity: 1; }
  45%         { transform: translate(4px, -1px); opacity: 1; }
  48%, 57.2%  { transform: translate(0, 0); opacity: 1; }      /* arrives, stops, waits */
  /* CAUGHT AND LIFTED INTO THE BEAK, over about a tenth of a second, while the
     beak is closing on it. -11 is not a look-nice number: it is the distance
     that puts this crab exactly where the beak-held one is drawn, once the
     head's own 5px dip is taken off. The two are measured together and the
     swap below is invisible only while they agree. */
  57.99%      { transform: translate(0, -11px); opacity: 1; }  /* snatched up */
  58%, 81%    { transform: translate(0, -11px); opacity: 0; }  /* handed to the beak */
  /* 81.02, NOT 81. The held crab leaves ON 81, and a `steps(1, end)` value
     read exactly on its own keyframe still resolves to the outgoing one - so
     both crabs were drawn for a single frame at 81%. Two crabs is far more
     noticeable than the 3ms in which neither is, so the ground one comes back
     immediately AFTER rather than exactly on. Found by scrubbing the timeline
     in 0.02% steps; it is not a thing the eye finds on a 14 second loop. */
  81.02%      { transform: translate(0, -11px); opacity: 1; }  /* let go, still up there */
  83%         { transform: translate(0, -3px); opacity: 1; }   /* falls */
  84.5%       { transform: translate(0, 0); opacity: 1; }      /* and lands */
  87%         { transform: translate(5px, -1px); opacity: 1; }
  90%         { transform: translate(11px, 0); opacity: 1; }
  94%         { transform: translate(17px, -1px); opacity: 1; }
  97%, 100%   { transform: translate(23px, 0); opacity: 0; }   /* gone */
}

/* THE CRAB IN THE BEAK. Visible only between the grab and the drop, and only
   ever one of the two crabs is on screen at a time. */
.gull__crab-held {
  animation: gull-crab-held 14s steps(1, end) infinite;
}
/* THE OTHER HALF OF THE SAME INSTANT. 58% here is 58% there, and the beak
   shuts on 58% too - three animations landing on one frame is what makes it
   look like one action. `steps(1, end)` keeps it a hard cut rather than a
   fade, which is right: the crab does not dissolve into the beak, it is in it.

   THE RELEASE IS THE SAME TRICK IN REVERSE. It leaves at 81%, one frame after
   the beak has opened at 80%, and the ground crab reappears on that exact
   frame already a pixel up, so it falls the last pixel instead of blinking
   into the sand. */
@keyframes gull-crab-held {
  0%, 57.99%  { opacity: 0; }
  58%, 80.99% { opacity: 1; }
  81%, 100%   { opacity: 0; }
}

/* A looping animation beside body text is exactly what this setting is for. */
@media (prefers-reduced-motion: reduce) {
  .gull__head, .gull__eye, .gull__pupil,
  .gull__beak-lo, .gull__crab, .gull__crab-held { animation: none; }
  .gull__crab { opacity: 1; }
  .gull__crab-held { opacity: 0; }
}

/* The subheading's own margin kept losing to the global `h1, h2, h3 {margin: 0}`
   reset — same weight, and the reset sits later in the file. Scoped to .about
   so it wins on specificity rather than on ordering, which is the fragile way
   to win. A subheading needs more room before it than after, or it reads as a
   caption for the paragraph above rather than a title for the one below. */
.about .about__h2 { margin-top: 44px; margin-bottom: var(--sp-2); }
.about .about__h2:first-of-type { margin-top: var(--sp-3); }

/* The region the footer serves, and who made it. Divided by a hairline rather
   than a bullet: the two are one thought read left to right, and a dot between
   them would make them look like two entries in a list.

   NOTHING BUT THE DIVIDER. It carried italics, its own colour and a smaller
   size, which made half of one sentence look like a caption pasted beside the
   other half. Inheriting all three means the line reads as one line — the rule
   is the only thing separating them, which is the whole job. */
.site-footer__where { margin-top: 6px; }
/* The store badges under the footer's line about who we are. Apple's badge
   artwork at its minimum height, on its own row, with a small label so it is
   plainly an offer and not a decoration. */
.site-footer__apps { margin: var(--sp-3) 0 0; display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3); }
.site-footer__apps-t { font-size: 13px; font-weight: 700; color: var(--ink-soft); text-transform: uppercase; letter-spacing: .04em; }
.app-badge img { display: block; height: 40px; width: auto; }
.site-footer__made {
  margin-left: 10px; padding-left: 10px;
  border-left: 1px solid var(--line-strong);
}
/* On a narrow footer the divider becomes a wrap point rather than a squeeze —
   the line breaks in two and the rule disappears with it, instead of two
   phrases fighting over one row. */
@media (max-width: 560px) {
  .site-footer__made {
    display: block; margin-left: 0; padding-left: 0;
    border-left: 0; margin-top: 3px;
  }
}

/* The share sheet's copy row: named, because on a desktop it is usually the
   only route that will actually do anything. */
.share-copy__lead {
  margin: var(--sp-3) 0 6px;
  color: var(--ink-soft); font-size: 13px; font-weight: 600;
}

/* ---- My Calendar header: one rhythm instead of four ------------------------
   Measured before this: 8px under the title, 16 under the intro, 26 under
   "Next up", 32 above the calendar. Four gaps, no relationship between them,
   which is what makes a block look unfinished even when nothing is obviously
   wrong — the eye reads inconsistent spacing as a mistake it cannot name.

   Now a single scale: tight inside the text block, one clear step out to the
   controls, one to the calendar. */
.mycal-head { margin-bottom: var(--sp-3); }
.mycal-title { margin-bottom: 6px; }

/* The switcher belongs to the calendar below it, not to the prose above, so
   it sits closer to what it controls than to what it follows. */
.mcv { margin-top: var(--sp-4); margin-bottom: var(--sp-2); }

/* The actions were pinned to the very top-right, level with the kicker, which
   left them stranded a long way from the text and made the empty right-hand
   side read as a hole rather than as margin. Aligned to the title instead. */
@media (min-width: 860px) {
  .mycal-head { align-items: flex-start; }
}

/* ---- Filters and town, side by side ---------------------------------------
   The two controls that narrow what you are looking at, together: what KIND of
   thing on the left, WHERE on the right. The town picker used to sit up in the
   site header beside My Calendar — the most consequential filter on the site,
   nowhere near the others.

   The picker is MOVED here from the header rather than rebuilt (see
   NL.placeTownPicker), so it keeps the styling it already had. Only its
   position changes. */
.filterbar__row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap;
}

/* ---- THE TWO CONTROLS, KEPT ON SCREEN -------------------------------------

   Asked for directly: "making those two buttons, the filter and the location
   ... fixed on that position, especially when the user scrolls up and down the
   page." They are the most consequential controls on a listing page and they
   scroll away after one screen, so narrowing a 139-item list means going back
   to the top to do it.

   FIXED, NOT STICKY, AND THAT IS FORCED. In `intro` mode the bar is dissolved
   into the heading's grid (`.intro .filterbar { display: contents }`), so the
   row's containing block is the .intro block itself — about 120px tall. A
   `position: sticky` row would stick for those 120px and then leave with its
   container, which is the one thing it must not do. `fixed` has no containing
   block to run out of.

   THE PRICE OF `fixed` IS THE HOLE IT LEAVES, and .filterbar__ghost pays it:
   an out-of-flow grid child stops sizing its track, so column 2 would collapse,
   the heading would gain 350px and re-wrap, and the whole page would shift
   under the reader at the exact moment they scrolled. The ghost is given the
   row's measured size and holds the space open. See NL.stickyFilters.

   FULL-BLEED BACKGROUND, PAGE-WIDTH CONTENT, one declaration: the padding grows
   to whatever the gutter needs at this viewport, so the strip spans the window
   while the buttons stay on the same vertical line they occupied unpinned.
   Nothing appears to move sideways when it pins. */
.filterbar__row.is-pinned {
  position: fixed; z-index: 35;
  top: var(--pin-top, 60px); left: 0; right: 0;
  margin: 0;
  /* `+ --sp-4` because .wrap--wide is a 1040px box with its own gutter inside
     it, so the row's unpinned right edge is the wrap's INNER edge. Without the
     gutter the buttons slid outward by one gutter the instant they pinned —
     small, and the kind of small that reads as the page twitching.

     `100%`, NOT `100vw`. 100vw counts the scrollbar and the centred .wrap does
     not, so the vw version was off by half a scrollbar and moved the pair 8px
     the OTHER way. A fixed box pinned left:0 right:0 resolves percentages
     against the scrollbar-free viewport, which is the same width the wrap is
     centred in. */
  padding: 7px max(var(--sp-4), calc((100% - var(--maxw-wide)) / 2 + var(--sp-4)));
  /* Right-aligned, because unpinned they sit at the top right of the heading
     row. Landing them anywhere else would move them at the moment they pin,
     which is exactly when somebody is looking for them. */
  justify-content: flex-end;
  background: rgba(239, 237, 228, .94);
  backdrop-filter: saturate(1.2) blur(10px);
  border-bottom: 1px solid var(--line);
}
/* On a phone the row is the full width already, so the pair keep the spread
   they have unpinned rather than bunching into the right-hand corner. */
@media (max-width: 560px) {
  .filterbar__row.is-pinned { justify-content: space-between; }
}
.filterbar__ghost { display: none; }
.filterbar__ghost.is-on { display: block; grid-column: 2; grid-row: 1; justify-self: end; }

/* THE PANEL HAS TO COME WITH IT. It is a separate grid item back up in the
   heading, so a pinned button that opened it would scroll the answer somewhere
   off the top of the screen. Pinned, it hangs directly under the strip and is
   given a ceiling — a filter panel taller than the window is one you cannot
   reach the bottom of. */
.filterbar__panel.is-pinned {
  position: fixed; z-index: 34;
  top: calc(var(--pin-top, 60px) + var(--pin-h, 58px)); left: 0; right: 0;
  margin: 0 auto; max-width: var(--maxw-wide);
  max-height: calc(100vh - var(--pin-top, 60px) - var(--pin-h, 58px) - 16px);
  overflow-y: auto;
  border-radius: 0 0 var(--radius) var(--radius);
  box-shadow: var(--shadow-hover);
}
.filterbar__town { display: inline-flex; }
/* In the header it was pinned to the right of a flex row; here it is already
   placed by the row above, so it must not also push itself around. */
.filterbar__town .city-select { margin: 0; }

/* The header keeps its shape once the picker leaves: without this the tools
   cluster collapses toward the middle on the pages that move it. */
.header-tools:empty { display: none; }

@media (max-width: 560px) {
  .filterbar__row { gap: 8px; }
  .filterbar__town { flex: 1 1 auto; justify-content: flex-end; }
}

/* The "or send us your list" line at the foot of the events-source box. A
   sentence, not a second control: it answers the same question the field above
   it does, for the people who cannot answer it that way. */
.signup-hint__or {
  margin: var(--sp-2) 0 0; padding: 10px 12px;
  border-radius: var(--radius-sm, 8px);
  background: var(--surface); border: 1px solid var(--line);
  color: var(--ink-soft); font-size: 13.5px; line-height: 1.55;
}
.signup-hint__or svg { vertical-align: -2px; color: var(--seaglass-ink); }
.signup-hint__or strong { color: var(--ink); }

/* ============================================================================
   Cards: the organization on the left, the time beside the title.

   The left column used to be the time — a number in a margin you had to pair
   up with a title. It is the ORGANIZATION now, which is what a reader
   recognises first when scanning a list of a town's evenings, and the time
   moved up into the headline where it belongs: "The Luca Burger · all day".
   ========================================================================== */
/* A COLUMN NOW, not a single square: the logo, and under it the category drawn
   as a picture (two of them when the listing is filed under two categories).

   THE GAP IS 16px AND IT WAS 9. Reported as the mark sitting "a little bit too
   close to the logo" — and it was, because the two are both filled, rounded,
   similarly sized squares. At 9px apart they read as one stacked object with a
   seam across it rather than as a logo with a mark under it. Sixteen is enough
   air to separate them and still obviously group them.

   That leaves the column's content at 2 + 72 + 16 + 46 = 136px, which is taller
   than the shortest tile body this list produces. On a one-line listing the
   card therefore grows by a few pixels rather than the column being cropped —
   the right way round, and worth the trade. Anything wider than this and the
   left column starts setting the height of every short tile in the list. */
.event-org {
  flex: none; display: flex; flex-direction: column; align-items: center;
  gap: 16px; padding-top: 2px;
}

/* SAVE AND SHARE, SEATED UNDER THE GLYPHS. See NL.eventCard, which moved them
   out of .event-foot.

   The base .event-acts is built for a footer: it right-aligns and leaves 6px
   between items, which came to 74px for two buttons in a column that is 70px
   wide on a phone, so they wrapped onto two rows. Centred, closed to 4px, and
   a size down from 34px — "a bit smaller and moved to the left" is what was
   asked for — they come to 68px and sit on one row.

   Not shrunk any further. These are already under the 44px a thumb wants, and
   this is the size the rest of the site uses for the same two controls. */
.event-org .event-acts {
  justify-content: center; gap: 4px; max-width: 100%;
}
.event-org .event-acts .event-share,
.event-org .event-acts .event-react { width: 32px; height: 32px; }

/* ON ANYTHING BIGGER THAN A PHONE THEY GO BACK TO THE RIGHT.

   Moving them into the left column was asked for about a PHONE, and I applied
   it at every width, which was my mistake. On a wide screen it does the
   opposite of what it does on a narrow one: the left column is a short stack
   next to a wide block of text, so adding a third item to it lengthens the one
   column that had nothing to prove, and the owner reported the tile getting
   taller for no reason.

   `position: absolute` rather than moving them back in the markup, because
   they cannot be in two containers at once and the phone wants them where they
   are. Out of flow, they stop lengthening the left column entirely; the card
   is already `position: relative` a few thousand lines down, so bottom-right
   of the tile is where they land, which is where they were before any of this.

   The footer keeps 84px clear on the right so a long row of tags cannot run
   underneath them. That is the one thing absolute positioning will not do for
   itself. */
@media (min-width: 621px) {
  .event-org .event-acts {
    position: absolute; right: var(--sp-4); bottom: var(--sp-4);
    justify-content: flex-end; gap: 6px; width: auto;
  }
  /* Back to 34px too. The 32 was to fit a 70px column, and there is no column
     to fit any more. */
  .event-org .event-acts .event-share,
  .event-org .event-acts .event-react { width: 34px; height: 34px; }
  .event-card:has(.event-org) .event-foot { padding-right: 84px; }
  /* AND WIDER AGAIN WHEN THERE IS A THIRD CONTROL WITH A WORD IN IT. 84px was
     measured for two 34px squares; the Buy tickets pill adds about 110px, and
     without this a long row of tags runs underneath it. Only on the tiles that
     have one — every other card keeps the 84px it was measured for. */
  .event-card:has(.event-acts .event-tix) .event-foot { padding-right: 200px; }
}

/* THE CATEGORY, DRAWN. Little filled illustrations from NL.catIcon (they live
   in stockart.js, beside the stock artwork whose style they share).

   NO BOX HERE. The earlier version was a 30px chrome square — border, surface
   tint — with a thin outline glyph centred in it. The drawing now brings its
   own rounded ground in the category's colour, so a CSS box would only put a
   second border around a picture that already has one.

   BIGGER, TOO, and that is not decoration: these went from a line drawing to an
   illustration with a figure and a guitar in it, and 30px is not enough room to
   tell that from a smudge. 46px is as large as the column allows under a 72px
   logo without the left of the tile setting its height.

   Stacked when there is one, side by side when there are two — two of these
   stacked WOULD set the height on a short listing, and the column must never be
   the thing that decides how tall a tile is. */
.cat-marks { display: flex; flex-direction: column; align-items: center; gap: 6px; }
.cat-marks--two { flex-direction: row; gap: 5px; }
.cat-mark { display: inline-flex; line-height: 0; }
/* 46 -> 54 (owner's decision, 28 Aug 2026). The drawings lost their tinted
   tile, and a mark with no ground reads smaller than the same mark with one
   even at an identical box size: the tile was carrying part of the weight.
   Measured against the venue logo beside it, which is 60px and steps to 72 on
   a wide screen: at 54 the two read as a pair rather than as a big thing and a
   small thing, and the mark still gives way to the logo, which is the right
   order for a card about a place. */
.cat-mark svg { width: 54px; height: 54px; display: block; }
/* Two have to share the width one had, so they step down — still bigger than
   the single glyph they replace. */
.cat-marks--two .cat-mark svg { width: 44px; height: 44px; }

/* A MONOGRAM, not an uploaded file and not a third-party favicon. See
   NL.orgMark for why. Hue is derived from the name, so an organization is the
   same colour on every card without anything being stored or kept in sync. */
.org-mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; border-radius: 12px;
  background: hsl(var(--h) 46% 94%);
  color: hsl(var(--h) 42% 32%);
  border: 1px solid hsl(var(--h) 40% 86%);
  font-family: var(--font-display);
  font-size: 15px; font-weight: 700; letter-spacing: .01em;
  user-select: none;
}

/* THE SAME SQUARE, WITH THEIR OWN FILE IN IT. The initials stay in the markup
   underneath rather than being replaced: a stored file that 404s removes its
   own <img> (see NL.orgMark), and what shows through is the monogram that was
   always the floor — not a broken-image glyph on every card they appear on.

   White behind it, not the name's hue. A transparent PNG logo drawn on a
   tinted square picks up a colour its designer never chose, and every one of
   these was made to sit on white. */
/* BIGGER ON A TILE THAN ANYWHERE ELSE, and scoped so it stays that way only
   there. 44px was too small for the wordmarks — several organizations' logos
   are their name set in type, and at 44px inside 3px of padding that is four
   or five pixels of letter height. Every other place `.org-mark` appears (the
   directory, the review frame, the logo sweep) keeps the size it was tuned at,
   which is why this is written as a descendant rule and not a change to the
   base. */
.event-org .org-mark { width: 60px; height: 60px; border-radius: 15px; font-size: 19px; }
@media (min-width: 620px) {
  .event-org .org-mark { width: 72px; height: 72px; border-radius: 17px; font-size: 22px; }
}

.org-mark--logo { position: relative; overflow: hidden; background: #fff; }
.org-mark__img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: contain;                     /* contain, not cover: a logo must not be cropped */
  padding: 3px; background: #fff;
}

/* Title and time on one line, time pushed to the end and never wrapping —
   it is two or three characters and must not be broken across lines. */
.event-head {
  display: flex; align-items: baseline; gap: 12px;
  justify-content: space-between;
}
.event-when {
  flex: none; white-space: nowrap;
  color: var(--ink-soft); font-size: 13px; font-weight: 700;
  font-variant-numeric: tabular-nums; letter-spacing: .02em;
}
/* The old stacked time styles applied inside the left column; here the parts
   sit inline instead. "All day" loses its line break. */
.event-when .event-time__t { font-size: 15px; color: var(--ink); }
.event-when .event-time__ampm { font-size: 12px; margin-left: 2px; text-transform: uppercase; }
.event-when .event-time__all { font-size: 12.5px; text-transform: uppercase; letter-spacing: .04em; }
/* THE DAY, BESIDE THE TIME AND ON ONE LINE: "Sep 2 @ 2:30 PM".

   ASKED FOR: "I understand it may be repetitive, but I want the abbreviated
   date next to the time in the live tiles... apply this to all tiles."

   It used to be `display: block`, stacking the day ABOVE the time, and that
   was the right call for what it then said: the old line carried the weekday
   and the year too, and "Fri, 12 Sep 2026 6:30 PM" on one row is the first
   thing to push a long title into three lines on a phone.

   What it says now is "Sep 2" - no weekday, no year, because the heading above
   a grouped list already carries the weekday and a tile is not read in a
   different year. That is short enough to sit inline, which is what was asked
   for and also what makes it read as one phrase rather than two facts. */
.event-when .event-time__day {
  display: inline; font-size: 12.5px; font-weight: 700;
  color: var(--ink-soft); letter-spacing: .03em;
}
/* Air around the separator, so it is "Sep 2 @ 2:30" and not "Sep 2@2:30". */
.event-when .event-time__at { margin: 0 3px; font-weight: 600; color: var(--ink-faint); }
.event-when:has(.event-time__day) { text-align: right; }

/* ON A PHONE THE DAY GOES ABOVE THE TITLE, not beside it.

   Measured rather than guessed: at 375px the date column takes 103px of the
   345 available, which left a real listing title 148px and six lines to say
   itself in. The date is short and the title is not, so on a narrow screen they
   stop sharing a row and the title takes the width.

   Only where the card carries a date. Every other card is a title and a time,
   which fit side by side at any size and always have. */
@media (max-width: 520px) {
  .event-head:has(.event-time__day) { flex-wrap: wrap; gap: 2px; }
  .event-head:has(.event-time__day) .event-when { order: -1; width: 100%; text-align: left; }
  .event-head:has(.event-time__day) .event-title { width: 100%; }
  .event-when .event-time__day { display: inline; }
  .event-when:has(.event-time__day) .event-time__t { margin-left: 6px; }
}

@media (max-width: 520px) {
  .org-mark { width: 38px; height: 38px; border-radius: 10px; font-size: 13.5px; }
  /* Still the biggest thing in the column on a phone, just less of it. Written
     out rather than left to the rule above, which this would otherwise
     out-specify in the wrong direction. */
  .event-org .org-mark { width: 52px; height: 52px; border-radius: 13px; font-size: 17px; }
  .event-card:has(.event-org) { grid-template-columns: 70px 1fr; gap: var(--sp-3); }
  /* The gap comes down with everything else. 16px between a 52px logo and a
     38px mark is proportionally a much wider gap than it is on a desktop tile,
     and it starts to read as two unrelated things rather than a pair. */
  .event-org { gap: 12px; }
  .cat-mark svg { width: 44px; height: 44px; }
  /* 2 x 32 + the 5px gap is 69, inside a 70px column. The pair is what sets
     this column's width, not the logo, so this one cannot simply scale with
     the single mark above it. */
  .cat-marks--two .cat-mark svg { width: 32px; height: 32px; }
  .event-head { gap: 8px; }
}

/* ---- The card form ------------------------------------------------------
   A Stripe iframe, given room to breathe and a border that matches the rest
   of the site's fields so it does not read as a foreign object dropped into
   the modal. Its inner styling is set in JS from these same design tokens —
   see NL.billing.cardForm. */
.cardm-el {
  margin: var(--sp-4) 0 var(--sp-3);
  min-height: 220px;                       /* holds its shape while Stripe loads */
}
.cardm-status {
  display: flex; align-items: center; gap: 7px;
  margin-bottom: var(--sp-3);
  font-size: 13.5px; font-weight: 600; color: var(--ink-soft);
}
.cardm-status svg { flex: none; }
.cardm-status--bad { color: var(--destructive); }

/* ---- Sign out -----------------------------------------------------------
   Deliberately quiet. It sits beside Settings on the dashboard and at the top
   of the resident's calendar, and it is a thing people look for rather than
   one that should catch the eye — an eye-catching sign-out is a sign-out
   pressed by accident. Matches .dash-settings so the pair reads as one group.
   Turns destructive-coloured only on hover, which is the moment it is about
   to be meant. */
.signout-link {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 12px; border-radius: var(--radius-pill);
  background: none; border: 1px solid transparent;
  color: var(--ink-faint); font-size: 13px; font-weight: 600;
  font-family: inherit; cursor: pointer;
  /* TWO WORDS, ONE LINE. `.dash-settings` beside it has carried this since it
     was written; this one did not, and on a phone the dashboard's top row is
     the heading, Settings and this, so it broke to "Sign / out" stacked in a
     pill built for one line. Reported from a phone. */
  white-space: nowrap;
}
.signout-link svg { color: var(--ink-faint); }
.signout-link:hover {
  color: var(--destructive); border-color: var(--line-strong);
}
.signout-link:hover svg { color: var(--destructive); }
.signout-link[disabled] { opacity: .6; cursor: default; }

/* Settings and Sign out as one cluster at the top right of the dashboard. */
.dash-top__acts { display: inline-flex; align-items: center; gap: 4px; }

/* ---- Favorites are red, everywhere --------------------------------------
   The heart means one thing on this site — "this is one of mine" — and it is
   red wherever it says so: on a card, on a saved row, and on the filter that
   shows only those. It was inheriting the pill's own colour here, which made
   it white-on-navy when active and grey otherwise: the one control explicitly
   about favorites was the one place the heart did not look like a favorite.
   Filled when the filter is ON, outlined when it is off, matching the button
   on a card exactly. */
.mcv__pill[data-all="0"] svg { color: var(--destructive); }
.mcv__pill.is-on[data-all="0"] svg { fill: currentColor; }

/* When a plan's price is temporary, the card says so directly under the price —
   quieter than the figure it qualifies, but not hidden. A discount with no end
   date printed beside it is a promise the invoice will break. */
.plan__term {
  margin-top: 6px; font-size: 12.5px; line-height: 1.45;
  color: var(--ink-faint);
}

/* ---- Billing history ----------------------------------------------------
   A receipt list, not a table. Three things per row — what it was, when, how
   much — and the amount is right-aligned so a column of figures can be
   scanned without reading the labels beside them. */
.pay-list { list-style: none; margin: var(--sp-3) 0 0; padding: 0; }
.pay-row {
  display: flex; align-items: baseline; gap: var(--sp-3);
  padding: 10px 0; border-bottom: 1px solid var(--line);
  font-size: 14px;
}
.pay-row:last-child { border-bottom: 0; }
.pay-row__what { flex: 1 1 auto; min-width: 0; color: var(--ink); font-weight: 600; }
.pay-row__what svg { vertical-align: -2px; color: var(--ink-faint); }
/* The description sits under the kind rather than beside it: a promotion post
   title is as long as somebody wants it to be. */
.pay-row__what em {
  display: block; margin-top: 2px;
  font-style: normal; font-weight: 500; font-size: 13px; color: var(--ink-soft);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pay-row__when { flex: none; color: var(--ink-faint); font-size: 13px; }
/* `margin-left: auto` so the figure is flush right in BOTH layouts. Without
   it the amount only aligned while the row was one line: once the description
   wrapped onto its own line on a narrow screen, the date and amount packed to
   the left and a wider figure — "$124.99 failed" — sat 10px further right than
   the ones above it. A column of numbers you cannot scan is the one thing a
   receipt list has to get right. */
.pay-row__amt {
  flex: none; min-width: 88px; margin-left: auto; text-align: right;
  font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums;
}
/* A failed charge is greyed rather than reddened throughout — it is a fact
   about the past, not a warning to act on. Only the word is coloured. */
.pay-row--failed .pay-row__what,
.pay-row--failed .pay-row__amt { color: var(--ink-faint); }
.pay-row--failed .pay-row__amt strong { color: var(--destructive); }
.pay-row__amt strong { font-weight: 700; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; }

@media (max-width: 520px) {
  .pay-row { flex-wrap: wrap; gap: 4px var(--sp-3); }
  .pay-row__what { flex: 1 1 100%; }
}

/* ---- "It's on another tab" ----------------------------------------------
   Search filters within one section, so looking for a class from Events found
   nothing and read as broken. This says where the thing actually is.

   Two weights, because it answers two different questions. When there are no
   results it IS the answer, so it gets room and a border. When there are
   results it is an aside and must not compete with them. */
/* NOT display:flex. It was, and `gap` then applied between the <strong> and
   the <a> as well as around the icon — the sentence rendered as
   "matches “ pizza ”, but there is 1 match in Food & Drinks ." with holes in
   it. This is a sentence, so it is laid out as one. */
.search-elsewhere {
  margin: var(--sp-3) 0 0;
  font-size: 14px; line-height: 1.5; color: var(--ink-soft);
}
.search-elsewhere svg {
  vertical-align: -2px; margin-right: 6px; color: var(--ink-faint);
}
.search-elsewhere a { font-weight: 600; }
.search-elsewhere--only {
  margin: var(--sp-4) auto 0; padding: 12px 14px;
  max-width: 46rem;
  background: var(--seaglass-tint); border: 1px solid var(--line);
  border-radius: var(--radius-sm, 8px);
  color: var(--ink); text-align: left;
}
.search-elsewhere--only svg { color: var(--seaglass-ink); }

/* The ZIP fallback under the newsletter's town checkboxes — for the reader
   whose town has no checkbox yet. Indented to read as part of the same
   question, not a new one. */
.nl-zip { margin-top: var(--sp-2); }
.nl-zip label {
  display: block; margin-bottom: 4px;
  font-size: 13px; font-weight: 600; color: var(--ink-soft);
}
.nl-zip input { max-width: 11rem; }
/* ---- THE LOGO, ATTACHED FROM THE POST FORM -------------------------------
   "a discrete section" - so it is a quiet block at the foot of the form, not a
   step. Hairline rule above it rather than a card: a bordered panel here reads
   as another thing to fill in, and this one is optional.

   THE DONE STATE IS A TICK AND A THUMBNAIL. "have I done this?" and "what does
   it look like?" are two questions and the answer has to serve both - a
   picture on its own leaves somebody wondering whether it saved. */
.logo-attach {
  margin-top: var(--sp-5); padding-top: var(--sp-4);
  border-top: 1px solid var(--line);
  display: grid; gap: var(--sp-2);
}
.logo-attach__row { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.logo-attach__img {
  width: 40px; height: 40px; border-radius: var(--radius);
  object-fit: contain; background: var(--surface-2); flex: none;
  border: 1px solid var(--line);
}
/* The seaglass "this went well" ink, the same one the sent calendar button
   uses - see .event-cal.is-sent, and the note there about not inventing a
   --good that resolves to nothing. */
.logo-attach__said {
  margin: 0; display: flex; align-items: center; gap: 7px; flex: 1 1 12rem;
  font-size: 13px; font-weight: 600; color: var(--seaglass-ink);
}
.logo-attach.is-done .logo-attach__img { border-color: var(--seaglass); }

/* The hint under the box, not beside it — a span next to an inline-block
   input otherwise lines up alongside and reads as a label for nothing. */
.nl-zip .field__hint { display: block; margin-top: 4px; }

/* The room within a venue, on a card. After the venue - "which building" is
   what somebody navigates by, "which room" is what they need once they are
   inside - but no longer quieter than it.

   IT WAS --ink-faint AND THAT STOPPED WORKING the moment the venue went to full
   text colour (owner, 14 Sep 2026: "the area/room location name should also be
   of normal color, otherwise it looks odd"). They are right, and the reason is
   worth keeping: three shades on one line - dark venue, faint room, faint
   separator - reads as something failing to load rather than as a hierarchy.
   The ORDER already says which matters more; the colour was saying it twice
   and, at this contrast, badly.

   NO COLOUR DECLARED NOW, so it inherits .event-meta. One place to change if
   that line is ever retoned again, rather than two that have to be remembered
   together - which is exactly how these two came apart. */
.event-meta__room {
  font-style: normal;
}
/* The separator stays faint: it divides, it does not say anything. */
.event-meta__room::before { content: ' · '; color: var(--ink-faint); }

/* ---- NOT ON A PHONE, AND NOT IN THE APP (owner, 13 Sep 2026) -------------

   "do not show the room/area location in the mobile and app versions."

   THE LINE HAD THREE JOBS AND ONE LINE TO DO THEM IN. "Norwalk Public Library
   · Norwalk · Children's Activity Room" wraps to two lines on a 375px card,
   and the part that wraps is the part nobody needs yet: which room matters
   once you are through the door, and you are not through the door while
   scrolling a list. The venue and the town are what a reader is choosing
   between.

   STILL ON THE LISTING PAGE, which is the page somebody opens on the way
   there - see the WHERE block in event.js, where the room sits between the
   building and the street in the order you walk it. Nothing is lost, it is
   just not spent on the tile.

   THE SAME PAIR OF RULES AS .event-desc ABOVE, and in the same order for the
   same reason: equal specificity, so the media query has to come after to
   win, and `.in-app` outranks both anyway. */
.in-app .event-meta__room { display: none; }
@media (max-width: 620px) {
  .event-meta__room { display: none; }
}

/* The town, between the venue and the room. Readable rather than faint: it is
   the thing somebody scanning a list of forty evenings filters on in their
   head, so it has to survive a glance. */
/* The town reads as part of the same answer as the venue, so it takes the
   same colour; only the separator stays faint, so it divides without
   competing. */
.event-meta__town { color: var(--ink); }
.event-meta__town::before { content: ' · '; color: var(--ink-faint); }

/* ---- Sync page: the three section tabs and the menu read ------------------
   The tabs reuse the site's own .toggle look; the margin separates them from
   the H1 without inventing a second toolbar style. */
.syn-tabs { margin: var(--sp-3) 0 var(--sp-4); }

/* A menu read's results. Same table family as the scan results (.syn-t), with
   the description column allowed to breathe — a dish's own write-up is the
   thing being judged, unlike an event row where the date is. */
.syn-menu { margin-top: var(--sp-3); }
.syn-menu__sec {
  font-style: normal; font-size: 11.5px; color: var(--ink-faint);
  text-transform: uppercase; letter-spacing: .04em;
}
.syn-menu__desc { font-size: 13px; color: var(--ink-soft); max-width: 34rem; }

/* ---- Instant tooltips on the scan table's icon buttons --------------------

   The row controls are icon-only — four of them, used dozens of times an hour
   by one person — and each already carried a `title`. A native tooltip takes
   a second or two to appear, renders in the OS's own small grey box, and is
   easy to miss entirely; the report that "there are no descriptions" was
   really "they arrive too late to be part of using the page".

   So the text moved to `data-tip` and is drawn here at once. `aria-label`
   still carries the same words for screen readers; `title` is gone rather
   than kept alongside, because two tooltips for one control is worse than
   the slow one on its own. */
.syn-act[data-tip], .syn-tag[data-tip], .syn-pick-no[data-tip] { position: relative; display: inline-block; }
.syn-act[data-tip]::after, .syn-tag[data-tip]::after, .syn-pick-no[data-tip]::after {
  content: attr(data-tip);
  position: absolute; bottom: calc(100% + 7px); left: 50%;
  transform: translateX(-50%);
  /* Sized to the words rather than the 34px button, and never wrapped to one
     character per line — which is what `width: max-content` prevents. */
  width: max-content; max-width: 15rem;
  padding: 6px 9px; border-radius: var(--radius-sm);
  background: var(--ink); color: #fff;
  font-family: var(--font-body); font-size: 12px; font-weight: 500;
  line-height: 1.35; text-align: center; white-space: normal;
  opacity: 0; visibility: hidden; transition: opacity .12s;
  /* `display: none` until hover, ON TOP of the visibility pair: a hidden box
     that is merely invisible still counts toward the scroll area, and the
     right-most button's tooltip was handing the whole results table a
     horizontal scrollbar over nothing. The fade-in is lost; the fade mattered
     less than the phantom scroll. */
  display: none;
  pointer-events: none;          /* never eats the click it describes */
  z-index: 20;
}
.syn-act[data-tip]:hover::after,
.syn-act[data-tip]:focus-visible::after,
.syn-tag[data-tip]:hover::after,
.syn-pick-no[data-tip]:hover::after {
  opacity: 1; visibility: visible; display: block;
}
/* A disabled button still explains itself — that is the one whose reason
   matters most, since the control is refusing to work. */
.syn-act[disabled][data-tip] { pointer-events: auto; }

/* Whose claim a scanned menu item is. "Their special" = the page marked it
   out; "From the menu" = we picked it. The distinction has to survive onto
   the screen, or our suggestion gets repeated back to a restaurant as their
   own idea. */
.syn-menu__flag {
  display: inline-block; margin-left: 6px; vertical-align: 1px;
  font-style: normal; font-size: 10.5px; font-weight: 700;
  letter-spacing: .04em; text-transform: uppercase;
  color: var(--ink-faint); background: var(--surface-2);
  border: 1px solid var(--line); border-radius: 999px; padding: 1px 7px;
}
.syn-menu__flag--theirs {
  color: var(--seaglass-ink); background: var(--seaglass-tint);
  border-color: var(--seaglass);
}

/* ---- A new client's first import ------------------------------------------
   The card on their dashboard that shows the events we read off their site on
   the day they paid, and asks whether they look right. See
   netlify/functions/lib/first-import.js for why it is a question rather than
   a publication. */
.dash-card--accent {
  border-color: var(--gold, #c9a227);
  box-shadow: 0 1px 0 rgba(0, 0, 0, .03), 0 0 0 3px rgba(201, 162, 39, .10);
}
.fi-list {
  list-style: none;
  margin: .75rem 0;
  padding: 0;
  border-top: 1px solid var(--line, #e6e1d6);
}
.fi-item {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: .5rem 0;
  border-bottom: 1px solid var(--line, #e6e1d6);
  font-size: .95rem;
}
.fi-item__when { color: var(--muted, #6b6455); white-space: nowrap; }
/* The verification note. Small, because it is a reassurance rather than an
   instruction — but not hidden, because it is the promise we are making. */
.dash-note {
  margin-top: .75rem;
  padding-top: .6rem;
  border-top: 1px solid var(--line, #e6e1d6);
  font-size: .85rem;
  color: var(--muted, #6b6455);
}
@media (max-width: 480px) {
  .fi-item { flex-direction: column; gap: .15rem; }
  .fi-item__when { white-space: normal; }
}

/* The "events on another page?" control on the first-import card. Their link
   is the most useful thing a client can give the scanner, so it sits on the
   card rather than three clicks away in Settings. */
.fi-source {
  margin-top: .9rem;
  padding-top: .8rem;
  border-top: 1px solid var(--line, #e6e1d6);
}
.fi-source label {
  display: block;
  font-weight: 600;
  font-size: .9rem;
  margin-bottom: .35rem;
}
.fi-source .coupon__row { display: flex; gap: .5rem; }
.fi-source input { flex: 1 1 auto; min-width: 0; }

/* ---- The owner's first-import desk ---------------------------------------
   A client whose scan came back blank has usually told us where their events
   are; these rows are where that gets acted on. */
.fi-owner {
  list-style: none;
  padding: .75rem 0;
  border-bottom: 1px solid var(--line, #e6e1d6);
}
.fi-owner:last-child { border-bottom: 0; }
.fi-owner--late .fi-owner__head strong { color: var(--red, #9b2c2c); }
.fi-owner__head { margin: 0 0 .2rem; }
.fi-owner__src { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .85em; }
.fi-owner__row { display: flex; gap: .5rem; margin: .5rem 0; }
.fi-owner__row input { flex: 1 1 auto; min-width: 0; }
.fi-owner__acts { margin-top: .35rem; }
@media (max-width: 480px) {
  .fi-owner__row { flex-direction: column; }
}

/* Posting on a client's behalf. Distinct from the "this page is yours" note
   beside it, because the two say opposite things about where a post lands. */
.signup-info--as {
  border-left: 3px solid var(--gold, #c9a227);
  background: rgba(201, 162, 39, .07);
}

/* ---- Files waiting to be sent with a submission --------------------------
   A client sending a flyer per workshop picks them a few at a time, so the
   list has to show what is already staged and let one be taken back out. */
.sub-files { list-style: none; margin: .5rem 0 0; padding: 0; }
.sub-files__item {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .3rem 0;
  border-bottom: 1px solid var(--line, #e6e1d6);
  font-size: .9rem;
}
.sub-files__item:last-child { border-bottom: 0; }
.sub-files__name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sub-files__size { color: var(--muted, #6b6455); font-size: .85em; white-space: nowrap; }
.btn--xs { padding: .15rem .5rem; font-size: .8rem; }

/* ---- An event that is not happening, or not happening yet ----------------
   Cancelling used to unpublish the listing, so it vanished — and the resident
   who read it on Tuesday and arranged their Saturday around it found nothing
   and turned up anyway. It now stays up wearing this until its own date
   passes. Above the title, because it changes what the title means. */
.event-off {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: .35rem;
  margin: 0 0 .4rem;
  padding: .3rem .55rem;
  border-radius: var(--radius-sm, 6px);
  font-size: .85rem;
  font-weight: 600;
  line-height: 1.35;
}
.event-off svg { flex: 0 0 auto; align-self: center; }
/* Red for "not happening", amber for "happening later" — the two need telling
   apart at a glance, and a reader who sees amber has a date to look for. */
.event-off--cancelled { background: rgba(155, 44, 44, .10); color: var(--red, #9b2c2c); }
.event-off--postponed { background: rgba(201, 162, 39, .16); color: #7a5c00; }
/* Their own words. Lighter, because the state is the headline and this is the
   detail — "back next Saturday" is what a reader actually wants next. */
.event-off__note {
  flex: 1 1 100%;
  font-style: normal;
  font-weight: 400;
  opacity: .85;
}
/* A called-off listing should not shout as loudly as a live one. */
.event-card:has(.event-off) .event-title { text-decoration-color: currentColor; }
.event-card:has(.event-off--cancelled) .event-title { text-decoration: line-through; }

/* Radio options in the call-off dialog. Each answer carries a consequence
   worth reading — one of them hands a post back — so they are rows with room
   for a sentence rather than a line of labels. */
.me-opt {
  display: flex;
  gap: .6rem;
  align-items: flex-start;
  padding: .55rem .65rem;
  margin-top: .4rem;
  border: 1px solid var(--line, #e6e1d6);
  border-radius: var(--radius-sm, 6px);
  cursor: pointer;
}
.me-opt:has(input:checked) {
  border-color: var(--ink, #1f2a44);
  background: rgba(31, 42, 68, .04);
}
.me-opt input { margin-top: .2rem; flex: 0 0 auto; }
.me-opt strong { display: block; font-size: .95rem; }
.me-opt em {
  display: block;
  font-style: normal;
  font-size: .85rem;
  color: var(--muted, #6b6455);
  margin-top: .1rem;
}

/* ---- A client's first import, on the sync page --------------------------
   A spanning row under the client it belongs to. It used to live on the hub,
   which meant scan state was in two places saying two different things about
   the same client. */
.mct__importrow > td { padding: 0 0 .75rem; border-top: 0; }
.syn-import {
  border: 1px solid var(--gold, #c9a227);
  border-left-width: 3px;
  border-radius: var(--radius-sm, 6px);
  background: rgba(201, 162, 39, .07);
  padding: .7rem .8rem;
}
.syn-import--late { border-color: var(--red, #9b2c2c); background: rgba(155, 44, 44, .07); }
/* WHAT THEY SENT US, in a different colour from what we owe them.

   Both blocks hang under the same client and both are work — but one is a scan
   we have not finished and the other is a document sitting in the bucket
   waiting to be read. Same shape, different edge, so a glance down the table
   tells them apart without reading either. */
.syn-import--sent {
  border-color: var(--seaglass);
  background: var(--seaglass-tint);
  margin-top: .5rem;
}
.syn-import--sent .sub-list { margin-top: .35rem; }
.syn-import--sent .sub-row { background: var(--surface); }
/* The buttons sit under everything else rather than beside the file name: a
   row can hold six files, and actions that reflow between them are unreadable. */
.sub-row__acts { grid-column: 1 / -1; margin-top: 6px; }
.syn-import__head { margin: 0 0 .35rem; }
.syn-import .coupon__row { display: flex; gap: .5rem; margin: .5rem 0; }
.syn-import .coupon__row input { flex: 1 1 auto; min-width: 0; }
@media (max-width: 640px) {
  .syn-import .coupon__row { flex-direction: column; }
}

/* A file input dressed as a button, so "upload a logo" sits beside "get logo"
   and reads as the same kind of action rather than a form field. */
.orgs-upload { cursor: pointer; display: inline-flex; align-items: center; }

/* The events an AI read found, listed on the owner's row so "found six" can be
   checked rather than trusted. */
.syn-import__found { margin: .5rem 0; }
.syn-import__found > summary {
  cursor: pointer;
  font-weight: 600;
  font-size: .9rem;
  padding: .2rem 0;
}
.syn-import__found .fi-list { margin: .35rem 0 0; }

/* ---- Quick post ----------------------------------------------------------
   The drafter, on the CLIENT's posting page. Louder than the owner's version,
   which is a closed fold reading "start from something I already have" — fine
   for somebody who uses the page daily, invisible to somebody who opens it
   twice a month. */
.quickpost {
  border: 2px solid var(--gold, #c9a227);
  background: linear-gradient(rgba(201, 162, 39, .06), rgba(201, 162, 39, .02));
  margin-bottom: var(--sp-4, 1.25rem);
}
.quickpost__title {
  display: flex;
  align-items: center;
  gap: .45rem;
  margin: 0 0 .3rem;
  font-size: 1.25rem;
}
.quickpost .form-actions { margin-top: .5rem; }

/* The full preview of what a first import found — enough to catch a wrong date
   or a description that is really a button. */
.fi-item--full { display: block; padding: .5rem 0; }
.fi-item--full strong { display: block; }
.fi-item--full .fi-item__when { display: inline; }
.fi-item__meta { color: var(--muted, #6b6455); font-size: .85em; margin-left: .5rem; }
.fi-item__desc {
  display: block;
  margin-top: .2rem;
  font-size: .85em;
  color: var(--muted, #6b6455);
}
.fi-item__desc--none { font-style: italic; opacity: .7; }

/* ---- Fixing a scan before it goes up -------------------------------------
   The preview IS the editor. Times are the field a scan misses most, so date
   and time sit on the surface and the rest folds away. */
.fi-list--edit { list-style: none; margin: .5rem 0 0; padding: 0; counter-reset: fi; }
.fi-edit {
  border: 1px solid var(--line, #e5e0d5);
  border-radius: var(--r-2, 10px);
  padding: .75rem;
  margin-bottom: .6rem;
  background: var(--card, #fff);
}
.fi-edit__keep {
  display: flex;
  align-items: center;
  gap: .4rem;
  font-size: .9rem;
  margin-bottom: .5rem;
}
.fi-edit__keep input { width: auto; }
.fi-edit__title { display: block; margin-bottom: .5rem; }
.fi-edit__when { display: flex; gap: .5rem; flex-wrap: wrap; }
.fi-edit__when label { flex: 1 1 8rem; min-width: 7rem; }
.fi-edit .field__label { display: block; font-size: .78rem; margin-bottom: .15rem; }
.fi-edit__more { margin-top: .5rem; }
.fi-edit__more summary { font-size: .85rem; cursor: pointer; }
.fi-edit__more label { display: block; margin-top: .45rem; }
/* An unticked event is going in the bin — it should look like it. */
.fi-edit.is-out { opacity: .5; }
.fi-edit.is-out .fi-edit__when,
.fi-edit.is-out .fi-edit__title,
.fi-edit.is-out .fi-edit__more { display: none; }

/* A repeating run that stops before the site stops showing it. Not an error —
   most runs are meant to end — so it flags rather than alarms. */
.syn-row--warn { border-left: 3px solid var(--gold, #c9a227); padding-left: .6rem; }

/* ---- Seeing the picture before it goes up --------------------------------
   "Did the scan get the images?" was a question nobody could answer without
   publishing and going to look. */
.fi-edit__pic { display: flex; gap: .6rem; align-items: flex-start; margin-bottom: .5rem; }
.fi-edit__thumb {
  width: 84px; height: 56px; object-fit: cover;
  border-radius: var(--r-1, 6px);
  border: 1px solid var(--line, #e5e0d5);
  background: var(--wash, #f4f0e6);
}
.fi-edit__picnote { font-size: .82em; color: var(--muted, #6b6455); line-height: 1.35; }
.fi-edit__picnote--none { font-style: italic; }
.fi-edit__pic.is-broken .fi-edit__thumb { display: none; }

/* The client's own review of the same scan. */
.fi-item--review { display: flex; gap: .6rem; align-items: flex-start; padding: .5rem 0; }
.fi-item__thumb {
  flex: 0 0 auto;
  width: 64px; height: 44px; object-fit: cover;
  border-radius: var(--r-1, 6px);
  border: 1px solid var(--line, #e5e0d5);
  background: var(--wash, #f4f0e6);
}
.fi-item__thumb--none {
  display: flex; align-items: center; justify-content: center;
  color: var(--muted, #6b6455);
}
.fi-item__body { display: block; }

/* ---- A post history that groups by name ----------------------------------
   A weekly class booked to the horizon is eighteen rows; a client of a year
   is hundreds. Folded, the run is one line that opens to its dates. */
.dash-sched__group { margin: 0; }
.dash-sched__group > summary { cursor: pointer; list-style: none; }
.dash-sched__group > summary::-webkit-details-marker { display: none; }
/* ONE CHEVRON FOR EVERY FOLD (owner, 16 Sep 2026: "let's just use the same
   drop-down format as we have already built"). These rows drew a text
   triangle of their own on the left; the Tickets card and every NL.oneLine
   fold (.join-more) carry the chevronD icon at the right edge, turned over
   when open. Same icon, same edge, same turn, here and on the two
   .dash-sched__older lines below.

   The title keeps its auto margin, so the count and the chevron sit together
   at the right edge and every row starts in the same column however long its
   title is. */
.dash-sched__row--group { font-weight: 600; justify-content: flex-start; gap: .45rem; }
.dash-sched__row--group .dash-sched__name { margin-right: auto; }
.dash-sched__row--group > svg:last-child { flex: none; color: var(--ink-faint); transition: transform .18s; }
.dash-sched__group[open] > summary > .dash-sched__row--group > svg:last-child { transform: rotate(180deg); }
.dash-sched__group > summary:hover .dash-sched__row--group > svg:last-child { color: var(--ink); }
.dash-sched__group[open] > summary { border-bottom: 1px solid var(--line, #e5e0d5); }
.dash-sched__group .dash-sched__row:not(.dash-sched__row--group) {
  padding-left: 1.4rem;
  background: var(--wash, #faf7f0);
}

/* ---- AND EVERYTHING OLDER THAN THE RECENT THREE, BEHIND ONE LINE ---------
   Owner, 14 Sep 2026: the post history is busy, so only the most recent three
   posts sit on the page and the rest fold away. See groupedHistory().

   A SEPARATE CLASS FROM .dash-sched__group ON PURPOSE, though the two look
   alike. The rule directly above indents its children and washes them, which
   is right for the dates inside one run and wrong here: what folds away is
   whole posts, and indenting them would say they belong to something. Runs
   nested inside this fold are still .dash-sched__group and still get their own
   indent, which is the behaviour worth keeping.

   QUIETER THAN A POST ROW. It is a way through to more, not another thing to
   read, so it sits smaller and softer than the titles above it — otherwise the
   line that exists to reduce the noise joins in. */
.dash-sched__older { margin: 0; border-top: 1px solid var(--line); }
.dash-sched__older > summary {
  display: flex; align-items: center; gap: .45rem;
  cursor: pointer; list-style: none;
  padding: var(--sp-2) 0;
  font-size: 13.5px; color: var(--ink-soft);
}
.dash-sched__older > summary::-webkit-details-marker { display: none; }
/* The chevron, not a triangle: see .dash-sched__row--group above. */
.dash-sched__older > summary > svg:last-child { margin-left: auto; flex: none; color: var(--ink-faint); transition: transform .18s; }
.dash-sched__older[open] > summary > svg:last-child { transform: rotate(180deg); }
.dash-sched__older > summary:hover { color: var(--ink); }
.dash-sched__older > summary:hover > svg:last-child { color: var(--ink); }
.dash-sched__older > summary:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }

/* A scanned run, folded into the one row that edits it. */
.fi-edit--run { border-left: 3px solid var(--gold, #c9a227); }
.fi-run__head { margin: 0 0 .2rem; font-size: .95rem; }
.fi-run__dates { margin-top: .6rem; }
.fi-run__dates summary { font-size: .85rem; cursor: pointer; }
.fi-run__date { display: flex; align-items: center; gap: .4rem; padding: .2rem 0; font-size: .9rem; }
.fi-run__date input { width: auto; }
.fi-run__date em { color: var(--muted, #6b6455); font-style: italic; }

/* Who a scanned listing is for, and a listing filed under a town nothing matches. */
.fi-edit__aud { margin: .5rem 0; }
.fi-edit__aud .checks { display: flex; flex-wrap: wrap; gap: .3rem .8rem; margin-top: .25rem; }
.fi-edit__aud .check { font-size: .85rem; }
.syn-stranded { border: 2px solid var(--gold, #c9a227); }

/* The rest of a post's form, on a scanned row. */
.fi-edit__town { display: block; margin: .5rem 0; }
.fi-edit__rep { margin: .5rem 0; }
.fi-edit__rep summary { font-size: .85rem; cursor: pointer; }
.fi-edit__rep .fi-run__every { margin-top: .4rem; }

/* ---- The prospect shortlist ------------------------------------------------
   A star in the first cell of the prospects table, and the row it belongs to.

   OUTLINE UNTIL IT IS ON. An unstarred row should read as ordinary — forty
   filled stars would be forty things shouting, which is the problem starring
   exists to solve. Off is a faint outline that only resolves into a control
   when you go near it; on is filled, in the accent colour, and stays that way
   without a hover.
--------------------------------------------------------------------------- */
.syn-star {
  float: left; margin: 1px var(--sp-2) 0 0; padding: 0;
  background: none; border: 0; cursor: pointer; line-height: 0;
  color: var(--line-strong);
  border-radius: var(--radius-sm);
  transition: color .12s ease, transform .12s ease;
}
.syn-star svg { display: block; fill: none; stroke: currentColor; stroke-width: 1.75; }
.syn-star:hover { color: var(--ink-soft); transform: scale(1.12); }
.syn-star.is-on { color: var(--accent); }
.syn-star.is-on svg { fill: currentColor; }
.syn-star:disabled { opacity: .5; cursor: default; transform: none; }
.syn-star:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* The row itself: a tint rather than a border, so a starred row reads as
   lifted out of the list without changing the table's alignment. */
.mct tr.is-starred > td { background: var(--surface-2); }
.mct tr.is-starred .mct__link strong { color: var(--ink); }

/* Town and Repeats: their WIDTHS live with every other column's, in the budget
   block above. Written here originally, a thousand lines from the total they
   had to fit inside, which is how the event name ended up 0px wide. */
.syn-t__town { white-space: nowrap; }
.syn-t__rep .syn-t__from { display: block; }

/* AND IT CANNOT LEAVE ITS OWN COLUMN, whatever ends up in it.

   REPORTED, circled: "Every month on the third Saturday" ran across the STATUS
   dropdown beside it. repeatCell now prints the short shape rather than the
   sentence, which is the real fix - see its own note. This is the guard behind
   it: `.syn-t__tab` is nowrap so a category pill never splits mid-label, and
   nowrap in a seven per cent column is a promise the layout cannot keep. In
   THIS cell the chip may wrap, and the cell clips, so a cadence nobody has
   written yet cannot reopen the same bug. */
.syn-t__rep { overflow: hidden; }
.syn-t__rep .syn-t__tab { white-space: normal; max-width: 100%; }

/* ---- Private invitations --------------------------------------------------
   invite.html, and the invitation strip on a private detail page. Everything
   else on that form reuses `.field`, `.signup-title` and `.btn`; these are the
   three things the site genuinely did not already have. */

/* THE FORK, AND THE TWO ACCESS MODES. Cards rather than radio buttons because
   each option carries a sentence explaining what it costs you — "just the link"
   against "the link and an account" is a real trade-off, not a preference, and
   a radio with a label cannot hold the reasoning. */
.pickcards { display: grid; gap: var(--sp-3); margin-top: var(--sp-4); }
@media (min-width: 620px) { .pickcards--two { grid-template-columns: 1fr 1fr; } }
.pickcard {
  display: flex; flex-direction: column; gap: 5px; text-align: left;
  padding: var(--sp-4); border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface); font: inherit; cursor: pointer;
  transition: border-color .15s, box-shadow .15s, background .15s;
}
.pickcard:hover { border-color: var(--line-strong); }
/* One of these is an <a> — the "private, by invitation" card on the member post
   form, which genuinely goes somewhere. It must not arrive underlined and blue
   in a row with a <button> that looks nothing like it. */
a.pickcard, a.pickcard:hover { text-decoration: none; color: inherit; }
.pickcard.is-on { border-color: var(--primary); box-shadow: inset 0 0 0 1px var(--primary); }
.pickcard__title {
  font-family: var(--font-display); font-size: 16px; font-weight: 600; color: var(--ink);
  display: inline-flex; align-items: center; gap: 7px;
}
.pickcard.is-on .pickcard__title { color: var(--primary-ink); }
.pickcard__sub { font-size: 13.5px; line-height: 1.5; color: var(--ink-soft); }
/* The road not taken. Shown rather than hidden so somebody can see what the
   site does and why this half is closed to them — see openOrPrivate(). */
.pickcard--off { background: var(--surface-2); border-style: dashed; cursor: default; }
.pickcard--off .pickcard__title { color: var(--ink-faint); }

/* THE GUEST LIST. A plain list of addresses with a way to take one out; it is
   a record of who you typed, not a table. */
.guests { list-style: none; margin: var(--sp-3) 0 0; padding: 0; display: grid; gap: 4px; }
.guest {
  display: flex; align-items: center; gap: 2px;
  padding: 7px 10px; border: 1px solid var(--line); border-radius: var(--radius-sm, 8px);
  background: var(--surface); font-size: 14px;
}
.guest__email { color: var(--ink); overflow-wrap: anywhere; }
.guest__name { color: var(--ink-faint); font-size: 13px; }
.guest__x {
  flex: none; display: inline-flex; align-items: center;
  justify-content: center; width: 26px; height: 26px; border-radius: 50%;
  border: 0; background: transparent; color: var(--ink-faint); cursor: pointer;
}
.guest__x:hover { background: var(--surface-2); color: var(--destructive); }

/* The share link, on the confirmation screen and on a private detail page.
   Readonly and selected on click: it is long, it is the only copy of it, and
   people will want to paste it somewhere. */
.linkbox {
  flex: 1; min-width: 220px; font-size: 13px; padding: 9px 11px;
  border: 1px solid var(--line); border-radius: var(--radius-sm, 8px);
  background: var(--surface-2); color: var(--ink-soft);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}

/* The guest list beside the form that fills it. Adding somebody moves them
   from the right-hand column to the left-hand one, which is the whole point —
   you can see that it worked. One column until there is room for two. */
.guest-cols { display: grid; gap: var(--sp-4); margin-top: var(--sp-4); }
@media (min-width: 760px) { .guest-cols { grid-template-columns: 1fr 1fr; align-items: start; } }
.guest-cols__list .guests { margin-top: 0; }
.guest-add .field:first-child { margin-top: 0; }
/* The text pushes the controls right, rather than each control pushing itself
   there. `margin-left: auto` on the buttons put a gap between the pencil and
   the cross — the first one shoved itself to the right edge and the second
   shoved itself past it, so a pair that belongs together sat half a row
   apart. */
.guest__who { display: flex; flex-direction: column; gap: 1px; min-width: 0; margin-right: auto; }
.guest__nm { font-weight: 700; color: var(--ink); }
.guest.is-editing { border-color: var(--primary); box-shadow: inset 0 0 0 1px var(--primary); }
/* The address book, folded away: it is a convenience for the second invitation
   and must not be the first thing somebody reads on their first. */
/* ABOVE THE FIELDS NOW, so the margin is below it rather than above. Open by
   default (see bookPicker in invite.js), and given a tinted ground so the two
   ways into the guest list read as two rather than as one long form. */
.guest-book {
  margin: 0 0 var(--sp-4); padding: 10px 12px;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--radius);
}
.guest-book > summary { display: flex; align-items: center; gap: 7px; }
.guest-book > summary { font-size: 13.5px; font-weight: 700; color: var(--ink-soft); cursor: pointer; }
.guest-book__list { list-style: none; margin: var(--sp-3) 0 0; padding: 0; display: grid; gap: 3px; max-height: 240px; overflow: auto; }
.guest-book__add {
  display: flex; align-items: baseline; gap: 7px; width: 100%; text-align: left;
  font: inherit; font-size: 14px; color: var(--ink); cursor: pointer;
  background: transparent; border: 1px solid transparent; border-radius: 8px; padding: 6px 8px;
}
.guest-book__add:hover { background: var(--surface-2); border-color: var(--line); }

/* The address book in resident settings. One person per block rather than a
   table: a table of four editable columns is unusable on a phone, and this is
   edited rarely and read even less. */
/* One line per person, opening to their fields. The gap is tight because these
   are now rows in a list rather than cards in a stack — the old spacing was
   sized for four open inputs each. */
.contact-rows { list-style: none; margin: var(--sp-4) 0 0; padding: 0; display: grid; gap: var(--sp-2); }
.contact-row { border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); }
.contact-row__sum {
  display: flex; align-items: baseline; gap: var(--sp-2);
  padding: 11px var(--sp-4); cursor: pointer; list-style: none;
}
.contact-row__sum::-webkit-details-marker { display: none; }
.contact-row__sum:hover { background: var(--surface-2); }
.contact-row__mail { color: var(--ink-faint); font-size: 13.5px; overflow-wrap: anywhere; }
.contact-row__note {
  color: var(--ink-faint); font-size: 13px; font-style: italic;
  margin-left: auto; padding-left: var(--sp-3); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; max-width: 40%;
}
.contact-row__chev { margin-left: auto; color: var(--ink-faint); display: flex; flex: none; }
.contact-row__note + .contact-row__chev { margin-left: var(--sp-2); }
.contact-row__d[open] .contact-row__chev { transform: rotate(180deg); }
.contact-row__body { padding: 0 var(--sp-4) var(--sp-4); border-top: 1px solid var(--line); }
.contact-row .field:first-child { margin-top: 0; }
/* Adding somebody is a different job from editing the book, so it gets its own
   panel above the list rather than a row inside it. */
.contact-add {
  padding: var(--sp-4); border: 1px solid var(--line);
  border-radius: var(--radius); background: var(--surface-2);
}
.contact-add .field:first-child, .contact-add .field-row { margin-top: 0; }
.contact-row__acts { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin: var(--sp-4) 0 0; }
.contact-row__acts { margin-top: var(--sp-3); display: flex; gap: var(--sp-2); flex-wrap: wrap; }

/* The guest column identifies itself before there is anything in it — see
   guestList(). Without a heading the left half of that row was one grey
   sentence with no sign it was where the list would build. */
.guests__head {
  font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
  color: var(--ink-faint); margin: 0 0 var(--sp-3);
  display: flex; align-items: baseline; gap: 8px;
}
.guests__count { text-transform: none; letter-spacing: 0; font-size: 12.5px; color: var(--ink-soft); }

/* ---- A HEADER THAT GETS OUT OF THE WAY ---------------------------------

   ASKED FOR, on the scan results page: "when I am in the scan results page I
   need space to see the items I am working with. I don't need any of that on
   the top, except the logo up top and the navy blue admin bar all the way up
   top. That's it."

   The scan desk is a wide table read a row at a time, and everything above it
   was costing about 150 pixels of the screen it is read on: the owner chip and
   Sign out, a second Sign in or join, the town picker, the Newsletter link and
   the Things to do / Sips & Bites tabs - none of which mean anything while
   somebody is checking what a scan made of a library's calendar.

   OPTED IN BY THE PAGE, not applied to every admin screen, because the other
   admin pages are short enough to want their own navigation. sync.html carries
   `data-chrome="focus"` and nothing else does.

   THE BRAND STAYS AND SO DOES THE ADMIN BAR. The logo is the way back to the
   site, and the navy bar carries the way back to the admin pages and the Exit
   button - which is also where signing out still lives, since Sign out goes
   with the rest of the strip. */
body[data-chrome="focus"] .header-tools,
body[data-chrome="focus"] .section-nav { display: none; }
/* Tighter, now that the row carries one thing. The header padding was set for
   two rows of controls wrapping under the brand. */
body[data-chrome="focus"] .site-header__inner { padding-top: 6px; padding-bottom: 6px; }

/* NO OFFSET FOR THE COLUMN HEADINGS, AND HERE IS WHY IT WAS TRIED.

   I gave them `top: 67px` here, reasoning that a heading sticky at `top: 0`
   would slide under a site header that is also sticky at `top: 0`. That is
   true of a table that scrolls with the PAGE and this one does not:
   `.syn-table-scroll` above gives it its own box with `overflow: auto` and a
   max-height, so the headings stick to the top of THAT and never meet the site
   header at all.

   The offset therefore pushed them 67px down inside their own container and
   left a gap above them, which the first row showed through - reported as "the
   very first result is overlaid, it's underneath the column headings", and
   correctly.

   The base rule was right all along. Left as a note rather than deleted
   because the reasoning is tempting and wrong, and the thing that makes it
   wrong is two hundred lines away. */

/* ---- SEARCHING A SCAN BY TITLE -----------------------------------------

   ASKED FOR: "give me a search bar so I can search for specific events,
   specifically in the titles, so I could tackle them all by the same name."

   Sits above the bulk bar, because it decides what the bulk bar is talking
   about: after a search, "Pick every ready draft" means the rows the search
   left. */
.syn-find {
  display: flex; align-items: center; gap: var(--sp-2);
  margin-top: var(--sp-2);
}
.syn-find__lab { display: inline-flex; color: var(--ink-faint); flex: none; }
.syn-find__in {
  flex: 1 1 auto; max-width: 22em;
  padding: 6px 10px; font-size: 13px;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--ink);
}
.syn-find__in:focus, .syn-find__in:focus-visible {
  outline: 2px solid var(--focus); outline-offset: 1px; border-color: var(--primary);
}
/* How many of how many, so a search that finds three of ninety says so rather
   than leaving somebody to wonder where the rest went. */
.syn-find__n { font-size: 12px; color: var(--ink-soft); white-space: nowrap; }
.syn-find__clear { font-size: 12px; }
@media (max-width: 700px) {
  .syn-find__in { max-width: none; }
}

/* The search box and the order control on "Post something again", side by side
   because they answer the same question together: which of four hundred live
   posts am I looking at. The select takes only what it needs. */
.republish-tools { display: flex; gap: var(--sp-2); align-items: center; flex-wrap: wrap; }
.republish-tools .field__input { flex: 1 1 14em; }
.republish-sort { flex: 0 0 auto; }
.republish-sort .field__input { flex: none; width: auto; }

/* ---- WHERE THE CARD FIELD IS NOT ----------------------------------------
   Shown inside the installed app in place of the Stripe form. Deliberately
   calm rather than an error: nothing has gone wrong, the person is simply
   being sent somewhere better to type a card number. */
.pay-on-web {
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface); padding: var(--sp-5); margin-top: var(--sp-4);
}
.pay-on-web__t {
  display: flex; align-items: center; gap: 8px;
  margin: 0 0 6px; font-weight: 700;
}
.pay-on-web__b { margin: 0 0 var(--sp-4); color: var(--ink-soft); font-size: 14.5px; }
.pay-on-web p:last-child { margin: 0; }

/* ---- The scan desk: where the work is (3 Sep 2026) -----------------------
   The Town and Drafts columns on the prospect list, the stage chips above it,
   the per-town headings, the work strip and the three section tables on a
   business's own page, and the "Already up?" badge on a draft row. */
.mct--sync .mct__c-town { width: 150px; }
.mct--sync .mct__c-work { min-width: 210px; }
.syn-townsel { max-width: 150px; font-size: 13px; }
/* A guess is drawn dashed until somebody picks it for real. */
.syn-townsel.is-guess { border-style: dashed; }

.syn-prog { display: flex; flex-direction: column; gap: 4px; min-width: 190px; }
.syn-prog__bar {
  display: flex; height: 6px; border-radius: 3px; overflow: hidden;
  background: var(--surface-2);
}
.syn-prog__seg { display: block; min-width: 3px; }
.syn-prog__seg--posted { background: var(--seaglass-ink); }
.syn-prog__seg--ready  { background: var(--primary); }
.syn-prog__seg--fix    { background: var(--sunset); }
.syn-prog__seg--aside  { background: var(--ink-faint); opacity: .5; }
.syn-prog__txt { font-size: 12.5px; line-height: 1.35; }
.syn-prog__k { font-weight: 700; white-space: nowrap; }
.syn-prog__k--posted { color: var(--seaglass-ink); }
.syn-prog__k--ready  { color: var(--primary-ink); }
.syn-prog__k--fix    { color: var(--sunset); }
.syn-prog__k--aside  { color: var(--ink-faint); font-weight: 500; }
.syn-prog__dot { color: var(--ink-faint); }
.syn-prog__by { display: block; }
/* Compact rows keep the bar and the words; the small print goes, as it does
   everywhere else on the table. */
.syn-dense .mct--sync .syn-prog .mct__sub { display: none; }

.syn-group {
  display: flex; align-items: center; gap: 8px;
  margin: var(--sp-4) 0 var(--sp-2); font-size: 17px;
}
.syn-group svg { color: var(--ink-faint); }
.syn-group:first-of-type { margin-top: var(--sp-2); }

.syn-part { margin-top: var(--sp-4); }
.syn-part__h {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: 0; padding-bottom: 6px; font-size: 17px;
  border-bottom: 1px solid var(--line);
}
.syn-part__h .syn-prog__txt { margin-left: auto; font-weight: 400; }

.syn-work {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3);
  margin: var(--sp-3) 0; padding: var(--sp-3);
  background: var(--surface-2); border-radius: var(--radius);
}
.syn-work .syn-prog { flex: 1 1 260px; }
.syn-work__town { display: flex; align-items: center; gap: 8px; font-size: 13.5px; font-weight: 600; }
.syn-work__town .syn-townsel { background: #fff; }
.syn-work__town .mct__sub { display: none; }

.syn-tag--maybe { background: var(--sunset-tint); color: var(--sunset); }
.syn-tag--up { text-decoration: none; }
.syn-tag--up:hover { filter: brightness(0.94); }
.syn-find__town.is-on { background: var(--primary-tint); border-color: var(--primary); }


/* ============================================================================
   YOUR NUMBERS - the analytics section at the foot of the dashboard.
   See assets/numbers.js. Same tiles for every plan; a plan with more gets
   more of them, never different ones, so upgrading adds tiles rather than
   changing what a member already knows how to read.
   ========================================================================== */

/* TWO GROUPS, SIDE BY SIDE (owner, 8 Sep 2026): what happened overall on the
   left, which posts did it on the right. Each is a quiet panel a shade under
   the card, so the split reads before any number does. */
.num-groups { display: grid; gap: var(--sp-4); margin-top: var(--sp-4); }
@media (min-width: 860px) { .num-groups { grid-template-columns: 1fr 1fr; } }
.num-group {
  background: var(--paper); border: 1px solid var(--line); border-radius: var(--radius);
  padding: var(--sp-3);
}
.num-group__name {
  display: flex; align-items: center; gap: 8px; margin: 0 4px var(--sp-3);
  font-size: 11.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  color: var(--ink-faint);
}
.num-group__name::before {
  content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--primary);
}
.num-tiles { display: grid; gap: var(--sp-3); grid-template-columns: repeat(2, minmax(0, 1fr)); }

/* THE TILE. Icon beside the label, the key number as the only large thing,
   the change under it, and what it is compared with under that. */
.num-tile {
  display: flex; flex-direction: column; gap: 8px; min-width: 0;
  padding: var(--sp-4) var(--sp-4) 13px;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
}
.num-tile__label {
  display: flex; align-items: center; gap: 7px;
  font-size: 12.5px; font-weight: 600; color: var(--ink-soft);
}
.num-tile__label svg { flex: none; color: var(--primary); }
.num-tile__n {
  font-family: var(--font-display); font-size: 34px; font-weight: 600;
  line-height: 1.05; letter-spacing: -.01em; color: var(--ink); overflow-wrap: anywhere;
}
.num-tile__n small {
  font-family: var(--font-body); font-size: 12.5px; font-weight: 600; color: var(--ink-faint);
  letter-spacing: 0; margin-left: 6px;
}
/* A title rather than a number: the answer is the post, the count is under it. */
.num-tile__title {
  font-family: var(--font-display); font-size: 17px; font-weight: 600; line-height: 1.25;
  letter-spacing: -.01em; color: var(--ink); overflow-wrap: anywhere;
}
.num-tile__title .num-tile__n { display: block; font-size: 26px; margin-top: 3px; }
.num-tile__word { font-size: 26px; }
.num-tile__word small { display: block; margin: 4px 0 0; }
/* THE CHANGE. Sage up, sunset down, grey for flat and for "New" - the same
   reasoning as .mstat__d on the owner's page: nothing is wrong when a Tuesday
   is quiet, and the arrow carries the direction for anyone who cannot read
   the colour. */
.num-delta {
  display: inline-flex; align-items: center; gap: 5px; width: fit-content;
  padding: 3px 8px 3px 6px; border-radius: var(--radius-pill);
  font-size: 12.5px; font-weight: 700; white-space: nowrap;
}
.num-delta svg { width: 12px; height: 12px; }
.num-delta--up   { color: var(--signal-green); background: var(--seaglass-tint); }
.num-delta--down { color: var(--sunset-ink); background: var(--sunset-tint); }
.num-delta--flat, .num-delta--new { color: var(--ink-faint); background: var(--surface-2); }
.num-tile__vs { font-size: 11.5px; color: var(--ink-faint); margin-top: -4px; line-height: 1.4; }
.num-tile__vs--only { margin-top: auto; }

/* The strip under the tiles: what people are looking for, as chips. Static,
   so they are spans and not the pressable .chip buttons the filter bars use. */
.num-demand {
  margin-top: var(--sp-4); padding: var(--sp-4);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
}
.num-demand__head {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  font-size: 12.5px; font-weight: 600; color: var(--ink-soft); margin-bottom: var(--sp-3);
}
.num-demand__head svg { color: var(--primary); }
.num-demand__when { margin-left: auto; font-weight: 500; color: var(--ink-faint); }
.num-cats { display: grid; gap: var(--sp-4) var(--sp-5); }
@media (min-width: 720px) { .num-cats { grid-template-columns: 1fr 1fr; } }
.num-cat__name {
  font-size: 12px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  color: var(--ink-faint); margin-bottom: 8px;
}
.num-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.num-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 11px 5px 12px; border-radius: var(--radius-pill);
  border: 1px solid var(--line); background: var(--paper);
  font-size: 13px; font-weight: 600; color: var(--ink);
}
/* SAGE for a term one of their own posts already answers to: guidance, the
   same hue the site uses for a tip, never the alarm colours. */
.num-chip--match { border-color: var(--seaglass); background: var(--seaglass-tint); color: var(--seaglass-ink); }
.num-chip__arr { font-size: 10px; font-weight: 800; }
.num-chip__arr--up { color: var(--signal-green); }
.num-chip__arr--down { color: var(--sunset-ink); }
.num-chip__arr--flat { color: var(--ink-faint); }
.num-legend { display: flex; gap: 14px; flex-wrap: wrap; margin-top: var(--sp-3); font-size: 11.5px; color: var(--ink-faint); }
.num-legend__k { display: inline-flex; align-items: center; gap: 5px; }
.num-legend__swatch { width: 10px; height: 10px; border-radius: 3px; background: var(--seaglass-tint); border: 1px solid var(--seaglass); }
.num-empty { font-size: 13px; color: var(--ink-faint); }

/* SUGGESTIONS, Full Shenanigans only. A sage panel each, like the site's
   other tips; the one button that does something is navy. */
.num-insights { margin-top: var(--sp-4); padding-top: var(--sp-4); border-top: 1px solid var(--line); display: grid; gap: 8px; }
.num-insights__title { display: flex; align-items: center; gap: 8px; font-size: 12.5px; font-weight: 600; color: var(--ink-soft); }
.num-insights__title svg { color: var(--seaglass-ink); }
.num-insight {
  display: grid; grid-template-columns: auto 1fr; gap: 12px; align-items: center;
  padding: 10px 12px; border-radius: var(--radius-sm); background: var(--seaglass-tint);
}
@media (min-width: 720px) { .num-insight { grid-template-columns: auto 1fr auto; } }
.num-insight__spark {
  width: 28px; height: 28px; border-radius: 8px; display: grid; place-items: center;
  background: var(--surface); color: var(--seaglass-ink);
}
.num-insight p { margin: 0; font-size: 13px; color: var(--ink); line-height: 1.45; }
.num-insight p b { font-weight: 700; }
.num-insight__why { display: block; color: var(--ink-faint); font-size: 12px; margin-top: 1px; }
.num-insight__acts { display: flex; gap: 6px; grid-column: 2; }
@media (min-width: 720px) { .num-insight__acts { grid-column: auto; } }
.num-insight[hidden] { display: none; }

/* THE LOCK, for a Basic account: the Plus tiles drawn from sample figures,
   blurred, with the one sentence that says what they would show. */
.num-locked { position: relative; }
.num-locked .num-groups, .num-locked .num-demand { filter: blur(4px); opacity: .7; pointer-events: none; user-select: none; }
.num-lock { position: absolute; inset: 0; display: grid; place-items: center; padding: var(--sp-4); }
.num-lock__card {
  background: var(--surface); border: 1px solid var(--line); box-shadow: var(--shadow);
  border-radius: var(--radius); padding: var(--sp-5) var(--sp-5) var(--sp-4);
  text-align: center; max-width: 340px;
}
.num-lock__ico {
  width: 38px; height: 38px; border-radius: 12px; margin: 0 auto 10px;
  display: grid; place-items: center; background: var(--primary-tint); color: var(--primary);
}
.num-lock__card h3 { font-family: var(--font-display); font-size: 19px; font-weight: 600; margin: 0; }
.num-lock__card p { margin: 6px 0 14px; color: var(--ink-faint); font-size: 13px; line-height: 1.5; }

/* The little sample note on the owner's preview, and the "counting" line. */
.num-note { font-size: 12px; color: var(--ink-faint); margin-top: var(--sp-3); }

/* ---- THE GUIDE, AS A DIALOG (owner, 9 Sep 2026) ---------------------------
   "What happens next" used to fold at the foot of the dashboard; it opens
   from the "i" beside "Your posts" now, in the site's wide dialog
   (.modal.modal--wide, above). Only the guide's own spacing is set here. */
.modal .dash-guide { margin: var(--sp-3) 0 0; }
.modal .dash-guide .dash-guide__step { font-size: 14.5px; }

/* A note that is a warning: the repeat preview saying the run would not start
   on the date chosen. Sunset, not red - nothing has gone wrong yet. */
.form-note--warn { color: var(--sunset-ink); font-weight: 600; }

/* ---- INVITATIONS YOU HAVE RECEIVED (owner, 9 Sep 2026) --------------------
   The count on the host tile's button, and the list under the tiles. */
.btn__n--alert { background: var(--accent); color: #fff; font-weight: 700; }
.inv-got {
  margin: var(--sp-3) 0 var(--sp-2); padding: var(--sp-4);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
}
.inv-got__head { margin-bottom: var(--sp-3); }
.inv-got__row {
  display: grid; grid-template-columns: 1fr; gap: var(--sp-2);
  padding: var(--sp-3) 0; border-top: 1px solid var(--line);
}
@media (min-width: 720px) { .inv-got__row { grid-template-columns: 1fr auto; align-items: center; } }
.inv-got__row.is-fresh {
  background: var(--seaglass-tint); border-top-color: transparent; border-radius: var(--radius-sm);
  margin: 0 calc(-1 * var(--sp-3)); padding-left: var(--sp-3); padding-right: var(--sp-3);
}
.inv-got__row.is-off .inv-got__title { color: var(--ink-faint); text-decoration: line-through; }
.inv-got__title {
  font-family: var(--font-display); font-size: 17px; font-weight: 600; color: var(--ink);
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
}
.inv-got__meta { font-size: 13px; color: var(--ink-soft); margin-top: 2px; }
.inv-got__answer { font-size: 12.5px; font-weight: 600; color: var(--seaglass-ink); margin-top: 2px; }
.inv-got__tag {
  display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: var(--radius-pill);
  font-family: var(--font-body); font-size: 11px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  background: var(--accent-tint); color: var(--accent);
}
.inv-got__tag--off { background: var(--surface-2); color: var(--ink-faint); }
.inv-got__acts { display: flex; flex-wrap: wrap; gap: 6px; }
/* Answered: the green tick, and the answer line in green. "See details" is
   a link, not a fourth button, with air before it. */
.inv-got__tag--ok { background: var(--seaglass-tint); color: var(--signal-green); }
.inv-got__answer--on { color: var(--signal-green); display: inline-flex; align-items: center; gap: 4px; }
.inv-got__answer--none { color: var(--ink-faint); }
.inv-got__acts { align-items: center; }
.inv-got__more {
  display: inline-flex; align-items: center; gap: 2px; margin-left: var(--sp-3);
  font-size: 13px; font-weight: 600; color: var(--ink-soft); text-decoration: none; white-space: nowrap;
}
.inv-got__more:hover { color: var(--primary); text-decoration: underline; }

/* ---- SEND A NUDGE (owner, 9 Sep 2026) ---------------------------------- */
.guests__head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); flex-wrap: wrap; }
.guests__act { display: inline-flex; align-items: center; gap: 8px; flex: 0 0 auto; }
.guests__nudged { font-size: 12px; color: var(--ink-faint); white-space: nowrap; }
/* ---- ON A PHONE: THE PERSON, THEN WHAT YOU CAN DO ABOUT THEM -------------

   REPORTED, circled: "similar issue here with the odd placement on the mobile
   web version... maybe all buttons go below the name and email?" Which is the
   fix, and the reason it looked accidental is worth naming: the row was three
   things sharing a line with `space-between`, and on a narrow screen they
   wrapped one at a time in source order. So a long email pushed the status
   chip onto a second line by itself, left-aligned under the address, and the
   nudge onto a third, right-aligned - three lines, three different edges, none
   of them chosen.

   TWO LINES, DELIBERATELY. The name and the email take the first on their own;
   the chip and the nudge share the second, chip left and action right, which
   is the same left-to-right shape the row has on a wide screen. Nothing wraps
   by accident any more because nothing is competing for the first line.

   `order` RATHER THAN NEW MARKUP: the status chip sits between the name and
   the nudge in the DOM - which is the right reading order and the right order
   for a screen reader - and only the visual arrangement changes here. */
@media (max-width: 560px) {
  .guests__row { flex-wrap: wrap; align-items: center; gap: 6px var(--sp-2); }
  .guests__who { flex: 1 1 100%; }
  .guests__said { order: 2; }
  /* `display: contents` DISSOLVES THE WRAPPER so the button and the date
     become items of the row itself rather than of a box inside it.

     WHY NOTHING ELSE WORKED. The date is full-width inside `.guests__act`, so
     the wrapper's own content demanded the whole row - measured at 327 of
     331px - and could never sit beside a 95px chip whatever flex or order was
     put on it: it was already as wide as the line. Flattening is the fix
     rather than a trick. On a phone these are three things in one list, not
     two things and a group; the grouping exists to hold them together on a
     wide screen and has nothing to do here.

     `margin-left: auto` is what pairs the button with the chip - chip left,
     button right, the same left-to-right shape the row has on a laptop. */
  .guests__act { display: contents; }
  .guests__act .btn { order: 3; margin-left: auto; }
  /* AND THE DATE UNDER BOTH. It comes before the button in the DOM because it
     qualifies the button that follows, which is the right reading order, so
     only where it sits changes. */
  .guests__nudged { order: 4; flex: 1 1 100%; text-align: right; }
}

/* ==========================================================================
   TICKETS SOLD THROUGH THE SITE (11 Sep 2026). Four places: the buy box on a
   listing (.tkt), the buyer's ticket page (.tkt-page), the door (.door__*),
   and the dashboard section (.dash-tickets). See assets/tickets.js,
   assets/ticket.js, assets/door.js, assets/dash-tickets.js.
   ========================================================================== */
.ticket-box { margin-top: var(--sp-5); }
.tkt { border: 1px solid var(--line-strong); border-radius: var(--radius); background: var(--surface); padding: var(--sp-4); box-shadow: var(--shadow); }
.tkt__title { display: flex; align-items: center; gap: 8px; margin: 0 0 var(--sp-3); font-family: var(--font-display); font-size: 20px; }
.tkt__types { display: grid; gap: 6px; }
.tkt__type { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 10px; cursor: pointer; }
.tkt__type:has(input:checked) { border-color: var(--primary); background: var(--primary-tint); }
.tkt__type.is-out { opacity: .55; cursor: default; }
.tkt__type input { margin: 0; }
.tkt__name { flex: 1 1 auto; font-weight: 600; }
.tkt__left { font-style: normal; font-weight: 500; font-size: 12.5px; color: var(--accent); margin-left: 6px; }
.tkt__price { text-align: right; font-weight: 700; white-space: nowrap; }
.tkt__price small { display: block; font-weight: 500; color: var(--ink-faint); font-size: 12px; }
.tkt__form { margin-top: var(--sp-4); }
.tkt__row { display: grid; grid-template-columns: 96px minmax(0, 1fr) minmax(0, 1.3fr); gap: var(--sp-3); }
.tkt__f { display: flex; flex-direction: column; gap: 4px; font-size: 13px; color: var(--ink-soft); }
.tkt__f input, .tkt__f select { font: inherit; font-size: 15px; padding: 9px 10px; border: 1px solid var(--line-strong); border-radius: 8px; background: var(--surface); color: var(--ink); min-width: 0; }
.tkt__foot { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); margin-top: var(--sp-3); flex-wrap: wrap; }
.tkt__total { font-size: 15px; }
.tkt__total small { display: block; color: var(--ink-faint); font-size: 12.5px; }
.tkt__note { margin-top: var(--sp-2); }

/* ---- THE TICKETS A RESIDENT HAS BOUGHT, on their week page ----------------
   Deliberately built out of the invitation list's parts (.inv-got__title,
   __meta, __tag, __more): the two sections sit one above the other on the
   same page and are the same idea - something addressed to you, waiting for
   you to do something about it - so they should not look like two different
   features that happen to share a screen. */
.tkt-mine { margin-top: var(--sp-5); }
.tkt-mine__row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  padding: var(--sp-3); margin-top: var(--sp-2);
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--paper);
}
.tkt-mine__row.is-off { opacity: .68; }
.tkt-mine__main { min-width: 0; }
.tkt-mine__acts { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; flex: none; }
@media (max-width: 620px) {
  .tkt-mine__row { flex-direction: column; align-items: stretch; }
  .tkt-mine__acts { flex-direction: row; align-items: center; justify-content: space-between; }
}
/* The address read back before paying. Quiet, and only there once there is
   something worth reading back. */
.tkt__echo { margin: var(--sp-3) 0 0; font-size: 13.5px; color: var(--ink-soft); }
.tkt__echo strong { color: var(--ink); overflow-wrap: anywhere; }
@media (max-width: 620px) {
  .tkt__row { grid-template-columns: 1fr 1fr; }
  .tkt__f:last-child { grid-column: 1 / -1; }
}

.wrap--narrow { max-width: 640px; }
.tkt-page__hello { display: flex; align-items: center; gap: 8px; color: var(--seaglass-ink); font-weight: 600; margin: 0 0 var(--sp-3); }
.tkt-page__title { font-family: var(--font-display); font-size: 30px; line-height: 1.15; margin: 4px 0 var(--sp-3); text-wrap: balance; }
.tkt-page__code { margin-top: var(--sp-5); text-align: center; border: 2px dashed var(--line-strong); border-radius: var(--radius); padding: var(--sp-5); background: var(--surface); }
.tkt-page__qr { width: min(260px, 70vw); margin: 0 auto var(--sp-3); }
.tkt-page__qr svg { width: 100%; height: auto; display: block; }
.tkt-page__digits { font-family: var(--font-display); font-size: 34px; letter-spacing: .22em; margin: 0; }
.tkt-page__state { margin: var(--sp-2) 0 0; color: var(--ink-faint); font-size: 14px; }
/* Save it to your account, under the code (16 Sep 2026). */
.tkt-page__save { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin: var(--sp-4) 0 0; }
.tkt-page__save .field__hint { flex: 1 1 200px; }
/* THE SAME BOX, ONCE IT IS A SENTENCE. The flex row above is right for the
   button and its hint; it is wrong for the line that replaces them, where the
   gap fell between "Saved." and the rest and again before the full stop, and
   `wrap` broke a short sentence across three lines. A paragraph flows instead:
   the tick sits on the text's own baseline and the stop stays with the link.
   Set by wireSave in ticket.js. */
.tkt-page__save.is-done { display: block; }
.tkt-page__save.is-done svg { vertical-align: -3px; margin-right: 2px; }
/* The code on the week page's ticket row: readable at arm's length. */
.tkt-mine__code { font-family: var(--font-display); font-size: 18px; letter-spacing: .14em; padding: 4px 10px; border: 1px dashed var(--line-strong); border-radius: 8px; background: var(--surface); }
.tkt-page__code.is-void { border-color: var(--destructive); }
.tkt-page__code.is-void .tkt-page__digits { text-decoration: line-through; color: var(--ink-faint); }
.tkt-page__code.is-used { border-color: var(--seaglass-ink); }
.tkt-page__code.is-pending .tkt-page__digits { color: var(--ink-faint); }
.tkt-page.is-invalid .tkt-page__qr { display: none; }

.door__pick, .door__code { display: flex; flex-direction: column; gap: 4px; margin-top: var(--sp-4); font-size: 13px; color: var(--ink-soft); }
.door__pick select, .door__code input { font: inherit; font-size: 16px; padding: 10px 12px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--surface); color: var(--ink); }
.door__code input { font-family: var(--font-display); font-size: 26px; letter-spacing: .18em; text-transform: uppercase; }
.door__cam { position: relative; margin-top: var(--sp-4); border-radius: var(--radius); overflow: hidden; background: #000; aspect-ratio: 4 / 3; }
/* THE CAMERA'S OWN ANSWER (16 Sep 2026): a white blink the instant a code is
   read, then the verdict over the picture - check, warning or cross, with the
   word - for a moment. Both sit on top of the video and let taps through. */
.door__flash { position: absolute; inset: 0; background: #fff; opacity: 0; pointer-events: none; transition: opacity .25s; }
.door__cam.is-blink .door__flash { opacity: .9; transition: none; }
.door__stamp {
  position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px;
  color: #fff; font-family: var(--font-display); font-size: 30px; font-weight: 600; text-align: center; padding: var(--sp-4);
  opacity: 0; pointer-events: none; transition: opacity .15s;
}
.door__stamp.is-on { opacity: 1; }
.door__stamp svg { width: 72px; height: 72px; stroke-width: 2.5; }
.door__stamp.is-ok { background: color-mix(in srgb, var(--seaglass-ink) 88%, transparent); }
.door__stamp.is-warn { background: rgba(154, 107, 30, .9); }
.door__stamp.is-bad { background: color-mix(in srgb, var(--destructive) 88%, transparent); }
.door__cam video { width: 100%; height: 100%; object-fit: cover; display: block; }
.door__row { margin-top: var(--sp-4); display: flex; gap: var(--sp-2); }
.door__form { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--sp-2); align-items: end; }
.door__form .btn { margin-bottom: 1px; }
.door__result { margin-top: var(--sp-4); border-radius: var(--radius); padding: var(--sp-5); text-align: center; color: #fff; }
.door__result.is-ok { background: var(--seaglass-ink); }
.door__result.is-warn { background: #9A6B1E; }
.door__result.is-bad { background: var(--destructive); }
.door__big { font-family: var(--font-display); font-size: 34px; margin: 0; line-height: 1.1; }
.door__small { margin: 8px 0 0; font-size: 15px; opacity: .92; }
.door__tally { margin-top: var(--sp-3); color: var(--ink-faint); font-size: 14px; }

/* ---- FINDING SOMEBODY WITH NO CODE (owner, 15 Sep 2026) -------------------
   Folded away behind one quiet line until it is needed, because the camera
   and the code box are the normal way in and this is the rescue. The rows are
   sized for a thumb in a doorway, not for a desk. */
.door__find { margin-top: var(--sp-4); }
.door__code--find input { font-family: inherit; font-size: 16px; letter-spacing: normal; text-transform: none; }
.door__hits { margin-top: var(--sp-3); display: grid; gap: var(--sp-2); }
.door__hit {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  padding: 10px 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--paper);
}
.door__hitmain { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.door__hitmain strong { font-size: 16px; }
.door__hitsub { font-size: 12.5px; color: var(--ink-faint); overflow-wrap: anywhere; }
.door__hit .btn { flex: none; }

/* ---- THE WHOLE GUEST LIST (owner, 16 Sep 2026) ----------------------------
   "In case the seller can only print the list and check against the buyers
   at the door." Under the name search, behind one line; the four ways out
   sit at the top of it, and the rows use the orders table the dashboard's
   Manage tickets dialog already draws. */
.door__back { margin: 0 0 var(--sp-4); }
.door__what { margin: 0 0 var(--sp-2); font-size: 15px; line-height: 1.5; color: var(--ink-soft); }
.door__list-open { margin-top: var(--sp-4); }
.door__pager { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); margin-top: var(--sp-3); font-size: 13.5px; color: var(--ink-soft); }
.door__pager span { margin-right: auto; }
.door__list { margin-top: var(--sp-4); padding-top: var(--sp-4); border-top: 1px solid var(--line); }
.door__listhead { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: var(--sp-3); }
.door__listtitle { display: block; font-family: var(--font-display); font-size: 20px; }
.door__listsub { display: block; font-size: 13px; color: var(--ink-soft); margin-top: 2px; }
.door__listacts { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.door__list .mct-scroll { margin-top: var(--sp-3); }
.mct--guests td code { font-family: var(--font-display); letter-spacing: .12em; }
/* EVERY COLUMN, ON A PHONE TOO. The resident calendar's narrow-window plan
   above (.mct:not(.mct--sync) ... display: none) drops the third, fourth and
   fifth cells of ANY .mct under 860px, which on the orders table and this
   one is the ticket count, the code and the status - the three things a door
   needs. These two tables keep their columns and scroll sideways inside
   .mct-scroll instead, which both are wrapped in. Found on 16 Sep 2026 while
   the guest list was being checked at phone width; the Manage tickets dialog
   had the same hole. */
@media (max-width: 860px) {
  .mct--orders, .mct--guests { table-layout: auto; min-width: 520px; }
  .mct--orders tbody td, .mct--guests tbody td { display: table-cell; }
}
/* ---- A ROW WEARS ITS STATE (owner, 17 Sep 2026) ---------------------------

   "Change the row colour of the buyers in the records and in the tables to
   check at the door once the ticket has been scanned or verified. It should
   also have another colour when the ticket is cancelled."

   Sage for through the door, blush for no longer valid - the two meanings the
   palette already carries, and the same pair on all three tables: the door's
   guest list, Manage tickets' orders and the Sales record. dash-tickets.js
   rowState() and door.js hand out the same two classes for it.

   ON THE td, NOT THE tr, on purpose: `.mct tbody tr:hover` sets a background
   of its own, and a cell's fill sits over the row's, so a scanned row keeps
   its colour under the pointer instead of flickering grey. The words say the
   same thing in the Status column either way - the colour is never the only
   way to read a row. */
.mct--guests tr.is-in td,
.mct--orders tr.is-in td { background: var(--seaglass-tint); color: var(--seaglass-ink); }
.mct--guests tr.is-off td,
.mct--orders tr.is-off td { background: var(--destructive-tint, #FDEDEA); color: var(--ink-faint); }
.mct--guests tr.is-off td { text-decoration: line-through; }
.mct--guests tr.is-off td code { text-decoration: line-through; }
.mct--orders tr.is-off td:first-child { text-decoration: line-through; }
.door__listnote { margin-top: var(--sp-3); }
/* ON PAPER, ONLY THE LIST. The header, the footer, the camera button, the
   code box and the buttons all go; what prints is the title, the count and
   the rows, with a blank column to tick. */
@media print {
  .door-list-open #site-header, .door-list-open #site-footer,
  .door-list-open #door > :not(#door-list),
  .door-list-open .door__listacts, .door-list-open .door__listnote, .door-list-open .door__pager { display: none !important; }
  .door-list-open #door-list { display: block !important; border-top: 0; padding-top: 0; margin-top: 0; }
  .door-list-open .mct--guests { font-size: 12px; }
  .door-list-open .mct--guests th, .door-list-open .mct--guests td { border-bottom: 1px solid #999; padding: 6px 8px; }
  .door-list-open .mct--guests tr.is-in td, .door-list-open .mct--guests tr.is-off td { color: #666; }
}

.linklike { background: none; border: 0; padding: 0; font: inherit; font-size: 13.5px; color: var(--primary); text-decoration: underline; cursor: pointer; }
.linklike:disabled { opacity: .5; cursor: default; }
.mct--orders td code { font-family: var(--font-display); letter-spacing: .12em; }
/* One word stays on one line: "Refunding…" wrapped inside the Refund column
   (owner, 16 Sep 2026). The button never wraps and the column is wide enough
   for its longest word. */
.mct--orders td .btn { white-space: nowrap; }
.mct--orders th:last-child, .mct--orders td:last-child { min-width: 7.5em; }
/* The Sales record shares the orders table and the Manage bar. Its event
   picker sits above the totals (16 Sep 2026: one event at a time). */
.tt-record__pick { display: flex; flex-direction: column; gap: 4px; margin: var(--sp-3) 0 0; font-size: 13px; color: var(--ink-soft); }
.tt-record__pick select { font: inherit; font-size: 15px; padding: 8px 10px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--surface); color: var(--ink); }
/* THE EVENT PICKER STANDS OUT (owner, 17 Sep 2026: "the selector to pick the
   event should always be a bit more distinguished. They are now a bit
   camouflaged and easy to miss, when they are a crucial part of checking
   tickets at the door, or looking at sales records"). One look in both
   places: a bold navy label, a navy two-line frame on cream, the chosen
   event in a heavier hand, and our own chevron so it reads as a choice
   rather than a text box. */
.door__pick > span, .tt-record__pick > span { font-weight: 700; font-size: 12.5px; letter-spacing: .06em; text-transform: uppercase; color: var(--primary); }
.door__pick select, .tt-record__pick select {
  font-weight: 600; font-size: 16px; padding: 12px 44px 12px 14px;
  border: 2px solid var(--primary); border-radius: 12px; background-color: var(--paper); color: var(--ink);
  appearance: none; -webkit-appearance: none; cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%231C3557' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 14px center;
}
.door__pick select:focus-visible, .tt-record__pick select:focus-visible { outline: 3px solid var(--seaglass); outline-offset: 2px; }
.mct--record td code { font-size: 12.5px; letter-spacing: .08em; }
@media (max-width: 860px) { .mct--record { min-width: 640px; } }
/* THE RECORD, LAID OUT (owner, 17 Sep 2026: "adapt the pop-up format so it
   fits things properly and professionally"). Wider than the other dialogs -
   two tables side by side in time, seven columns in the first - with the
   buyer column given room so an email is not broken mid-word, and every
   figure right-aligned under its heading. */
.modal.modal--wide.modal--record { max-width: 820px; }
.mct--record th:first-child, .mct--record td:first-child { width: 30%; }
.mct--record .mct__sub { overflow-wrap: anywhere; }
.mct--kinds { margin-top: var(--sp-3); }
/* ---- SEVEN COLUMNS THAT FIT (owner, 17 Sep 2026) -------------------------

   "Looks like the last column is cut off, along with its description... make
   the font smaller and make things fit properly."

   `.mct` is table-layout: FIXED, which hands every column the same width and
   then lets a nowrap figure spill past the table's own right edge - and a
   spill is not scrollable, so "To you" was sliced off at the dialog's edge
   with no way to reach it, headline and figures both. Auto layout instead:
   each column takes what its figures need, the name column keeps a floor, and
   if seven still cannot fit the table grows and .mct-scroll scrolls to it.

   Smaller and tighter with it, because seven columns of figures inside a
   dialog is what this is - 13px here, against the 14px of a full-width table
   on a page of its own. */
.mct--kinds { table-layout: auto; font-size: 13px; }
.mct--kinds th, .mct--kinds td { padding: 8px 9px; }
.mct--kinds th:first-child, .mct--kinds td:first-child { min-width: 9em; }
/* ON A PHONE, EACH ROW IS A CARD (owner, 17 Sep 2026: "ensure the mobile/app
   format looks good and professional"). Seven columns do not fit in 340px
   and a sideways scroll hides five of them. So under 860px the header row
   goes, each <tr> becomes a bordered block with the type's name on top, and
   every figure sits on its own line with its column name printed beside it
   from data-l. The record's orders table gets the same treatment. */
@media (max-width: 860px) {
  .mct--kinds, .mct--kinds tbody, .mct--kinds tr, .mct--kinds td { display: block; min-width: 0; width: auto; }
  /* The plan for any .mct under 860px hides its third, fourth and fifth cells (above, by position, at the same weight as these); the class is written twice so these win the tie and keep every cell, as cards. */
  .mct.mct--kinds.mct--kinds tbody td, .mct.mct--record.mct--record tbody td { display: block; }
  .mct.mct--kinds.mct--kinds tbody td.num, .mct.mct--record.mct--record tbody td[data-l] { display: flex; }
  .mct--kinds thead { display: none; }
  .mct--kinds tr { padding: 10px 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface); }
  .mct--kinds tr + tr { margin-top: var(--sp-2); }
  .mct--kinds td { padding: 0; border: 0; }
  .mct--kinds td:first-child { font-weight: 600; margin-bottom: 6px; }
  .mct--kinds td:first-child .mct__sub { display: inline; margin-left: 6px; font-weight: 400; }
  .mct--kinds td.num { display: flex; justify-content: space-between; gap: var(--sp-3); padding: 3px 0; font-size: 14px; }
  .mct--kinds td.num::before { content: attr(data-l); color: var(--ink-faint); font-weight: 400; }
  .mct--kinds .mct--kinds__all { border-color: var(--line-strong); background: var(--paper); }
  .mct--kinds tr.mct--kinds__all td { border: 0; }   /* outweighs the desktop total-row rule below */
  .mct--kinds .mct--kinds__all td.num { font-weight: 600; }

  .mct--record { min-width: 0; }
  .mct--record, .mct--record tbody, .mct--record tr, .mct--record td { display: block; width: auto; }
  .mct--record thead { display: none; }
  .mct--record tr { padding: 10px 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface); }
  .mct--record tr + tr { margin-top: var(--sp-2); }
  .mct--record tr.is-in { background: var(--seaglass-tint); border-color: var(--seaglass); }
  .mct--record tr.is-off { color: var(--ink-faint); background: var(--destructive-tint, #FDEDEA); border-color: var(--line-strong); }
  /* The cells are blocks in here, so their own fill would tile the card. */
  .mct--record tr.is-in td, .mct--record tr.is-off td { background: none; }
  .mct--record td { padding: 0; border: 0; }
  .mct--record tr td:first-child { width: auto; font-weight: 600; margin-bottom: 6px; }   /* outweighs the 30% column above */
  .mct--record td:first-child .mct__sub { font-weight: 400; }
  .mct--record td[data-l] { display: flex; justify-content: flex-start; align-items: baseline; gap: 6px; padding: 3px 0; font-size: 14px; text-align: right; }
  .mct--record td[data-l]::before { content: attr(data-l); color: var(--ink-faint); text-align: left; margin-right: auto; }   /* the value and its note sit together at the right */
  .mct--record td[data-l] br { display: none; }
  .mct--record td[data-l] .mct__sub { display: inline; margin: 0 0 0 6px; }
}
.mct--kinds th.num, .mct--kinds td.num { text-align: right; white-space: nowrap; }
.mct--kinds th.num { cursor: help; }
.mct--kinds td.num { font-variant-numeric: tabular-nums; }
.mct--kinds .mct--kinds__all td { font-weight: 600; border-top: 2px solid var(--line-strong); }
.tt-record__sum { margin-top: var(--sp-3); }
/* Sell tickets here, on the post form (assets/promote.js sellBlock). */
.sell { margin-top: var(--sp-4); border: 1px dashed var(--line-strong); border-radius: var(--radius); padding: var(--sp-4); background: var(--surface); }
.sell__on { align-items: flex-start; }
/* The step that comes first, above the switch, on an account not yet on Stripe (16 Sep 2026). */
.sell__first { margin: 0 0 var(--sp-3); display: flex; gap: 6px; align-items: flex-start; }
.sell__first svg { flex: none; margin-top: 2px; color: var(--accent); }
/* A post being edited that already sells: what is on sale, and where it is
   managed, in place of the switch (16 Sep 2026). */
.sell__have { margin: 0 0 6px; font-size: 14.5px; line-height: 1.5; }
/* Website for this event: theirs by default, or a different link (16 Sep 2026). */
.web-pick { display: grid; gap: 6px; margin: 4px 0 8px; }
.web-pick .check { align-items: center; }
.sell__on .field__hint { display: block; font-weight: 400; margin-top: 2px; }
.sell__body { margin-top: var(--sp-3); display: grid; gap: var(--sp-2); }
.sell__row { display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(0, .8fr) minmax(0, 1fr); gap: var(--sp-2); align-items: end; padding: 8px 10px; border: 1px solid var(--line); border-radius: 10px; background: var(--paper); }
.sell__f { display: flex; flex-direction: column; gap: 3px; font-size: 12.5px; color: var(--ink-soft); }
.sell__f em { font-style: normal; color: var(--ink-faint); }
.sell__f input { font: inherit; font-size: 15px; padding: 8px 10px; border: 1px solid var(--line-strong); border-radius: 8px; background: var(--surface); color: var(--ink); min-width: 0; }
.sell__fee { grid-column: 1 / -1; font-size: 12.5px; color: var(--ink-faint); min-height: 1em; }
.sell__share { font-size: 13.5px; color: var(--ink-soft); overflow-wrap: anywhere; }

/* ---- AS MANY KINDS OF TICKET AS THEY SELL (owner, 15 Sep 2026) ------------
   The rows grow on a press, so each one carries its own footer: the figures
   on the left, the way out on the right. Remove is a quiet link and not a
   button, because adding a kind of ticket is the thing being encouraged
   here and removing one is the correction. */
.sell__rows { display: grid; gap: var(--sp-2); }
.sell__foot { grid-column: 1 / -1; display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-2); }
.sell__drop { flex: none; font-size: 12.5px; color: var(--ink-faint); }
.sell__drop:hover { color: var(--ink); }
.sell__addrow { margin-top: 2px; }

/* ---- WHO PAYS THE CARD FEE, on the post form (owner, 14 Sep 2026) ---------
   Two options, each carrying the real figures for the price they typed. Laid
   out as a short stack rather than a row: what is being compared is the
   sentence under each label, and two of those side by side is a squeeze at any
   width worth reading at. */
/* ---- "WAY LESS THAN EVENTBRITE!" -----------------------------------------
   Written into promote.js on 15 Sep 2026 and never given a rule, so it had
   been rendering as a plain italic fragment of the sentence beside it - which
   is why the comparison did not read as a comparison. It is a badge: small,
   warm, and leaning, so it reads as a remark rather than a label.

   THE SAME CLASS ON THE DASHBOARD's Tickets heading, so the two places that
   make this claim look like one thing. */
.sell__badge {
  display: inline-block; vertical-align: middle; margin-left: 6px;
  /* 12.5px, not 11px (owner, 15 Sep 2026: "font is too small, increase a
     bit"). It is a remark rather than a label, but it was reading as a
     footnote. */
  font-style: normal; font-size: 12.5px; font-weight: 700; letter-spacing: .01em;
  color: var(--accent);
  background: var(--accent-tint);
  border-radius: 999px; padding: 3px 10px;
  transform: rotate(-1.5deg);
}
@media (prefers-reduced-motion: reduce) { .sell__badge { transform: none; } }

/* ---- MANAGE TICKETS: the bar and the sortable table ----------------------
   The freeze button and the search box share a row and wrap to two on a
   phone. Column headings are pressable, so they say so on hover and carry the
   arrow for whichever one is doing the sorting. */
.tt-manage__bar {
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  margin: var(--sp-3) 0;
}
.tt-manage__freeze { display: inline-flex; align-items: center; }
/* The corner of Manage tickets: Freeze selling, Refund all, Call it off
   (16 Sep 2026). A wrapping row under the title on a phone; up beside the
   title, right-aligned, when the dialog is wide enough for both. */
.tt-manage__top { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: 8px; margin: 0 0 var(--sp-3); }
@media (min-width: 760px) {
  .modal.modal--manage { position: relative; }
  .modal--manage .tt-manage__top { position: absolute; top: 20px; right: var(--sp-5); margin: 0; }
}
/* The kinds of ticket inside Manage tickets (16 Sep 2026): a fold of the post
   form's own ticket rows, each with Save and Remove, and a row for one more. */
.tt-kinds { margin: var(--sp-3) 0; border: 1px solid var(--line); border-radius: 10px; background: var(--paper); }
/* THE "i" ON THE BUTTON'S TOP CORNER (owner, 17 Sep 2026). Smaller than the
   usual tip, sat on the button's top-right corner like a badge, and its
   bubble opens BELOW the button, hanging from the right edge: these buttons
   are at the very top of a dialog that scrolls, so a bubble opening upward
   was cut off by the dialog's own box. */
.tt-manage__act { position: relative; display: inline-flex; align-items: center; }
.tt-manage__act .info-tip {
  position: absolute; top: -6px; right: -6px; margin: 0; width: 15px; height: 15px; font-size: 10px;
  background: var(--surface); color: var(--ink); opacity: .85; z-index: 2;
}
.tt-manage__act .info-tip::after { top: calc(100% + 8px); bottom: auto; left: auto; right: 0; transform: none; text-align: left; max-width: 17rem; }
.tt-manage__act .btn { padding-right: 16px; }
.tt-kinds > summary { display: flex; align-items: center; gap: 6px; padding: 10px 12px; cursor: pointer; list-style: none; font-weight: 600; font-size: 14px; }
.tt-kinds > summary::-webkit-details-marker { display: none; }
.tt-kinds > summary > svg:last-child { margin-left: auto; color: var(--ink-faint); transition: transform .18s; }
.tt-kinds[open] > summary > svg:last-child { transform: rotate(180deg); }
.tt-kinds__body { padding: 0 12px 12px; display: grid; gap: var(--sp-2); }
.tt-kind { display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(0, .8fr) minmax(0, 1fr); gap: var(--sp-2); align-items: end; padding: 8px 10px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface); }
.tt-kind--new { border-style: dashed; }
/* THE REFUND CHOICE (17 Sep 2026): whole order, some tickets, or an amount. */
.tt-refund { margin: 0 0 var(--sp-2); }
.tt-refund__opt { display: flex; gap: 10px; align-items: flex-start; padding: 10px 0; border-top: 1px solid var(--line); font-size: 14px; cursor: pointer; }
.tt-refund__opt:first-child { border-top: 0; padding-top: 0; }
.tt-refund__opt input[type="radio"] { margin-top: 3px; flex: 0 0 auto; }
.tt-refund__opt small { display: block; margin-top: 2px; opacity: .75; }
.tt-refund__n { width: 76px; padding: 2px 6px; font: inherit; border: 1px solid var(--line); border-radius: 6px; background: var(--surface); }
.tt-refund__n--pct { width: 62px; }
/* WHETHER THE CASE IS CLOSED (owner, 17 Sep 2026). One line under the three
   choices, not a fourth choice: it answers a different question - what happens
   to the ticket - and it is ticked unless the seller says otherwise. Disabled
   on "the whole order", where it has never been a choice. */
.tt-refund__close { display: flex; gap: 10px; align-items: flex-start; margin-top: 2px; padding: 10px 0 0; border-top: 1px solid var(--line); font-size: 14px; cursor: pointer; }
.tt-refund__close input[type="checkbox"] { margin-top: 3px; flex: 0 0 auto; }
.tt-refund__close small { display: block; margin-top: 2px; opacity: .75; }
.tt-refund__close input[type="checkbox"]:disabled { cursor: default; }
.tt-refund__close input[type="checkbox"]:disabled ~ span { opacity: .6; }
.tt-kind__acts { display: flex; align-items: center; gap: 10px; }
@media (max-width: 560px) { .tt-kind { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } .tt-kind .sell__f--name { grid-column: 1 / -1; } }
.tt-manage__find { flex: 1 1 200px; min-width: 0; display: block; }
.tt-manage__find input {
  width: 100%; font: inherit; font-size: 14px; padding: 8px 12px;
  border: 1px solid var(--line-strong); border-radius: 999px;
  background: var(--surface); color: var(--ink);
}
.mct th[data-sort] { cursor: pointer; user-select: none; white-space: nowrap; }
.mct th[data-sort]:hover { color: var(--ink); text-decoration: underline; }
.mct th.is-sorted { color: var(--ink); }
.mct__sortmark { margin-left: 4px; font-size: 9px; color: var(--ink-faint); }

/* ---- US AND THEM, SIDE BY SIDE (owner, 15 Sep 2026) ----------------------
   Inside the Tickets card's "i". Ours is the one coloured in and the gap sits
   between the two wearing the warm accent, so the eye lands on the saving
   rather than on either price. Wraps to a stack on a narrow phone, where the
   middle badge becomes a line of its own and still reads in order. */
.vs {
  display: flex; align-items: stretch; gap: var(--sp-2); flex-wrap: wrap;
  margin: var(--sp-3) 0;
}
.vs__side {
  flex: 1 1 130px; min-width: 0; display: flex; flex-direction: column; gap: 2px;
  padding: 10px 12px; border: 1px solid var(--line); border-radius: 12px;
  background: var(--surface);
}
/* The mark in each box's top-right corner: our logo on ours, their name in
   their colour on theirs (16 Sep 2026). The label and the mark share one
   line, label left and mark right, so neither can sit on the other. */
.vs__head { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 24px; min-width: 0; }
.vs__head--them { justify-content: flex-end; }
img.vs__logo { width: 24px; height: 24px; flex: none; }
.vs__logo--word { font-weight: 800; font-size: 13px; letter-spacing: -.01em; line-height: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vs__side em { font-style: normal; font-size: 11.5px; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--ink-faint); }
.vs__side strong { font-family: var(--font-display); font-size: 25px; line-height: 1.05; color: var(--ink); }
/* The formula under each total: the numbers the owner wants read side by
   side (16 Sep 2026), smaller than the total and larger than the label. */
.vs__rate { font-size: 13px; font-weight: 600; color: var(--ink-soft); margin-top: 2px; }
.vs__side--us .vs__rate { color: var(--primary-ink); }
/* THE $50 IS THE HEADLINE (owner, 16 Sep 2026: "make the $50 design bigger
   because I almost didn't see it"). The example every figure hangs off, set
   in the display face at twice the badge's size. */
/* A smaller box than the default dialog, and air between the badge and the
   title (owner, 16 Sep 2026). */
.modal.modal--vs { max-width: 440px; }
.modal--vs .modal__badge { font-size: 13px; padding: 6px 14px 6px 12px; gap: 7px; margin-bottom: var(--sp-5); }
.modal--vs .modal__badge strong {
  font-family: var(--font-display); font-size: 22px; line-height: 1; font-weight: 700;
  letter-spacing: 0; text-transform: none;
}
.vs__side--us { border-color: var(--primary); background: var(--primary-tint); }
.vs__side--us em { color: var(--primary); }
.vs__save {
  flex: 0 0 auto; display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: 6px 12px; border-radius: 999px;
  background: var(--accent-tint); color: var(--accent);
}
.vs__save strong { font-family: var(--font-display); font-size: 19px; line-height: 1; }
.vs__save em { font-style: normal; font-size: 10.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
@media (max-width: 420px) {
  .vs__side { flex: 1 1 100%; }
  .vs__save { flex: 1 1 100%; flex-direction: row; gap: 6px; }
}

.sell__cardfee { margin-top: var(--sp-3); display: grid; gap: 6px; }
.sell__cardfee-q { font-size: 12.5px; font-weight: 600; color: var(--ink-soft); }
.sell__pick {
  display: flex; align-items: flex-start; gap: 8px;
  padding: 8px 10px; border: 1px solid var(--line); border-radius: 10px;
  background: var(--paper); cursor: pointer; font-size: 13.5px;
}
.sell__pick:has(input:checked) { border-color: var(--primary); background: var(--primary-tint); }
.sell__pick input { margin: 2px 0 0; flex: none; }
/* The figures sit under the label they belong to, never beside it: a number
   that wraps away from its own sentence is worse than no number at all. */
.sell__pick .sell__fee { display: block; margin-top: 2px; font-style: normal; }
@media (max-width: 620px) {
  .sell__row { grid-template-columns: 1fr 1fr; }
  .sell__f--name { grid-column: 1 / -1; }
}

/* The two-line tickets note under the plan cards (assets/business.js). */
.tickets-band { margin-top: var(--sp-5); display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); flex-wrap: wrap; padding: var(--sp-4) var(--sp-5); border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); }
.tickets-band__t { margin: 0; display: flex; align-items: center; gap: 10px; font-family: var(--font-display); font-size: 18px; color: var(--ink); }
.tickets-band__t svg { flex: 0 0 auto; color: var(--accent); }
.tkts-page__steps { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-3); margin-top: var(--sp-4); }
.tkts-page__step { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: var(--sp-4); }
.tkts-page__step b { display: block; font-family: var(--font-display); font-size: 18px; margin-bottom: 4px; }
.tkts-page__step p { margin: 0; color: var(--ink-soft); font-size: 14.5px; }
@media (max-width: 620px) { .tkts-page__steps { grid-template-columns: 1fr; } }
.legal .lede { font-size: 18px; color: var(--ink-soft); line-height: 1.5; }
/* ---- THE MARQUEE ------------------------------------------------------
   The diner sign that offers the other tab (assets/marquee.js). Navy frame,
   cream face, a ring of warm bulbs that chase, and a band of scrolling
   lettering in the display face. Sunset is the paid colour and it appears
   here only as the dot on a business's own post.

   WHERE IT SITS. On a wide screen, in the empty margin to the right of the
   list column: the column is --maxw-wide (1040px) centred, so the sign's
   right inset is half the viewport minus half the column, a gap and the
   sign's own width. Below about 1644px that comes out under 14px and the
   max() drops it to the corner, above the report button (.report-fab, 24px
   at bottom 14px). On a phone it is a strip at the list's width, pinned to
   the bottom like a last tile that follows you, and body.marquee-up pads
   the page so the real last card can still scroll clear of it. */
.marquee {
  --marquee-w: 264px; --marquee-gap: 24px; --marquee-bulb: #F3C46A; --marquee-glow: #E9A23B;
  position: fixed; z-index: 70;
  right: max(14px, calc(50% - (var(--maxw-wide) / 2) - var(--marquee-gap) - var(--marquee-w)));
  bottom: 52px; width: var(--marquee-w);
  background: var(--primary); border-radius: var(--radius); padding: 11px;
  box-shadow: 0 18px 40px -18px rgba(27, 43, 68, .55), 0 0 0 1px rgba(255, 255, 255, .06) inset;
  opacity: 0; transform: translateY(8px); transition: opacity .35s ease, transform .35s ease;
  font-family: var(--font-body, system-ui, sans-serif); font-size: 14px; line-height: 1.4;
}
.marquee.is-on { opacity: 1; transform: none; }
.marquee.is-warming { animation: marquee-warm .9s steps(1, end) both; }
@keyframes marquee-warm { 0% { opacity: 0 } 12% { opacity: 1 } 22% { opacity: .15 } 34% { opacity: 1 } 48% { opacity: .4 } 60% { opacity: 1 } 100% { opacity: 1 } }
.marquee.is-out { opacity: 0; transform: translateY(10px); transition: opacity 1.2s ease, transform 1.2s ease; pointer-events: none; }
.marquee__bulbs { position: absolute; inset: 0; pointer-events: none; }
.marquee__bulbs i {
  position: absolute; width: 6px; height: 6px; margin: -3px 0 0 -3px; border-radius: 50%;
  background: var(--marquee-bulb); box-shadow: 0 0 6px 1px var(--marquee-glow);
  animation: marquee-chase 1.6s ease-in-out infinite;
}
@keyframes marquee-chase { 0%, 100% { opacity: .55; box-shadow: 0 0 3px 0 var(--marquee-glow) } 50% { opacity: 1; box-shadow: 0 0 8px 2px var(--marquee-glow) } }
.marquee:hover .marquee__bulbs i, .marquee.is-held .marquee__bulbs i { animation-play-state: paused; }
.marquee__close {
  position: absolute; top: -8px; right: -8px; z-index: 2; width: 22px; height: 22px; padding: 0;
  border: 1.5px solid var(--primary); border-radius: 50%; background: var(--surface); color: var(--primary);
  font-size: 13px; font-weight: 700; line-height: 1; cursor: pointer; display: grid; place-items: center;
  box-shadow: 0 2px 6px -2px rgba(27, 43, 68, .5);
}
.marquee__close:hover { background: var(--primary); color: #fff; }
.marquee__face { background: var(--surface); border-radius: var(--radius-sm); overflow: hidden; position: relative; padding-bottom: 8px; }
.marquee__top { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 7px 10px 0 12px; }
.marquee__eyebrow { font-size: 11.5px; font-style: italic; color: var(--ink-faint); line-height: 1.3; padding-top: 3px; }
.marquee__clock { width: 16px; height: 16px; display: block; flex: none; }
.marquee__ring {
  fill: none; stroke: var(--primary); stroke-width: 1.6; stroke-linecap: round;
  transform: rotate(-90deg); transform-origin: 50% 50%; stroke-dasharray: 37.7 37.7;
}
.marquee__core { fill: var(--primary); }
.marquee__row { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 0 10px 0 12px; }
.marquee__title {
  display: flex; align-items: center; gap: 6px; min-width: 0;
  font-family: var(--font-display, Georgia, serif); font-weight: 700; font-size: 20px; line-height: 1.1;
  letter-spacing: -.01em; color: var(--primary);
}
.marquee__dot { width: 6px; height: 6px; border-radius: 50%; background: var(--sunset); margin-left: 2px; }
.marquee__dot[hidden] { display: none; }
.marquee__icon { display: flex; gap: 2px; flex: none; }
.marquee__icon svg { width: 30px; height: 30px; display: block; }
.marquee__bandwrap { position: relative; margin: 7px 0 0; background: var(--primary); }
.marquee__band {
  display: block; overflow: hidden; white-space: nowrap; margin: 0 22px; padding: 6px 0;
  color: var(--marquee-bulb); text-decoration: none; cursor: pointer;
  font-family: var(--font-display, Georgia, serif); font-weight: 900; font-style: italic; font-size: 15px;
  letter-spacing: .02em; text-transform: uppercase;
}
.marquee__band:hover { color: #fff; }
.marquee__band:focus-visible { outline: 2px solid var(--marquee-bulb); outline-offset: -2px; }
.marquee__band.is-blink { animation: marquee-blink .35s steps(1, end); }
@keyframes marquee-blink { 0% { color: transparent } 50% { color: var(--marquee-bulb) } 70% { color: transparent } 100% { color: var(--marquee-bulb) } }
.marquee__track { display: inline-block; animation: marquee-slide var(--dur, 14s) linear infinite; will-change: transform; }
.marquee__track span { display: inline-block; padding-right: 2.2em; }
.marquee__track b { font-weight: 900; color: #fff; font-style: normal; padding: 0 .6em; }
@keyframes marquee-slide { to { transform: translateX(-50%) } }
.marquee:hover .marquee__track, .marquee.is-held .marquee__track { animation-play-state: paused; }
.marquee__arrow {
  position: absolute; top: 0; bottom: 0; width: 22px; padding: 0; border: 0; cursor: pointer;
  background: #132741; color: var(--marquee-bulb);
  font-family: var(--font-display, Georgia, serif); font-weight: 700; font-size: 16px; line-height: 1;
}
.marquee__arrow:hover { color: #fff; }
.marquee__arrow--l { left: 0; }
.marquee__arrow--r { right: 0; }
.marquee--one .marquee__arrow { display: none; }
.marquee--one .marquee__band { margin: 0 12px; }
.marquee__dots { display: flex; justify-content: center; gap: 5px; padding-top: 6px; }
.marquee__dots i { width: 4px; height: 4px; border-radius: 50%; background: var(--line); }
.marquee__dots i.is-on { background: var(--primary); }
.marquee--one .marquee__dots { display: none; }
.marquee__close:focus-visible, .marquee__arrow:focus-visible { outline: 2px solid var(--sunset); outline-offset: 2px; }

@media (max-width: 720px) {
  .marquee { left: 12px; right: 12px; bottom: 10px; width: auto; padding: 8px; border-radius: 12px; }
  .marquee__face { border-radius: 8px; }
  .marquee__title { font-size: 17px; }
  .marquee__band { font-size: 13.5px; padding: 5px 0; }
  .marquee__icon svg { width: 26px; height: 26px; }
  .marquee__bulbs i { width: 5px; height: 5px; margin: -2.5px 0 0 -2.5px; }
  body.marquee-up { padding-bottom: 132px; }
  body.marquee-up .report-fab { bottom: 142px; }
}
@media (prefers-reduced-motion: reduce) {
  .marquee__bulbs i, .marquee.is-warming, .marquee__band.is-blink { animation: none; }
  .marquee__band { white-space: normal; }
  .marquee__track { animation: none; }
  .marquee__track span:last-child { display: none; }
}

/* ===========================================================================
   THE BOARD — a card anyone can buy, pinned inside the list (assets/board.js)
   An index card on a corkboard: cream face with faint ruled lines, a sunset
   rule along the top, a pushpin, a small "Sponsored" chip. Sunset is the
   palette's colour for paid placements — the same mark the Marquee puts on a
   paid pick — so the site has one way of saying somebody paid for this. It
   shares the listing's grid (64px media column, then text) so it sits in the
   list at the list's own rhythm, and is deliberately NOT an .event-card: the
   listing rules (:has(.event-org), the cancelled strike-through, the day
   grouping's own selectors) must never reach it.
   ========================================================================= */
.board-card {
  position: relative; display: grid; grid-template-columns: 64px 1fr; gap: var(--sp-4);
  padding: var(--sp-4) var(--sp-4) 14px; margin: var(--sp-2) 0 var(--sp-3);
  color: inherit; text-decoration: none;
  background:
    repeating-linear-gradient(to bottom, transparent 0 27px, rgba(28, 53, 87, .07) 27px 28px) 0 14px / 100% 100% no-repeat,
    var(--surface);
  border: 1px solid var(--line); border-top: 3px solid var(--sunset);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow);
  transition: box-shadow .15s, border-color .15s, transform .06s;
}
a.board-card:hover { box-shadow: var(--shadow-hover); border-color: var(--line-strong); border-top-color: var(--sunset); text-decoration: none; }
a.board-card:active { transform: translateY(1px); }
a.board-card:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }

/* The pushpin: a drawn one (the SVG in board.js), head over the top rule and
   needle into the paper, tilted a few degrees so it reads as pushed in rather
   than printed on. Decorative, so aria-hidden in the markup. */
.board-card__pin {
  position: absolute; top: -20px; left: 50%; width: 30px; height: 36px; margin-left: -15px;
  filter: drop-shadow(0 1px 1px rgba(38, 48, 58, .25));
  pointer-events: none;
}
.board-card__pin svg { display: block; width: 30px; height: 36px; }

.board-card__media {
  width: 64px; height: 64px; border-radius: var(--radius-sm); overflow: hidden;
  background: var(--surface-2); border: 1px solid var(--line);
  display: flex; align-items: center; justify-content: center; align-self: start;
}
.board-card__media img { width: 64px; height: 64px; object-fit: cover; display: block; }
.board-card__icon { display: block; width: 64px; height: 64px; }
.board-card__icon svg { width: 64px; height: 64px; display: block; }
/* The picker of drawings on advertise.html: a wrap of small round buttons. */
.board-icons-fold { margin-top: 6px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface); }
.board-icons-fold > summary { cursor: pointer; padding: 10px 14px; font-size: 14px; font-weight: 600; color: var(--ink); list-style: none; display: flex; align-items: center; gap: 8px; }
.board-icons-fold > summary::-webkit-details-marker { display: none; }
.board-icons-fold > summary::before { content: ''; width: 8px; height: 8px; border-right: 2px solid var(--ink-faint); border-bottom: 2px solid var(--ink-faint); transform: rotate(-45deg); transition: transform .15s; }
.board-icons-fold[open] > summary::before { transform: rotate(45deg); }
/* A grid that fills the width: no empty strip at the right. */
.board-icons { display: grid; grid-template-columns: repeat(auto-fill, minmax(48px, 1fr)); justify-items: center; gap: 4px;
  padding: 6px 8px 10px; border-top: 1px solid var(--line); max-height: 240px; overflow-y: auto; }
.board-icons__b { width: 46px; height: 46px; padding: 2px; border: 2px solid transparent; border-radius: 12px;
  background: none; cursor: pointer; display: flex; align-items: center; justify-content: center; }
.board-icons__b svg { width: 40px; height: 40px; display: block; }
.board-icons__b:hover { border-color: var(--line-strong); }
.board-icons__b.is-on { border-color: var(--sunset); background: var(--sunset-tint); }
.board-icons__b:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
.board-card__mono {
  font-family: var(--font-display); font-size: 22px; font-weight: 600; letter-spacing: -.02em;
  color: var(--sunset-ink);
}

.board-card__body { min-width: 0; }
/* The Sponsored chip sits in the card's top-right corner (owner, 17 Sep
   2026), clear of the pin in the middle; the headline keeps out of its way. */
.board-card__chip {
  display: inline-flex; align-items: center; gap: 5px; font-size: 10.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em; color: var(--sunset-ink);
  background: var(--sunset-tint); border-radius: var(--radius-pill); padding: 2px 8px 2px 6px;
  margin-bottom: 5px;
}
.board-card__title { padding-right: 96px; }
.board-card__chip i { width: 6px; height: 6px; border-radius: 50%; background: var(--sunset); }
.board-card__title {
  font-family: var(--font-display); font-size: 16px; font-weight: 600; letter-spacing: -.01em;
  color: var(--ink); line-height: 1.25;
}
a.board-card:hover .board-card__title { color: var(--sunset-ink); }
.board-card__text { margin: 4px 0 0; font-size: 13.5px; line-height: 1.45; color: var(--ink-soft); }
.board-card__foot {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  margin-top: 8px; font-size: 12.5px;
}
.board-card__biz { color: var(--ink-faint); font-weight: 600; }
.board-card__go {
  margin-left: auto; display: inline-flex; align-items: center; gap: 4px;
  color: var(--primary); font-weight: 700;
}
.board-card__go svg { color: var(--primary); }

/* ---- The Advertise page (advertise.html, assets/advertise.js) ------------ */
/* The pitch at the top of the Advertise page: the Your numbers tiles and a
   thirty-day line. Navy line on a navy tint, the palette's colour for what
   matters; sunset stays for the paid mark. */
.pitch { margin: var(--sp-5) 0 var(--sp-6); padding: var(--sp-4) var(--sp-5) var(--sp-4);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); }
.pitch__t { font-family: var(--font-display); font-size: 22px; font-weight: 600; margin: 0; letter-spacing: -.01em; }
/* "Live feed": the Sponsored chip's shape, in the destructive red, with a
   dot that breathes the way a camera's recording light does — slow and
   steady, never a hard blink. Still under reduced motion. */
.pitch__live {
  display: inline-flex; align-items: center; gap: 6px; vertical-align: middle;
  margin-left: 10px; padding: 3px 9px 3px 7px; border-radius: var(--radius-pill);
  font-family: system-ui, sans-serif; font-size: 10.5px; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--destructive); background: #F7E3E0;
}
.pitch__live i {
  width: 7px; height: 7px; border-radius: 50%; background: var(--destructive);
  box-shadow: 0 0 0 0 rgba(179, 58, 43, .45);
  animation: pitch-rec 2.4s ease-in-out infinite;
}
@keyframes pitch-rec {
  0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(179, 58, 43, .45); }
  50%      { opacity: .35; box-shadow: 0 0 0 5px rgba(179, 58, 43, 0); }
}
@media (prefers-reduced-motion: reduce) { .pitch__live i { animation: none; } }
.pitch__tiles { margin-top: var(--sp-3); }
.pitch__chart { position: relative; margin-top: var(--sp-4); padding-bottom: 18px; }
.pitch__spark { display: block; width: 100%; height: 64px; }
.pitch__line { fill: none; stroke: var(--primary); stroke-width: 2; vector-effect: non-scaling-stroke; stroke-linejoin: round; }
.pitch__area { fill: var(--primary-tint); }
/* Sunday to Sunday: a full-height line each week; Wednesday a lighter, shorter one. */
.pitch__week { stroke: var(--line-strong); stroke-width: 1.2; vector-effect: non-scaling-stroke; }
.pitch__mid { stroke: var(--line); stroke-width: 1; stroke-dasharray: 3 3; vector-effect: non-scaling-stroke; }
.pitch__axis { position: absolute; left: 0; right: 0; bottom: 0; height: 16px; }
.pitch__lbl {
  position: absolute; transform: translateX(-50%); white-space: nowrap;
  font-size: 11px; font-weight: 600; color: var(--ink-faint);
}
.pitch__lbl--first { transform: none; }
.pitch__lbl--last { transform: translateX(-100%); }
.pitch__cap { margin: 6px 0 0; font-size: 12.5px; color: var(--ink-faint); }

/* The calendar on the Advertise page is the Highlight's (.avail), at a
   width a month reads well at; the run note sits under it. */
/* The whole width, with bigger days (owner, 16 Sep 2026): a centred 420px
   calendar left its label stranded to the left, and a day is a thing you tap
   with a thumb. */
.board-cal { margin-top: var(--sp-2); }
.board-cal .avail { max-width: none; }
.board-cal .avail-cell { aspect-ratio: auto; min-height: 46px; font-size: 15px; }   /* wide, not square: 7 squares across a page are too tall */
.board-cal .avail__grid, .board-cal .avail__dow { gap: 4px; }
.board-cal .avail__dow span { font-size: 12px; }
/* The legend, readable (owner, 16 Sep 2026: "way too small ... hard to
   see"): bigger words in the body ink, bigger swatches with firm borders,
   and the cells they stand for drawn the same way so the two agree. */
.board-cal .avail__key { font-size: 14px; color: var(--ink); gap: 6px 16px; margin-top: var(--sp-4); font-weight: 600; }
.board-cal .avail-dot { width: 14px; height: 14px; border-radius: 4px; margin-right: 6px; }
.board-cal .avail-dot--free { background: var(--seaglass-tint); border: 1.5px solid var(--seaglass-ink); }
.board-cal .avail-dot--taken { background: var(--surface-2); border: 1.5px solid var(--ink-faint); opacity: 1; }
.board-cal .avail-dot--nofit { background: var(--paper); border: 1.5px dashed var(--ink-faint); opacity: 1; }
.board-cal .avail-cell--free { border-color: var(--seaglass); background: var(--seaglass-tint); color: var(--seaglass-ink); font-weight: 700; }
.board-cal .avail-cell--taken { border-color: var(--line-strong); color: var(--ink-faint); }
.board-cal .avail-cell--nofit { border: 1px dashed var(--line-strong); color: var(--ink-faint); opacity: .8; }
/* THE RUN, ON THE GRID (owner, 16 Sep 2026): the tapped day in solid navy,
   every day the run covers in the navy tint, so the calendar shows the
   booking and the note below only repeats it. Declared after the available
   look above, which would otherwise paint over it. */
.board-cal .avail-cell.is-inrun {
  background: var(--primary-tint-2); border-color: var(--primary); color: var(--primary-ink, var(--ink)); font-weight: 700;
}
.board-cal .avail-cell.is-selected {
  background: var(--primary); border-color: var(--primary); color: #fff; font-weight: 800;
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--primary);
}
.board-price-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--sp-3); margin: var(--sp-4) 0; }
@media (max-width: 720px) { .board-price-row { grid-template-columns: 1fr 1fr; } }
.board-price {
  display: block; text-align: left; cursor: pointer; font: inherit;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  padding: var(--sp-4); box-shadow: var(--shadow); color: inherit;
}
.board-price.is-on { border-color: var(--sunset); background: var(--sunset-tint); }
.board-price__n { font-family: var(--font-display); font-size: 26px; font-weight: 600; color: var(--ink); }
.board-price__l { display: block; font-size: 13px; color: var(--ink-soft); margin-top: 2px; }
.board-price__per { display: block; font-size: 12px; color: var(--ink-faint); margin-top: 6px; }
.board-form { display: grid; gap: var(--sp-4); margin-top: var(--sp-5); }
.board-form__row { display: grid; gap: var(--sp-4); grid-template-columns: 1fr 1fr; }
.board-form__row--top { align-items: start; }
/* The picture field is boxed (owner, 17 Sep 2026): the file button and the
   drawings fold answer one question, and a thin line says so. */
#f-bd-photo { padding: 12px 14px 14px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface); }
#f-bd-photo .board-icons-fold { background: var(--paper); }
.board-form__row--top > div > .field:first-child, .board-form__row--top > .field { margin-top: 0; }
@media (max-width: 560px) { .board-form__row { grid-template-columns: 1fr; } }
.board-towns { display: flex; flex-wrap: wrap; gap: 8px; }
.board-towns label {
  display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px;
  border: 1px solid var(--line); border-radius: var(--radius-pill); background: var(--surface);
  font-size: 14px; cursor: pointer;
}
.board-towns label.is-on { border-color: var(--sunset); background: var(--sunset-tint); }
.board-towns label.is-soon { color: var(--ink-soft); border-style: dashed; }
.board-towns label.is-off { opacity: .45; cursor: default; }
.board-towns label.board-towns__all { flex-basis: 100%; justify-content: center; font-weight: 700; }
.board-towns label.board-towns__all small { text-transform: none; letter-spacing: 0; font-size: 12px; font-weight: 600; color: var(--sunset-ink); }
.board-towns label small {
  font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
  color: var(--ink-faint); margin-left: 2px;
}
.board-count { font-size: 12.5px; color: var(--ink-faint); text-align: right; margin-top: 4px; }
.board-count.is-over { color: var(--destructive); font-weight: 600; }
/* The draft of the card sits on the navy haze (owner, 16 Sep 2026): part of
   the form, but not a field — a look at the thing being bought. */
.board-preview {
  margin-top: var(--sp-5); padding: var(--sp-4) var(--sp-4) var(--sp-4);
  background: var(--primary-tint); border-radius: var(--radius);
}
.board-preview__t {
  font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
  color: var(--primary); margin-bottom: 12px;
}
.board-preview .board-card { margin: 12px 0 0; }

.board-total {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3);
  padding: var(--sp-4); border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface-2);
}
.board-total__n { font-family: var(--font-display); font-size: 24px; font-weight: 600; }
.board-adm { align-items: flex-start; }
.board-adm__card { flex: 1 1 320px; min-width: 0; }
.board-adm__card .board-card { margin: 12px 0 0; }
.board-adm__facts { flex: 1 1 260px; min-width: 0; }
.board-adm__acts { display: flex; gap: 6px; flex-wrap: wrap; }
.board-adm__n { font-size: 14px; color: var(--ink-faint); font-weight: 600; }
.board-adm__phase { display: inline-block; font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .06em;
  padding: 2px 8px; border-radius: var(--radius-pill); background: var(--surface-2); color: var(--ink-soft); }
.board-adm__phase--live { background: var(--seaglass-tint); color: var(--seaglass-ink); }
.board-adm__phase--booked { background: var(--primary-tint); color: var(--primary); }
.board-adm__phase--removed { background: #F7E3E0; color: var(--destructive); }
.board-manage__stats { display: flex; gap: var(--sp-5); margin: var(--sp-4) 0; }
.board-manage__stat b { display: block; font-family: var(--font-display); font-size: 26px; font-weight: 600; }
.board-manage__stat span { font-size: 13px; color: var(--ink-soft); }

/* ---- "Is this yours? Claim this listing" (claim.html) ---------------------
   Built from the signup page's parts — kicker, title, news-card, signup-info,
   success — so it reads as one more step of the same account, not a new
   product. Only the listing card and the sibling list are its own. */
.claim-listing {
  display: flex; gap: 14px; align-items: flex-start;
  padding: var(--sp-4); margin-top: var(--sp-4);
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--radius);
}
.claim-listing svg { flex: none; color: var(--primary); margin-top: 2px; }
.claim-listing__title { margin: 0; font-family: var(--font-display); font-size: 18px; font-weight: 600; line-height: 1.3; }
.claim-listing__meta { margin: 4px 0 0; font-size: 14px; color: var(--ink-soft); }
.claim-count { margin: var(--sp-5) 0 0; font-size: 15px; line-height: 1.6; }
.claim-list { list-style: none; margin: var(--sp-3) 0 0; padding: 0; display: grid; gap: 6px; font-size: 14px; color: var(--ink-soft); }
.claim-list li { display: flex; gap: 10px; align-items: baseline; }
.claim-list__when { flex: none; min-width: 96px; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.claim-list__more { color: var(--ink-faint); font-style: italic; }
.claim-actions { margin-top: var(--sp-5); }
.claim-quiet { margin: var(--sp-4) 0 0; font-size: 14px; color: var(--ink-faint); }
.claim-quiet a { color: var(--ink-soft); }
.claim-note textarea { min-height: 120px; }
/* The dashboard's "being checked" card: guidance, so it wears sage. */
.dash-card.claim-notice { border-color: var(--seaglass); background: var(--seaglass-tint); }
.dash-card.claim-notice .dash-card__title svg { color: var(--seaglass-ink); }
.dash-card.claim-notice--released { border-color: var(--accent); background: var(--accent-tint); }
.dash-card.claim-notice--released .dash-card__title svg { color: var(--accent); }
/* The owner's review line inside a client's Manage panel: its own row above
   the buttons, which is what flex-basis 100% does in that wrapping row. */
.org-row__more .org-claim {
  flex: 1 1 100%; margin: 0 0 4px; font-size: 13px; line-height: 1.5; color: var(--ink-soft);
}
.org-row__more .org-claim strong { color: var(--ink); }
.org-row__more .org-claim em { color: var(--ink-soft); }
/* ---- The buyer's card after the money (advertise.html?key=) ---------------
   A RECEIPT, NOT A FORM (owner, 17 Sep 2026): "it needs to be kind of settled
   so it looks done ... perhaps the user needs to click a button to edit
   specific parts". Everything is a stated line with a hairline under it; only
   the row being edited grows a box. The two blocks differ by one thing - the
   bought half has no Edit buttons, because the money decided it. */
.board-rev {
  margin: var(--sp-5) 0 0;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  overflow: hidden;
}
.board-rev__h {
  margin: 0; padding: 12px var(--sp-4);
  font-family: var(--font-display); font-size: 13px; font-weight: 600;
  letter-spacing: .06em; text-transform: uppercase; color: var(--ink-soft);
  background: var(--surface-2); border-bottom: 1px solid var(--line);
}
.board-rev__row {
  display: grid; grid-template-columns: 160px minmax(0, 1fr) auto;
  gap: var(--sp-3); align-items: baseline;
  padding: 14px var(--sp-4); border-top: 1px solid var(--line);
}
.board-rev__h + .board-rev__row { border-top: 0; }
.board-rev__k { font-size: 13px; color: var(--ink-soft); }
.board-rev__v { font-size: 15px; line-height: 1.5; overflow-wrap: anywhere; }
.board-rev__none { color: var(--ink-soft); font-style: italic; }
/* The Edit button is quiet until the row is wanted: this page is meant to
   read as finished, and four loud buttons would undo that on their own. */
.board-rev__edit { padding: 6px 14px; min-height: 0; font-size: 14px; }
.board-rev__row.is-editing { grid-template-columns: 160px minmax(0, 1fr); background: var(--surface-2); }
.board-rev__box input, .board-rev__box textarea {
  font: inherit; color: var(--ink); background: var(--surface);
  border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  padding: 10px 12px; min-height: 44px; width: 100%;
}
.board-rev__box textarea { min-height: 80px; resize: vertical; }
.board-rev__go { display: flex; gap: 8px; margin-bottom: 8px; }
.board-rev__go label {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 12px; font-size: 14px;
  background: var(--surface); border: 1px solid var(--line-strong); border-radius: 999px; cursor: pointer;
}
.board-rev__go label.is-on { border-color: var(--primary); box-shadow: inset 0 0 0 1px var(--primary); }
.board-rev__acts { margin-top: 10px; }
.board-rev__acts .btn { padding: 8px 16px; min-height: 0; font-size: 14px; }
.board-rev__err { display: none; margin-top: 6px; font-size: 13px; color: var(--destructive); font-weight: 600; }
.board-rev__err.is-on { display: block; }
/* ---- THE STATUS OF A BOUGHT CARD ------------------------------------------
   Three facts, three objects (owner, 17 Sep 2026). The stamp answers "did it
   work" and is meant to feel like a hand coming down on paper; the tab is
   drawn as the tab the buyer will actually look at; the days are tickets.
   Prose between them is glue and is set as glue. */
.board-stat { display: flex; align-items: center; flex-wrap: wrap; gap: 10px 12px; margin: var(--sp-3) 0 var(--sp-4); }
.board-stat__say { font-size: 15px; line-height: 2.1; color: var(--ink-soft); }
.board-stamp {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 7px 16px 7px 8px; border-radius: var(--radius-pill);
  font-family: var(--font-display); font-size: 15px; font-weight: 600;
  letter-spacing: .05em; text-transform: uppercase; white-space: nowrap;
}
.board-stamp--good { color: var(--seaglass-ink); background: var(--seaglass-tint); box-shadow: inset 0 0 0 1px rgba(125, 158, 151, .45); }
.board-stamp--wait { color: var(--signal-yellow); background: #FBF1DA; box-shadow: inset 0 0 0 1px rgba(138, 100, 16, .25); padding-left: 14px; }
.board-stamp--off  { color: var(--ink-faint); background: var(--surface-2); box-shadow: inset 0 0 0 1px var(--line-strong); padding-left: 14px; }
.board-stamp--done { color: var(--primary); background: var(--primary-tint); box-shadow: inset 0 0 0 1px var(--primary-tint-2); padding-left: 14px; }
.board-stamp__tick {
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; border-radius: 50%;
  background: var(--paint); color: #fff;
}
.board-stamp__dot { width: 9px; height: 9px; border-radius: 50%; background: var(--signal-yellow); }
/* The stamp lands once, on arrival, and never again - a confirmation that
   keeps moving stops being a confirmation. */
.board-stamp--good { animation: boardStamp .42s cubic-bezier(.2, 1.3, .4, 1) both; }
.board-stamp--good .board-stamp__tick { animation: boardTick .5s .12s cubic-bezier(.2, 1.3, .4, 1) both; }
@keyframes boardStamp { from { transform: scale(.88) rotate(-1.5deg); opacity: 0; } to { transform: none; opacity: 1; } }
@keyframes boardTick { from { transform: scale(0); } to { transform: none; } }
@media (prefers-reduced-motion: reduce) {
  .board-stamp--good, .board-stamp--good .board-stamp__tick { animation: none; }
}
.board-tag {
  display: inline-flex; align-items: center; gap: 5px; vertical-align: baseline;
  padding: 3px 10px; border-radius: var(--radius-pill);
  font-size: 14px; font-weight: 600; white-space: nowrap;
}
.board-tag--tab { color: var(--primary); background: var(--primary-tint); }
.board-tag--day { color: var(--ink); background: var(--surface); border: 1px dashed var(--line-strong); }
.board-tag__join { margin: 0 4px; color: var(--ink-soft); font-size: 14px; }
@media (max-width: 640px) {
  .board-stat { gap: 8px 10px; }
  .board-stat__say { line-height: 2.2; }
}
/* The Board's band on the plans page, beside the tickets one. The drawing is
   bigger than an icon and has to sit on the line without pushing it about. */
.tickets-band--ad { margin-top: var(--sp-3); }
.tickets-band--ad .tickets-band__t svg { vertical-align: -10px; margin-right: 2px; }

/* THE CHIP SITS IN THE CORNER, and this is the rule that puts it there.

   The card was drawn with the chip pinned top-right from the start (owner,
   17 Sep 2026) and it looked that way in every preview - but the three
   declarations that do it lived only in a working tree, never in a commit,
   so the live card wore its chip inline above the headline instead and the
   96px the headline reserves for it was just a gap. Committed here.

   THE RESERVE IS SIZED FOR THE LONGEST CHIP. "Sponsored" is narrow;
   "Your ad here", on the house card, measures 108px. At the old 96 it sat on
   top of the headline's last word. */
.board-card__chip { position: absolute; top: 10px; right: 12px; }
.board-card__title { padding-right: 120px; }

/* A SHORTER CARD (owner, 17 Sep 2026: "make the ad tile slightly smaller
   vertically"). The drawing drives the height - it is taller than the three
   lines beside it - so it comes down from 64 to 56 and the padding closes in
   around it. Measured on a 1100px window: 105px down to 94px, a tenth off.
   The picture and the column move together or nothing happens - a 64px
   drawing inside a 56px box leaves the card exactly as tall as it was. */
.board-card { padding: 12px var(--sp-4) 10px; grid-template-columns: 56px 1fr; }
.board-card__media, .board-card__media img, .board-card__icon, .board-card__icon svg { width: 56px; height: 56px; }
.board-card__text { margin-top: 3px; }
.board-card__foot { margin-top: 6px; }

.board-rev__foot { margin: calc(var(--sp-3) * -1) 0 0; font-size: 13px; color: var(--ink-soft); }

/* THE VIEWS BAND. One number, the width of the page, with the word beside it
   at the same weight and the definition underneath (owner, 17 Sep 2026). The
   count is the point of the page for a buyer deciding whether to run it
   again, so it is the loudest thing on it after the card itself. */
.board-views {
  display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center;
  gap: var(--sp-5);
  margin: var(--sp-5) 0 0; padding: var(--sp-4) var(--sp-5);
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--radius);
}
.board-views__n {
  margin: 0; display: flex; align-items: baseline; gap: 12px;
  font-family: var(--font-display); line-height: 1; white-space: nowrap;
}
.board-views__n b { font-size: 46px; font-weight: 600; letter-spacing: -.01em; }
.board-views__n span { font-size: 30px; font-weight: 600; color: var(--ink-soft); }
/* The sentence sits BESIDE the number and runs to the edge of the band. Set
   under it and capped at a reading measure, it left a third of a wide box
   empty and the whole thing looked like a mistake (owner, 17 Sep 2026). */
.board-views__d {
  margin: 0; padding-left: var(--sp-5); border-left: 1px solid var(--line);
  font-size: 15px; line-height: 1.55; color: var(--ink-soft);
}
/* One column on a phone, and the hairline goes back to being a horizontal
   rule, because a vertical one between stacked things divides nothing. */
@media (max-width: 720px) {
  .board-views { grid-template-columns: 1fr; gap: 12px; padding: var(--sp-4); }
  .board-views__n b { font-size: 38px; }
  .board-views__n span { font-size: 26px; }
  .board-views__d { padding: 12px 0 0; border-left: 0; border-top: 1px solid var(--line); font-size: 14px; }
}
.board-rev .board-count { display: block; margin-top: 6px; font-size: 13px; color: var(--ink-soft); }
.board-rev .board-count.is-over { color: var(--destructive); font-weight: 600; }

/* On a phone the label goes above what it labels, and Edit keeps its place on
   the first line so the eye still finds it in the same column. */
@media (max-width: 640px) {
  .board-rev__row, .board-rev__row.is-editing { grid-template-columns: minmax(0, 1fr) auto; row-gap: 4px; }
  .board-rev__k { grid-column: 1; }
  .board-rev__v { grid-column: 1 / -1; }
  .board-rev__edit { grid-column: 2; grid-row: 1; }
}
