/* Crashers — app-only styles.
 *
 * tokens.css and crashers.css are the design, copied verbatim from the
 * handoff's reference/ folder. Nothing in them is edited or overridden here.
 * This file is only the parts that folder deliberately leaves out, listed in
 * reference/IMPLEMENTING.md under "What is deliberately not here":
 *
 *   the phone frame          the reference sheet has its own .frame scaffold
 *   onboarding + alerts      "classes already defined here", markup is ours
 *   the Leaflet map          keep Leaflet, restyle it
 *
 * plus the deliberate additions marked ADDITION below, and the desktop app
 * shell, which the handoff does not specify at all.
 */

/* ---------- shell ---------- */
html, body { height: 100%; }
body {
  background: var(--paper);
  font-size: 15px;
  -webkit-font-smoothing: antialiased;
  overscroll-behavior-y: none;
}
button { font: inherit; color: inherit; background: none; border: 0; padding: 0;
         cursor: pointer; text-align: left; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
[hidden] { display: none !important; }

/* Space above the header for the OS status bar. The reference sheet fakes one
   with its own .statusbar, which IMPLEMENTING.md calls scaffolding; the real
   app uses the system bar. On a device the notch inset supplies the space
   (viewport-fit=cover is set); the desktop frame has no OS to draw one, so the
   media query below substitutes a fixed value.

   It is a token rather than a padding override because .header comes from
   crashers.css, which loads first - a later padding-top here would fight its
   padding shorthand. */
:root { --top-inset: env(safe-area-inset-top, 0px); }
.header { padding-top: calc(10px + var(--top-inset)); }

/* ---------- the phone frame ---------- */
.stage  { height: 100%; display: flex; align-items: center; justify-content: center; }
.device { width: 100%; height: 100%; display: flex; }
.screen { flex: 1; min-width: 0; position: relative; overflow: hidden; }

/* On a phone the app is the viewport. On a computer it sits in a device frame
   at a real iPhone's size - 420x892, 19.5:9. Fixed in both dimensions: three
   attempts to fit the frame to the window all failed, the last by holding
   width while height shrank until the ratio collapsed to 1:1.76. If the
   window is shorter than the phone, the page scrolls to it.

   `pointer: fine` is doing real work here and min-width alone cannot replace
   it. Rotating a phone to landscape makes the viewport WIDER - a 390x844
   screen becomes 844x390 - so a width-only query put the frame up on the
   rotated phone: a 420x892 phone drawn inside a 390px-tall window, which is
   why landscape showed a slice of a portrait app that had to be scrolled.
   A touch screen reports a coarse pointer in either orientation, so this asks
   the question that was actually meant - "is this a computer?" - rather than
   inferring it from width. */
/* The rail and the footer are the web view's furniture; on a phone the chip
   rail and the You tab do those jobs. Declared HERE, above the media query
   that turns them on - both rules are one class deep, so source order is
   what decides, and with this at the bottom of the file the rail rendered
   into a 232px column of nothing. */
.rail, .foot { display: none; }

@media (min-width: 520px) and (pointer: fine) {
  /* ---------------------------------------------------------------------
     The web view. Not the phone made wider - a different arrangement of the
     same content, because the first attempt WAS the phone made wider and
     read exactly like it.

     Everything is inside (pointer: fine), so a touch device sees none of it
     and the phone view is untouched. min-width alone cannot do that job: a
     phone in landscape is 844px wide.
     --------------------------------------------------------------------- */
  html, body { height: 100%; }
  body { background: var(--sand); }
  .stage { display: block; height: 100%; padding: 0; }
  .device { display: block; width: 100%; height: 100%; margin: 0; padding: 0; }
  .screen { display: block; border-radius: 0; overflow: hidden; height: 100%; }
  :root { --top-inset: 0px; }

  /* ---------------------------------------------------------------------
     The desktop app shell, after Spotify's.

     The first web view was a PAGE - one column of paper, the whole document
     scrolling, furniture at the top and bottom. Widened, it read as the phone
     dragged sideways, because structurally that is what it was.

     Spotify's desktop app is not a page. It is a set of PANELS laid on a
     ground with gaps between them, each scrolling on its own: a navigation
     panel and a library panel down the left, one main pane holding the
     shelves, a utility bar pinned across the bottom. The window never
     scrolls - only the panes inside it do. That is the arrangement copied
     here, and it is the one the shelves want, since a shelf is a horizontal
     rail living inside a vertical scroller.

     What is NOT copied is Spotify's palette. It is a dark app, and this
     design specifies one theme and one accent. So the ground is --sand and
     the panels are --paper: the same structure in the design's own colours,
     which is the half that makes it read as an app rather than a website.
     --------------------------------------------------------------------- */
  #main {
    display: grid !important;
    grid-template-columns: 280px minmax(0, 1fr);
    /* title | segment | content (fills) | bottom bar. minmax(0,1fr) on the
       content row is what lets the panes scroll instead of stretching the
       shell past the window. */
    grid-template-rows: auto auto minmax(0, 1fr) auto;
    height: 100%;
    /* Row gap is zero because the right column's three rows are ONE pane and
       a gap would saw it into three. The left column buys its 8px back with a
       margin, and so does the bottom bar. */
    column-gap: 8px;
    row-gap: 0;
    padding: 8px;
    background: var(--sand);
    align-items: stretch;
  }

  /* The left column: two panels, the way Spotify stacks nav over library.

     Both span the full content height and are stacked by hand rather than
     given a row each, because a row is shared with the column beside it - and
     with the nav panel in row 1, row 1 became as tall as the nav and the main
     pane's title was pushed 140px down its own pane, sitting at the bottom of
     a band of nothing. The nav's height is therefore SET here, not measured,
     and the filter panel is offset by that same number plus the gap, so the
     two cannot drift apart. */
  :root { --nav-h: 178px; }
  #main > .tabs {
    grid-column: 1; grid-row: 1 / 4;
    align-self: start; height: var(--nav-h);
  }
  .rail {
    grid-column: 1; grid-row: 1 / 4; display: block;
    margin-top: calc(var(--nav-h) + 8px);
  }

  /* The right column is ONE pane - title, segment, content - stacked in three
     rows so the segment sits above the scroller instead of inside it. That is
     what keeps it in place while the shelves move, with no sticky rule. */
  .header       { grid-column: 2; grid-row: 1; }
  .seg          { grid-column: 2; grid-row: 2; }
  .screen__body { grid-column: 2; grid-row: 3; }
  .map          { grid-column: 2; grid-row: 3; }
  .foot         { grid-column: 1 / -1; grid-row: 4; margin-top: 8px; }

  /* ---- panels ---- */
  #main > .tabs, .rail, .header, .seg, .screen__body, .map, .foot {
    background: var(--paper);
    border-radius: var(--r-block);
  }
  .header       { border-radius: var(--r-block) var(--r-block) 0 0; }
  .seg          { border-radius: 0; }
  .screen__body { border-radius: 0 0 var(--r-block) var(--r-block); }
  .map          { border-radius: 0 0 var(--r-block) var(--r-block); }

  /* ---- the nav panel ---- */
  #main > .tabs {
    border-top: 0;
    padding: 14px 12px;
    display: flex; flex-direction: column; gap: 2px;
  }
  #main > .tabs .tab {
    flex: none; min-height: 0;
    flex-direction: row; align-items: center; justify-content: flex-start;
    gap: 12px;
    padding: 11px 14px;
    border-radius: var(--r-option);
  }
  #main > .tabs .tab:hover { background: var(--paper-2); }
  /* The dot is the phone's active marker, and on a column of nav rows it is
     the bullet in front of the label - so it stays, rather than being hidden
     as the page layout hid it. */
  #main > .tabs .tab__dot { opacity: 0; }
  #main > .tabs .tab--on .tab__dot { opacity: 1; }
  #main > .tabs .tab__label {
    font-size: 13px; letter-spacing: .1em; color: var(--muted-2);
  }
  #main > .tabs .tab--on { background: var(--paper-2); }
  #main > .tabs .tab--on .tab__label { color: var(--ink); }

  /* ---- the filter panel: Spotify's library column ---- */
  .rail {
    position: static;
    padding: 18px 18px 22px;
    overflow-y: auto;
    scrollbar-width: none;
  }
  .rail::-webkit-scrollbar { display: none; }
  .rail__head {
    display: flex; align-items: baseline; justify-content: space-between;
    padding-bottom: 8px; border-bottom: 1px solid var(--rule);
  }
  .rail__reset { font: 600 11.5px var(--font-ui); color: var(--accent); cursor: pointer; }
  .rail .opts { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
  .rail .opt { min-height: 34px; padding: 0 10px; font-size: 12px; }
  .rail .t-eyebrow { padding: 18px 0 7px !important; }

  /* ---- the main pane's header ----
     Spotify's is a shallow bar carrying the view's title, not a masthead. So
     the 62px wordmark goes: at the top of a pane 8px from the window edge it
     was a magazine cover stuck on an app. */
  .header {
    position: relative;
    display: flex; align-items: flex-end; justify-content: space-between;
    gap: 20px;
    padding: 20px 26px 0;
    margin-bottom: 0;
    /* The segment underneath carries the pane's rule; two would be a ladder. */
    border-bottom: 0;
  }
  .header__title { font-size: 34px; letter-spacing: -.01em; }
  .header__sub { margin-top: 7px; padding-bottom: 0; border-top: 0; }
  .bell { position: static; }

  /* ---- the content pane scrolls; the window never does ---- */
  .screen__body {
    overflow-y: auto;
    overflow-x: hidden;
    scrollbar-width: none;
    padding: 0 26px 26px;
  }
  .screen__body::-webkit-scrollbar { display: none; }
  /* The pane's rule lives on the segment, and Saved and You do not draw one -
     so on those the content pane carries it instead and the title still sits
     on a line. Its top corners are square, so the border meets them cleanly. */
  .seg[hidden] ~ .screen__body { border-top: 1px solid var(--rule); }

  /* ---- the list under the shelves ----
     Spotify's "Show all" view: once browsing is done in the rails, the rest
     is a grid of the same cards. The day heading is a real date with a rule
     under it rather than a 10.5px eyebrow, and the next event on takes the
     full measure as a lead - a pane has a front, the same way a page does. */
  .list {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: 40px; padding-left: 0; padding-right: 0;
  }
  .list > .group-label,
  .list > .t-note,
  .list > div:not(.card) { grid-column: 1 / -1; }

  .group-label {
    font: 400 15px/1 var(--font-display) !important;
    letter-spacing: 0 !important; text-transform: none !important;
    color: var(--muted) !important;
    padding: 26px 0 10px !important;
    border-bottom: 1px solid var(--rule);
    margin-bottom: 4px;
  }
  .list > .group-label:first-child { padding-top: 0 !important; }

  .list > .group-label:first-child + .card {
    grid-column: 1 / -1;
    padding: 22px 0 26px;
    border-top: 0;
  }
  .list > .group-label:first-child + .card .card__serves {
    font-size: 42px; line-height: 1.04; margin-top: 14px;
  }
  .list > .group-label:first-child + .card .card__title {
    font-size: 16px; max-width: 46ch;
  }
  .list > .group-label:first-child + .card .card__meta { font-size: 12px; }
  /* The lead spans every track, and .save's margin-left:auto was throwing
     Save to the far right edge with 800px of paper between it and the tag it
     belongs with. Cap the row to the title's measure so the controls stay
     with the text. */
  .list > .group-label:first-child + .card .card__foot { max-width: 46ch; }

  .card {
    break-inside: avoid;
    display: flex; flex-direction: column;
    padding: 20px 0 24px;
    border-top: 1px solid var(--rule);
  }
  .card__foot { margin-top: auto; padding-top: 14px; }
  .card:hover .card__serves { color: var(--accent); }
  .card__serves { transition: color .12s ease-out; }
  .tile:hover .tile__serves { color: var(--accent); }
  .tile--ink:hover .tile__serves,
  .tile--img:hover .tile__serves { color: var(--accent-ink); }

  /* Its own row above the scroller, so it holds still while the shelves move
     without needing a sticky rule - Spotify's row of view filters, in the
     same place and doing the same job. */
  .seg {
    padding: 0 26px;
    margin: 0;
    gap: 26px;
    border-bottom: 1px solid var(--rule);
  }
  .seg__btn { padding: 14px 0 11px; font-size: 13px; }

  /* The chip rail is a thumb affordance; the filter panel replaced it. */
  .chips { display: none !important; }

  /* ---- the bottom bar ----
     Spotify's now-playing strip: pinned, compact, always there. Ours carries
     what a reader should never have to go hunting for - the sources, the rule
     the whole project rests on, and the map credit that OpenFreeMap's and
     OpenStreetMap's terms require us to keep reachable. */
  .foot {
    display: flex !important;
    align-items: center; gap: 26px;
    padding: 0 22px;
    min-height: 56px;
    border-top: 0;
    margin-top: 0;
    /* Hidden, not auto. Three flex columns of nowrap content will each spill
       out of its own box rather than shrink, and the source list printed
       straight through the map credit. Each column is allowed to shrink and
       clip instead. */
    overflow: hidden;
  }
  .foot .t-eyebrow { display: none; }
  .foot__col {
    display: flex; align-items: center; gap: 10px;
    min-width: 0; overflow: hidden;
  }
  .foot__col:first-child  { flex: 0 1 auto; }
  .foot__col:nth-child(2) { flex: 1 1 24ch; }
  .foot__col:last-child   { flex: 0 0 auto; }
  .foot p {
    font: 400 12px/1.4 var(--font-ui); color: var(--muted-2);
    margin: 0; max-width: 62ch;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  /* Only the first line of each column survives the strip; the rest is detail
     the You screen already carries in full. */
  .foot__col p:nth-of-type(2) { display: none; }
  .foot a { color: var(--muted); text-decoration: underline; }
  .foot__srcs {
    list-style: none; margin: 0; padding: 0;
    display: flex; gap: 14px; white-space: nowrap;
  }
  .foot__srcs li {
    display: flex; gap: 5px;
    font: 500 12px var(--font-ui); color: var(--muted);
    padding: 0; max-width: none; border-bottom: 0;
  }
  .foot__srcs li span { color: var(--muted-3); }

  /* ---- sheets become centred dialogs ----
     Fixed, not absolute: the wrapper is positioned against .screen, which is
     only as wide as the container, so an absolute scrim left both page
     margins bright and the dialog looked like it belonged to a column. */
  .sheet-wrap {
    position: fixed; inset: 0;
    align-items: center; justify-content: center; padding: 32px;
  }
  .sheet {
    max-width: 620px; width: 100%;
    border-radius: var(--r-block); max-height: 86vh;
  }
  .sheet--filters, .sheet--detail { max-height: 86vh; padding: 30px 34px 34px; }
  /* A grabber says "drag me" and has nothing to say to a mouse. */
  .grabber { display: none; }

  .overlay {
    position: fixed; inset: 0; display: flex;
    align-items: center; justify-content: center; padding: 32px;
    background: rgba(23, 19, 15, .45);
  }
  .overlay > .header,
  .overlay > .screen__body {
    background: var(--paper); width: 100%; max-width: 560px;
  }
  .overlay > .header {
    border: 0; border-bottom: 1px solid var(--rule);
    border-radius: var(--r-block) var(--r-block) 0 0;
    padding: 22px 24px 16px; margin: 0;
  }
  .overlay > .header .header__title { font-size: 27px; }
  .overlay > .screen__body {
    border-radius: 0 0 var(--r-block) var(--r-block);
    max-height: 70vh; overflow-y: auto; padding: 0 24px 22px;
  }

  /* ---- map ---- */
  .map { min-height: 66vh; border-radius: var(--r-block); }
  /* The sheet behaves the same on the page as on the phone - it is inside
     the map box, so it overlays the bottom of the tiles rather than taking a
     row of its own. The chip rail is hidden here, so Leaflet's own controls
     go back to the top. */
  .map .leaflet-top { top: 0; }
}

/* Three tracks and a wider rail once there is genuinely room. */
@media (min-width: 1240px) and (pointer: fine) {
  #main { grid-template-columns: 232px minmax(0, 1fr); }
  .list { grid-template-columns: repeat(3, minmax(0, 1fr)); column-gap: 34px; }
  .list > .group-label:first-child + .card .card__serves { font-size: 54px; }
}


/* ---------- ADDITION: the rust pill tab bar ----------
   The handoff's bar is four uppercase words and a 5px dot, and on a phone it
   read as flat - nothing to recognise, nothing moving. Chosen from nine
   options on 2026-09-23: the current tab becomes a rust pill carrying its
   icon and name, and the other three shrink to icons.

   A modifier class on .tabs, not new rules on .tab, so check_design.py sees
   no override - the same arrangement as .mpin beside the design's .pin. It is
   phone-only: the desktop shell turns .tabs into a nav column of its own, and
   the negated query is exactly the one that turns that shell on. The icons
   are hidden outside it, so the desktop column is unchanged.

   The pill is the accent on screen. That stretches "exactly one accent" when
   a feed card below carries its own rust mark; the user chose it knowing. */
.tab__ico { display: none; }
@media not all and (min-width: 520px) and (pointer: fine) {
  .tabs--pill { gap: 6px; padding: 10px 10px 22px; }
  .tabs--pill .tab {
    flex: 1 1 0; flex-direction: row; gap: 7px;
    border-radius: var(--r-pill);
    color: var(--muted-2);
    transition: flex-grow .35s cubic-bezier(.3,.7,.2,1), background-color .25s, color .25s;
  }
  .tabs--pill .tab:active { transform: scale(.95); }
  .tabs--pill .tab__dot { display: none; }
  .tabs--pill .tab__ico { width: 22px; height: 22px; flex: none; }
  .tabs--pill .tab__ico--line { display: block; }
  .tabs--pill .tab__label {
    font: 700 13.5px var(--font-ui); letter-spacing: .01em; text-transform: none;
    color: inherit; white-space: nowrap; overflow: hidden;
    max-width: 0; transition: max-width .35s cubic-bezier(.3,.7,.2,1);
  }
  .tabs--pill .tab--on { flex-grow: 2.1; background: var(--accent); color: var(--accent-ink); }
  .tabs--pill .tab--on .tab__ico--line { display: none; }
  .tabs--pill .tab--on .tab__ico--fill { display: block; }
  .tabs--pill .tab--on .tab__label { max-width: 80px; color: inherit; }
}
@media (prefers-reduced-motion: reduce) {
  .tabs--pill .tab, .tabs--pill .tab__label { transition: none; }
}

/* ---------- landscape, and any short viewport ----------
   The handoff is a portrait design and specifies nothing for this, so these
   are deviations by necessity rather than choice. A phone on its side has
   about 390px of height; the header, chip rail and tab bar were built for 892
   and between them took more than half of it. This gives the list back its
   room by trimming only vertical padding - no type sizes change, so the
   design's scale is intact and the screen is the same screen, just less
   generously spaced.

   These land on values crashers.css sets, which is what a media query for an
   unspecified viewport has to do. check_design.py reports them under
   "media-query deviations" rather than passing them silently. */
@media (max-height: 560px) {
  .header { padding-top: calc(5px + var(--top-inset)); padding-bottom: 7px; }
  .header__sub { margin-top: 3px; }
  .chips { padding-bottom: 8px; }
  .tabs, .tabs--pill { padding: 5px 8px 7px; }
  .tab { min-height: 38px; gap: 3px; }
  .list { padding-bottom: 12px; }
  .group-label { padding: 9px 0 2px; }
  .card { padding: 11px 0 12px; }
  /* The sheets are the real squeeze: a bottom sheet capped at 86% of 390px is
     335px, and the filter groups alone are taller than that. Let them use the
     full height and scroll, which .sheet already does. */
  .sheet--filters, .sheet--detail { max-height: 96%; }
  /* On a short screen the sheet would leave the map a letterbox strip, so
     it peeks lower and the map keeps the space. */
  .mapsheet[data-state="peek"] { height: 180px; }
  .locbtn { margin-bottom: 192px !important; }
  .mcard__serves { -webkit-line-clamp: 2; }
}

/* ---------- ADDITION 1: the display face, and its optical size ----------
   The handoff names Bodoni Moda. It is a Didone - thick stems, hairline
   horizontals - with an optical-size axis from 6 (text) to 96 (display).
   font-optical-sizing:auto maps opsz to the font size, so a 27px line renders
   at opsz 27 and the 40px hero at opsz 40: the display cuts, where the
   hairlines are meant to be nearly invisible. That survives the handoff's 2x
   screen captures, where a hairline still gets two device pixels, and it does
   not survive 1x - the horizontals disappeared and the type was reported
   unreadable.

   Pinning opsz to the text end and taking the weight 400 -> 500 recovered
   some of it and was not enough, so the family moves too. Playfair Display is
   the same drawing - Didone, vertical stress, ball terminals, the same air in
   a headline - with thins thick enough to hold at 1x and at 22px in a tile.
   It is the readable cut of this face, not a different one.

   The swap is one token. tokens.css is NOT edited: check_design.py holds it
   byte-identical to the handoff, so the override lands here, where every
   other deliberate deviation already lives. The fallbacks are unchanged and
   still carry the design if the webfont never arrives.

   Playfair has no opsz axis, so the two rules below are inert on it and live
   on only for those fallbacks. --serif-wt still does its job. */
:root {
  --font-display: 'Playfair Display', Didot, 'Times New Roman', serif;
  --opsz: 10; --serif-wt: 500;
}
.t-hero, .t-ob-head, .t-title, .t-serves, .t-serves-sm, .t-alert, .t-quote,
.t-empty, .header__title, .ob__head, .ex__line, .sheet-heading {
  font-optical-sizing: none;
  font-variation-settings: 'opsz' var(--opsz);
}
.t-serves, .t-serves-sm, .t-alert, .t-empty, .ex__line { font-weight: var(--serif-wt); }
.t-quote { font-weight: var(--serif-wt); }

/* The audience tag is a fourth item in a row built for three, and long labels
   exist - "Veterans and families only". crashers.css sets .card__foot's
   display, gap and alignment but not its wrapping, so allowing a wrap is an
   addition rather than an override. .save keeps margin-left:auto, so if the
   row does wrap, Save sits at the right of the last line. */
.card__foot { flex-wrap: wrap; }   /* its gap is the design's, both axes */
/* Same outline as the access tag, one step quieter: what is on the table and
   how to get in are the decisions; who it is for is a qualifier on them. */
.tag--who { color: var(--muted-2); }

/* ---------- ADDITION 2: an estimate is not set like a fact ----------
   "12% likely" was rendering identically to "Host stated" - same size, weight
   and grey. No second hue, since the handoff forbids one and says the mark
   carries the meaning in words, so the signal is typographic. */
.mark--est { font-style: italic; color: var(--muted-3); }

/* ---------- ADDITION 3: clamps ----------
   A real serves phrase is two or three words and never reaches these. An
   estimate has no phrase, so the event's own title takes the slot, and a long
   one ran to three lines in the feed and six in the sheet. */
.card__serves, .t-hero {
  display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden;
}
.card__serves { -webkit-line-clamp: 3; }
.t-hero       { -webkit-line-clamp: 4; }
/* A clamp clips at the line box, and Playfair's descenders hang below the
   design's 1.06-1.18 leading - the g of "Village Café" lost its tail. Enough
   leading to hold a descender is the only fix that does not also show a
   sliver of the clamped-off line. The detail sheet's .t-hero keeps the
   design's leading (its font shorthand is the handoff's), so it gets the
   room as padding instead, handed back by the margin so nothing moves. */
.card__serves { line-height: 1.24; }   /* the tile sets its own, below */
.t-hero { padding-bottom: .2em; margin-bottom: -.2em; }

/* Every full-width row the reference styles as a block is a <button> here, so
   the whole row is one tap target instead of a div with a click listener.
   That is a markup choice and this is its only consequence: a form control is
   shrink-to-fit, and unlike a div it stays that way even when the rule sets
   display:flex, because Chromium gives buttons an intrinsic width:fit-content.
   So each of these sized to its longest line instead of the gutter:

     .card        the top rule stopped short, and `.save`'s margin-left:auto
                  pushed Save to the card's own ragged right edge
     .switch-row  the switch sat mid-row rather than at the right edge
     .btn--text   justify-content:center centred the label in an 81px box
     .alert-row   same as .card

   Not overrides - crashers.css never declares a width on any of them. The
   rows that must keep hugging their label are left alone: .chip, .opt and
   .tag size to their text, .tab has its own flex:1, and the empty state's
   "Back to the feed" is inline by design. */
.card, .alert-row       { display: block; width: 100%; }
.switch-row, .btn--text { width: 100%; }   /* their display:flex is the design's */
.sheet--filters > .btn  { width: 100%; }   /* the sheet's one full-width CTA */

/* Fade the chip rail's ends so a chip cut off by the scroll reads as
   more-to-come rather than as broken layout. */
.chips {
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 16px,
                      #000 calc(100% - 16px), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 16px,
              #000 calc(100% - 16px), transparent 100%);
}

/* ---------- onboarding ---------- */
.ob { flex: 1; min-height: 0; display: flex; flex-direction: column;
      padding: var(--top-inset) var(--gutter-ob) 34px; }
.ob__bars { flex: none; display: flex; gap: 6px; padding: 18px 0 0; }
.ob__bars i { width: 26px; height: 3px; border-radius: 2px; background: var(--rule); }
.ob__bars i.on { background: var(--accent); }
.ob__mid { flex: 1; min-height: 0; display: flex; flex-direction: column;
           justify-content: center; padding-bottom: 20px; overflow-y: auto;
           scrollbar-width: none; }
.ob__mid::-webkit-scrollbar { display: none; }
.ob__head { font: 400 42px/1.08 var(--font-display); margin: 14px 0 0; }
.ob__body { margin: 18px 0 0; }
.ex { background: var(--paper-2); border-radius: var(--r-block);
      padding: 16px 18px; margin-top: 26px; }
.ex__line { font: 400 24px/1.2 var(--font-display); margin-top: 9px; }
.ex__sub  { font: 400 13px/1.5 var(--font-ui); color: var(--muted-2); margin-top: 7px; }
.ob__foot { flex: none; display: flex; flex-direction: column; }

/* ---------- alerts overlay ---------- */
.overlay { position: absolute; inset: 0; z-index: 30; background: var(--paper);
           display: flex; flex-direction: column; }
.overlay .header { align-items: baseline; padding-bottom: 14px; }
.done { font: 600 13px var(--font-ui); color: var(--accent);
        min-height: var(--tap); display: flex; align-items: center; }
.alert-row { padding: 15px 0; border-top: 1px solid var(--rule); cursor: pointer; }
.alert-row__kind { display: flex; justify-content: space-between; gap: 10px; }
.alert-row h3 { margin: 8px 0 0; }
.alert-row p  { font: 500 12.5px/1.5 var(--font-ui); color: var(--muted-2); margin: 7px 0 0; }

/* ---------- the map: Leaflet, restyled ---------- */
/* Contain Leaflet's own z-indices. It stacks its panes at 400 and its
   controls at 800, and crashers.css gives .map position:relative with no
   z-index - which is not a stacking context, so those numbers competed
   directly with the sheets'. The filters sheet was therefore painted
   underneath the map tiles whenever it was opened from the map screen, and
   the only part of it visible was the strip below the map, which read as the
   top of the sheet being cut off. isolation:isolate makes .map a stacking
   context so nothing inside it can outrank an overlay again. It adds a
   property crashers.css does not set, so it is not an override. */
.map { isolation: isolate; }
#map { position: absolute; inset: 0; background: var(--map-land); }
.leaflet-container { background: var(--map-land); font-family: var(--font-ui); }
.leaflet-control-attribution {
  background: rgba(250, 246, 239, .82) !important;
  font: 400 9.5px var(--font-ui) !important; color: var(--muted-2) !important;
}
.leaflet-control-attribution a { color: var(--muted) !important; }
.leaflet-bar { border: 1px solid var(--border) !important; box-shadow: none !important; }
.leaflet-bar a { background: var(--paper) !important; color: var(--ink) !important;
                 border-bottom-color: var(--rule) !important; }

/* ---------- the markers ----------
   These are `.mpin`, not the design's `.pin`. The reference's marker is a word
   pill and its rules stay in crashers.css untouched and now unused - the
   markers show an icon instead, on request. A separate class rather than a
   `.pin--icon` modifier keeps this from being an override of a design rule,
   which is what check_design.py is there to prevent.

   32px is the smallest circle that reads as a tapped-on marker while leaving
   the 17px glyph legible; 44px of tap target comes from Leaflet's own marker
   box, which is larger than the drawn circle. */
.mpin {
  width: 32px; height: 32px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--accent); color: var(--accent-ink);
  border: 2px solid var(--paper);
  box-shadow: 0 2px 7px rgba(23, 19, 15, .28);
}
.mpin svg { width: 17px; height: 17px; display: block; }
/* Selected grows instead of changing hue. There is one accent in this design,
   and the hollow treatment below is already spent on estimates. */
