/* ============================================================
   Pelican Row Estate & Market
   Single-page site — plain HTML/CSS/JS, no build step.
   Palette is pulled from the storefront sign: cream board,
   terracotta pelican, dark brown lettering.
   ============================================================ */

:root {
  color-scheme: light;

  /* Surfaces */
  --paper:      #faf5ea;
  --paper-tint: #f2e9d8;
  --paper-card: #fffdf8;

  /* Ink */
  --ink:        #33271f;
  --ink-soft:   #6d5c4e;
  --ink-faint:  #746452;   /* 4.7:1 on the tinted sections — keeps small caps legible */

  /* Accent — the terracotta from the pelican on the sign */
  --brick:      #9a4e30;
  --brick-deep: #7b3b22;
  --brick-wash: #f0e0d6;

  /* The navy off the storefront: the roofline reads around #294366 and the
     pelican on the sign is darker again. The only cool colour in the
     palette, so use it sparingly or it stops meaning anything. */
  --navy:       #223e65;
  --navy-wash:  rgba(34, 62, 101, .08);   /* hover tint, as --brick-wash is for brick */

  /* Teal, for the "New" chip on a vendor card and nothing else so far.
     Near enough the complement of --brick to read as a deliberate second
     accent rather than a stray colour. Lettered in white rather than --paper,
     which the other chips use: cream on it is 3.5:1 and white 3.9:1.

     Note that 3.9:1 is under the 4.5:1 AA asks for text this small — a
     deliberate choice, made with the trade named. It clears the 3:1 that
     applies to the chip as a UI component, and the word "New" is decoration
     over information the card already carries. #3f7f86 was the previous value
     and reaches 4.6:1 on white if that matters more later. */
  --teal:       #528b90;

  /* Lines */
  --ink-wash:   rgba(51, 39, 31, .05);    /* hover tint for ink-lettered buttons */

  /* The drawn edge on cards, the reviews frame and form fields. It used to
     be --ink at full strength, which at 13:1 against the paper read as a
     heavy black box once there were ten of them on a page; thinning the
     border from 1.5px to 1px barely touched that, because the weight was
     the colour and not the width. At .55 it still reads as a deliberate
     dark edge and still clears WCAG 1.4.11's 3:1 for a form field's border
     (3.4:1 against --paper, 3.5:1 against --paper-card). Darken it here if
     the cards ever need to sit harder on the page; nothing else changes. */
  --line-strong: rgba(51, 39, 31, 0.55);
  --line:       rgba(51, 39, 31, 0.14);
  --line-soft:  rgba(51, 39, 31, 0.08);

  --serif: "EB Garamond", Georgia, "Times New Roman", serif;
  --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
          "Helvetica Neue", Arial, sans-serif;

  --wrap: 1120px;
  /* The gap under a page's banner and above its footer. One value so
     the two ends of a page always match. */
  --page-edge: clamp(2.5rem, 2rem + 2vw, 3.75rem);
  --radius: 4px;
  --shadow: 0 1px 2px rgba(51, 39, 31, .05), 0 12px 32px -18px rgba(51, 39, 31, .35);
}

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

html { scroll-behavior: smooth; scroll-padding-top: 88px; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
  /* The global rule above only kills the timing; without this the card would
     still jump to its scaled size. Colour and shadow still respond. */
  .card:hover,
  .vendor:hover { transform: none; }
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; display: block; }

a { color: var(--brick); text-decoration-color: rgba(154, 78, 48, .35); text-underline-offset: 3px; }
a:hover { color: var(--brick-deep); text-decoration-color: currentColor; }

/* Focus rings are BRICK. Navy was tried here and reverted: a field resting
   in navy that turns brick when you click into it is a clearer change than
   one that stays the same colour and merely gains a ring. The error state
   stays distinguishable by its pink fill, which focus does not have. */
:where(a, button, input, textarea, iframe):focus-visible {
  outline: 2px solid var(--brick);
  outline-offset: 3px;
  border-radius: 2px;
}

.wrap { width: min(100% - 2.5rem, var(--wrap)); margin-inline: auto; }

.skip {
  position: absolute; left: 50%; top: -100px; transform: translateX(-50%);
  z-index: 100; background: var(--ink); color: var(--paper);
  padding: .7rem 1.2rem; border-radius: 0 0 var(--radius) var(--radius);
  text-decoration: none; font-size: .9rem;
}
.skip:focus { top: 0; color: var(--paper); }

/* ── Shared type ─────────────────────────────────────────── */

h1, h2, h3 { font-family: var(--serif); font-weight: 500; line-height: 1.15; margin: 0; }

h2 {
  font-size: clamp(1.9rem, 1.2rem + 2.2vw, 2.9rem);
  letter-spacing: -.01em;
  margin-bottom: .6rem;
}

h3 { font-size: 1.22rem; letter-spacing: .005em; }

.eyebrow {
  font-family: var(--sans);
  font-size: .73rem;
  font-weight: 600;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--brick);
  margin: 0 0 .85rem;
}

.section-lede {
  color: var(--ink-soft);
  font-size: 1.08rem;
  max-width: 58ch;
  margin: 0 0 2rem;
}

.center { text-align: center; }
.center.section-lede { margin-inline: auto; }

/* ── Wordmark (mirrors the sign out front) ───────────────── */

.wordmark {
  display: inline-grid;
  justify-items: center;
  gap: .18rem;
  text-decoration: none;
  color: var(--ink);
  line-height: 1;
}
.wordmark-top {
  font-family: var(--serif);
  font-size: 1.32rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  white-space: nowrap;
}
.wordmark-rule {
  width: 100%;
  height: 1px;
  background: var(--brick);
  opacity: .5;
}
.wordmark-sub {
  font-family: var(--sans);
  font-size: .56rem;
  letter-spacing: .26em;
  text-transform: uppercase;
  color: var(--ink-soft);
  white-space: nowrap;
}

/* ── Header ──────────────────────────────────────────────── */

.site-header {
  position: sticky; top: 0; z-index: 50;
  background: rgba(250, 245, 234, .92);
  backdrop-filter: saturate(150%) blur(10px);
  border-bottom: 1px solid var(--line-soft);
}

.header-inner {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1.5rem;
  min-height: 72px;
  padding-block: .75rem;
}

.site-nav { display: flex; align-items: center; gap: 1.9rem; }

.site-nav a {
  font-size: .82rem;
  font-weight: 500;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink);
  text-decoration: none;
  padding-block: .35rem;
  border-bottom: 1px solid transparent;
  transition: border-color .18s, color .18s;
}
.site-nav a:hover { color: var(--brick); border-bottom-color: var(--brick); }
/* Marks which page you're on. */
.site-nav a[aria-current="page"] { color: var(--brick); border-bottom-color: var(--brick); }

.nav-fb { display: inline-flex; align-items: center; gap: .45rem; }
.nav-fb svg { width: 17px; height: 17px; fill: currentColor; }

.nav-toggle {
  display: none;
  align-items: center; gap: .55rem;
  background: none; border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: .5rem .8rem;
  font: inherit; font-size: .74rem; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink); cursor: pointer;
}
.nav-toggle-bars { display: grid; gap: 3.5px; }
.nav-toggle-bars i { display: block; width: 16px; height: 1.5px; background: currentColor; }

/* ── Hero ────────────────────────────────────────────────── */

/* Text and photo each get their own column — nothing overlaps, so the
   storefront needs no scrim at all and renders at full strength. */
.hero { background: var(--paper); }

/* Both columns sit inside .wrap, so the photo stops at the same right margin
   the rest of the page uses instead of running to the window edge. */
.hero-split {
  display: grid;
  grid-template-columns: minmax(0, 1.08fr) minmax(0, 1fr);
  gap: clamp(2rem, 1rem + 3vw, 4rem);
  align-items: center;
  padding-block: clamp(3.25rem, 2rem + 4vw, 5.5rem);
}

.hero-text { display: flex; align-items: center; }
.hero-card { width: 100%; }

/* Same panel treatment as the map and the cards further down the page.
   The storefront photo is 960x720, which is exactly this 4:3 panel, so
   nothing is cropped. cover + bottom anchoring are kept anyway so that a
   replacement photo of a different shape still behaves: it would lose sky
   off the top rather than the sign or the forecourt. */
.hero-media {
  position: relative;
  aspect-ratio: 4 / 3;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow);
}
.hero-media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 100%;   /* bottom-anchored: crop comes off the top */
}

h1 { display: grid; gap: .5rem; margin-bottom: 1.5rem; }
.h1-top {
  /* Sized for the half-width column rather than the full page. */
  font-size: clamp(2.4rem, 1.4rem + 3.2vw, 4.1rem);
  letter-spacing: .015em;
  text-transform: uppercase;
}
.h1-sub {
  font-family: var(--sans);
  font-size: clamp(.72rem, .6rem + .4vw, .92rem);
  font-weight: 600;
  letter-spacing: .42em;
  text-transform: uppercase;
  color: var(--brick);
  padding-top: .4rem;
  border-top: 1px solid var(--line);
  justify-self: start;
  padding-right: .3rem;
}

.lede {
  font-family: var(--serif);
  font-size: clamp(1.2rem, 1.05rem + .5vw, 1.45rem);
  line-height: 1.5;
  color: var(--ink-soft);
  max-width: 34ch;
  margin: 0 0 1.4rem;
}

.hero-address {
  font-size: .87rem;
  font-weight: 500;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 2rem;
}

/* ── Buttons ─────────────────────────────────────────────── */

.btn-row { display: flex; flex-wrap: wrap; gap: .8rem; }

.btn {
  display: inline-flex; align-items: center; justify-content: center;
  padding: .82rem 1.7rem;
  border: 1px solid transparent;
  border-radius: var(--radius);
  font-family: var(--sans);
  font-size: .78rem; font-weight: 600;
  letter-spacing: .13em; text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  transition: background-color .18s, color .18s, border-color .18s, transform .18s;
}
.btn:active { transform: translateY(1px); }

