/*
 * Mobile-first. The base rules ARE the phone layout; desktop is reached only
 * through min-width queries further down.
 *
 * Palette is mostly white with navy and gold as accents, per the brief. The
 * Cask Marque values are sampled from the supplied artwork so the page matches
 * the badges it displays.
 */

:root {
  --navy: #002e4f;
  --gold: #dbb05a;
  --cream: #fffcc7;

  --ppp-navy: #1b1d36;
  --ppp-cream: #f4f1ea;

  --ink: #1b1d36;
  --ink-muted: #5b6472;
  --rule: #e4e7ec;
  --surface: #ffffff;

  --font-body: 'Archivo', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-display: 'ivymode', 'Archivo', Georgia, serif;

  --gutter: 1rem;
  --radius: 10px;
  --max-width: 100rem;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  background: var(--surface);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.5;
  -webkit-text-size-adjust: 100%;
}

img {
  max-width: 100%;
  height: auto;
}

/*
 * View switching uses the `hidden` attribute. Any element given an explicit
 * `display` (.panel--map is a grid) would otherwise out-specify the UA default
 * of `display: none` and stay on screen while inactive.
 */
[hidden] {
  display: none !important;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ------------------------------------------------------------------ *
 * Header
 * ------------------------------------------------------------------ *
 *
 * Not here. The header is the shared component in
 * wordpress/ppp-child/header/ppp-header.{html,css}, stamped into index.html by
 * `npm run header` and styled by the stylesheet index.html loads from the child
 * theme. Header changes go there, so Divi pages and this module stay identical.
 */

/* ------------------------------------------------------------------ *
 * Controls
 * ------------------------------------------------------------------ */

.main {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: var(--gutter);
}

.controls {
  display: grid;
  gap: 0.75rem;
  position: sticky;
  top: 0;
  z-index: 5;
  padding: 0.75rem 0;
  background: var(--surface);
}

.controls__input {
  width: 100%;
  /* 16px minimum stops iOS Safari zooming the page on focus. */
  font-size: 1rem;
  font-family: inherit;
  color: inherit;
  padding: 0.75rem 0.875rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
}

.controls__input:focus-visible {
  outline: 2px solid var(--navy);
  outline-offset: 1px;
  border-color: var(--navy);
}

.controls__select {
  width: 100%;
  /*
   * A select's min-content width is its longest option, and "Perfect Pint
   * Promise Accredited" is ~270px against ~288px of usable width on a 320px
   * phone. Without this the grid column would take that as a floor and push the
   * page into horizontal scroll on the narrowest handsets; with it the UA
   * truncates the closed label instead, and the open picker is unaffected.
   */
  min-width: 0;
  /* 16px minimum, same reason as the search input: iOS zooms below it. */
  font-size: 1rem;
  font-family: inherit;
  font-weight: 600;
  color: var(--navy);
  min-height: 44px;
  padding: 0 2.25rem 0 0.875rem;
  border: 1px solid var(--navy);
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
  /*
   * The native arrow sits in a UA-coloured box that ignores the border radius on
   * Windows, so it is replaced with a chevron drawn in brand gold. Inline SVG in
   * a data: URI keeps this request-free, which matters under the CSP-free static
   * host and the no-build-step rule.
   */
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='none' stroke='%23dbb05a' stroke-width='2' d='M1 1l5 5 5-5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.875rem center;
}

.controls__select:focus-visible {
  outline: 2px solid var(--navy);
  outline-offset: 1px;
}

/*
 * The rating filter is a checkbox, not a second <select>: it has two states and
 * no "off" option worth naming. It borrows the select's height, border, radius
 * and weight so the two read as one row of controls rather than two unrelated
 * widgets, and the whole pill is the label, so the 44px target is the pill and
 * not just the 18px box.
 */
.controls__check {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  min-height: 44px;
  padding: 0 0.875rem;
  /* 16px minimum, same reason as the search input: iOS zooms below it. */
  font-size: 1rem;
  font-weight: 600;
  color: var(--navy);
  white-space: nowrap;
  border: 1px solid var(--navy);
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
  /* Double-tapping a label should tick it, not select its text. */
  user-select: none;
  -webkit-user-select: none;
}

/*
 * Filled when active, matching the pressed view-toggle button — the state has to
 * be legible at a glance in a sticky bar. `:has()` is progressive enhancement
 * here: without it the pill stays white and the checkbox itself still carries
 * the state, which is the part that matters.
 */
.controls__check:has(:checked) {
  background: var(--navy);
  color: #fff;
}

.controls__checkbox {
  width: 1.125rem;
  height: 1.125rem;
  margin: 0;
  /* Gold, so the tick still reads once the pill fills navy. */
  accent-color: var(--gold);
  cursor: pointer;
}

.controls__check:has(:focus-visible) {
  outline: 2px solid var(--navy);
  outline-offset: 1px;
}

/* Navy on navy is invisible once the pill fills, so the ring switches to gold. */
.controls__check:has(:checked):has(:focus-visible) {
  outline-color: var(--gold);
}

.controls__toggle {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem;
}

.controls__toggle button {
  /* 44px minimum tap target. */
  min-height: 44px;
  font: inherit;
  font-weight: 600;
  color: var(--navy);
  background: var(--surface);
  border: 1px solid var(--navy);
  border-radius: var(--radius);
  cursor: pointer;
}

.controls__toggle button[aria-pressed='true'] {
  background: var(--navy);
  color: #fff;
}

.status {
  display: flex;
  align-items: center;
  /* Only ever spaces the busy star from the text; inert without it. */
  gap: 0.5em;
  margin: 0 0 0.75rem;
  font-size: 0.875rem;
  color: var(--ink-muted);
}

.status[data-busy='true'] {
  color: var(--navy);
}

/*
 * Loading star.
 *
 * A pseudo-element, not a real node: app.js sets the status with `textContent`,
 * which would wipe any child on the next message. Keying it off [data-busy]
 * means the spinner appears and disappears with no JS involvement at all.
 *
 * Drawn with clip-path rather than a glyph or an image — U+2605 renders as a
 * colour emoji on some platforms, and an SVG would be another request on a page
 * whose whole point is being cheap to load. It is `content: ''`, so the live
 * region announces the message and nothing else.
 */
.status[data-busy='true']::before {
  content: '';
  flex: none;
  width: 1.1em;
  height: 1.1em;
  background: var(--gold);
  clip-path: polygon(
    50% 0%,
    61% 35%,
    98% 35%,
    68% 57%,
    79% 91%,
    50% 70%,
    21% 91%,
    32% 57%,
    2% 35%,
    39% 35%
  );
  animation: star-rev 2.2s ease-in-out infinite;
}

/*
 * Revving, not spinning. The rotation is not linear: each pair of keyframes is
 * a hard surge followed by an almost-stall, and `ease-in-out` smooths the
 * entry and exit of each one, so it winds up and falls off three times per
 * cycle like a saw being blipped.
 *
 * It ends on 720deg — two whole turns — so the loop point is seamless. Any
 * multiple of 360 works; the star's own five-fold symmetry means even 72deg
 * would, but whole turns keep the keyframe maths readable.
 */
@keyframes star-rev {
  0% {
    rotate: 0deg;
  }
  20% {
    rotate: 190deg;
  }
  32% {
    rotate: 215deg;
  }
  52% {
    rotate: 405deg;
  }
  64% {
    rotate: 430deg;
  }
  84% {
    rotate: 660deg;
  }
  100% {
    rotate: 720deg;
  }
}

/* ------------------------------------------------------------------ *
 * List view
 * ------------------------------------------------------------------ */

.outlets {
  margin: 0;
  padding: 0;
  list-style: none;
}

.outlet {
  display: grid;
  gap: 0.75rem;
  padding: 1rem 0;
  border-top: 1px solid var(--gold);
}

.outlet__name {
  margin: 0;
  font-size: 1.0625rem;
  font-weight: 700;
  color: var(--navy);
}

.outlet__address,
.outlet__postcode {
  margin: 0.125rem 0 0;
  font-size: 0.9375rem;
  color: var(--ink-muted);
}

.outlet__postcode {
  font-weight: 600;
  color: var(--ink);
}

/*
 * Accreditation. Shared by the list card and the map info window, so it is
 * scoped to neither. Every outlet carries exactly one of the two schemes.
 *
 * PPP takes the gold treatment because it is the narrower award — 1,051 of
 * 6,447 — while Cask Marque is the baseline the whole feed already holds, and
 * a page where every row is highlighted highlights nothing.
 */
.outlet__accreditation {
  display: inline-flex;
  align-items: center;
  gap: 0.4375rem;
  /* Never wider than the map info window, which is only 16rem. */
  max-width: 100%;
  margin: 0.5rem 0 0;
  padding: 0.1875rem 0.875rem 0.1875rem 0.375rem;
  border: 1px solid var(--rule);
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
  line-height: 1.4;
  color: var(--ink-muted);
}

/*
 * Both marks are 240x240 with transparent corners, so one square box suits both;
 * the Cask Marque oval letterboxes inside it, giving 39.3x23.2px of ink against
 * the PPP roundel's 26.8x28.8px — different shapes, comparable weight.
 *
 * 40px is a floor, not a preference. These are detailed lockups, not icons:
 * rendered at 26 or 32px both collapse into an illegible smudge, and 40px is
 * where "CASK MARQUE" and "PINT" resolve. Going further to 48px reads well but
 * makes the pill taller than the address block it sits under. Do not shrink
 * this to tidy up the row height — the mark stops meaning anything below ~40px.
 *
 * The height must be explicit: the global `img { height: auto }` would otherwise
 * win over the box and let the two render at different heights.
 */
.outlet__accreditation-mark {
  flex: none;
  width: 40px;
  height: 40px;
  object-fit: contain;
}

/* A <p>, so it arrives with a UA margin the flex row does not want. */
.outlet__accreditation-label {
  margin: 0;
  text-align: center;
}

.outlet__accreditation[data-accreditation='ppp'] {
  border-color: var(--gold);
  color: var(--navy);
}

/*
 * In a list row the accreditation is a section in its own right, sitting beside
 * a rating badge up to 22rem wide. The compact pill above — which is what the
 * map info window keeps, where there is only 16rem to play with — is dwarfed by
 * it, so here the pill chrome comes off and the mark carries the block.
 *
 * Scoped by `.outlet >` so the info window is untouched: the accreditation is a
 * direct child of .outlet in the list and of .infowindow on the map.
 */
.outlet > .outlet__accreditation {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  /* The row's own grid gap spaces this now; the pill's margin would double it. */
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
}

.outlet > .outlet__accreditation .outlet__accreditation-mark {
  width: 104px;
  height: 104px;
}

/* ------------------------------------------------------------------ *
 * Outbound links to a maps app — shared by the list row and the info window
 * ------------------------------------------------------------------ */

.map-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1rem;
  margin-top: 0.375rem;
}