.mpin--on { width: 40px; height: 40px; background: var(--ink); }
.mpin--on svg { width: 21px; height: 21px; }
/* An estimate has no host statement about food, so it gets no glyph: drawing
   a cup for a listing that never mentioned one would be the no-quote-no-claim
   rule broken in pictures. Hollow and smaller - present, clearly unconfirmed. */
.mpin--est {
  width: 20px; height: 20px;
  background: var(--paper); border: 2px solid var(--muted-3);
  box-shadow: 0 1px 4px rgba(23, 19, 15, .18);
}
.mpin--est.mpin--on { width: 26px; height: 26px; border-color: var(--ink); }

.leaflet-marker-icon .mpin { position: static; transform: none; }
.me-ring { pointer-events: none; }

/* Recolour the cluster bubbles and leave every dimension to
   MarkerCluster.Default.css, which owns the circle. */
.marker-cluster-small, .marker-cluster-medium, .marker-cluster-large {
  background-color: rgba(180, 68, 42, .20);
}
.marker-cluster-small div, .marker-cluster-medium div, .marker-cluster-large div {
  background-color: var(--accent); color: var(--accent-ink);
}
.marker-cluster div { font-family: var(--font-ui); font-weight: 600; }

/* ---------- motion ---------- */
/* A sheet rises from the bottom edge while its scrim fades in, the way
   Uber Eats opens an item (2026-09-27; it was a 14px nudge). The close is
   the same motion in reverse, in index.html's closeDetail. */