.btn-primary { background: var(--brick); color: #fff; }
.btn-primary:hover { background: var(--brick-deep); color: #fff; }

.btn-ghost { background: transparent; color: var(--ink); border-color: var(--line); }
/* The pattern every ink-lettered button follows: the letters stay put and
   the button itself darkens. Brick and navy are reserved for buttons that
   are brick or navy to begin with. */
.btn-ghost:hover { background: var(--ink-wash); color: var(--ink); border-color: var(--ink-faint); }

.btn-block { width: 100%; }

/* ── Sections ────────────────────────────────────────────── */

.section { padding-block: clamp(3.75rem, 2.5rem + 5vw, 6.5rem); }

/* Every page opens and closes on the same gap, and a section that touches
   either end carries that gap on BOTH sides, so it is evenly padded in
   itself rather than tight at one edge and loose at the other.

   The generous .section padding is left for sections with a neighbour above
   and below them, which is what it is for: it separates one band from the
   next. Only the home page currently has such a section.

   Written as relationships rather than per-page classes, so a new page
   picks it up with no extra markup. .page-head covers the photo banner too,
   since that carries both classes. */
.page-head + .section,
.hero + .section,
main > :last-child {
  padding-block: var(--page-edge);
}
.section-tint { background: var(--paper-tint); }

/* ── Google reviews carousel ──────────────────────────────
   Lives in the copy column beside the Facebook feed, filling the space
   under the Follow button. One review at a time, because the column is
   narrow and these are screenshots at their natural size.
   The track is a plain scroller, so swipe and arrow keys work on their
   own; the buttons and dots just drive scrollLeft. */

/* Sits clear of the Follow button above it, so the reviews read as their
   own block rather than as part of the Facebook copy. */
.reviews {
  /* The gap is split either side of the rule rather than stacked on one
     side, so the line sits centred in it. 3rem + 3rem = the 6rem total. */
  margin-top: 3rem;
  padding-top: 3rem;
  border-top: 1px solid var(--line);
  max-width: 400px;
}

.reviews-title {
  display: flex;
  align-items: center;
  gap: .4rem;
  flex-wrap: wrap;
  font-family: var(--sans);
  font-size: .8rem;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin: 0 0 .7rem;
}

/* Gold sampled from the screenshots so these stars and the ones inside the
   images are the same colour rather than nearly the same. */
.g-stars { display: inline-flex; gap: .09rem; }
.g-stars svg { width: 15px; height: 15px; fill: #ffbb29; }

/* Google's own four-colour mark, used as attribution. Sized to the cap
   height of the text beside it so the line doesn't look pasted together. */
.g-mark { display: inline-flex; }
.g-mark svg { width: 14px; height: 14px; }

/* Hairline divider between the rating and the call to action. Decorative,
   so it is aria-hidden in the markup and drawn here rather than typed as a
   character a screen reader would read out. */
.reviews-sep {
  flex: none;
  width: 1px;
  height: 1.1em;
  background: var(--line);
  margin-inline: .2rem;
}

/* Deep-links straight into Google's review form for this place. The place
   id was derived from the listing's feature id and checked against the
   live listing; see the README before changing it. */
.reviews-cta {
  color: var(--brick);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: border-color .18s, color .18s;
}
.reviews-cta:hover {
  color: var(--brick-deep);
  border-bottom-color: currentColor;
}


.reviews-track {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  overscroll-behavior-x: contain;
  border-radius: var(--radius);
  /* Pure white, not --paper-card, and this is the one place on the site
     that should be. The screenshots are #ffffff to their edges, so the
     warm off-white left a visible seam where the letterboxing met the
     image. If the screenshots are ever retaken against a different
     background, re-sample their edges and match this to it. */
  background: #fff;
  /* A REAL BORDER, not a box-shadow ring. The ring was drawn with a spread,
     and a spread is not rounded to whole device pixels the way a border
     width is — so with the frame landing on half pixels (left 72.5, top
     386.25) each edge antialiased differently: one side a solid line,
     another a half-strength grey. Against the drop shadow underneath, that
     read as embossed.

     A border rounds per edge like the cards' do, so all four match. It costs
     no width either, because box-sizing is border-box: the frame stays the
     same size and the slides, at 100% of the content box, simply follow. */
  border: 1px solid var(--line-strong);
  box-shadow: var(--shadow);
}
.reviews-track::-webkit-scrollbar { display: none; }
.reviews-track:focus-visible { outline: 2px solid var(--brick); outline-offset: 3px; }

/* Each slide is the full width of the frame: one review on screen at a
   time, whatever the column is doing. The screenshots differ in height,
   so they sit centred in a slot as tall as the tallest one instead of
   making the panel jump between reviews. */
.review-slide {
  flex: 0 0 100%;
  scroll-snap-align: center;
  display: grid;
  place-items: center;
  padding: .5rem;
}
.review-slide img { width: 100%; height: auto; }

.carousel-controls {
  display: flex;
  align-items: center;
  gap: .75rem;
  margin-top: .85rem;
}

.carousel-btn {
  flex: none;
  width: 34px; height: 34px;
  display: grid; place-items: center;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--paper-card);
  color: var(--ink);
  cursor: pointer;
  transition: background-color .18s, border-color .18s, color .18s, opacity .18s;
}
.carousel-btn svg {
  width: 15px; height: 15px;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
.carousel-btn:hover:not(:disabled) {
  background: var(--ink-wash);
  color: var(--ink);
  border-color: var(--ink-faint);
}
.carousel-btn:disabled { opacity: .35; cursor: default; }

.carousel-dots { display: flex; margin-inline: auto; }

/* The flyer carousel. The group of images in the markup becomes the track:
   a horizontal scroller one slide wide, so swipe and arrow keys work on
   their own and the arrows and dots below just drive the same scroll.

   Two flyers side by side in half a column are two small flyers; stacked,
   they push the words that explain them off the screen. One at a time keeps
   each at full size.

   The slot is as tall as the tallest flyer and the shorter one sits centred
   in it, so paging does not change the height of the entry and bump the text
   under it around. */
.event-flyers.is-carousel {
  display: flex;
  gap: 0;
  max-width: none;
  /* A fixed slot, so the height does not change as you page and the text
     under it never moves. 4:3 is the shape of the flyers the market
     publishes, so the common case fills the box exactly. */
  aspect-ratio: 4 / 3;
  /* NOT a user-scrollable strip on a pointer device. It was `overflow-x:
     auto` with scroll snap, which gave swipe and arrow keys for free, but
     it also made the slot a scroll container sitting in the middle of the
     page: a trackpad gesture that drifted even slightly sideways got
     captured by it, and the page stopped moving under the cursor. Reading
     past a flyer should never be interrupted by the flyer.

     Hidden on both axes on purpose. `overflow-x: auto` alone computes
     overflow-y to auto as well, which is a second way for the same thing to
     happen. scrollLeft still works from script, which is all the arrows and
     the dots need.

     Touch devices get the scroller back further down — see the coarse
     pointer block. */
  overflow: hidden;
  align-items: center;

  /* A visible slot, which is the whole reason a centred flyer reads as
     centred. The slot is the full width of the column and the flyers in it
     are different shapes, so a portrait one sits with 100px of nothing
     either side. With no edge to that nothing, the picture looked
     indented against the lone flyers above and below it, which start at
     the column's edge. With an edge, the same picture is centred in a
     frame and the spare width belongs to the frame.

     A hairline and nothing else — no fill. The border is doing the whole
     job on its own: it draws the viewer as an object, and once the object
     is there the picture inside it is plainly centred rather than badly
     left-aligned. A card-paper fill was tried and is one thing too many;
     the frame does not need to be a surface, and leaving the page showing
     through keeps a flyer sitting on the same paper as everything else.

     The same hairline the flyers themselves carry, so the viewer and the
     picture in it belong to one family. */
  box-shadow: 0 0 0 1px var(--line);
  border-radius: var(--radius);
}
/* Both selectors, and the second one on purpose: inside a column
   `.events-split .flyer-zoom` caps a flyer at 340px, and it is further down
   the file, so a slide needs the extra class to outrank it rather than tie
   with it. A slide that is narrower than its track shows the next one
   peeking in beside it, which is exactly the crowding this replaced. */
/* Centred in the slide, and the frame on the track is what makes that read.
   The flyers in a carousel are not all the same shape — the sale poster is
   4:3 and fills the slot, the parking map is portrait and about 330px wide
   — so a centred portrait has empty space either side of it. Against bare
   paper that looked like a picture nudged off the left margin every other
   image and every line of text on the page starts from. Inside a frame it
   reads as a picture centred in a viewer, which is what it is.

   Flush left was tried instead and is wrong: it lines the pictures up but
   leaves the slot's spare width hanging off one side, and what was missing
   was the frame, not the alignment.

   The padding is on the SLIDE, not on the track. A scroll container clips
   at its padding edge, so padding on the track would let the next slide
   show through the gap between the frame and the picture — the poster is
   exactly as wide as a slide, so a strip of it would sit inside the frame's
   right-hand margin. On the slide the inset comes out of the slide's own
   width and the clip still happens exactly at the frame. */
.event-flyers.is-carousel > *,
.event-flyers.is-carousel > .flyer-zoom {
  flex: 0 0 100%;
  max-width: 100%;
  height: 100%;
  padding: .85rem;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* ── Swipe, on touch devices only ─────────────────────────
   The slot goes back to being a real scroller where the primary input is a
   finger, which is the one place the original scroller was never the
   problem. What broke was the trackpad: a wheel gesture with any sideways
   drift in it latches onto a horizontal scroller and the page stops moving
   under the cursor. Touch does not behave that way — the browser locks a
   pan to one axis as it starts, so a vertical swipe scrolls the page and a
   horizontal one pages the flyers, and neither steals the other.

   So the fix is not to make one element behave for both inputs, it is to
   give the scroller only to the input it works for. `hover: none` and
   `pointer: coarse` together mean a device whose main pointer is a finger:
   phones and tablets. A laptop with a touchscreen reports a fine pointer
   and keeps the hidden overflow, which is the right way round — that is
   the machine with the trackpad that caused this.

   `overflow-y: hidden` is what keeps it to one axis. It has to be stated:
   leaving it at `visible` beside an `overflow-x` that is not visible makes
   the browser compute it to auto, and a vertically scrollable strip in the
   middle of the page is the whole thing we are avoiding.

   overscroll-behavior-x stops a swipe past the last flyer from chaining
   outward and triggering the browser's back gesture. Snapping does the
   paging; script.js already turns it off while an arrow animation is
   running and back on after, so the two cannot fight.

   Everything else still works on a phone: the arrows are 44px, the dots
   are buttons, and a tap still opens the flyer full size — a touch that
   turns into a pan does not fire a click. */
@media (hover: none) and (pointer: coarse) {
  .event-flyers.is-carousel {
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
  }
  .event-flyers.is-carousel::-webkit-scrollbar { display: none; }
  .event-flyers.is-carousel > *,
  .event-flyers.is-carousel > .flyer-zoom { scroll-snap-align: start; }
}

/* Every flyer is sized to the HEIGHT of the slot, not its width, and keeps
   its own proportions. A poster and a portrait map do not share an aspect
   ratio, so fitting both to the width left whichever was wider sitting in a
   band of empty space half the height of the slot. Fitting to the height
   fills it: the landscape flyer spans the full width, the portrait map is
   full height and narrower, and neither is cropped — these are pictures with
   words on them, so `cover` is not an option.

   The same rule has to beat `.events-split .event-flyer`, which caps a flyer
   at 340px for the stacked layout and is further down the file. That cap was
   what kept these pictures at 340px inside a 536px slide.

   Sized with max-width and max-height rather than height: 100% and
   object-fit: contain. Both fit the picture inside the slot, but object-fit
   letterboxes it INSIDE an element box that stays the full size of the slot,
   and a flyer carries a 1px ring and a drop shadow — which would then have
   been drawn around the empty space rather than around the picture. Letting
   the element take its own proportions keeps the frame on the artwork. */
.event-flyers.is-carousel .event-flyer {
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 100%;
}

/* Controls under the flyers rather than over them: a flyer is a poster and
   an arrow sitting on top of one covers the part somebody is reading.

   Wherever the flyers are pulled above the text with order: -1, the
   controls have to come with them, or they drop to the bottom of the entry
   and end up steering a carousel three paragraphs away. They sit after the
   track in the markup, so matching its order is enough to keep them
   together. */
/* The flyer controls sit together as one group rather than pushed to the
   ends of the row. `.carousel-dots` has `margin-inline: auto`, which is what
   throws the arrows to the edges on the reviews carousel: there the track is
   a single wide card and the arrows mark its corners. A flyer slot is
   narrower and changes width with the column, so arrows pinned to its edges
   drifted away from the dots they belong with. Scoped to .flyers-controls,
   so the reviews carousel keeps its own arrangement.

   The group is centred on the SLOT, and stays centred even though the
   slides themselves are flush left. It was briefly moved to the left edge
   to follow them; that was wrong. The controls belong to the whole
   carousel, not to whichever picture is showing, so they stay put on the
   centre line of the box while the pictures inside it change shape. */
.flyers-controls {
  margin-top: .9rem;
  justify-content: center;
  gap: 1rem;
}
.flyers-controls .carousel-dots { margin-inline: 0; }

.events-split .flyers-controls { order: -1; }


/* The button IS the touch target (20x24), and the visible 8px dot is drawn
   by the pseudo-element inside it. An earlier version kept 8px buttons and
   overflowed a bigger ::before out of each one, but neighbouring dots then
   overlapped and stole each other's clicks. */
.carousel-dot {
  width: 20px; height: 24px;
  padding: 0;
  border: 0;
  background: none;
  display: grid;
  place-items: center;
  cursor: pointer;
}
.carousel-dot::before {
  content: "";
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--line);
  transition: background-color .18s, transform .18s;
}
.carousel-dot:hover::before { background: var(--ink-faint); }
.carousel-dot[aria-current="true"]::before { background: var(--brick); transform: scale(1.25); }

/* ── Cards ───────────────────────────────────────────────── */

.cards {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: 1.25rem;
  grid-template-columns: repeat(auto-fit, minmax(255px, 1fr));
}

.card {
  position: relative;          /* so a hovered card can sit above its neighbours */
  background: var(--paper-card);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  padding: 2rem 1.75rem;
  box-shadow: var(--shadow);
  transition: transform .22s ease, box-shadow .22s ease, border-color .22s ease;
}

/* Only on devices that really hover. On a touch screen :hover can stick
   after a tap and leave a card stranded in its lifted state. */
@media (hover: hover) {
  .card:hover {
    transform: translateY(-4px) scale(1.02);
    border-color: var(--line);
  }
  /* .card:hover would otherwise wash the outline out to a hairline grey on
     the way up. The vendor cards hold theirs through the lift; these do the
     same. */
  .card-link:hover {
    border-color: var(--line-strong);
    box-shadow: 0 2px 4px rgba(51, 39, 31, .06), 0 22px 44px -22px rgba(51, 39, 31, .45);
    z-index: 1;
  }
  .card:hover .card-mark {
    background: var(--brick);
    color: var(--paper-card);
  }
}
.card h3 { margin-bottom: .45rem; }
.card p { margin: 0; color: var(--ink-soft); font-size: .97rem; }

/* These cards lead somewhere. The link sits in the heading but its ::before
   covers the whole card, so the entire card is clickable while there is
   still only one thing for a keyboard to land on. */
.card-link {
  cursor: pointer;
  /* The same 2px outline the vendor cards carry, in --ink: the darkest
     colour in the palette and the one the body text is already set in, so
     it reads as black on cream without the glare of a true #000.

     The base .card keeps its hairline, so a card that is only a card and
     not a link would still look different from one that goes somewhere.

     1px, not 2: enough to read as a drawn edge, light enough that a grid
     of them does not turn into a wall of boxes — 1.5px was tried first and
     read as heavy once there were ten cards on a page. The vendor cards and
     the reviews frame use the same weight. */
  border: 1px solid var(--line-strong);
}
.card-link h3 a {
  color: inherit;
  text-decoration: none;
}
.card-link h3 a::before {
  content: "";
  position: absolute;
  inset: 0;                       /* the click target */
}
/* The arrow marks which cards go somewhere, so the hover lift isn't the only
   clue and the card that doesn't link can't be mistaken for one. */
.card-link h3 a::after {
  content: " \2192";
  display: inline-block;
  font-family: var(--sans);       /* the serif has no arrow glyph */
  color: var(--brick);
  transition: transform .22s ease;
}
.card-link:hover h3 a { color: var(--brick); }
.card-link:hover h3 a::after { transform: translateX(4px); }

/* Keyboard focus should ring the whole card, not just the heading text. */
.card-link:has(a:focus-visible) {
  outline: 2px solid var(--brick);
  outline-offset: 3px;
}
.card-link h3 a:focus-visible { outline: none; }

.card-mark {
  display: grid; place-items: center;
  width: 42px; height: 42px;
  margin-bottom: 1.1rem;
  border-radius: 50%;
  background: var(--brick-wash);
  color: var(--brick);
  transition: background-color .22s ease, color .22s ease;
}
.card-mark svg {
  width: 21px; height: 21px;
  fill: none; stroke: currentColor; stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round;
}

/* ── Facebook feed (inside The Market) ───────────────────── */

.feed {
  /* ONE number controls the feed's size. Facebook renders it at 500px max
     (tested: asking for 900 returns 498), so anything wider comes from
     magnifying it here, not from Facebook. The panel width is derived, so
     the feed always exactly fills its frame. 1 = no magnification. */
  --feed-scale: 1;
  --feed-w: calc(500px * var(--feed-scale));

  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, var(--feed-w));
  gap: clamp(2rem, 1rem + 3vw, 4rem);
  /* Top, not center: the feed is tall, and centered copy floated in the
     middle of it. Aligned to the top it reads as the feed's heading. */
  align-items: start;
  margin-top: clamp(3rem, 2rem + 3vw, 4.5rem);
  padding-top: clamp(3rem, 2rem + 3vw, 4.5rem);
  border-top: 1px solid var(--line);
}

.feed-copy h3 {
  font-size: clamp(1.6rem, 1.2rem + 1.2vw, 2.1rem);
  margin-bottom: .6rem;
}
/* Direct children only. This is the column's body copy: the 40ch measure is
   right for a paragraph and wrong for anything nested below, which is how it
   ended up clamping the reviews title to 294px and wrapping the review link
   onto its own row. The child combinator keeps it aimed at what it is for. */
.feed-copy > p:not(.eyebrow) {
  color: var(--ink-soft);
  max-width: 40ch;
  margin: 0 0 1.6rem;
}

/* The feed opening a section rather than following one (Our Partners). It
   keeps the rule above it — that line is what separates the section's own
   lede from the featured partner beneath — but not the home page's spacing,
   which is sized to put daylight between the feed and a grid of cards. Here
   there is only a paragraph above, so the gap is roughly half. */
.feed-tight {
  margin-top: 2.25rem;
  /* The same token the section below the rule is padded with, so the space
     under the line and the space from the feed to the next section are the
     one measurement rather than two that happen to match at one width. */
  padding-top: var(--page-edge);
}

/* The partner's name in that column. The home page's .feed-copy h3 IS its
   section heading and is sized accordingly; this one sits under an h2, so it
   has to be visibly smaller or the hierarchy reads backwards. */
.feed-copy .featured-name {
  font-size: clamp(1.25rem, 1.05rem + .7vw, 1.55rem);
  margin-bottom: .6rem;
}

/* The plugin's insides can't be styled (it's another site's frame), so the
   frame around it does the work: same panel treatment as the map. */
.feed-frame {
  position: relative;
  width: 100%;
  max-width: var(--feed-w);
  justify-self: end;
  background: #fff;
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: 0 0 0 1px var(--line), var(--shadow);
}

/* Our own header strip, in place of Facebook's cropped one. */
.feed-head {
  display: flex;
  align-items: center;
  gap: .65rem;
  padding: .7rem .9rem;
  background: var(--paper-card);
  border-bottom: 1px solid var(--line);
}
.feed-avatar { flex: none; width: 34px; height: 34px; border-radius: 50%; }
.feed-head-name {
  flex: 1;
  min-width: 0;
  font-family: var(--serif);
  font-size: 1.02rem;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.feed-head-follow {
  flex: none;
  padding: .32rem .7rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font-size: .7rem;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  /* Navy, as the one piece of Facebook's own chrome we draw ourselves —
     it reads as belonging to the panel below it rather than to the brick
     links in the copy beside it. */
  color: var(--navy);
  text-decoration: none;
  transition: background-color .18s, border-color .18s;
}
/* The hover follows the text rather than staying brick, or the button would
   change colour family on the way in. The tint is --navy-wash, the navy
   counterpart of --brick-wash, shared with the other Facebook controls.

   The colour is restated here and NOT left to inherit: the base a:hover rule
   sets --brick-deep, so without this line the label turned brick the moment
   you pointed at it. Same reason the vendor-card chips restate theirs. */
.feed-head-follow:hover {
  color: var(--navy);
  background: var(--navy-wash);
  border-color: var(--navy);
}

/* The window the feed shows through.
   --feed-scale > 1 magnifies the feed past Facebook's 500px ceiling: the
   iframe is rendered narrow and scaled up, so everything inside grows.
   --feed-crop hides Facebook's own 70px header (fixed at every width). */
.feed-body {
  --feed-height: 740px;
  --feed-crop: 70px;

  position: relative;
  height: var(--feed-height);
  overflow: hidden;
}
.feed-scale {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: 0 0;          /* script.js sets the transform and offset */
}
.feed-scale iframe { display: block; border: 0; }

/* Set by script.js when this browser refuses to load Facebook. The empty
   iframe is removed so it can't swallow clicks, and with no feed to make
   room for, the panel shrinks to fit the message. */
.feed-frame.is-blocked .feed-body { height: auto; }
.feed-frame.is-blocked .feed-scale { display: none; }
.feed-frame.is-blocked .feed-fallback { position: static; padding: 2.75rem 2rem; }

/* ── Visit ───────────────────────────────────────────────── */

.visit-grid {
  display: grid; gap: clamp(2rem, 1rem + 4vw, 3.75rem);
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
  align-items: start;
}

.detail { margin-top: 1.9rem; }
.detail h3 {
  font-family: var(--sans);
  font-size: .72rem; font-weight: 700;
  letter-spacing: .17em; text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: .5rem;
}
.detail p { margin: 0; }

.stack { display: grid; gap: .2rem; justify-items: start; }

.hours { margin: 0; display: grid; gap: .35rem; }
.hours > div { display: flex; justify-content: space-between; gap: 1.5rem; max-width: 24rem; }
.hours dt { color: var(--ink-soft); }
.hours dd { margin: 0; font-variant-numeric: tabular-nums; }

/* The gift card denominations. A run of figures separated by hairlines, not
   a bulleted list and NOT a row of bordered plates.

   The plates were the first attempt and they were wrong: a bordered,
   rounded, filled box at button size is a signifier for "press me", and
   there is nothing here to press. An element that looks interactive and is
   not earns a failed click, and a visitor who has made one trusts the rest
   of the page a little less. Nothing on this page is buyable, so nothing on
   it should look buyable.

   Set in the serif at reading size, because these are prices rather than
   labels, with tabular figures so the digits line up.

   Separated by a bullet, the way the market writes the list. It hangs off
   the END of each figure rather than sitting before the next one: a border
   or a leading mark on the following item gets stranded at the start of a
   wrapped row, reading as though a figure were missing from the line above.
   A trailing one at the end of a wrapped row reads as "continues", which is
   what it means.

   `content: "\2022" / ""` gives the bullet an empty alternative text, so a
   screen reader says six amounts rather than six amounts with "bullet"
   between each. Browsers without that syntax read the character, which is
   no worse than the markup it replaced. */
.amounts {
  list-style: none;
  margin: 0 0 1.25rem;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  column-gap: .85rem;
  row-gap: .3rem;
  font-size: 1.2rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.amounts li { margin: 0; }
.amounts li:not(:last-child)::after {
  content: "\2022" / "";
  margin-left: .85rem;
  color: var(--ink-faint);
}

.link-arrow {
  display: inline-block;
  margin-top: .55rem;
  font-size: .84rem; font-weight: 600;
  letter-spacing: .09em; text-transform: uppercase;
  text-decoration: none;
}
.link-arrow::after { content: " \2192"; }
.link-arrow:hover { text-decoration: underline; }

.visit-map {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow);
  background: var(--paper-card);
}
.visit-map iframe {
  display: block;
  width: 100%;
  height: clamp(300px, 42vh, 440px);
  border: 0;
  filter: saturate(.82);
}

/* ── Contact ─────────────────────────────────────────────── */

.contact-grid {
  display: grid; gap: clamp(2rem, 1rem + 4vw, 3.75rem);
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: start;
}

.fb-button {
  display: inline-flex; align-items: center; gap: .6rem;
  padding: .7rem 1.2rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper-card);
  color: var(--ink);
  text-decoration: none;
  font-size: .82rem; font-weight: 600;
  letter-spacing: .08em;
  transition: border-color .18s, color .18s;
}
/* The f mark is navy, the one cool colour on the site — it reads as the
   Facebook mark rather than as another piece of the button, and the words
   beside it stay in ink. Left navy on hover too: the mark belongs to
   Facebook, so it should not change when the button reacts. */
.fb-button svg { width: 18px; height: 18px; fill: var(--navy); }
/* Every Facebook control on the site hovers the same way: navy text, navy
   border, navy wash. The colour is restated rather than inherited because
   the base a:hover rule sets --brick-deep and would otherwise win. */
.fb-button:hover {
  color: var(--navy);
  border-color: var(--navy);
  background: var(--navy-wash);
}


.form {
  background: var(--paper-card);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  padding: clamp(1.5rem, 1rem + 2vw, 2.25rem);
  box-shadow: var(--shadow);
}

.field-row { display: grid; gap: 1rem; grid-template-columns: 1fr 1fr; margin: 0; }

.field { display: grid; gap: .4rem; margin: 0 0 1rem; }

.field label {
  font-size: .72rem; font-weight: 700;
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink-faint);
}

.field input,
.field select,
.field textarea {
  font: inherit;
  font-size: .97rem;
  color: var(--ink);
  background-color: var(--paper);
  /* --ink, like the cards: boxes on this site are outlined in the same
     near-black. 1px, not the cards' 2px — a form is a column of these and
     at 2px they would read as a stack of boxes rather than as a form. */
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  padding: .68rem .8rem;
  width: 100%;
  transition: border-color .18s, background-color .18s;
}
.field textarea { resize: vertical; min-height: 7rem; }

/* Drop the OS dropdown styling and draw our own chevron, so the select
   matches the text inputs beside it. */
.field select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 2.4rem;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' fill='none' stroke='%236d5c4e' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right .85rem center;
  background-size: 12px 8px;
}
.field select:invalid { color: var(--ink-faint); }   /* the "Choose one" state */