.map-links__item {
  display: inline-block;
  /* Comfortable target without forcing the row or the bubble taller. */
  padding: 0.25rem 0;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--navy);
}

/* The rating is the most prominent element in a listing, per the brief. */
.outlet__badge {
  width: 100%;
  max-width: 20rem;
  height: auto;
  justify-self: start;
}

.outlets__sentinel {
  height: 1px;
}

/* ------------------------------------------------------------------ *
 * Map view
 * ------------------------------------------------------------------ */

.panel--map {
  position: relative;
}

.map__canvas {
  width: 100%;
  height: min(72vh, 34rem);
  border-radius: var(--radius);
  background: var(--ppp-cream);
}

/*
 * Overlaid rather than stacked above the map: on a phone a full-width button
 * here costs ~60px of map before anything has been shown.
 */
.map__locate {
  position: absolute;
  z-index: 2;
  top: 0.625rem;
  left: 0.625rem;
  min-height: 44px;
  padding: 0 1rem;
  font: inherit;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--navy);
  background: var(--surface);
  border: 1px solid var(--navy);
  border-radius: var(--radius);
  box-shadow: 0 1px 4px rgb(0 0 0 / 0.25);
  cursor: pointer;
}

/* Counted bubble standing in for several outlets in one grid square. */
.cluster {
  display: grid;
  place-items: center;
  width: var(--cluster-size, 36px);
  height: var(--cluster-size, 36px);
  border-radius: 50%;
  background: var(--navy);
  border: 2px solid var(--gold);
  color: #fff;
  font-family: var(--font-body);
  font-size: 0.8125rem;
  font-weight: 700;
  line-height: 1;
  box-shadow: 0 1px 4px rgb(0 0 0 / 0.3);
}