.sheet { animation: sheet-up .32s cubic-bezier(.2,.9,.3,1); }
.sheet-scrim { animation: scrim-in .32s ease-out; }
@keyframes sheet-up { from { transform: translateY(100%); } to { transform: translateY(0); } }
@keyframes scrim-in { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .sheet, .sheet-scrim { animation: none; } }

/* ---------- ADDITION 4: swipe to save or skip ----------
   The transform is on .card__slide, not .card: the card element has to stay
   put, and wrapping the card would make it a `.list > div:not(.card)` and
   break the web grid. crashers.css sets .card's padding, border and cursor
   but not its position or overflow, so both are additions.

   The action labels are fixed to the viewport, not drawn inside the card, for
   two reasons: they come in from the screen edge, which means escaping the
   card's clip, and one pair serves every card. */
.card { position: relative; overflow: hidden; }
.card__slide { display: block; will-change: transform; }

.swb {
  position: fixed; z-index: 1450;
  transform: translate(100%, -50%); opacity: 0;
  padding: 9px 16px; border-radius: 999px;
  font: 600 13px var(--font-ui); white-space: nowrap;
  pointer-events: none; will-change: transform, opacity;
  box-shadow: 0 4px 16px -6px rgba(0,0,0,.4);
}
.swb--save { right: 0; border-radius: 999px 0 0 999px; background: var(--accent); color: var(--paper); }
.swb--skip { left: 0;  border-radius: 0 999px 999px 0; background: var(--ink);    color: var(--paper); }