/* Clicking in lifts the fill to white and turns the navy border brick, so
   the field you are typing in is unmistakable. */
.field input:focus,
.field select:focus,
.field textarea:focus { background-color: #fff; border-color: var(--brick); }

/* The browser's own placeholder grey (#757575) lands at 3.8:1 on our paper
   and slightly worse on the tinted search box — under the 4.5:1 floor for
   text. --ink-faint was picked for exactly this job and measures 4.7:1 on
   the tint, while still reading as a hint rather than as a typed value. */
.field input::placeholder,
.field textarea::placeholder { color: var(--ink-faint); opacity: 1; }

/* Repeatable field rows (the social media entries). */
.repeat { display: grid; gap: .5rem; }
.repeat-row { display: flex; gap: .5rem; align-items: center; }
.repeat-row input { flex: 1; min-width: 0; }

.row-remove {
  flex: none;
  width: 2.35rem;
  align-self: stretch;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: none;
  color: var(--ink-faint);
  font: inherit;
  font-size: 1.15rem;
  line-height: 1;
  cursor: pointer;
  transition: color .18s, border-color .18s;
}
.row-remove:hover { color: var(--brick); border-color: var(--brick); }

.add-row {
  justify-self: start;
  margin-top: .2rem;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: .8rem;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--brick);
  cursor: pointer;
}
.add-row::before { content: "+ "; }
/* The mobile block below gives .add-row display:inline-flex, which would
   outrank the UA's [hidden]{display:none} and leave the button on screen
   after the row limit is reached. Same trap as .carousel-nav and .vendor. */