.map__locate:disabled,
.map__expand:disabled {
  opacity: 0.6;
  cursor: default;
}

/*
 * Sits inside the map panel rather than using the page status line, so it stays
 * visible when the map is expanded over the rest of the page.
 */
.map__message {
  position: absolute;
  z-index: 3;
  top: 4rem;
  left: 0.625rem;
  right: 0.625rem;
  margin: 0;
  padding: 0.625rem 0.75rem;
  font-size: 0.875rem;
  color: #fff;
  background: var(--navy);
  border: 1px solid var(--gold);
  border-radius: var(--radius);
  box-shadow: 0 2px 8px rgb(0 0 0 / 0.3);
}

.map__expand {
  position: absolute;
  z-index: 2;
  top: 0.625rem;
  right: 0.625rem;
  min-height: 44px;
  padding: 0 1rem;
  font: inherit;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--navy);
  background: var(--surface);
  border: 1px solid var(--navy);
  border-radius: var(--radius);
  box-shadow: 0 1px 4px rgb(0 0 0 / 0.25);
  cursor: pointer;
}

/*
 * Expanded map — a fixed overlay, deliberately not the Fullscreen API.
 * iPhone Safari does not implement it, so Google's own fullscreenControl is
 * absent there entirely. This behaves identically on Android, iPhone and desktop.
 */