/* ---------- ADDITION 5: undo ----------
   Above the tab bar, clear of the home indicator. A dismissal is the only
   gesture in the app that removes an event, so it gets a way back. */
.undo {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(72px + var(--bottom-inset, 0px)); z-index: 1500;
  display: flex; align-items: center; gap: 14px;
  background: var(--ink); color: var(--paper);
  padding: 11px 16px; border-radius: 999px;
  font: 500 13.5px var(--font-ui); box-shadow: 0 6px 22px -8px rgba(0,0,0,.45);
}
.undo button {
  background: none; border: 0; padding: 0; cursor: pointer;
  font: 600 13.5px var(--font-ui); color: var(--paper);
  text-decoration: underline; text-underline-offset: 2px;
}

/* ---------- ADDITION 6: the locate button ----------
   Bottom right on its own, the place every maps app puts it, rather than a
   third entry in the top-right settings stack. It names its state because
   the old silent crosshair gave no sign it had done anything. */
/* Clear of the sheet's peek height, so the button is never under it. */
.locbtn { margin: 0 10px 216px 0 !important; }   /* the peek sheet's 204px + 12 */
/* Raised, the sheet is taller than any margin, and the button sat on the list. */
.map:has(.mapsheet[data-state="up"]) .locbtn { display: none; }
.locbtn button {
  display: flex; align-items: center; gap: 8px;
  background: var(--paper); color: var(--ink);
  border: 1px solid var(--border); border-radius: 999px;
  padding: 9px 15px 9px 12px; cursor: pointer;
  font: 600 13px var(--font-ui);
  box-shadow: 0 2px 10px -3px rgba(0,0,0,.28);
}
.locbtn button[data-state="finding"] { color: var(--muted-2); cursor: default; }
.locbtn button[data-state="found"]   { color: var(--accent); }
.locbtn button[data-state="denied"]  { color: var(--muted-3); }
.locbtn button[data-state="finding"] svg { animation: locspin 1.1s linear infinite; }
@keyframes locspin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .locbtn button[data-state="finding"] svg { animation: none; }
}