.add-row[hidden] { display: none; }
.add-row:hover { color: var(--brick-deep); text-decoration: underline; }

/* ── Reveal ───────────────────────────────────────────────
   A <details> styled as a link. Native, so it needs no JavaScript and
   stays usable with scripting off, where a JS-hidden panel would just be
   unreachable. */

.reveal { margin: 1.3rem 0 1.9rem; }

.reveal > summary {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  padding-block: .5rem;
  cursor: pointer;
  font-family: var(--sans);
  font-size: .82rem;
  font-weight: 600;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--brick);
  list-style: none;                  /* drop the default triangle ... */
}
.reveal > summary::-webkit-details-marker { display: none; }   /* ... and in Safari */
.reveal > summary:hover { color: var(--brick-deep); }

/* A chevron that turns when the panel opens, so the control says which way
   it is going rather than only that it is clickable. */
.reveal > summary::after {
  content: "";
  width: .42rem;
  height: .42rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  transition: transform .2s ease;
}
.reveal[open] > summary::after { transform: translateY(1px) rotate(-135deg); }

.reveal-body { margin-top: 1.1rem; max-width: 34rem; }

/* The quiet line above a submit button that sets expectations. */
.form-hint {
  margin: 0 0 1.1rem;
  font-size: .88rem;
  color: var(--ink-soft);
}

/* Marks an optional field without shouting about it. */
.field-opt {
  font-weight: 400;
  letter-spacing: .02em;
  text-transform: none;
  color: var(--ink-faint);
}

.field input[aria-invalid="true"],
.field select[aria-invalid="true"],
.field textarea[aria-invalid="true"] { border-color: #b4462f; background-color: #fdf3f0; }

.form-note {
  margin: .9rem 0 0;
  min-height: 1.3em;
  font-size: .87rem;
  color: var(--ink-soft);
}
.form-note[data-state="error"] { color: #a8402a; }

/* ── Inner pages (About, Return Policy, Terms) ───────────── */

.page-head {
  background: var(--paper-tint);
  border-bottom: 1px solid var(--line);
  padding-block: clamp(2.75rem, 2rem + 3vw, 4.5rem);
}
/* ── Banner slideshow ─────────────────────────────────────
   A page-head with the shop running behind the heading. Only used on Our
   Vendors so far; adding it elsewhere is a class and a list of images. */

.page-head-media {
  position: relative;
  overflow: hidden;
  background: var(--ink);            /* shows before the first image paints */
  border-bottom: 0;
  min-height: clamp(19rem, 14rem + 16vw, 26rem);
  display: flex;
  align-items: center;
}

.slideshow { position: absolute; inset: 0; }

.slide {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 1.4s ease;
}
.slide.is-active { opacity: 1; }

/* Hold the right of the frame when the banner has to crop. object-fit:cover
   trims from both sides evenly, which is right for a photo of a room and
   wrong for one with its subject off to one side: the gift cards sit right
   of centre in that picture and the left is empty slate, so an even crop
   loses cards and keeps nothing. No effect at desktop, where that banner is
   wider than it is cropped; it earns its place at the widths where the
   banner goes squarer and would otherwise cut the cards in half. */
.page-head-right .slide { object-position: 72% center; }

/* A light wash only, enough to tie ten different photos into one band and
   to stop the brightest of them glaring. The text does not depend on it:
   legibility is the plate's job, which is what lets this stay light enough
   to actually see the shop. */
.slideshow::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, rgba(28, 21, 17, .22), rgba(28, 21, 17, .32));
}

/* A lighter wash for a banner whose photo should carry more of the frame.
   Used on Become a Vendor, where the room is bright and the standard wash
   was flattening it. Safe to lighten: legibility is the plate's job, and
   the plate's own .64 over a near-white image still measures ~5:1 with no
   wash under it at all, so this cannot drop the text below the floor. */
.page-head-light .slideshow::after {
  background: linear-gradient(to bottom, rgba(28, 21, 17, .10), rgba(28, 21, 17, .18));
}

.page-head-inner { position: relative; }   /* above the scrim */

/* The panel the words sit on. Sized to the text rather than the page, so
   most of the photograph is still photograph.

   The alpha is load-bearing. Worst case is a near-white image behind it:
   at .64, over the .22 wash, cream text still lands around 6:1. Lighten
   this and check that number rather than guessing, because the photos
   rotate and one of them will be the bright one. */