body.is-map-expanded {
  overflow: hidden;
}

body.is-map-expanded .panel--map {
  position: fixed;
  inset: 0;
  z-index: 60;
}

body.is-map-expanded .map__canvas {
  height: 100vh; /* fallback where dvh is unsupported */
  height: 100dvh;
  border-radius: 0;
}

.infowindow {
  max-width: 16rem;
  font-family: var(--font-body);
}

.infowindow__name {
  margin: 0 0 0.25rem;
  font-size: 1rem;
  color: var(--navy);
}

.infowindow__address,
.infowindow__postcode {
  margin: 0;
  font-size: 0.875rem;
  color: var(--ink-muted);
}

.infowindow__badge {
  display: block;
  width: 100%;
  height: auto;
  margin: 0.5rem 0;
}

/* The info window's links use the shared .map-links rules above. */

/*
 * Google reserves a whole header row (.gm-style-iw-chr) above the content purely
 * to hold the close button, plus an empty title slot (.gm-style-iw-ch) that still
 * carries padding. On a phone that is ~40px of a bubble that only has ~500px of
 * map to sit in.
 *
 * Lift the button out of the flow and pin it to the corner so it sits level with
 * the top of .infowindow. These are Google's own class names and are not part of
 * any public API — recheck after a Maps JS release.
 */
.gm-style-iw-c {
  position: relative;
  padding: 0.75rem !important;
}

.gm-style-iw-chr {
  position: absolute;
  top: 0;
  right: 0;
  z-index: 1;
  display: block;
  height: auto;
  min-height: 0;
  margin: 0;
  padding: 0;
}

.gm-style-iw-ch {
  display: none;
  padding: 0;
}

.gm-style-iw-chr button.gm-ui-hover-effect {
  display: grid !important;
  place-items: center;
  width: 32px !important;
  height: 32px !important;
  margin: 0 !important;
  padding: 0 !important;
}

.gm-style-iw-chr button.gm-ui-hover-effect > span {
  width: 16px !important;
  height: 16px !important;
  margin: 0 !important;
}

.gm-style-iw-d {
  overflow: auto !important;
  padding: 0 !important;
}

/* Keep the pub name from running under the close button. */
.infowindow__name {
  padding-right: 1.75rem;
}

/* ------------------------------------------------------------------ *
 * Notices
 * ------------------------------------------------------------------ */

.notice {
  padding: 1rem;
  border: 1px solid var(--gold);
  border-radius: var(--radius);
  background: var(--ppp-cream);
}

.notice__title {
  margin: 0 0 0.25rem;
  font-size: 1rem;
  color: var(--navy);
}

.notice__body {
  margin: 0;
  font-size: 0.9375rem;
  color: var(--ink-muted);
}

/* ------------------------------------------------------------------ *
 * Footer
 * ------------------------------------------------------------------ */