/* ---------- ADDITION 7: who is putting it on ----------
   Asked for on the card rather than only in the sheet: whether it is a council
   library or a property developer changes whether the event is worth the trip,
   and that was a tap away. Set in the design's own .t-meta, so this adds a
   position in the stack and nothing else. 11 of 232 rows name no host, and
   those fall back to the source, as the detail sheet already does. */
.card__host { margin-top: 4px; }

/* ---------- ADDITION 8: the map screen's sheet and floating rail ----------
   The handoff's map screen is a map with a static strip under it: one card,
   only after you tap a marker, and no way to see what else is out there. This
   is the arrangement every maps app that also has a list uses instead - the
   map runs the full height of the screen, the filters float on it, and the
   results sit in a sheet on top that pulls up.

   Nothing here overrides a design rule. `.map-card` keeps its rules in
   crashers.css and is simply no longer drawn, exactly as `.pin` was left when
   the markers became icons, and `.chips--over` is a modifier on our own
   element rather than a second declaration of `.chips`. */

/* The chip rail, floating on the tiles. Leaflet's panes sit at 400 and its
   controls at 800 inside .map, which isolation:isolate has already contained,
   so one z-index above that stacking context is enough. */
.chips--over {
  position: absolute;
  top: calc(10px + var(--top-inset));
  left: 0; right: 0;
  z-index: 600;
  padding-bottom: 0;
}
.chips--over .chip {
  box-shadow: 0 2px 10px -3px rgba(23, 19, 15, .3);
}
/* Push Leaflet's own top controls clear of that rail - zoom on the left,
   layers on the right, both are in .leaflet-top. */
.map .leaflet-top { top: 54px; }

.mapsheet {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 900;      /* above Leaflet's controls, inside .map's isolation */
  display: flex;
  flex-direction: column;
  background: var(--paper);
  /* The top half of --r-sheet, and square at the bottom. The token ends
     40px 40px because the design's sheets sit over a scrim, where that curve
     reads as the phone's own screen corner. This one is flush on the bottom
     of the map, so those corners just cut two notches out of the sheet and
     showed map tiles underneath it. */
  border-radius: 22px 22px 0 0;
  box-shadow: 0 -10px 30px -18px rgba(23, 19, 15, .55);
  transition: height .26s ease;
}
/* Sized to hold a card with a three-line title - 26 grab + 24 heading + 136
   card + 16 - so a long name is read whole rather than cut mid-line. */
.mapsheet[data-state="peek"] { height: 204px; }
.mapsheet[data-state="up"]   { height: 62%; }
@media (prefers-reduced-motion: reduce) { .mapsheet { transition: none; } }

/* The handle is the whole strip, not just the bar: a 4px target is a miss. */
.mapsheet__grab {
  flex: none;
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 26px;
  padding: 0; cursor: grab;
  touch-action: none;
}
.mapsheet__grab i { width: 40px; height: 4px; border-radius: var(--r-pill); background: var(--rule); }
.mapsheet__head { flex: none; padding: 0 var(--gutter) 10px; }
.mapsheet__none { padding: 6px var(--gutter) 0; }

.mapsheet__rail {
  flex: 1;
  min-height: 0;
  display: flex;
  gap: 10px;
  /* 12px, not the 22px page gutter: on request (2026-09-27) the cards sit
     closer to the screen edge than the heading above them. */
  padding: 0 12px 16px;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  scroll-snap-type: x mandatory;
  /* The same trap as .shelf__row: a mandatory snap lines the first card up
     with the scrollport edge, which ignores padding, so it sat flush against
     the side of the screen. This moves the snap line in to match. */
  scroll-padding-inline: 12px;
}
.mapsheet__rail::-webkit-scrollbar { display: none; }
/* Raised, the same cards become the list. One markup, two arrangements - the
   rail and the list cannot drift because there is only one of them. */