.page-head-plate {
  max-width: 46rem;
  padding: clamp(1.4rem, 1rem + 1.6vw, 2.1rem) clamp(1.4rem, 1rem + 1.6vw, 2.25rem);
  background: rgba(28, 21, 17, .64);
  border-radius: var(--radius);
  box-shadow: 0 20px 44px -28px rgba(0, 0, 0, .7);
  /* Progressive: without it the alpha above already carries the contrast. */
  backdrop-filter: blur(10px) saturate(1.08);
  -webkit-backdrop-filter: blur(10px) saturate(1.08);
}

/* Paler than the accent on the dark reviews band: that sits on near-black,
   this sits on a translucent plate over a photograph, where #d9a183
   measured 3.2:1 and failed. An eyebrow is 11px uppercase, so it needs the
   full 4.5:1, not the large-text 3:1. This measures 5.06:1. */
.page-head-media .eyebrow { color: #f2d3bf; }
.page-head-media h1 { color: var(--paper); }
.page-head-media .page-lede { color: rgba(250, 245, 234, .82); }
.page-head-media .page-meta { color: rgba(250, 245, 234, .62); }

/* A slideshow is motion nobody asked for. Under reduced motion the script
   stops advancing; this makes sure nothing fades either. */
@media (prefers-reduced-motion: reduce) {
  .slide { transition: none; }
}

.page-head h1 {
  display: block;                     /* the home page h1 is a grid; this isn't */
  font-size: clamp(2.1rem, 1.5rem + 2.2vw, 3.1rem);
  letter-spacing: -.01em;
  margin: 0;
}
.page-lede {
  font-family: var(--serif);
  font-size: clamp(1.1rem, 1rem + .4vw, 1.3rem);
  line-height: 1.5;
  color: var(--ink-soft);
  max-width: 52ch;
  margin: .9rem 0 0;
}
/* The opening line of a two-part lede: a statement, with the explanation
   under it. Same size and colour as the lede it sits above, set bold.

   600, not 700: the font is loaded at 400/500/600, so bold would snap to
   600 anyway on the way past. Asking for what is actually there keeps the
   rule honest about what renders. It is also the heaviest weight on the
   site, above the 500 the headings use.

   A CLASS, not .page-lede:first-of-type. Every other page has a single lede
   paragraph, and a lone element is also its own first-of-type, so that
   selector would quietly restyle all of them. */
.page-lede-lead { font-weight: 600; }
.page-meta {
  margin: 1.25rem 0 0;
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.page-body { padding-block: clamp(2.5rem, 2rem + 2vw, 3.75rem) clamp(3.5rem, 2.5rem + 4vw, 5.5rem); }

/* ── News & events ────────────────────────────────────────
   A dated list, newest first. Entries with no date lay out the same way,
   because "upstairs is open now" belongs here as much as a sale does. */

.events {
  list-style: none;
  margin: 0;
  padding: 0;
  max-width: 68ch;              /* a comfortable measure for text-only entries */
}

/* Entries with a flyer need room for both columns: at 68ch the text was
   squeezed to 335px beside a 260px image. Widened only for lists that
   actually contain one, so a text-only list keeps its reading measure. */
.events:has(.event-media) { max-width: 820px; }

.event {
  padding: 1.75rem 0;
  border-top: 1px solid var(--line-soft);
}
.event:first-child { border-top: 0; padding-top: 0; }

/* More air either side of the separators, for a list whose entries are tall
   enough that 1.75rem leaves the rule sitting on the bottom of the one above
   it. Used on Current & future events, where an entry carries two images.

   The first child's exception is restated: both selectors are (0,2,0), so
   without this the rule below would win on source order and put padding back
   above the first entry, pushing it off the section heading. */
.events-roomy .event { padding-block: 2.75rem; }
.events-roomy .event:first-child { padding-top: 0; }

.event-when {
  margin: 0 0 .35rem;
  font-family: var(--sans);
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--brick);
}

.event h2,
.event h3 {
  font-size: clamp(1.3rem, 1.1rem + .7vw, 1.6rem);
  margin: 0 0 .5rem;
}
.event p:not(.event-when) { margin: 0; color: var(--ink-soft); }

/* An entry with a flyer puts it beside the text. The flyer is the poster
   the market published, so it is the entry's most recognisable part; it
   leads on a phone and sits alongside on a wide screen. Used by past
   entries and by the upcoming placeholder, so both halves of the page are
   the same shape. */
.event-media {
  display: grid;
  gap: 1.25rem 1.75rem;
  grid-template-columns: minmax(0, 1fr) minmax(0, 260px);
  align-items: start;
}
.event-body { min-width: 0; }

.event-flyer {
  width: 100%;
  height: auto;
  border-radius: var(--radius);
  box-shadow: 0 0 0 1px var(--line), var(--shadow);
}

/* An entry with more than one image: the flyer and the parking map, side by
   side. The wrapper becomes the grid child in place of the <img>, so it is
   added to the mobile ordering rule below alongside the other two.

   auto-fit with a 230px floor rather than a fixed pair of columns: the two
   sit together while there is room and drop to one above each other when
   there is not, without a second breakpoint to keep in step. */
.event-flyers {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  align-items: start;
}

/* A lone flyer in this wrapper would stretch across the whole entry, which
   is a poster at 820px. Capped to the width one of a pair gets, so entries
   stacked on the page show their flyers at the same size: the row less its
   gap, halved. Derived rather than typed, so changing the gap cannot leave
   the two out of step. */
.event-flyers > :only-child { max-width: calc((100% - 1rem) / 2); }

/* Two images side by side need more width than the 260px media column, so
   an entry carrying a PAIR drops to a single column and runs them under its
   text. The text is then capped at a readable measure rather than running
   the full width of the entry.

   Scoped with :has() to the entries that actually have a .event-flyers
   wrapper. It used to apply to every .event-upcoming, which was fine while
   only one upcoming entry existed and it had two flyers; a second upcoming
   entry with a single flyer inherited the full-width layout and blew that
   flyer up to 820px. One image still belongs in the 260px media column. */
.event-upcoming.event-media:has(.event-flyers) {
  grid-template-columns: minmax(0, 1fr);
}
.event-upcoming:has(.event-flyers) .event-body { max-width: 62ch; }

/* ── Flyer lightbox ───────────────────────────────────────
   script.js wraps each flyer in .flyer-zoom, so that button becomes the
   grid child and has to carry the layout rules the image had. */

.flyer-zoom {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  cursor: zoom-in;
  border-radius: var(--radius);
}
.flyer-zoom:focus-visible { outline: 2px solid var(--brick); outline-offset: 4px; }
.flyer-zoom .event-flyer { transition: transform .22s ease; }

@media (hover: hover) {
  .flyer-zoom:hover .event-flyer { transform: scale(1.02); }
}

/* Pinned to the viewport and centred explicitly rather than leaning on the
   UA's dialog:modal rule. Left to itself this computed to position:absolute,
   which anchors the dialog to the top of the DOCUMENT: open it halfway down
   a long page and the browser scrolls up to reach it, leaving you at the top
   once it closes. fixed also makes this the containing block for the close
   button, which is what position:relative was there for. */
.lightbox {
  position: fixed;
  inset: 0;
  margin: auto;
  width: fit-content;
  height: fit-content;
  max-width: min(92vw, 60rem);
  max-height: 92vh;
  padding: 0;
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  overflow: visible;
}
.lightbox::backdrop { background: rgba(28, 21, 17, .84); }

.lightbox img {
  display: block;
  width: 100%;
  height: auto;
  max-height: 88vh;
  object-fit: contain;
  border-radius: var(--radius);
  background: #fff;                 /* a flyer's own white, not the backdrop */
  box-shadow: 0 30px 60px -30px rgba(0, 0, 0, .8);
}

.lightbox-close {
  position: absolute;
  top: .55rem;
  right: .55rem;
  z-index: 1;
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background: rgba(28, 21, 17, .72);
  color: var(--paper);
  cursor: pointer;
  transition: background-color .18s;
}
/* Previous / next, for a listing with more than one image. Same disc as the
   close button so the three read as one set of controls, but pinned to the
   sides and vertically centred. script.js hides them with the hidden
   attribute when there is only one image, so there is deliberately NO
   display here that could defeat it — the guard below does that job. */
.lightbox-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 1;
  width: 40px;
  height: 40px;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background: rgba(28, 21, 17, .72);
  color: var(--paper);
  cursor: pointer;
  transition: background-color .18s;
}
.lightbox-nav:not([hidden]) { display: grid; }
.lightbox-prev { left: .55rem; }
.lightbox-next { right: .55rem; }
.lightbox-nav:hover { background: rgba(28, 21, 17, .9); }
.lightbox-nav svg {
  width: 17px;
  height: 17px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* An SVG cross, not a "times" character. The glyph sits off-centre inside
   its own line box, so no amount of centring the box centres the mark. */
.lightbox-close svg {
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
}
.lightbox-close:hover { background: rgba(28, 21, 17, .92); }

@media (prefers-reduced-motion: reduce) {
  .flyer-zoom:hover .event-flyer { transform: none; }
}

/* ── Upcoming placeholder ─────────────────────────────────
   Stands in for a flyer that does not exist yet: a wrecked copy of an old
   one, faded back, with a banner across it so nobody mistakes it for a
   real announcement. */

.event-flyer-slot { position: relative; }

.event-flyer.is-placeholder {
  opacity: .5;
  background: var(--paper-tint);   /* reads as an empty slot, not a dark photo */
}

.flyer-banner {
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  padding: .5rem .4rem;
  /* --navy at the same alpha. Kept as a literal rgba() because the token is
     a hex and this needs to let a little of the photo through, which is what
     stops it reading as a sticker laid on top. */
  background: rgba(34, 62, 101, .94);
  color: var(--paper);
  font-family: var(--sans);
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .18em;
  text-transform: uppercase;
  text-align: center;
}

/* An upcoming date is promoted from caption to CHIP: same words, but filled
   so it registers as an object before anything is read. This is what event
   listings everywhere do, and it is the cheapest way to make one line in a
   stack of text win — a filled shape is seen at a glance, where a colour or
   weight change still has to be read to be noticed.

   Navy, the colour already outlining the vendor cards and the search box on
   Our Vendors. It is the site's second accent, so the chip is not a new
   colour, and it matches the "Coming soon" banner beside it: the two labels
   on this entry are the same kind of thing and now look it.

   ONLY on .event-upcoming. Past entries keep the plain caption — a loud date
   on something that already happened competes with the thing you can still
   go to, which is the opposite of the point. The "News" caption in this same
   section stays plain too: it is a label, not a date. */
.event-upcoming .event-when {
  display: inline-block;
  margin-bottom: .7rem;
  /* The .14em tracking hangs after the last letter, so an even padding looks
     heavier on the right. Taking it back off balances the chip optically. */
  padding: .42rem calc(.78rem - .14em) .42rem .78rem;
  border-radius: var(--radius);
  background: var(--navy);
  color: var(--paper);
  /* On a phone the line is too long for the column and wraps to two. Balance
     evens them out so it reads as a deliberate two-line label rather than a
     full line with a short stub under it. Ignored where unsupported, which
     only costs the ragged wrap it was going to have anyway. */
  text-wrap: balance;
}

/* An entry with more than one date: a chip each, rather than two dates in
   one. They sit in a wrapper so the gap between them is set once, and the
   chips themselves drop their own bottom margin — the wrapper now owns the
   space under the pair. Flex-wrapped, so a narrow column stacks them and a
   wide one can run them side by side. */
.event-whens {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  margin-bottom: .7rem;
}
.event-whens .event-when { margin: 0; }

.event-upcoming .fb-button { margin-top: 1.2rem; }

/* Stands in for the upcoming list once everything in it has expired. Same
   dashed card as the vendor search's no-results note: both say "there is
   nothing here right now", and a visitor who has seen one recognises the
   other. */
.events-empty {
  margin: 0;
  padding: 1.5rem;
  background: var(--paper-card);
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  color: var(--ink-soft);
}

/* Events on the left, news on the right, in both halves of the page. One
   long interleaved list meant a sale and an announcement competed for the
   same slot and a visitor scanning for "when is the next sale" had to read
   past the news to find it. Two columns keep each kind findable without
   either disappearing.

   align-items: start so a short column does not stretch to match a tall
   one, and minmax(0, 1fr) rather than 1fr because a grid track's default
   min-width is auto: a wide flyer inside would otherwise push the column
   past its share and overflow the page. */
.events-split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 2.5rem 3rem;
  align-items: start;
}