.footer {
  max-width: var(--max-width);
  margin: 2rem auto 0;
  padding: 1rem var(--gutter) 2rem;
  border-top: 1px solid var(--rule);
  font-size: 0.8125rem;
  color: var(--ink-muted);
}

.footer p {
  margin: 0;
}

/* ------------------------------------------------------------------ *
 * Larger screens
 * ------------------------------------------------------------------ */

@media (min-width: 40em) {
  /*
   * Search on its own row; the two filters and the view toggle share the next,
   * with the accreditation select taking the slack because its labels are the
   * long ones.
   *
   * All four abreast does not fit until 64em. At 640px the toggle (~164px), the
   * rating pill (~186px) and the search box (~175px min-content) leave the
   * select about 47px — a chevron and two characters. Costing one 44px row here
   * and promoting to a single row at 64em keeps every control readable at every
   * width, and the row only exists on tablets: below 40em the controls are
   * stacked anyway.
   */
  .controls {
    grid-template-columns: 1fr auto auto;
    align-items: center;
  }

  .controls__search {
    grid-column: 1 / -1;
  }

  .controls__toggle {
    grid-template-columns: auto auto;
  }

  .controls__toggle button {
    padding: 0 1.5rem;
  }

  /*
   * Two columns here, three at 64em. At 640px three equal columns would leave
   * the badge about 197px wide, at which point the rating numerals stop being
   * readable — so the accreditation tucks under the address instead.
   */
  .outlet {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      'detail        badge'
      'accreditation badge';
    align-items: center;
    gap: 1rem 1.5rem;
  }

  .outlet__detail {
    grid-area: detail;
  }

  .outlet > .outlet__accreditation {
    grid-area: accreditation;
  }

  .outlet__badge {
    grid-area: badge;
    width: 22rem;
    max-width: none;
  }
}

@media (min-width: 64em) {
  /*
   * Now there is room for all four across, so the search rejoins the row and
   * takes the slack. ~300px of search at 1024px, with the select at its full
   * "Perfect Pint Promise Accredited" width.
   */
  .controls {
    grid-template-columns: 1fr auto auto auto;
  }

  .controls__search {
    grid-column: auto;
  }

  /*
   * The three sections of a row — address, accreditation, rating — on one line
   * with genuinely even gutters between them.
   *
   * Equal thirds, NOT content-sized tracks with `space-between`. Every .outlet
   * is its own grid, so content-sized tracks are measured per row: a row with a
   * short address gets a narrower first track than the row above it, and the
   * accreditation mark then sits at a different x on every row. Down a list of
   * thousands the middle column visibly wanders. Equal thirds are identical on
   * every row because every row is the same width, so the three sections line
   * up vertically as well as spacing evenly across.
   *
   * The gutters are the grid `gap`, so they are equal by construction. What
   * varies is the whitespace after the address text, and that is a property of
   * how long each address happens to be, not of the layout.
   */
  .outlet {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    grid-template-areas: 'detail accreditation badge';
    gap: 1.5rem;
  }

  /*
   * The accreditation is deliberately NOT centred in its third. It stretches to
   * fill the track and its contents start at the left edge, so the marks form a
   * clean vertical column down the list.
   *
   * Centring would shrink the box to its content, and the content has exactly
   * two possible widths — "Perfect Pint Promise Accredited" and "Cask Marque
   * Accredited". Every row would then place its mark at one of two different x
   * positions, alternating visibly all the way down. Left-aligned, every mark
   * and every label starts on the same line regardless of which scheme it is.
   *
   * That is the default behaviour for a stretched grid item holding a flex row,
   * so there is nothing to declare here — this note exists to stop someone
   * adding `justify-self: center` to "tidy" it.
   */

  /*
   * Fills its third rather than taking a fixed 22rem. A fixed width overflows
   * whenever a third is narrower than it — which is what happens at the low end
   * of this breakpoint, and would happen again if --max-width were reduced.
   * Capping instead of fixing keeps it correct at any container width.
   */
  .outlet__badge {
    width: 100%;
    max-width: 22rem;
    justify-self: end;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    /*
     * Needed alongside the duration cap now the page has an `infinite`
     * animation. Capping duration alone leaves the loading star repeating
     * thousands of times a second rather than stopping — it has to be pinned to
     * a single iteration to actually come to rest.
     */
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