.mapsheet[data-state="up"] .mapsheet__rail {
  flex-direction: column;
  overflow-x: hidden;
  overflow-y: auto;
  scroll-snap-type: none;
}

.mcard {
  flex: none;
  width: 232px;
  display: flex;
  flex-direction: column;
  gap: 7px;
  padding: 13px 14px;
  border: 1px solid var(--border);
  border-radius: var(--r-block);
  background: var(--paper);
  text-align: left;
  scroll-snap-align: start;
}
.mapsheet[data-state="up"] .mcard { width: 100%; }
.mcard--on { border-color: var(--ink); box-shadow: inset 0 0 0 1px var(--ink); }
/* Smaller than the saved row's 25px: at that size a 232px card fitted two
   words a line and the second line was cut by the sheet. */
.mcard__serves {
  font: var(--serif-wt) 18px/1.25 var(--font-display);
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3;
  overflow: hidden;
}
.mcard__foot { font: 500 11.5px var(--font-ui); color: var(--muted-2); }

/* ---------- ADDITION 9: shelves ----------
   The feed's home screen sorted by reason rather than by day. Every class
   here is a new one - none of them appears in crashers.css - for the same
   reason `.mpin` and `.mapsheet` are new classes: the design's own rules stay
   untouched and unused rather than overridden, so check_design.py sees an
   addition and not a deviation.

   The design's type scale is used as given. A shelf heading is the reference's
   21px Bodoni, a tile's phrase its 22px, and the eyebrows are .t-eyebrow's
   10.5px Karla - nothing here invents a size. */

/* ---- the day segment ---- */
.seg {
  flex: none;
  display: flex;
  gap: 22px;
  padding: 0 var(--gutter);
  margin-bottom: 4px;
  border-bottom: 1px solid var(--rule);
}
.seg__btn {
  padding: 11px 0;
  font: 600 13.5px var(--font-ui);
  color: var(--muted-2);
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  cursor: pointer;
}
.seg__btn--on { color: var(--ink); border-bottom-color: var(--accent); }

/* ---- a shelf ---- */
.shelf { padding-top: 20px; }
.shelf__hd {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 10px; padding: 0 var(--gutter) 2px;
}
.shelf__title { margin: 0; font: 400 21px/1.15 var(--font-display); }
.shelf__n {
  font: 500 10.5px var(--font-ui); letter-spacing: .12em;
  text-transform: uppercase; color: var(--muted-3);
}
.shelf__all { flex: none; font: 600 12px var(--font-ui); color: var(--accent); cursor: pointer; }
/* The text is 14px tall; the target is not. Padding out and margin back in
   lifts the hit area to 30px without moving the word a pixel. */
.shelf__all { padding: 8px 0 8px 14px; margin: -8px 0; }
/* The line that makes this a shelf rather than a category: it says why the
   row exists, which a date heading never does. */
.shelf__why {
  margin: 0; padding: 7px var(--gutter) 12px;
  font: 400 12.5px/1.5 var(--font-ui); color: var(--muted-2);
}
.shelf__row {
  display: flex; gap: 12px;
  padding: 0 var(--gutter) 2px;
  scroll-snap-type: x mandatory;
  /* Required, and NOT the same thing as the padding above. A mandatory snap
     container aligns its first child to the scrollport edge, which ignores
     padding - so on first paint the row scrolled itself by exactly the 22px
     gutter (measured: scrollLeft 22) and the leftmost tile sat flush against
     the side of the screen. scroll-padding moves the snap line in to meet the
     gutter. Removing this puts the tile back on the edge. */
  scroll-padding-inline: var(--gutter);
  -webkit-overflow-scrolling: touch;
}

/* ---- a tile ----
   Deliberately less than a card: the phrase, who is putting it on, and a
   time. Distance, access, audience and the mark are all one tap in, on the
   card the shelf's See all hands back. */
/* display:flex is load-bearing and it is not styling. A <button> centres its
   content box vertically when it is taller than the content, and a flex row
   stretches every tile to the tallest one - so a tile whose caption wrapped to
   two lines made every OTHER tile in the rail taller than its own content and
   each one centred its square by a different amount. Measured on the feed:
   three 158x158 squares sitting 15.1px, 7px and 0px down from the top of their
   tiles, which reads as three different sizes. Stacking the tile's own
   children from the top puts them back on one line. */
.tile {
  flex: none; width: 158px; scroll-snap-align: start; cursor: pointer;
  display: flex; flex-direction: column;
}
.tile__sq {
  flex: none;                    /* a column flex item, so say no to shrinking */
  height: 158px; padding: 14px;
  display: flex; flex-direction: column; justify-content: space-between;
  border-radius: var(--r-block);
  background: var(--sand); color: var(--ink);
  position: relative; overflow: hidden;   /* the frame the photo is cropped to */
}

/* ---- the listing's own photo ----
   Hotlinked, never copied: the host serves the bytes and the tile sends a
   reader to them, which is the conduct the README already states for text.
   Cropped to the square with object-fit:cover rather than fitted inside it -
   sources hand back three aspect ratios and a rail of letterboxed images is
   the ragged edge the fixed square exists to prevent.

   Where there is no photo the tile keeps its coloured ground, so a rail is
   never holed. 359 of 601 classified rows carry one. */
.tile__img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  /* Where the host serves a square, add_images.py asks for one and this does
     nothing. Where it cannot - Eventbrite's urls are signed, Humanitix
     publishes only a 2:1 - something still has to be cut, and cutting evenly
     off the top and bottom takes the heads out of a photo of a room. A
     listing image puts its subject high, so the window sits high. */
  object-position: center 35%;
}
/* The phrase has to survive whatever the photo turns out to be, and the photo
   is whatever the host uploaded - so the scrim has a FLOOR rather than a
   pretty ramp. The first version faded to .18 through the middle and a bright
   classroom photo swallowed the phrase whole; against a white image that is
   about 2.7:1, which is not a readable tile, it is a caption-shaped smudge.

   .66 is where --paper clears 4.5:1 over the worst case a photo can be
   (a white one), so it holds for the 10.5px eyebrow as well as the 22px
   phrase. It deepens to .86 at the foot, where the phrase actually sits, and
   a third of the picture still reads through it. */
.tile--img .tile__sq::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(to bottom,
              rgba(23, 19, 15, .66) 0%, rgba(23, 19, 15, .66) 45%,
              rgba(23, 19, 15, .86) 100%);
}
/* z-index, not just position. The scrim is an ::after, so it is the LAST
   child and paints over everything before it - the first fix put the phrase
   UNDER the scrim and dimmed the type by .66 instead of the photograph, which
   looked identical to having no scrim at all. */
.tile--img .tile__eyebrow, .tile--img .tile__serves { position: relative; z-index: 1; }
.tile--img .tile__sq { background: var(--ink); }
/* A host's logo standing in for a listing with no photo. Fitted, never
   cropped - half a wordmark is worse than none - in the clear band under the
   eyebrow, on the white it was drawn for. No scrim: the scrim exists because
   a photo's brightness is unknown, and this ground is known to be white, so
   the type goes back to ink and the logo keeps its own colours. */