.events-col-title {
  margin: 0 0 1.1rem;
  font-family: var(--sans);
  font-size: .73rem;
  font-weight: 600;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ink-faint);
  padding-bottom: .7rem;
  border-bottom: 1px solid var(--line);
}

/* Inside a column an entry stacks: the flyer on top, the text under it.
   The side-by-side layout wants about 500px for the text beside a 260px
   flyer, and half the wrap does not have it — the text would have been
   squeezed into a column of three-word lines. This is the same arrangement
   the entries already take on a phone, for the same reason. */
.events-split .event-media { grid-template-columns: minmax(0, 1fr); }
.events-split .event-body { max-width: none; }
.events-split .event-flyer,
.events-split .event-flyer-slot,
.events-split .event-flyers,
.events-split .flyer-zoom { order: -1; }
.events-split .event-flyer,
.events-split .event-flyer-slot,
.events-split .flyer-zoom { max-width: 340px; }
/* A pair fills the column instead of being capped: two flyers inside 340px
   would be thumbnails. */
.events-split .event-flyers { max-width: none; }

@media (max-width: 900px) {
  /* One column under the breakpoint, events first. The order is the order
     in the markup, so nothing is needed to keep it. */
  .events-split { grid-template-columns: minmax(0, 1fr); gap: 2.75rem; }
}

/* Section heading for either events list. Tighter under-margin than a
   default h2 because an eyebrow sits above it. */
.events-title { margin-bottom: .6rem; }

/* Past events has a lede paragraph under its heading, and that paragraph's
   2rem bottom margin is what sets the list off from the words above it.
   Current & future events has no lede, so its list came up against the
   heading's .6rem and read as cramped beside the other section.

   This gives a list that follows the heading DIRECTLY the same 2rem of air.
   Adjacent margins collapse, so this replaces the heading's .6rem rather
   than stacking on top of it, and the two lists end up with an identical
   gap above them. If a lede is ever added here, the selector stops matching
   and the spacing goes back to coming from the lede, as it does next door. */
.events-title + .events { margin-top: 2rem; }


@media (max-width: 640px) {
  /* Stack, flyer first: it carries the name and the date in one glance.
     Both selectors are needed. A past entry's grid child is the <img>
     itself; the placeholder's is the wrapper holding its banner, and
     ordering only applies to a grid child, so targeting the image alone
     left the placeholder stranded below its text. */
  .event-media { grid-template-columns: minmax(0, 1fr); }
  .event-flyer,
  .event-flyer-slot,
  .event-flyers,
  .flyer-zoom { order: -1; max-width: 340px; }
  /* The carousel's arrows and dots follow its track up the entry. */
  .flyers-controls { order: -1; }
  /* Squarer on a phone: at 340px across, a 4:3 slot leaves a portrait flyer
     about 250px tall, which is too small to read anything on. */
  .event-flyers.is-carousel { aspect-ratio: 1 / 1; }

  /* The lone-flyer cap is for a pair sitting side by side. Down here the
     pair is stacked, so halving the column would show a single flyer at
     half the width of the stacked ones above it. The wrapper's own 340px
     is the cap that matters now. */
  .event-flyers > :only-child { max-width: none; }
}

/* ── Vendor directory ─────────────────────────────────────
   A list of independent sellers with links out to their own sites. The
   search filters the markup that is already on the page rather than
   fetching anything, so it works on a static host and needs no index. */

.vendor-search { max-width: 34rem; margin-bottom: 1.75rem; }
.vendor-search .field { margin-bottom: .5rem; }

/* The shared .field rule gives inputs a hairline border in --line, which on
   this page's cream is 1.3:1 — readable on a desktop monitor, easy to miss
   on a phone, where it stopped looking like something you could type in.

   The border does the work here, not a fill. Every background that can sit
   on --paper is stuck in a narrow band (the tinted option reached 1.11:1),
   because the page is already nearly white. A border has no such ceiling:
   navy against this cream is 9.93:1.

   The border itself now comes from the shared .field rule, which every form
   field on the site uses. What stays here is the icon. */
.vendor-search input {
  /* A magnifying glass, so the box announces what it is before anyone reads
     the label above it. On a cream page a field cannot rely on its fill to
     say "search" — white is spoken for by the focus state — so the icon
     carries that meaning instead.

     Drawn as a background image rather than an element in the markup: no
     wrapper, no extra node to position, and nothing for a screen reader to
     announce twice when the field is already labelled. The trade is that
     the stroke colour is baked in and cannot follow currentColor, which
     costs nothing while the field has one resting colour. --ink-faint,
     %23 being the # that a data URI will not take raw. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23746452' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='10.5' cy='10.5' r='6.5'/%3E%3Cpath d='M15.6 15.6 21 21'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: left .8rem center;
  background-size: 18px 18px;
  padding-left: 2.7rem;               /* clear of the glass, not over it */
}

/* Safari draws its own clear button inside type=search once there is text.
   It sits on the right, away from the glass, so it is left alone — two
   affordances, neither on top of the other. */

/* Focus still lifts the fill to white and turns the border brick, from the
   .field rules above. Spelled out because that rule outranks this one by
   only a single class, and the state change should not depend on a margin
   a later edit could close by accident. */
.vendor-search input:focus { background-color: #fff; border-color: var(--brick); }

.vendor-count {
  margin: 0;
  min-height: 1.4em;            /* reserve the line so results don't jump */
  font-size: .82rem;
  color: var(--ink-faint);
}

.vendors {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 1.25rem;
  grid-template-columns: repeat(auto-fit, minmax(255px, 1fr));
}

/* Partner cards carry a full sentence of description where a vendor's is half
   a line, and four to a row broke that into a column of two-word lines. A
   wider track floor caps the grid at three across the 1120px wrap — four
   columns would need 4 * 300 + 3 * 20 = 1260 — without pinning a column count,
   so every narrower breakpoint still collapses on its own. */
.vendors-wide { grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); }

.vendor {
  position: relative;          /* so a hovered card sits above its neighbours */

  /* auto-fit collapses the empty tracks when the search filters the list,
     so a single result inherited the whole row and stretched to 1120px.
     Capping it at half the wrap keeps one card looking like a card.
     A fixed value, not 50%: a percentage on a grid item resolves against
     its own track, so with four cards showing it would halve all of them. */
  max-width: calc(var(--wrap) / 2);

  display: flex;
  flex-direction: column;
  gap: .5rem;
  background: var(--paper-card);
  border: 1px solid var(--line-strong);  /* matches the home cards and the reviews frame */
  border-radius: var(--radius);
  padding: 1.5rem 1.4rem;
  box-shadow: var(--shadow);
  transition: transform .22s ease, box-shadow .22s ease, border-color .22s ease;
}

/* display:flex above outranks the UA's [hidden]{display:none}, so a card
   the search had hidden stayed on screen: the count said "1 vendor
   matches" while all four were still visible. Same trap as .carousel-nav. */
.vendor[hidden] { display: none; }

/* Matches the cards on the home page. Scoped to devices that really hover,
   because on a touch screen :hover can stick after a tap and leave a card
   stranded in its lifted state. */
@media (hover: hover) {
  /* No border-color change here, unlike .card: the outline is already a
     solid navy, so shifting it on hover would only pull it away from the
     colour the card is meant to have. The lift and the shadow carry it. */
  /* Partner cards sit still. A vendor card's lift is an invitation to look
     closer at a list someone is scanning; the partners list is short and
     read once, so the movement is just movement. The shadow stays put with
     it — a lifted shadow under a card that has not lifted reads as a
     rendering fault. */
  .vendor.partner:hover { transform: none; box-shadow: var(--shadow); }
  /* And the logo keeps its resting ring: a partner card does nothing at all
     on hover, so a brick ring appearing on the one round thing in it would
     be the only thing moving. */
  .vendor.partner:hover .vendor-avatar { box-shadow: 0 0 0 1px var(--line); }

  .vendor:hover {
    transform: translateY(-4px) scale(1.02);
    box-shadow: 0 2px 4px rgba(51, 39, 31, .06), 0 22px 44px -22px rgba(51, 39, 31, .45);
    z-index: 1;
  }
  .vendor:hover .vendor-avatar { box-shadow: 0 0 0 1px var(--brick); }
}

.vendor-head { display: flex; align-items: center; gap: .8rem; }

/* The vendor's own Facebook profile picture, served from assets/vendors/.
   alt is empty on purpose: the name sits right beside it, so describing
   the image again would just make a screen reader say it twice. The tinted
   background shows through while it loads and if the file is missing, so a
   gap never collapses the row. */
.vendor-avatar {
  flex: none;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--paper-tint);
  box-shadow: 0 0 0 1px var(--line);
}

.vendor-name { font-size: 1.15rem; margin: 0; }

.vendor-meta {
  margin: 0;
  color: var(--ink-soft);
  font-size: .95rem;
}