.tile--logo .tile__img { box-sizing: border-box; object-fit: contain; object-position: center; background: #fff; padding: 22% 14% 50%; }
.tile--logo .tile__sq::after { content: none; }
.tile.tile--img.tile--logo .tile__eyebrow { color: var(--muted-3); }
.tile.tile--img.tile--logo .tile__serves { color: var(--ink); -webkit-line-clamp: 2; }  /* keeps the logo band clear */
.tile.tile--img.tile--logo:hover .tile__serves { color: var(--accent); }
/* Both in --paper, not .tile--ink's --muted-4 eyebrow: that grey is read
   against flat ink and has the contrast for it, and over an unknown
   photograph it does not. The eyebrow stays the quieter of the two on size
   and letter-spacing, which is how it was already doing most of that work. */
.tile--img .tile__eyebrow { color: var(--paper); }
.tile--img .tile__serves { color: var(--paper); }
.tile__eyebrow {
  font: 500 10.5px var(--font-ui); letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted-3);
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
}
/* The uppercase tracking above is for letters; on a glyph it just pushes it
   off the right edge. */
.tile__fed { flex: none; font-size: 13px; letter-spacing: 0; }
.tile__serves {
  font: 400 22px/1.24 var(--font-display);   /* 1.1 clipped descenders */
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 4;
  overflow: hidden;
}
.tile__cap {
  display: block; margin-top: 9px;
  font: 600 12.5px/1.3 var(--font-ui); color: var(--ink-2);
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
}
.tile__meta {
  display: block; margin-top: 4px;
  font: 500 11.5px var(--font-ui); color: var(--muted-2);
  font-variant-numeric: tabular-nums;
  /* One line. "9:30am · 0.0 km from Penrith" is wider than a 158px tile and
     wrapping it put the rail's captions at two different heights. */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Three grounds in rotation so a rail reads as separate objects without a
   border drawn round every one of them. Still one accent and one theme. */
.tile--ink  .tile__sq { background: var(--ink); color: var(--paper); }
.tile--ink  .tile__eyebrow { color: var(--muted-4); }
.tile--soft .tile__sq { background: var(--paper-2); }
/* An estimate has no serves phrase to print, so its tile shows the event
   title instead, at a smaller size, inside a dashed outline. Drawing it like
   a stated one would be the no-quote-no-claim rule broken in furniture. */
.tile--est .tile__sq {
  background: var(--paper); border: 1px dashed var(--border);
}
.tile--est .tile__serves { font-size: 18px; color: var(--ink-2); -webkit-line-clamp: 5; }
/* An estimate with a photo keeps BOTH: the picture fills the square and the
   dashed outline is still drawn over it, because the outline is the hedge and
   a photo is not a food claim. The border paints on the border box and the
   image is inset to the padding box, so the dash stays visible without any
   stacking work. */
.tile--est.tile--img .tile__sq { background: var(--ink); }
.tile--est.tile--img .tile__serves { color: var(--paper); }

/* ---- the affinity row ----
   One card, so it is a wide row rather than a shelf of one - the three-card
   rule is about rails, and this is not a rail. */
.wide {
  margin: 0 var(--gutter);
  display: flex; gap: 13px; align-items: center;
  padding: 13px 14px;
  background: var(--paper-2); border-radius: var(--r-block);
  cursor: pointer;
}
.wide__sq {
  width: 56px; height: 56px; flex: none;
  border-radius: 10px; background: var(--sand);
  display: grid; place-items: center;
  font: 400 15px var(--font-display);
  overflow: hidden;              /* so a photo is cropped to it, not past it */
}
/* Same square, same crop. With no photo it falls back to the initial it has
   always drawn. */
.wide__img { width: 100%; height: 100%; object-fit: cover; }
.wide__sq--logo { background: #fff; }
.wide__sq--logo .wide__img { box-sizing: border-box; object-fit: contain; padding: 6px; }
.wide__txt { min-width: 0; }
.wide__serves { display: block; font: 600 13.5px/1.3 var(--font-ui); }
.wide__meta { display: block; margin-top: 4px; font: 500 11.5px var(--font-ui); color: var(--muted-2); }

/* ---- the list under the shelves ----
   Shelves sit on top of the feed, they do not replace it: an event that no
   shelf claimed still has to be reachable, so the day-grouped list stays. */
.shelves__rest { padding: 30px var(--gutter) 2px; }

/* ---- the thin day ----
   The real risk in this layout. Below the three-card rule every shelf falls
   away, and four rows of one card would be worse than the list they replaced,
   so the shelves drop out and the app says so. */
.thin { padding: 34px var(--gutter) 0; }
.thin__head { margin: 0; font: 400 26px/1.22 var(--font-display); }
.thin__body { margin: 14px 0 0; font: 400 15px/1.6 var(--font-ui); color: var(--ink-3); }
.thin__cta { margin-top: 20px; display: inline-block; padding: 13px 18px; font-size: 13.5px; }

/* ---- back out of an opened shelf ---- */
.backlink {
  display: inline-block; padding: 14px 0 2px;
  font: 600 13px var(--font-ui); color: var(--muted); cursor: pointer;
}

@media (min-width: 520px) and (pointer: fine) {
  /* The segment is laid out by the app shell above, which owns it as a row of
     the main pane - nothing to add here.

     The shelves sit inside the content pane, and the pane already supplies
     the 26px gutter, so every one of these drops its own. */
  .shelf { padding-top: 30px; }
  .shelf:first-child { padding-top: 22px; }
  .shelf__hd, .shelf__why, .shelf__row, .shelves__rest, .thin { padding-left: 0; padding-right: 0; }
  .shelf__title { font-size: 26px; }
  .wide { margin: 0; }
  /* Wider tiles, and enough of them in view that the rail reads as a shelf
     rather than as a row that ran out of room. */
  .tile { width: 208px; }
  .tile__sq { height: 208px; }
  .tile__serves { font-size: 26px; }
  .tile--est .tile__serves { font-size: 21px; }
  .shelf__row { gap: 20px; }
  .shelves__rest { padding-top: 48px; }
  .thin { padding-top: 0; }
}

@media (max-height: 560px) {
  /* Short viewports: the same trim the rest of the app takes - vertical
     padding only, no type sizes, so the scale is intact. */
  .seg__btn { padding: 7px 0; }
  .shelf { padding-top: 12px; }
  .shelf__why { padding-bottom: 8px; }
  .tile { width: 138px; }
  .tile__sq { height: 138px; }   /* a square, not a 138x128 near-miss */
}

/* ---------- no double-tap zoom ----------
   Asked for 2026-09-24: pinch zooms, a double tap does not. `manipulation`
   keeps pan and pinch and drops only the double-tap gesture; the map turns
   off Leaflet's own double-click zoom in initMap. */
html { touch-action: manipulation; }

/* ---- the listing's picture on the detail sheet ----
   Its own shape, full width, capped so a portrait flyer cannot push the
   facts off the first screen. A logo is fitted on white, as on the tile. */
.detail__img {
  display: block; width: 100%; height: auto; max-height: 300px;
  object-fit: cover; object-position: center 35%;
  margin-top: 10px; border-radius: var(--r-block); background: var(--sand);
}
.detail__img--logo {
  box-sizing: border-box; height: 120px; padding: 22px 18%;
  object-fit: contain; object-position: center; background: #fff;
  border: 1px solid var(--border);
}

/* ---------- ADDITION: one emoji per event, and photo cards (2026-09-26) ----------
   The emoji is the same on card, tile and pin (index.html MARKS). A pale one -
   the clinking glasses above all - vanished on paper, so every emoji carries a
   hairline of ink round it; on the rust pin and the ink tag it just reads as
   an edge. Text-shadow, not a filter, so the circle behind a pin is untouched. */
.emo, .mpin {
  font-family: 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', sans-serif;
  text-shadow: 0 0 1px rgba(23, 19, 15, .55), 0 0 1px rgba(23, 19, 15, .55);
}
.mpin { font-size: 18px; line-height: 1; }
.mpin--on { font-size: 23px; }
.mpin--est { font-size: 0; }

/* A card with a photo: the picture behind the whole card under a scrim, the
   type in paper. The slide carries it so a swipe moves the photo too, which
   is why the card's own padding moves onto the slide. */
.card--img { padding: 0; border-top: 0; margin: 10px 0; }
.card--img + .card { border-top: 0; }
.card--img .card__slide {
  position: relative; isolation: isolate; overflow: hidden;
  padding: 15px 16px 16px; border-radius: var(--r-block);
  background: var(--ink);
}
.card__bg {
  position: absolute; inset: 0; z-index: -2;
  width: 100%; height: 100%; object-fit: cover;
}
.card--img .card__slide::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(to bottom,
              rgba(23, 19, 15, .55) 0%, rgba(23, 19, 15, .70) 45%,
              rgba(23, 19, 15, .88) 100%);
}
.card--img .card__meta, .card--img .card__host { color: rgba(255, 248, 239, .78); }
.card--img .card__dist { color: var(--paper); }
.card--img .card__serves { color: var(--paper); }
.card--img .card__title { color: rgba(255, 248, 239, .9); }
.card--img .tag { border-color: rgba(255, 248, 239, .45); color: var(--paper); }
.card--img .tag--filled { background: var(--accent); color: var(--accent-ink); }  /* rust, as the pin: a pale emoji vanished on paper */
.card--img .mark, .card--img .mark--est { color: rgba(255, 248, 239, .75); }
.card--img .save { background: rgba(255, 248, 239, .16); color: var(--paper); }

/* The sheet's photo is a link out to the listing. */
.detail__imglink { display: block; }

/* ---------- ADDITION: sweep fixes (2026-09-26) ----------
   No focus ring after a tap or click; a keyboard still gets it (index.html
   toggles .pointer on the last input). */
.pointer :focus-visible { outline: none; }
/* The week button on a thin day sits inside .list, which already has the
   gutter, so .thin's own gutter indented it a second time. */
.list .thin { padding: 22px 0 0; }

/* ---------- ADDITION: drawn marks and photo map cards (2026-09-27) ----------
   bbq.svg and crisps.svg stand in for emoji Unicode does not have, sized to
   sit like a glyph. The drop-shadow is the .emo ink hairline, which a
   text-shadow cannot give an image - without it the red grill sank into the
   rust pin. */
.emo-img {
  width: 1.1em; height: 1.1em; vertical-align: -.2em;
  filter: drop-shadow(0 0 .6px rgba(23, 19, 15, .6));
}
/* The map rail's cards take their photo the way .card--img does. */
.mcard--img {
  position: relative; isolation: isolate; overflow: hidden;
  background: var(--ink); border-color: transparent;
}
.mcard--img::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(to bottom,
              rgba(23, 19, 15, .55) 0%, rgba(23, 19, 15, .70) 45%,
              rgba(23, 19, 15, .88) 100%);
}
.mcard--img .mcard__meta, .mcard--img .mcard__foot { color: rgba(255, 248, 239, .78); }
.mcard--img .mcard__serves { color: var(--paper); }
.mcard--img.mcard--on { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }

/* ---------- ADDITION: preferences on the You screen (2026-09-27) ----------
   The reference has no text field, so these are drawn from its own tokens:
   the block's border and radius, paper ground, the UI face. 16px type keeps
   iOS from zooming the page when the field takes focus. */
.pref__label { padding: 18px 0 8px; }
.pref__note { margin: 8px 0 0; color: var(--muted-2); }
.pref__input {
  display: block; width: 100%; box-sizing: border-box;
  padding: 11px 14px;
  font: 500 16px var(--font-ui); color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--border); border-radius: var(--r-block);
}
.pref__input:focus { outline: none; border-color: var(--ink); }
.pref__input--age { width: 120px; }
.pref__clear { margin-top: 18px; }

/* ---------- ADDITION: the save heart (2026-09-27) ----------
   Instagram's: an outline in the card's ink, filled red when saved, with a
   short pop on the tap that saved it. The red is on request and is the
   heart's alone - nothing else in the UI takes it. .save--heart drops the
   pill ground so the heart reads as the control. */
.save--heart { padding: 0 6px; background: transparent; }
.card--img .save--heart { background: transparent; }
.heart {
  width: 24px; height: 24px; display: inline-block; vertical-align: middle;
  fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linejoin: round;
}
.btn--text .heart { width: 18px; height: 18px; margin-top: -2px; }
.heart--on { fill: #ff3040; stroke: #ff3040; }
.heart--pop { animation: heart-pop .35s ease-out; }
@keyframes heart-pop { 0% { transform: scale(1); } 40% { transform: scale(1.3); } 100% { transform: scale(1); } }
@media (prefers-reduced-motion: reduce) { .heart--pop { animation: none; } }

/* ---------- ADDITION: saved-event history on the You screen (2026-09-27) ----------
   One row per past save: when, what, who. The rows share .fact's rule. */
.hist {
  display: flex; flex-direction: column; gap: 3px;
  padding: 12px 0; border-bottom: 1px solid var(--rule);
  color: inherit; text-decoration: none;
}
.hist__name { font: 500 15px/1.3 var(--font-ui); color: var(--ink); }
.hist__when, .hist__host { color: var(--muted-2); }

/* ---------- ADDITION: the detail sheet's heart, beside its close (2026-09-27) ----------
   Same round .close button, so the two read as a pair; the heart is sized
   to the ✕ glyph. overscroll-behavior stops the page bouncing under the
   swipe-down that closes the sheet. */
.detail__acts { display: flex; gap: 8px; flex: none; }
.detail__save .heart { width: 20px; height: 20px; }
.sheet--detail { overscroll-behavior: contain; }

/* ---------- ADDITION: the filter icon (2026-09-27) ----------
   Google Maps' sliders in place of the word. On the feed it is the header's
   round button (.close's shape) beside the title; on the map it leads the
   floating chip rail. The count of filters on sits on the icon. */
.fbtn { position: relative; color: var(--ink); align-self: center; }
.fbtn__n {
  position: absolute; top: 2px; right: 2px;
  min-width: 17px; height: 17px; padding: 0 4px; box-sizing: border-box;
  border-radius: 9px; background: var(--accent); color: var(--accent-ink);
  font: 700 10.5px/17px var(--font-ui); text-align: center;
}
.chip--ico { display: inline-flex; align-items: center; gap: 4px; }

/* ---------- ADDITION: share location, in the sheet's How far (2026-09-29) ----------
   Shown only when there is no fix and no home suburb, so no distance. */
.locshare {
  margin-top: 8px; padding: 7px 12px; border: 1px solid var(--border);
  border-radius: var(--r-button); font: 600 13px var(--font-ui); color: var(--ink-2);
  cursor: pointer;
}

/* ---------- ADDITION: the You screen's "Share my location" (2026-09-29) ---------- */
.pref__loc { display: inline-block; margin-top: 10px; padding: 11px 16px; font-size: 13.5px; }
/* Saved's Suggested rail sits in a .list, which already has the gutter, so
   the shelf's own inner gutter is dropped rather than doubled. */
.list > .shelf .shelf__hd, .list > .shelf .shelf__why { padding-left: 0; padding-right: 0; }
.list > .shelf .shelf__row { padding-left: 0; scroll-padding-inline: 0; }
/* On desktop .list is a card grid; anything here that is not a card spans it
   (You's prefs, Saved's note, Suggested rail and past events). Harmless on a
   phone, where .list is not a grid. */
.list > .prefs, .list > p, .list > section, .list > .hist, .list > .pref__clear { grid-column: 1 / -1; }
/* The suburb box sat flush under the My location / A suburb buttons. */
.opts + .pref__input { margin-top: 12px; max-width: 420px; }