/* "New" on a card the market added in the last week. script.js writes it in
   from data-added, so the window moves on its own and nothing here needs a
   date in it. Lives inside the <h2> rather than beside it in .vendor-head:
   as a third flex child it would sit a full gap away from a short name and
   not wrap with a long one. A screen reader reads it as part of the
   heading, "Faerybug Curios New", which is what it means. */
/* Flags on a card — just "New" at the moment — in a strip pinned inside the
   top right corner rather than inline after the name.

   They land in the same place on every card. Inline, the chip followed the
   name, so it sat mid-line on a short name and dropped to a second line on a
   long one — a label that moves is a label you have to look for.

   They are a layer, not content. The strip is absolutely positioned, so it
   costs no height and no margin: a flagged card is exactly as tall as an
   unflagged one, and a second flag could never reflow the name, the blurb or
   the booth line.

   Straddling the border says what it is: a tag ON the card, not a line IN it.
   The half that hangs inside covers nothing, because the card's 1.5rem top
   padding is empty there; the half that hangs outside reaches about 10px into
   the grid's 20px row gap, which is why that gap is not tightened.

   On the right. The left was tried, where a "Featured" chip once lived, and
   it crowded the avatar — two round things in the same corner. On the right
   the chip has the empty end of the head row to itself. Nothing collides
   there either: straddling the border puts its lower edge about 12px in,
   above the head row, which does not start until the 1.5rem padding ends. */
.vendor-flags {
  position: absolute;
  top: 0;
  transform: translateY(-50%);

  display: flex;
  flex-wrap: wrap-reverse;     /* a wrapped second flag stacks upward */
  gap: .35rem;

  margin: 0;
  padding: 0;
  list-style: none;
  /* Half the card, less the gutter, so a strip can never reach the far side. */
  max-width: calc(50% - 1.1rem);
}

.vendor-flags-end { right: 1.1rem; justify-content: flex-end; }

.vendor-flag {
  /* Rounder than the site's 4px --radius: a pill reads as a tag pinned to the
     card, where a softened rectangle just looked like a small version of the
     card's own corners. The side padding goes up with it, or the letters
     crowd the curve. The .1em tracking hangs after the last letter, so the
     right padding is taken back by that much — same correction as the event
     date chip. */
  padding: .22rem calc(.62rem - .1em) .22rem .62rem;
  border-radius: 999px;
  /* A ring a shade or two down from the fill. It draws the pill's edge
     against the card border it is sitting across, which the flat fill alone
     left looking like a smudge where the two met. Not --line-strong: a brown
     ring on a teal pill reads as dirt rather than as an outline. Inside the
     box, because box-sizing is border-box, so the chip does not grow. */
  border: 1px solid #3c686c;
  color: var(--paper);
  font-family: var(--sans);
  font-size: .6rem;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  white-space: nowrap;
}

/* Teal for "New", which is a fact about the card and states itself. White
   letters, not --paper: see the note on --teal. */
.vendor-flag-new { background: var(--teal); color: #fff; }

/* A band across the grid, naming the run of cards under it. script.js writes
   both of these in, because it is also what moves the featured cards up — the
   label and the order it describes have to arrive together or the band is a
   lie. (With JavaScript off, neither happens: a plain alphabetical list,
   which is honest.)

   This is the quiet way to say "featured". A chip on each card repeats the
   same word four times in the busiest part of the card; one line above the
   row says it once and leaves the cards alone. The band spans every column
   so it reads as a divider rather than a short label stranded over the
   first card. */
.vendors-band {
  grid-column: 1 / -1;
  max-width: none;           /* .vendor's half-wrap cap is not wanted here */
  margin: 0;
  font-family: var(--sans);
  font-size: .73rem;
  font-weight: 600;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--brick);
}

/* One rule for the whole page, above the second band, and none trailing off
   the words. A trailing rule on each band was tried: two lines where the job
   needs one, and the Featured one in brick pulled more attention than the
   group it was labelling.

   Spacing is the thing to watch here. The first version of this separator
   used 1.1rem + 1.6rem around it and pushed the rest of the vendors out of
   sight below the featured row. This is deliberately tight — enough that the
   rule is clearly dividing the two groups, not enough to make the second
   group feel like another page. */
.vendors-band-rest {
  margin-top: .5rem;
  padding-top: .9rem;
  border-top: 1px solid var(--line);
  color: var(--ink-faint);
}

.vendor-booth {
  display: block;
  margin-top: .3rem;
  font-family: var(--sans);
  font-size: .7rem;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* margin-top: auto puts the links on a common baseline across the row.
   It belongs in this shorthand rather than in a rule of its own: a later
   `margin: 0` would quietly win and the links would ride up under short
   cards. It sits on the links, not on .vendor-meta, because not every
   vendor has a description and those cards must still line up. */
.vendor-links {
  list-style: none;
  margin: auto 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: .4rem;
}
.vendor-links a {
  display: inline-flex;
  align-items: center;
  min-height: 34px;
  padding: .3rem .7rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font-family: var(--sans);
  font-size: .76rem;
  font-weight: 600;
  letter-spacing: .06em;
  color: var(--ink);
  text-decoration: none;
  transition: color .18s, border-color .18s, background-color .18s;
}
/* The plain chips — a website, anything that is not Facebook or Instagram.
   Those two keep their own colours through the rules below, which outrank
   this one by an attribute selector. */
.vendor-links a:hover {
  color: var(--ink);
  border-color: var(--ink-faint);
  background: var(--ink-wash);
}

/* The Facebook link on a card is lettered navy, like the Follow button on
   the feed panel. Matched on the href rather than a class, so a vendor added
   later gets it from their URL and nobody has to remember to tag the markup.
   The hover follows the text into navy for the same reason it does there:
   otherwise the chip would change colour family the moment you touch it. */
.vendor-links a[href*="facebook.com"] { color: var(--navy); }

/* Instagram is lettered brick, the site's other accent, so the two platforms
   are told apart at a glance without either one being filled in.

   It carries its own hover, and it has to. It used to inherit the shared
   .vendor-links a:hover, which was brick at the time — then that rule went
   ink to match the ghost buttons and Instagram quietly went with it. A chip
   that states its colour only at rest will follow whatever the default
   becomes. */
/* Etsy and eBay share Instagram's brick rather than taking a colour each:
   with Facebook in navy, the split a visitor needs is "their page" against
   "somewhere to buy or browse", not one hue per platform. Grouped in one
   selector so they can never drift apart. */
.vendor-links a[href*="instagram.com"],
.vendor-links a[href*="etsy.com"],
.vendor-links a[href*="ebay.com"] { color: var(--brick); }
.vendor-links a[href*="instagram.com"]:hover,
.vendor-links a[href*="etsy.com"]:hover,
.vendor-links a[href*="ebay.com"]:hover {
  color: var(--brick);
  border-color: var(--brick);
  background: var(--brick-wash);
}
.vendor-links a[href*="facebook.com"]:hover {
  color: var(--navy);
  border-color: var(--navy);
  background: var(--navy-wash);
}

.vendor-none { margin: auto 0 0; font-size: .86rem; color: var(--ink-faint); }

.vendor-empty {
  margin: 2rem 0 0;
  padding: 1.5rem;
  background: var(--paper-card);
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  color: var(--ink-soft);
}

.vendor-cta { margin-top: 3.5rem; }

/* Prose column: narrower than .wrap, because full-width body text is hard
   to read. ~68 characters a line. */
.prose { max-width: 68ch; }
.prose h2 {
  font-size: clamp(1.4rem, 1.2rem + .8vw, 1.75rem);
  margin: 2.5rem 0 .5rem;
}
.prose h2:first-child { margin-top: 0; }
.prose p { margin: 0 0 1.1rem; color: var(--ink-soft); }
.prose ul { margin: 0 0 1.25rem; padding-left: 1.2rem; color: var(--ink-soft); }
.prose li { margin-bottom: .45rem; }
.prose strong { color: var(--ink); font-weight: 600; }
.prose .link-arrow { margin-top: 0; }

/* Photos inside a prose column. Same panel treatment as the rest of the site,
   and the image keeps its own proportions so nothing is cropped. */
.prose-figure { margin: 1.75rem 0 2rem; }
.prose-figure img {
  width: 100%;
  height: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.prose-figure figcaption {
  margin-top: .6rem;
  font-size: .87rem;
  color: var(--ink-faint);
}

/* Visible "this isn't finished yet" banner. Delete the element from the page
   once the real wording is in. */
/* The one line of a policy page a customer must not miss. */
.policy-headline {
  font-family: var(--serif);
  font-size: clamp(1.3rem, 1.1rem + .9vw, 1.65rem);
  line-height: 1.35;
  color: var(--ink);
  border-left: 3px solid var(--brick);
  padding: .1rem 0 .1rem 1.1rem;
  margin: 0 0 1.6rem;
}

/* ── Footer ──────────────────────────────────────────────── */

.site-footer {
  background: var(--ink);
  color: var(--paper);
  padding-block: 3.25rem;
}
.footer-inner { display: grid; gap: 1.5rem; justify-items: center; text-align: center; }

.wordmark-footer { color: var(--paper); }
.wordmark-footer .wordmark-sub { color: rgba(250, 245, 234, .6); }
.wordmark-footer .wordmark-rule { background: var(--paper); opacity: .35; }

.footer-address { margin: 0; font-size: .95rem; color: rgba(250, 245, 234, .8); line-height: 1.9; }
.footer-address a { color: var(--paper); text-decoration-color: rgba(250, 245, 234, .4); }
.footer-address a:hover { color: #fff; text-decoration-color: currentColor; }

.footer-links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 1.25rem;
}
.footer-links a {
  font-size: .78rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: rgba(250, 245, 234, .82);
  text-decoration-color: rgba(250, 245, 234, .35);
}
.footer-links a:hover { color: #fff; text-decoration-color: currentColor; }

/* Copyright and credit sit as one small group, closer together than the
   footer's other rows. */
.footer-meta { display: grid; gap: .45rem; }

.footer-legal {
  margin: 0;
  font-size: .74rem;
  letter-spacing: .1em; text-transform: uppercase;
  color: rgba(250, 245, 234, .62);
}

/* Builder credit: deliberately quieter than the copyright line above it. */
.footer-credit {
  margin: 0;
  font-size: .74rem;
  letter-spacing: .03em;
  color: rgba(250, 245, 234, .62);
}
.footer-credit a {
  color: rgba(250, 245, 234, .82);
  text-decoration-color: rgba(250, 245, 234, .35);
}
.footer-credit a:hover { color: #fff; text-decoration-color: currentColor; }

/* ── Responsive ──────────────────────────────────────────── */

@media (max-width: 900px) {
  .visit-grid,
  .contact-grid { grid-template-columns: minmax(0, 1fr); }
  .visit-map { order: -1; }

  .feed { grid-template-columns: minmax(0, 1fr); }
  .feed { --feed-scale: 1; }        /* already full width; no need to magnify */
  .feed-frame { justify-self: center; }

  /* Below this the two columns get too narrow — stack them, photo underneath. */
  .hero-split { grid-template-columns: minmax(0, 1fr); gap: 2.25rem; }
  /* Same 4:3 crop on phones — taken off the top, same as desktop. */
}

/* The menu collapses to the button earlier than the rest of the mobile
   rules, and on its own breakpoint on purpose: otherwise the nav wraps onto
   a second row while the button is still hidden.

   Measured, not guessed, and re-measured whenever the menu changes.
   Wordmark 167px + 24px gap + five links 679px = 870px of content, which
   with the wrap's gutters runs out at about 920px. At 921 the row still
   fits with nothing to spare, so there is no headroom left: adding a link,
   or lengthening one, means measuring again rather than assuming.

   History, because both moves were for this reason. It was 1020 when
   Return Policy and Our Partners were both in the menu and six links needed
   978px; moving Return Policy to the footer took it back under. Gift Cards
   later pushed it back to six, and with About Us also still there the
   header was being squeezed between 920 and roughly 1020 — the links
   compressed and the bar grew from 72px to 80px rather than wrapping, which
   is why it was easy to miss. Moving About Us to the footer took it back to
   five and the bar back to 72px. */
@media (max-width: 920px) {
  .nav-toggle { display: inline-flex; }

  /* The panel stays display:flex and hides with visibility, because
     display cannot be transitioned: the old none/flex swap is why this
     used to appear instantly. visibility still takes it out of the tab
     order and the accessibility tree while closed, which opacity alone
     would not, and its transition is delayed until the fade finishes so
     the panel is not yanked away mid-animation. */
  .site-nav {
    display: flex;
    position: absolute; left: 0; right: 0; top: 100%;
    flex-direction: column; align-items: stretch; gap: 0;
    background: var(--paper);
    border-bottom: 1px solid var(--line);
    box-shadow: 0 18px 30px -22px rgba(51,39,31,.6);
    padding: .5rem 1.25rem 1.25rem;

    opacity: 0;
    visibility: hidden;
    transform: translateY(-.7rem);
    transition:
      opacity .2s ease,
      transform .26s cubic-bezier(.2, .7, .3, 1),
      visibility 0s linear .26s;
  }
  .site-nav.is-open {
    opacity: 1;
    visibility: visible;
    transform: none;
    transition:
      opacity .2s ease,
      transform .26s cubic-bezier(.2, .7, .3, 1),
      visibility 0s;
  }

  /* Each link rises into place a beat after the one above it, so the menu
     unfolds rather than arriving all at once. */
  .site-nav a {
    padding-block: .9rem;
    border-bottom: 1px solid var(--line-soft);
    opacity: 0;
    transform: translateY(.45rem);
    transition: opacity .22s ease, transform .22s ease, color .18s;
  }
  .site-nav.is-open a { opacity: 1; transform: none; }
  .site-nav.is-open a:nth-child(1) { transition-delay: .05s; }
  .site-nav.is-open a:nth-child(2) { transition-delay: .09s; }
  .site-nav.is-open a:nth-child(3) { transition-delay: .13s; }
  .site-nav.is-open a:nth-child(4) { transition-delay: .17s; }
  .site-nav.is-open a:nth-child(5) { transition-delay: .21s; }
  .site-nav a:hover { border-bottom-color: var(--line-soft); }
  .site-nav a:last-child { border-bottom: 0; }

  /* The global reduced-motion rule zeroes durations but not delays, and a
     240ms visibility delay would leave the panel hanging after it closed.
     Kill the movement outright instead. */
  @media (prefers-reduced-motion: reduce) {
    .site-nav,
    .site-nav.is-open,
    .site-nav a,
    .site-nav.is-open a {
      transform: none;
      transition-delay: 0s;
    }
  }

}

@media (max-width: 760px) {
  body { font-size: 16px; }

  .lede { max-width: none; }

  .field-row { grid-template-columns: 1fr; gap: 0; }
  .btn { width: 100%; }

  /* ── Mobile: form fields ──────────────────────────────────
     16px is not a style choice. iOS Safari zooms the page in when you
     focus a control whose text is smaller than 16px, and it does not
     zoom back out afterwards, so the rest of the form is then filled in
     on a page scrolled sideways. These were .97rem (15.5px), which is
     just under the line. */
  .field input,
  .field select,
  .field textarea { font-size: 16px; }

  /* ── Mobile: tap targets ──────────────────────────────────
     Everything a thumb has to hit gets at least 44px, which is Apple's
     guideline and comfortably over the 24px WCAG 2.5.8 floor. Desktop
     keeps the smaller, tidier sizes; this only applies where there is
     no cursor. */
  .nav-toggle { min-height: 44px; }
  .reveal > summary { min-height: 44px; }
  .wordmark { min-height: 44px; justify-content: center; }

  /* Footer links sat at 16-21px. These are secondary, and they are inline
     in running text, so they get vertical padding to grow the hit area
     past the 24px WCAG 2.5.8 floor without moving anything: padding on an
     inline box expands what you can tap but does not change the line. */
  .footer-address a,
  .footer-links a,
  .footer-credit a,
  .prose p a,
  .event-body p a { padding-block: .35rem; }

  /* A flex item, so padding would grow the whole line. min-height lifts it
     the 2px it needs to clear the 24px floor; inline-flex keeps the label
     centred in the slightly taller box. */
  .reviews-cta {
    display: inline-flex;
    align-items: center;
    min-height: 24px;
  }

  /* Phone numbers and email addresses are the main thing anyone does on
     a phone here, and they were 26px tall. */
  .stack { gap: .1rem; }
  .stack a {
    display: flex;
    align-items: center;
    min-height: 44px;
  }

  .link-arrow {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }

  .feed-head-follow { padding: .6rem .85rem; }

  .carousel-btn { width: 44px; height: 44px; }
  .carousel-dot { width: 26px; }

  .add-row {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    margin-top: 0;
  }

  /* The Facebook panel was 797px tall against an 812px viewport, so on a
     phone it filled the entire screen and nothing around it was visible.
     script.js reads this and resizes the iframe to match. */
  .feed-body { --feed-height: 560px; }

  /* The sticky header is 73px here, not the 88px the desktop value
     assumes, so anchored jumps were landing low. */
  html { scroll-padding-top: 80px; }
}

/* ============================================================
   PASSWORD GATE
   Self-contained on purpose: at launch the <script src="gate.js">
   tags come out of the seven pages and everything below can be
   left alone or deleted in one piece. See gate.js for what this
   does and does not protect.
   ============================================================ */

/* data-locked is set on <html> in the MARKUP of every page, and gate.js
   removes it in <head> once it knows this browser has access — so the site
   is hidden by default and stays hidden with JavaScript off, and neither
   state is ever painted. display:none and not visibility/opacity: hidden
   content still has layout, and a hidden page that is still 4000px tall
   leaves a scrollbar next to the password box. */
html[data-locked] { overflow: hidden; }
/* <noscript> is exempt, or the no-JS message would be hidden along with
   everything else — its contents only render when scripting is off, and a
   display:none on the element takes them with it. */
html[data-locked] body > *:not(.gate):not(noscript) { display: none !important; }

.gate {
  position: fixed;
  inset: 0;
  z-index: 200;                       /* over the sticky header's 100 */
  display: grid;
  place-items: center;
  padding: 1.5rem;
  background: var(--paper);
  overflow-y: auto;                   /* small screens, on-screen keyboard */
}

.gate-card {
  width: 100%;
  max-width: 24rem;
  padding: clamp(1.75rem, 1.4rem + 1.6vw, 2.5rem);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper-card);
  box-shadow: var(--shadow);
  text-align: left;
}

/* The market's name set as a wordmark rather than pulled in as the logo
   image: this screen should render the moment the CSS does, without waiting
   on a file that may not be cached yet. */
.gate-name {
  margin: 0 0 1.5rem;
  font-family: var(--serif);
  font-size: 1.15rem;
  font-weight: 600;
  line-height: 1.2;
  color: var(--brick);
}
.gate-name span {
  display: block;
  margin-top: .2rem;
  font-family: var(--sans);
  font-size: .62rem;
  font-weight: 600;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.gate-title {
  margin: 0;
  font-family: var(--serif);
  font-size: 1.6rem;
  font-weight: 500;
  line-height: 1.2;
  color: var(--ink);
}
.gate-note {
  margin: .6rem 0 1.5rem;
  font-family: var(--serif);
  font-size: 1rem;
  line-height: 1.5;
  color: var(--ink-soft);
}

.gate-label {
  display: block;
  margin-bottom: .4rem;
  font-family: var(--sans);
  font-size: .7rem;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
/* Positioning context for the show-password button. */
.gate-field { position: relative; }

.gate-input {
  width: 100%;
  padding: .7rem 3rem .7rem .85rem;   /* room on the right for the button */
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: #fff;
  font-family: var(--sans);
  font-size: 1rem;                    /* 16px, or iOS zooms the page in */
  color: var(--ink);
}
.gate-input:focus-visible {
  outline: 2px solid var(--brick);
  outline-offset: 1px;
}
.gate-input[aria-invalid="true"] { border-color: var(--brick); }
/* Edge draws its own reveal control inside password fields; without this
   there would be two eyes side by side. */
.gate-input::-ms-reveal { display: none; }

.gate-peek {
  position: absolute;
  top: 50%;
  right: .25rem;
  transform: translateY(-50%);
  width: 40px; height: 40px;
  display: grid; place-items: center;
  padding: 0;
  border: 0;
  border-radius: var(--radius);
  background: none;
  color: var(--ink-faint);
  cursor: pointer;
  transition: color .18s;
}
.gate-peek:hover { color: var(--ink); }
.gate-peek:focus-visible { outline: 2px solid var(--brick); outline-offset: 1px; }
.gate-peek svg {
  width: 19px; height: 19px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.gate-go {
  width: 100%;
  min-height: 44px;
  margin-top: 1rem;
  padding: .82rem 1.7rem;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: var(--brick);
  color: #fff;
  font-family: var(--sans);
  font-size: .78rem;
  font-weight: 600;
  letter-spacing: .13em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background-color .18s, transform .18s;
}
.gate-go:hover { background: var(--brick-deep); }
.gate-go:active { transform: translateY(1px); }
.gate-go[disabled] { opacity: .65; cursor: default; transform: none; }

.gate-error {
  margin: .9rem 0 0;
  font-family: var(--sans);
  font-size: .82rem;
  line-height: 1.45;
  color: var(--brick-deep);
}
/* The message is toggled through the hidden attribute, and this file sets
   no display on it — a display here would override [hidden] and leave the
   error on screen permanently. Same trap as .carousel-nav and .vendor. */
