/* ==========================================================================
   MonitorDirectory - one stylesheet, the whole design system.
   The cache-busting version follows the build, so nothing has to be bumped
   by hand here.
   ========================================================================== */

:root {
  /* brand */
  --bright-blue: #12c3f4;  --bright-blue-600: #0fa9d6;
  --bright-blue-100: #d6f4fd;  --bright-blue-050: #ecfaff;
  --dark-blue: #262262;    --dark-blue-400: #4b4789;
  --blood-orange: #f05125; --orange: #f79024;
  --purple: #63288a;       --pink: #ee759b;

  /* neutrals */
  --grey: #f3f3f3; --white: #fff;
  --ink: #262262; --ink-700: #3c3a6b; --ink-500: #6f6d92; --ink-300: #a6a4c0;
  --line: #e4e4ee; --line-200: #eeeef4;

  /* semantic - --accent and its tints are overridden per client on .directory, computed by
     ThemePalette from the one colour the client has stored. These are the fallbacks. */
  --accent: var(--bright-blue);
  --accent-wash: rgba(18,195,244,.10);
  --accent-tint: rgba(18,195,244,.18);
  --accent-soft: rgba(18,195,244,.30);
  --accent-ink: rgb(19,111,136);
  --success: #1fa971; --success-bg: #e4f7ef;
  --warning: var(--orange); --warning-bg: #fdeedd;
  --danger: var(--blood-orange); --danger-bg: #fde6df;

  /* type + shape */
  --font-sans: 'Poppins', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-mono: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;
  --r-sm: 10px; --r-md: 14px; --r-lg: 20px; --r-pill: 999px;
  --shadow-sm: 0 1px 2px rgba(38,34,98,.06), 0 1px 3px rgba(38,34,98,.08);
  --shadow-md: 0 4px 12px rgba(38,34,98,.08), 0 2px 4px rgba(38,34,98,.06);
  --shadow-lg: 0 12px 32px rgba(38,34,98,.12), 0 4px 8px rgba(38,34,98,.06);

  --bar-height: 64px;

  /* Style-template tokens. Every one of these has the stylesheet's own value as its default,
     so a template that sets none of them changes nothing - which is what makes a half-filled
     template safe to save. */
  --font-head: var(--font-sans);
  --font-size: 14px;
  --head-weight: 600;
  --card-border: 1px;
  --shadow-card: var(--shadow-sm);
  --pad-card: 18px;
  --pad-row: 12px;
  --gap-card: 12px;
  --btn-bg: var(--accent);
  --btn-fg: var(--on-accent, #fff);
  --btn-border: transparent;
  --chip-bg: var(--bright-blue-100);
  --chip-fg: var(--dark-blue);
}

* { box-sizing: border-box; }

html, body { height: 100%; }

body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--font-size);
  line-height: 1.5;
  color: var(--ink);
  background: var(--white);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 {
  margin: 0 0 .4em;
  font-family: var(--font-head);
  font-weight: var(--head-weight);
  line-height: 1.25;
}
h1 { font-size: 1.5rem; }
h2 { font-size: 1.15rem; }
h3 { font-size: 1rem; }

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

code { font-family: var(--font-mono); font-size: .9em; }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.muted { color: var(--ink-500); }

/* --- shell --------------------------------------------------------------- */

.directory-shell { height: 100dvh; display: flex; flex-direction: column; }

/* The four inherited properties are re-declared here, not left to body. A style template
   sets its variables on this element, and an inherited property inherits the value already
   computed on the ancestor - so font, text size, ink and page colour would otherwise be the
   app's own however the template was filled in. Everything else works through descendant
   rules and re-resolves on its own. */
.directory {
  display: flex; flex-direction: column; height: 100%; min-height: 0;
  font-family: var(--font-sans); font-size: var(--font-size);
  color: var(--ink); background: var(--grey);
}

.directory-bar {
  flex: none;
  display: flex; align-items: center; gap: 12px;
  padding: 10px 16px;
  min-height: var(--bar-height);
  background: var(--white);
  border-bottom: 1px solid var(--line);
  box-shadow: var(--shadow-sm);
  position: relative; z-index: 20;
}

/* The body is the scroll container, not the page: an embedded directory must never make
   the host page scroll. */
.directory-body { flex: 1; min-height: 0; position: relative; overflow: hidden; }

.directory-map { position: absolute; inset: 0; }
.directory-map.is-hidden { visibility: hidden; pointer-events: none; }

/* --- map ----------------------------------------------------------------- */

.map-canvas, .map-surface { position: absolute; inset: 0; }
.map-surface { background: var(--grey); }

.maplibregl-ctrl-group { border-radius: var(--r-sm) !important; box-shadow: var(--shadow-md) !important; }

.info-message {
  position: absolute; top: 12px; left: 12px; z-index: 5;
  max-width: min(360px, calc(100% - 24px));
  display: flex; align-items: flex-start; gap: 8px;
  padding: 10px 12px;
  background: var(--white); color: var(--ink-700);
  border: 1px solid var(--line); border-radius: var(--r-md);
  box-shadow: var(--shadow-md);
  font-size: 12.5px;
}
.info-message-icon { flex: none; margin-top: 1px; color: var(--accent); font-size: 15px; }

/* Collapsed, it is the same card shrunk to its icon, in the same corner, so the map keeps
   the space and the panel is obviously still there. */
.info-toggle {
  position: absolute; top: 12px; left: 12px; z-index: 5;
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px;
  background: var(--white); color: var(--accent);
  border: 1px solid var(--line); border-radius: 50%;
  box-shadow: var(--shadow-md);
  font-size: 16px; cursor: pointer;
  transition: border-color .15s;
}
.info-toggle:hover { border-color: var(--accent); }

/* --- filter bar ---------------------------------------------------------- */

.filter-bar {
  display: flex; align-items: center; gap: 8px;
  flex: 1; min-width: 0; flex-wrap: wrap;
}

.search-box {
  display: flex; align-items: center; gap: 6px;
  padding: 0 10px; height: 38px; min-width: 200px;
  background: var(--grey); border: 1px solid transparent; border-radius: var(--r-pill);
  transition: border-color .15s, background .15s;
}
.search-box:focus-within { background: var(--white); border-color: var(--accent); }
.search-box i { color: var(--ink-300); }
.search-box input {
  flex: 1; min-width: 0; border: 0; background: transparent; outline: none;
  font: inherit; color: inherit;
}
.search-box input::-webkit-search-cancel-button { display: none; }


/* --- multiselect --------------------------------------------------------- */

.multiselect { position: relative; }

.multiselect-trigger {
  display: inline-flex; align-items: center; gap: 6px;
  height: 38px; padding: 0 12px;
  background: var(--white); color: var(--ink-700);
  border: 1px solid var(--line); border-radius: var(--r-pill);
  font: inherit; cursor: pointer;
  transition: border-color .15s, color .15s;
}
.multiselect-trigger:hover { border-color: var(--ink-300); }
.multiselect.is-open .multiselect-trigger { border-color: var(--accent); color: var(--ink); }

.multiselect-label { white-space: nowrap; }

.chip-count {
  min-width: 20px; padding: 0 6px; border-radius: var(--r-pill);
  background: var(--accent); color: var(--white);
  font-size: 11px; font-weight: 600; text-align: center;
}

.multiselect-panel {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 30;
  min-width: 260px; max-width: 420px;
  padding: 8px;
  background: var(--white);
  border: 1px solid var(--line); border-radius: var(--r-md);
  box-shadow: var(--shadow-lg);
}

.multiselect-search {
  width: 100%; padding: 7px 10px; margin-bottom: 6px;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  font: inherit; outline: none;
}
.multiselect-search:focus { border-color: var(--accent); }

.multiselect-options { max-height: 260px; overflow-y: auto; }

.multiselect-option {
  /* Positioned so the hidden input is absolute against its own row rather than escaping to
     the panel, where it would sit over another option and lengthen the scroll. */
  position: relative;
  display: flex; align-items: center; gap: 10px;
  padding: 8px; border-radius: var(--r-sm);
  cursor: pointer;
}
.multiselect-option:hover { background: var(--bright-blue-050); }

/* The real control is still a checkbox - it is hidden rather than replaced, so it keeps its
   keyboard behaviour, its focus order and what a screen reader announces. The box beside it
   is the picture of its state. */
.multiselect-option input[type="checkbox"] {
  position: absolute; width: 1px; height: 1px; opacity: 0; margin: 0;
  pointer-events: none;
}

.tick-box {
  flex: none; width: 16px; height: 16px;
  border: 2px solid var(--swatch, var(--ink-300));
  border-radius: 5px;
  background: transparent;
  position: relative;
  transition: background-color .12s, border-color .12s;
}
.multiselect-option:hover .tick-box { border-color: var(--swatch, var(--accent)); }

.multiselect-option input:checked + .tick-box { background: var(--swatch, var(--accent)); }
.multiselect-option input:checked + .tick-box::after {
  content: ""; position: absolute; left: 4px; top: 1px;
  width: 3px; height: 7px;
  border: solid var(--swatch-ink, #fff); border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

/* The hidden input cannot show focus itself, so the box does it. */
.multiselect-option input:focus-visible + .tick-box {
  outline: 2px solid var(--accent); outline-offset: 2px;
}

/* One line each, ellipsised, with the full name on the title. Three-line rows made the list
   twice as tall as the map could give it and made scanning it slower, not easier. */
.multiselect-option-label {
  flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  line-height: 1.4;
}

.multiselect-count {
  flex: none; color: var(--ink-500);
  font-size: 12px; font-variant-numeric: tabular-nums;
}
.multiselect-count.is-zero { color: var(--danger); }

.swatch {
  width: 10px; height: 10px; border-radius: 50%; flex: none;
  box-shadow: 0 0 0 1px rgba(20,24,51,.42);
}

/* Matches with no pin, beside the Map/List counts it explains. A quiet line, not a card:
   it is the footnote to those two numbers, not a thing to act on. */
.offmap-notice {
  display: inline-flex; align-items: center; gap: 6px; flex: none;
  color: var(--ink-500); font-size: 12.5px; white-space: nowrap;
}
.offmap-notice i { flex: none; }

/* One on each button, always. Muted on the view you are not in, accent on the one you are,
   so the pair reads as "this view is showing 6, the other is showing 11". */
.view-toggle-count {
  min-width: 18px; margin-left: 6px; padding: 0 5px;
  border-radius: var(--r-pill);
  background: var(--line); color: var(--ink-700);
  font-size: 11px; font-weight: 600; line-height: 18px; text-align: center;
  font-variant-numeric: tabular-nums;
}
.view-toggle-btn.is-active .view-toggle-count { background: var(--accent); color: var(--white); }

.multiselect-empty { margin: 8px; color: var(--ink-500); }

.multiselect-actions {
  display: flex; margin-top: 6px; border-top: 1px solid var(--line-200);
}

.multiselect-action {
  flex: 1; padding: 7px;
  background: none; border: 0;
  color: var(--accent); font: inherit; cursor: pointer;
}

.multiselect-action:hover { background: var(--bright-blue-050); }
.multiselect-action + .multiselect-action { border-left: 1px solid var(--line-200); }

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

.btn {
  /* inline-flex and centred explicitly, because .btn is also worn by anchors. A <button>
     centres its own label; an <a> with a fixed height does not, and leaves the text sitting
     on the normal baseline near the top of the pill. */
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  height: 38px; padding: 0 18px;
  /* --on-accent, not --white: a style template can set a dark surface colour, and --white is
     what every card background reads. Without a separate token for "text sitting on the
     accent", choosing a dark surface would also turn button labels dark on a dark button. */
  /* Three tokens rather than three rules: the template picks solid, outline or soft and
     nothing here has to know which. */
  background: var(--btn-bg); color: var(--btn-fg);
  border: 1px solid var(--btn-border); border-radius: var(--r-pill);
  font: inherit; font-weight: 500; text-decoration: none; cursor: pointer;
}
a.btn:hover { text-decoration: none; }
/* Brightness, not a second colour: it works for a solid fill, a transparent outline and a
   tint alike, and never needs a hover token of its own. */
.btn:hover { filter: brightness(.94); }
.btn:disabled:hover { filter: none; }

.btn-quiet {
  background: none; border: 0; padding: 4px 8px;
  color: var(--accent); font: inherit; cursor: pointer;
}
.btn-quiet:hover { text-decoration: underline; }
.btn-quiet--tiny { font-size: 12px; padding: 0; }

.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; flex: none;
  background: none; border: 0; border-radius: 50%;
  color: var(--ink-500); cursor: pointer;
}
.icon-btn:hover { background: var(--grey); color: var(--ink); }

.view-toggle {
  display: inline-flex; flex: none;
  background: var(--grey); border-radius: var(--r-pill); padding: 3px;
}
.view-toggle-btn {
  display: inline-flex; align-items: center; gap: 6px;
  height: 32px; padding: 0 14px;
  background: none; border: 0; border-radius: var(--r-pill);
  color: var(--ink-500); font: inherit; cursor: pointer;
  transition: background .15s, color .15s;
}
.view-toggle-btn.is-active { background: var(--white); color: var(--ink); box-shadow: var(--shadow-sm); }

/* --- list + cards -------------------------------------------------------- */

.list-view { position: absolute; inset: 0; overflow-y: auto; background: var(--grey); padding: 16px; }

.card-grid {
  display: grid; gap: 16px;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}

.business-card {
  display: flex; flex-direction: column; gap: var(--gap-card);
  padding: var(--pad-card);
  background: var(--white);
  border: var(--card-border) solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-card);
  transition: box-shadow .15s, transform .15s;
}
.business-card:hover { box-shadow: var(--shadow-md); }
.business-card.is-compact { border: 0; box-shadow: none; padding: 0; }

.business-body { display: flex; flex-direction: column; gap: var(--gap-card); min-width: 0; }

.business-name {
  padding: 0; border: 0; background: none;
  font: inherit; font-size: 1rem; font-weight: 600; color: var(--ink);
  text-align: left; cursor: pointer;
}
button.business-name:hover { color: var(--accent); }

.business-description {
  margin: 0; color: var(--ink-700);
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden;
}
.business-description.is-expanded { -webkit-line-clamp: unset; }

/* Hidden until card-clamp.js finds the description is genuinely cut off. Hidden by default
   rather than shown by default, so a short description never flashes a button it does not
   need; revert rather than a named value, so the button keeps whatever display its context
   gives it. */
.clamp-toggle { display: none; }
.business-description[data-clamped] + .clamp-toggle { display: revert; }

.chip-row { display: flex; flex-wrap: wrap; gap: 6px; }

.chip {
  padding: 3px 10px;
  background: var(--chip-bg); color: var(--chip-fg);
  border-radius: var(--r-pill);
  font-size: 11.5px; font-weight: 500;
}
.chip--tag { color: var(--white); }

/* --- contact actions ------------------------------------------------------ */
/* Whole rows rather than small links in a definition list. This is the part of the card that
   earns the business something, and on a phone a 13px mailto: is not a target. */

.business-actions { display: flex; flex-direction: column; gap: 6px; }

.action-row {
  display: flex; align-items: center; gap: 10px;
  padding: var(--pad-row) 10px;
  border: 1px solid var(--line-200);
  border-radius: var(--r-sm);
  background: var(--white);
  color: var(--ink);
  text-decoration: none;
  transition: border-color .15s, background .15s, transform .15s;
}
.action-row:hover { border-color: var(--accent-soft); background: var(--accent-wash); transform: translateX(2px); }
.action-row:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.action-icon {
  flex: none;
  width: 32px; height: 32px;
  display: grid; place-items: center;
  border-radius: 9px;
  background: var(--accent-tint);
  color: var(--accent-ink);
  font-size: 16px;
}

/* min-width:0 so a long address can ellipsis instead of stretching the row */
.action-text { min-width: 0; display: flex; flex-direction: column; line-height: 1.25; }
.action-label {
  font-size: 10.5px; font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; color: var(--ink-300);
}
.action-value {
  font-size: 13.5px; font-weight: 500; color: var(--accent-ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.action-go { margin-left: auto; flex: none; font-size: 15px; color: var(--ink-300); }
.action-row:hover .action-go { color: var(--accent-ink); }

/* --- address section ------------------------------------------------------ */

.business-section {
  padding: 10px 12px;
  border-radius: var(--r-sm);
  /* No accent bar down the side and no band across the top of the popup: solid blocks of the
     client's colour read as chrome rather than as energy. The colour stays where it is doing
     work - the icon tiles, the link text, the header wash. */
  background: var(--accent-wash);
}

.section-title {
  margin: 0 0 4px;
  display: flex; align-items: center; gap: 6px;
  font-size: 10.5px; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--accent-ink);
}

.section-body { margin: 0; font-size: 13.5px; color: var(--ink-700); }
.address-line { display: block; }

/* --- card footer: labels left, logo right --------------------------------- */

.business-foot {
  display: flex; align-items: flex-end; justify-content: space-between; gap: 12px;
  padding-top: 10px;
  border-top: 1px solid var(--line-200);
}

.business-labels { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }

.business-logo { flex: none; display: flex; align-items: flex-end; justify-content: flex-end; }
.business-logo img { max-height: 44px; max-width: 112px; width: auto; object-fit: contain; }

/* Smaller and softer than a real logo: it is filling a gap, not claiming the space. */
.business-logo img.is-placeholder { width: 38px; height: 38px; opacity: .75; }

.list-more { display: flex; align-items: center; justify-content: center; gap: 12px; padding: 24px 0; }

/* --- business popup ------------------------------------------------------- */

.popup-scrim {
  position: absolute; inset: 0; z-index: 45;
  background: rgba(38, 34, 98, .34);
  animation: popup-scrim-in .18s ease;
}

.popup-card {
  position: absolute; z-index: 46;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: min(420px, calc(100% - 32px));
  /* Capped rather than sized to content: the consent detail list can be long, and a card
     taller than the pane would be clipped by .directory-body's overflow. */
  max-height: min(78%, 620px);
  display: flex; flex-direction: column;
  background: var(--white);
  border-radius: 14px;
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  animation: popup-card-in .2s cubic-bezier(.2, .8, .3, 1);
}

/* Focus is moved here when the card opens, so the ring must not show for a pointer user. */
.popup-card:focus { outline: none; }
.popup-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

@keyframes popup-scrim-in { from { opacity: 0; } to { opacity: 1; } }

@keyframes popup-card-in {
  from { opacity: 0; transform: translate(-50%, calc(-50% + 10px)) scale(.97); }
  to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}

.popup-head {
  flex: none;
  display: flex; align-items: flex-start; gap: 12px;
  padding: 14px 16px 12px;
  background: var(--accent-wash);
  border-bottom: 1px solid var(--accent-soft);
}
/* min-width:0 or a long business name shoves the close button past the overflow edge */
.popup-head h2 { flex: 1; min-width: 0; margin: 0; font-size: 17px; color: var(--accent-ink); }

.popup-body { flex: 1; overflow-y: auto; padding: 16px; }

.detail-list {
  margin: 16px 0 0; padding-top: 16px;
  border-top: 1px solid var(--line-200);
  display: flex; flex-direction: column; gap: 10px;
}
.detail-list dt { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--ink-300); }
.detail-list dd { margin: 0; }

/* --- states -------------------------------------------------------------- */

.state-panel {
  flex: 1;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 8px; padding: 40px 24px; text-align: center;
  color: var(--ink-500);
}
.state-panel--inline { position: absolute; inset: 0; background: var(--grey); }
.state-panel i { font-size: 32px; color: var(--ink-300); }
.state-panel h1, .state-panel h2 { color: var(--ink); margin: 0; }
.state-panel p { margin: 0; max-width: 42ch; }

.spinner {
  width: 22px; height: 22px; border-radius: 50%;
  border: 2px solid var(--line); border-top-color: var(--accent);
  animation: spin .7s linear infinite;
}
.spinner--inline { width: 13px; height: 13px; display: inline-block; vertical-align: -2px; }

@keyframes spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
  .business-card, .view-toggle-btn { transition: none; }
  .popup-card, .popup-scrim { animation: none; }
  .spinner { animation-duration: 2s; }
}

/* --- reports -------------------------------------------------------------
   Chart colours are their own tokens, not the brand accent: the accent is overridden per
   customer, and a series that changes hue because a council likes green is a series whose
   identity means nothing. Validated as a categorical set - distinguishable under deuteran,
   protan and tritan vision - so nothing here rests on colour alone in any case: every mark
   carries a direct label and the raw table below says the same thing in words.
   -------------------------------------------------------------------------- */

:root {
  --chart-1: #0fa9d6;
  --chart-2: #f05125;
  --chart-3: #1fa971;
  --chart-grid: #eeeef4;
  --chart-axis: #d8d8e4;
}

.stat-row {
  display: grid; gap: 14px; margin-bottom: 4px;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
}
.stat {
  display: flex; flex-direction: column; gap: 2px;
  padding: 14px 16px;
  background: var(--grey); border-radius: var(--r-md);
}
.stat-value { font-size: 26px; font-weight: 600; color: var(--ink); line-height: 1.1;
              font-variant-numeric: tabular-nums; }
.stat-label { font-size: 12.5px; color: var(--ink-700); }
.stat-note { font-size: 11.5px; color: var(--ink-500); }

.report-grid {
  display: grid; gap: 18px;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
}
.report-grid .admin-card { margin-bottom: 0; }

.range-presets, .chart-metrics { display: inline-flex; gap: 4px; flex-wrap: wrap; }
.range-presets .btn-quiet, .chart-metrics .btn-quiet {
  padding: 6px 10px; border-radius: var(--r-pill);
}
.range-presets .btn-quiet.is-active, .chart-metrics .btn-quiet.is-active {
  background: var(--accent-wash); color: var(--ink);
}
.chart-metrics { margin-bottom: 10px; }

.chart { margin: 0; }
.chart-svg { display: block; width: 100%; overflow: visible; }
.chart-grid { stroke: var(--chart-grid); stroke-width: 1; }
.chart-axis { stroke: var(--chart-axis); stroke-width: 1; }
.chart-mark { fill: var(--chart-1); }
.chart-mark:hover { fill: var(--ink); }
.chart-value { fill: var(--ink-700); font-size: 11px; font-weight: 600;
               font-variant-numeric: tabular-nums; }

.chart-axis-labels {
  display: flex; justify-content: space-between;
  margin-top: 4px; color: var(--ink-500); font-size: 11.5px;
}

/* A ranked list that happens to be drawn. The label is the identity, the bar is the
   magnitude, and the number is always there - so the chart still works in print, in forced
   colours, and for anyone who cannot separate the hues. */
.rank-bars { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.rank-bars li {
  display: grid; grid-template-columns: minmax(90px, 180px) 1fr auto;
  align-items: center; gap: 12px;
}
.rank-label { font-size: 13px; color: var(--ink-700); overflow: hidden;
              text-overflow: ellipsis; white-space: nowrap; }
.rank-track { height: 10px; background: var(--grey); border-radius: var(--r-pill); overflow: hidden; }
.rank-fill { display: block; height: 100%; background: var(--chart-1); border-radius: var(--r-pill); }
.rank-value { font-size: 12.5px; font-weight: 600; color: var(--ink);
              font-variant-numeric: tabular-nums; }

/* Two parts of one whole. The 2px gap is the surface showing through, which is what keeps
   the boundary readable when the two colours sit against each other. */
.split-bar {
  display: flex; gap: 2px; height: 16px; margin: 8px 0 10px;
  border-radius: var(--r-pill); overflow: hidden; background: var(--grey);
}
.split-seg { display: block; height: 100%; }
.split-seg--1 { background: var(--chart-1); }
.split-seg--2 { background: var(--chart-2); }

.split-legend {
  list-style: none; margin: 0; padding: 0;
  display: flex; gap: 18px; flex-wrap: wrap;
  font-size: 12.5px; color: var(--ink-700);
}
.split-legend li { display: inline-flex; align-items: center; gap: 6px; }
.split-legend strong { font-variant-numeric: tabular-nums; }
.swatch--1 { background: var(--chart-1); }
.swatch--2 { background: var(--chart-2); }

/* --- narrow -------------------------------------------------------------- */

@media (max-width: 640px) {
  .directory-bar { gap: 8px; padding: 8px 12px; }
  .search-box { min-width: 0; flex: 1 1 100%; order: -1; }
  /* The counts on the buttons carry the same information in less room. */
  .offmap-notice { display: none; }
  /* A bottom sheet on a phone: a centred card leaves dead space above and below, and the
     thumb is at the bottom of the screen. */
  .popup-card {
    top: auto; bottom: 0; left: 0;
    transform: none;
    width: 100%;
    max-height: 82%;
    border-radius: 14px 14px 0 0;
    animation: popup-sheet-in .22s cubic-bezier(.2, .8, .3, 1);
  }

  @keyframes popup-sheet-in {
    from { opacity: 0; transform: translateY(14px); }
    to { opacity: 1; transform: none; }
  }
  .card-grid { grid-template-columns: 1fr; }
}

/* --- circuit failure ------------------------------------------------------ */
/* Blazor unhides this when the circuit drops. UseExceptionHandler does not cover circuit
   exceptions, so without it a dropped connection is a silently frozen page. */

#blazor-error-ui {
  display: none;
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 1000;
  padding: 12px 16px;
  background: var(--warning-bg); color: var(--ink);
  border-top: 1px solid var(--warning);
  box-shadow: var(--shadow-lg);
  text-align: center;
}
#blazor-error-ui .reload { margin-left: 10px; font-weight: 500; }
#blazor-error-ui .dismiss {
  position: absolute; right: 12px; top: 8px;
  cursor: pointer; font-size: 20px; line-height: 1; color: var(--ink-500);
}

/* ==========================================================================
   Admin. Same tokens, different furniture: the public directory is chrome-free
   because it lives in someone else's iframe; the admin is a normal page.
   ========================================================================== */

.admin-shell { min-height: 100dvh; background: var(--grey); }

.admin-nav {
  display: flex; align-items: center; gap: 4px; flex-wrap: wrap;
  padding: 0 16px;
  background: var(--dark-blue); color: var(--white);
  box-shadow: var(--shadow-md);
}
.admin-brand { display: inline-flex; align-items: center; gap: 8px; padding: 14px 16px 14px 0; font-weight: 600; }
.admin-nav a {
  padding: 16px 12px; color: rgba(255,255,255,.72);
  border-bottom: 2px solid transparent; text-decoration: none;
}
.admin-nav a:hover { color: var(--white); text-decoration: none; }
.admin-nav a.active { color: var(--white); border-bottom-color: var(--bright-blue); }

.admin-nav-end { margin-left: auto; display: flex; align-items: center; gap: 12px; }
.admin-nav-end .muted { color: rgba(255,255,255,.6); }
.admin-nav-end .btn-quiet { color: var(--bright-blue); }

.admin-main { max-width: 1100px; margin: 0 auto; padding: 28px 16px 64px; }
.admin-main h1 { margin-bottom: 16px; }

.admin-card {
  padding: 20px; margin-bottom: 20px;
  background: var(--white);
  border: 1px solid var(--line); border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
}
.admin-card h2 { margin-bottom: 14px; }

.admin-toolbar { display: flex; align-items: flex-end; gap: 14px; flex-wrap: wrap; margin-bottom: 18px; }

.admin-table {
  width: 100%; border-collapse: collapse;
  background: var(--white);
  border: 1px solid var(--line); border-radius: var(--r-lg);
  overflow: hidden;
}
.admin-table th, .admin-table td { padding: 11px 14px; text-align: left; border-bottom: 1px solid var(--line-200); }
.admin-table th { font-size: 11.5px; text-transform: uppercase; letter-spacing: .04em; color: var(--ink-300); }
.admin-table tr:last-child td { border-bottom: 0; }
.admin-table tr.is-inactive { color: var(--ink-300); }
.admin-row-actions { display: flex; gap: 12px; white-space: nowrap; }

.mono { font-family: var(--font-mono); font-size: .92em; }

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

.field { display: flex; flex-direction: column; gap: 5px; margin-bottom: 14px; }
.field > span:first-child { font-size: 12px; font-weight: 500; color: var(--ink-700); }
.field--inline { margin-bottom: 0; }

.field input[type="text"], .field input[type="password"], .field input[type="email"],
.field input[type="number"], .field select, .field textarea {
  padding: 9px 11px;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  font: inherit; color: inherit; background: var(--white);
  outline: none;
}
.field input:focus, .field select:focus, .field textarea:focus { border-color: var(--accent); }
.field input[readonly], .field textarea[readonly] { background: var(--grey); font-family: var(--font-mono); font-size: 12px; }
.field input[type="color"] { padding: 2px; width: 56px; height: 36px; }

.btn--block { width: 100%; margin-top: 6px; }
.btn:disabled { opacity: .55; cursor: default; }
.btn-quiet--danger { color: var(--danger); }

.switch-grid {
  display: grid; gap: 10px;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}
.switch-grid label { display: flex; align-items: center; gap: 8px; }
.switch-grid input { accent-color: var(--accent); }

/* One filter per row in the admin: the switch that shows it and the name it carries on the
   map, side by side. The name follows the switch - greyed out and unreachable while the
   filter is off, so it cannot be filled in for a dropdown nobody will see. */
.filter-rows { display: grid; gap: 10px; margin-bottom: 14px; }

.filter-row {
  display: grid; grid-template-columns: minmax(190px, 240px) minmax(0, 1fr);
  align-items: center; gap: 12px;
}
.filter-row > label { display: flex; align-items: center; gap: 8px; }
.filter-row input[type="checkbox"] { accent-color: var(--accent); }

.filter-row input[type="text"] {
  width: 100%; padding: 8px 11px;
  background: var(--white); color: inherit;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  font: inherit; outline: none;
}
.filter-row input[type="text"]:focus { border-color: var(--accent); }
.filter-row input[type="text"]:disabled {
  background: var(--grey); color: var(--ink-300); cursor: not-allowed;
}

@media (max-width: 560px) { .filter-row { grid-template-columns: 1fr; gap: 6px; } }

.form-error {
  padding: 9px 12px; margin: 0 0 14px;
  background: var(--danger-bg); color: var(--ink);
  border-radius: var(--r-sm);
}
.validation-message { color: var(--danger); font-size: 12px; }

/* --- notices ------------------------------------------------------------- */

.notice { padding: 12px 14px; margin-bottom: 18px; border-radius: var(--r-md); }
.notice--warn { background: var(--warning-bg); border: 1px solid var(--warning); }
.notice--ok { background: var(--success-bg); border: 1px solid var(--success); }
.notice p:first-child { margin-top: 0; }
.notice p:last-child { margin-bottom: 0; }

.chip--ok { background: var(--success-bg); color: var(--success); }
.chip--warn { background: var(--warning-bg); color: #8a5200; }
.chip--danger { background: var(--danger-bg); color: var(--danger); }

/* --- auth ---------------------------------------------------------------- */

.auth-panel {
  max-width: 380px; margin: 12vh auto; padding: 28px;
  background: var(--white);
  border: 1px solid var(--line); border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg);
}
.auth-panel h1 { font-size: 1.25rem; }
.auth-panel > .muted { margin: 0 0 20px; }

.auth-note {
  display: flex; align-items: flex-start; gap: 8px;
  margin: 18px 0 0; padding-top: 16px;
  border-top: 1px solid var(--line-200);
  font-size: 12.5px; color: var(--ink-500);
}

/* --- diagnostics --------------------------------------------------------- */

.diag-list { margin: 0; display: flex; flex-direction: column; gap: 16px; }
.diag-list > div { display: grid; grid-template-columns: 200px 1fr; gap: 14px; align-items: start; }
.diag-list dt { font-weight: 500; color: var(--ink-700); }
.diag-list dd { margin: 0; }
.diag-list ul { margin: 6px 0 0; padding-left: 18px; }

@media (max-width: 640px) {
  .diag-list > div { grid-template-columns: 1fr; gap: 4px; }
  .admin-main { padding: 20px 12px 48px; }
}

/* --- login: the master-code panel ---------------------------------------- */

.auth-master { margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--line-200); }

.auth-master summary {
  cursor: pointer; list-style: none;
  text-align: center; font-size: 12.5px; color: var(--ink-500);
}
.auth-master summary::-webkit-details-marker { display: none; }
.auth-master summary:hover { color: var(--ink); }
.auth-master[open] summary { margin-bottom: 14px; }

/* Deliberately not the accent colour: this is the break-glass route, not the front door. */
.btn--master { background: var(--dark-blue); }

.auth-devhint {
  display: flex; align-items: center; gap: 6px;
  margin: -4px 0 12px; padding: 6px 10px;
  background: var(--bright-blue-050); color: var(--bright-blue-600);
  border: 1px solid var(--bright-blue-100); border-radius: 8px;
  font-size: 11.5px;
}

/* --- two-column form rows ------------------------------------------------ */

.form-grid {
  display: grid; gap: 0 16px;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}

@media (max-width: 560px) { .form-grid { grid-template-columns: 1fr; } }

/* Development-only failure detail on the directory page. */
.failure-detail {
  max-width: min(680px, 100%);
  margin: 4px 0 12px; padding: 12px 14px;
  background: var(--dark-blue); color: #ffd9cf;
  border-radius: var(--r-sm);
  font-family: var(--font-mono); font-size: 11.5px; line-height: 1.6;
  text-align: left; white-space: pre-wrap; word-break: break-word;
  overflow-x: auto;
}

/* Shown over the map pane when the style itself fails to load - otherwise a bad
   Map:StyleUrl just looks like a broken app. */
.map-overlay {
  position: absolute; inset: 0; z-index: 4;
  display: flex; align-items: center; justify-content: center;
  padding: 24px; text-align: center;
  background: var(--grey); color: var(--ink-500);
}

/* --- basemap picker ------------------------------------------------------ */

.provider-grid {
  display: grid; gap: 12px;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}

.provider-card {
  display: flex; flex-direction: column; align-items: flex-start; gap: 6px;
  padding: 14px 16px; text-align: left;
  background: var(--white); color: inherit;
  border: 1px solid var(--line); border-radius: var(--r-md);
  font: inherit; cursor: pointer;
  transition: border-color .15s, box-shadow .15s;
}
.provider-card:hover { border-color: var(--ink-300); }
.provider-card.is-selected {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent);
}

.provider-name { font-weight: 600; }
.provider-blurb { font-size: 12.5px; color: var(--ink-500); }

.map-preview {
  position: relative;
  height: 340px; margin-top: 16px;
  border: 1px solid var(--line); border-radius: var(--r-md);
  overflow: hidden;
}

/* The "fit to area" control, beside MapLibre's own zoom buttons. Its icon is inline SVG in a
   data URI so it needs no icon font inside the map's own control markup. */
.map-fit-btn {
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23262262' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 8V5a1 1 0 0 1 1-1h3M16 4h3a1 1 0 0 1 1 1v3M20 16v3a1 1 0 0 1-1 1h-3M8 20H5a1 1 0 0 1-1-1v-3'/%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
  background-size: 18px 18px;
}

/* --- admin: tabs and page furniture -------------------------------------- */

.page-head { display: flex; align-items: flex-start; gap: 16px; margin-bottom: 4px; }
.page-head h1 { margin: 4px 0 2px; }
.page-head p { margin: 0; }

.back-link {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 12.5px; color: var(--ink-500);
}
.back-link:hover { color: var(--ink); text-decoration: none; }

.tabstrip {
  display: flex; gap: 2px; flex-wrap: wrap;
  margin: 18px 0 22px;
  border-bottom: 1px solid var(--line);
}

.tabstrip-tab {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 10px 14px;
  color: var(--ink-500); text-decoration: none;
  border-bottom: 2px solid transparent;
  border-radius: var(--r-sm) var(--r-sm) 0 0;
}
.tabstrip-tab:hover { color: var(--ink); background: var(--white); text-decoration: none; }
.tabstrip-tab.is-active {
  color: var(--ink); font-weight: 500;
  background: var(--white);
  border-bottom-color: var(--accent);
}

/* The save bar follows you down a long settings tab, so "Save changes" is never scrolled
   off while you are still editing. */
.admin-toolbar--sticky {
  position: sticky; bottom: 0; z-index: 10;
  margin: 0 -16px -16px; padding: 14px 16px;
  background: color-mix(in srgb, var(--grey) 88%, transparent);
  backdrop-filter: blur(6px);
  border-top: 1px solid var(--line);
}

.admin-table tr.is-clickable { cursor: pointer; }
.admin-table tr.is-clickable:hover { background: var(--bright-blue-050); }

/* Revealed embed link: a full-width row under its own token row. The last field loses its
   bottom margin so the row does not gain a dead strip under the iframe box. */
.admin-table tr.token-reveal > td { background: var(--grey); padding: 14px; }
.admin-table tr.token-reveal .field:last-child { margin-bottom: 0; }
.admin-table tr.token-reveal .field textarea { max-width: 780px; }
.admin-table tr.token-reveal .field input[type="text"] { max-width: 780px; }

/* --- rotating map labels -------------------------------------------------- */
/* Sizes here are load-bearing and mirrored in directory-map.js: .map-label is LABEL_W x
   LABEL_H (120x36), .map-label-name is LABEL_ROW_H (28) because the slide moves the track by
   exactly one row, and the tail is TAIL_PX (6). --map-accent* are set on the map container by
   applyAccent(); font-size and line-height on .map-label-text are set per name by
   fontSizeFor(), and the values here are the fallback if that never runs. */

.map-label {
  /* MUST be absolute, and must stay that way.
     This element IS the MapLibre marker, and MapLibre positions markers by writing a
     translate() into their transform - which is measured from wherever the element already
     sits. maplibre-gl.css takes them out of the flow with position:absolute so that origin is
     the container's corner; app.css loads after it, so a `position: relative` here (added to
     give the tail a containing block) silently put every label back INTO the flow. Each one
     then started 36px - one label height - below the last, and MapLibre's translate was
     applied from there: label n was displaced n x 36px down the page. Zoomed right in there
     are only one or two labels and it looked perfect; zoomed out there are a dozen and they
     appeared to fly off with the zoom.
     `absolute` is a containing block for the tail just as `relative` was, and it is what
     MapLibre needs. */
  position: absolute;
  top: 0;
  left: 0;
  width: 120px;
  height: 36px;
  /* The root takes no pointer events so its transparent corners do not eat map drags; the
     bubble takes them. :hover still matches here, because an ancestor of the hovered
     element counts as hovered. */
  pointer-events: none;
  user-select: none;
  /* No fill mode: a finished animation holding opacity would outrank .is-hidden in the
     cascade and the label could never fade out again. */
  animation: map-label-in .35s ease;
  transition: opacity .4s ease;

  --tail-edge: var(--map-accent-soft, rgba(38, 34, 98, .38));
  --tail-fill: rgba(255, 255, 255, .96);
}

/* Not every marker can carry a label at once, so which ones do rotates on a slower beat than
   the names inside them. Faded rather than removed, so a label returning does not pop. */
.map-label.is-hidden { opacity: 0; }
.map-label.is-hidden .map-label-bubble { pointer-events: none; }

/* Held for the whole of a zoom, and released only once the labels have been laid out for the
   new scale. Cluster centroids are recomputed at every zoom level, so a label that has not
   caught up is anchored to a point that no longer exists - and the further you zoom, the
   further it sits from the bubble it belongs to. Better to show none than to show one in the
   wrong place. Declared after .is-hidden so it wins at equal specificity. */
.map-labels-held .map-label { opacity: 0; transition: none; animation: none; }
.map-labels-held .map-label-bubble { pointer-events: none; }

@keyframes map-label-in { from { opacity: 0; } to { opacity: 1; } }

/* A speech bubble rather than haloed text. The basemap is already full of its own labels in
   greys and blues, and plain dark text among them reads as more of the map rather than as the
   thing the directory is advertising. The border and text take the tenant's accent, so a name
   is visibly the same object as the marker it names. */
.map-label-bubble {
  height: 36px;
  overflow: hidden;
  padding: 3px 8px;
  border-radius: 9px;
  background: rgba(255, 255, 255, .96);
  border: 1px solid var(--map-accent-soft, rgba(38, 34, 98, .38));
  box-shadow: 0 1px 5px rgba(16, 16, 38, .2);
  pointer-events: auto;
  cursor: pointer;
}

.map-label:hover .map-label-bubble {
  background: #fff;
  border-color: var(--map-accent, #12c3f4);
  box-shadow: 0 2px 9px rgba(16, 16, 38, .28);
}

.map-label:hover { --tail-edge: var(--map-accent, #12c3f4); --tail-fill: #fff; }

.map-label-track {
  transition: transform .5s cubic-bezier(.4, 0, .2, 1);
  will-change: transform;
}

.map-label-name {
  height: 28px;
  display: flex; align-items: center; justify-content: center;
  transition: opacity .16s ease;
}

/* Much quicker than the slide: the outgoing name should be gone by the time the incoming one
   is properly in the window, not sharing it. */
.map-label-name.is-leaving { opacity: 0; }

.map-label-text {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  max-width: 100%;
  text-align: center;
  font-size: 12.5px; line-height: 14px; font-weight: 600;
  color: var(--map-accent-ink, #262262);
}

/* Off-screen twin used to pick the font size for a name. Must inherit the same font as the
   labels, which is why directory-map.js parents it to the map container. */
.map-label-probe {
  position: absolute; left: -9999px; top: 0;
  visibility: hidden; pointer-events: none;
  text-align: center; font-weight: 600;
  white-space: normal;
}

/* The tail. Two triangles per direction: the outer one is the border colour, the inner one is
   the fill shifted 1px over it, which leaves the 1px outline showing. It hangs off the root
   rather than the bubble so the bubble can keep overflow:hidden for the slide. */
.map-label-tail { position: absolute; width: 0; height: 0; }

.map-label-tail::before,
.map-label-tail::after {
  content: '';
  position: absolute;
  width: 0; height: 0;
  border: 6px solid transparent;
}

.map-label[data-tail="top"] .map-label-tail { top: 0; }
.map-label[data-tail="top"] .map-label-tail::before { bottom: 0; left: -6px; border-bottom-color: var(--tail-edge); }
.map-label[data-tail="top"] .map-label-tail::after { bottom: -1px; left: -6px; border-bottom-color: var(--tail-fill); }

.map-label[data-tail="bottom"] .map-label-tail { top: 100%; }
.map-label[data-tail="bottom"] .map-label-tail::before { top: 0; left: -6px; border-top-color: var(--tail-edge); }
.map-label[data-tail="bottom"] .map-label-tail::after { top: -1px; left: -6px; border-top-color: var(--tail-fill); }

.map-label[data-tail="left"] .map-label-tail { left: 0; }
.map-label[data-tail="left"] .map-label-tail::before { right: 0; top: -6px; border-right-color: var(--tail-edge); }
.map-label[data-tail="left"] .map-label-tail::after { right: -1px; top: -6px; border-right-color: var(--tail-fill); }

.map-label[data-tail="right"] .map-label-tail { left: 100%; }
.map-label[data-tail="right"] .map-label-tail::before { left: 0; top: -6px; border-left-color: var(--tail-edge); }
.map-label[data-tail="right"] .map-label-tail::after { left: -1px; top: -6px; border-left-color: var(--tail-fill); }

@media (prefers-reduced-motion: reduce) {
  .map-label-track { transition: none; }
  .map-label { animation: none; transition: none; }
  .map-label-name { transition: none; }
}

/* ==========================================================================
   Rich text editor. Used by the subscribe form's header, footer and thank-you
   fields, which hold HTML that is rendered on someone else's website.
   ========================================================================== */

.rte { margin-bottom: 18px; }
.rte-label { display: block; margin-bottom: 5px; font-size: 12px; font-weight: 500; color: var(--ink-700); }

.rte-toolbar {
  display: flex; align-items: center; gap: 2px; flex-wrap: wrap;
  padding: 5px 6px;
  background: var(--grey);
  border: 1px solid var(--line); border-bottom: 0;
  border-radius: var(--r-sm) var(--r-sm) 0 0;
}

.rte-btn {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 30px; height: 30px; padding: 0 7px;
  background: transparent; color: var(--ink-700);
  border: 1px solid transparent; border-radius: 7px;
  font: inherit; cursor: pointer;
}
.rte-btn:hover:not(:disabled) { background: var(--white); border-color: var(--line); }
.rte-btn:disabled { opacity: .4; cursor: default; }
.rte-btn.is-on { background: var(--accent-wash); border-color: var(--accent-soft); color: var(--accent-ink); }
.rte-btn--text { font-size: 11px; font-weight: 600; letter-spacing: .04em; }

.rte-sep { width: 1px; height: 20px; margin: 0 4px; background: var(--line); }
.rte-spacer { flex: 1 1 auto; }

.rte-linkbar {
  display: flex; gap: 8px; align-items: center;
  padding: 8px;
  background: var(--bright-blue-050);
  border: 1px solid var(--line); border-bottom: 0;
}
.rte-linkbar input {
  flex: 1 1 auto; padding: 7px 9px;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  font: inherit; color: inherit;
}

.rte-surface {
  padding: 11px 13px;
  background: var(--white);
  border: 1px solid var(--line); border-radius: 0 0 var(--r-sm) var(--r-sm);
  outline: none;
  overflow-wrap: anywhere;
}
.rte-surface:focus { border-color: var(--accent); }
.rte-surface.is-hidden { display: none; }
.rte-surface > :first-child { margin-top: 0; }
.rte-surface > :last-child { margin-bottom: 0; }
.rte-surface p { margin: 0 0 .6em; }

.rte-source {
  display: block; width: 100%; padding: 11px 13px;
  background: var(--dark-blue); color: #e8e7f5;
  border: 1px solid var(--line); border-radius: 0 0 var(--r-sm) var(--r-sm);
  font-family: var(--font-mono); font-size: 12px; line-height: 1.6;
  outline: none; resize: vertical;
}

.rte-hint { display: block; margin-top: 5px; }

/* ==========================================================================
   Subscribe form. One component renders both the public page and the admin
   preview, so these styles serve both.
   ========================================================================== */

/* Same again for the form. The page colour goes on the outer element so it reaches the
   margin around the form rather than stopping at its edge. */
.subscribe-page { max-width: 720px; margin: 0 auto; padding: 32px 16px 64px; }

.subscribe {
  font-family: var(--font-sans); font-size: var(--font-size);
  color: var(--ink);
}

.subscribe-banner { margin-bottom: 20px; }
.subscribe-banner img { display: block; width: 100%; height: auto; border-radius: var(--r-md); }

.subscribe-header, .subscribe-footer { color: var(--ink-700); }
.subscribe-header { margin-bottom: 20px; }
.subscribe-footer { margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--line-200); font-size: 13px; }

.subscribe-form {
  padding: 24px;
  background: var(--white);
  border: 1px solid var(--line); border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
}
.subscribe-title { margin-bottom: 18px; }

.subscribe-grid {
  display: grid; gap: 0 16px;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}

.subscribe-business { position: relative; }

/* A result list, not a dropdown: it pushes the form down rather than covering it, which on a
   phone is the difference between seeing your options and losing the field you were typing in. */
.subscribe-matches {
  margin: 6px 0 0; padding: 0; list-style: none;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  overflow: hidden;
}
.subscribe-matches li + li { border-top: 1px solid var(--line-200); }
.subscribe-matches button {
  display: block; width: 100%; padding: 9px 12px;
  background: var(--white); color: inherit;
  border: 0; font: inherit; text-align: left; cursor: pointer;
}
.subscribe-matches button:hover { background: var(--accent-wash); }

.subscribe-chosen { display: flex; align-items: center; gap: 10px; margin: 8px 0 0; }

.subscribe-tags { margin: 18px 0; padding: 14px 16px; border: 1px solid var(--line); border-radius: var(--r-sm); }
.subscribe-tags legend { padding: 0 6px; font-size: 12px; font-weight: 500; color: var(--ink-700); }
.subscribe-tags label { display: flex; align-items: center; gap: 8px; padding: 3px 0; }
.subscribe-tags input { accent-color: var(--accent); }

.subscribe-primary { margin-top: 18px; }
.subscribe-primary .admin-toolbar { margin-bottom: 0; }

.subscribe-recaptcha { margin: 18px 0; }

.subscribe-submit { margin-top: 8px; }

.subscribe-form ul.form-error { margin: 0 0 14px; padding: 9px 12px 9px 30px; }
.subscribe-form ul.form-error li + li { margin-top: 4px; }

/* The preview is the same component with nothing wired up. Marked, so it can never be mistaken
   for the live form sitting in an admin tab. */
.subscribe--preview { pointer-events: auto; }
.subscribe--preview .subscribe-form { box-shadow: none; }

/* --- the admin's editor: form on the left, live preview on the right ------ */

.subscribe-editor { display: grid; gap: 20px; grid-template-columns: minmax(0, 1fr) minmax(0, 440px); align-items: start; }
.subscribe-editor-pane { min-width: 0; }

.subscribe-editor-preview { position: sticky; top: 16px; min-width: 0; }
.subscribe-preview-frame {
  padding: 16px;
  background: var(--grey);
  border: 1px solid var(--line); border-radius: var(--r-lg);
  max-height: calc(100dvh - 120px); overflow: auto;
}

@media (max-width: 1000px) {
  .subscribe-editor { grid-template-columns: 1fr; }
  .subscribe-editor-preview { position: static; }
}

/* --- banner picker -------------------------------------------------------- */

.banner-current { display: flex; align-items: flex-start; gap: 14px; margin-bottom: 14px; }
.banner-current img { max-width: 260px; height: auto; border-radius: var(--r-sm); border: 1px solid var(--line); }

.banner-grid {
  display: grid; gap: 10px; margin-bottom: 16px;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
}

.banner-choice {
  display: flex; flex-direction: column; gap: 6px; padding: 6px;
  background: var(--white); color: inherit;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  font: inherit; text-align: left; cursor: pointer;
}
.banner-choice:hover { border-color: var(--accent); }
.banner-choice.is-on { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-tint); }
.banner-choice img { width: 100%; height: 70px; object-fit: cover; border-radius: 7px; }

.banner-choice-none {
  display: flex; align-items: center; justify-content: center;
  height: 70px; border-radius: 7px;
  background: var(--grey); color: var(--ink-300);
}
.banner-choice-name {
  font-size: 11px; color: var(--ink-500);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* The build label in the admin nav. Quiet: it is there to be found when someone is checking
   what a deploy left behind, not to be read on every page. */
.admin-version {
  font-size: 11px;
  color: rgba(255,255,255,.45);
  white-space: nowrap;
  cursor: default;
}
.admin-version:hover { color: rgba(255,255,255,.75); }

@media (max-width: 720px) { .admin-version { display: none; } }

/* The subscribe-form editor reuses the tab strip with buttons rather than links: its tabs are
   local state, because a real navigation would discard unsaved edits. Only the chrome a
   button brings with it needs undoing — the bottom border is load-bearing. */
button.tabstrip-tab {
  background: none;
  border: 0; border-bottom: 2px solid transparent;
  font: inherit; cursor: pointer;
}
button.tabstrip-tab.is-active { background: var(--white); border-bottom-color: var(--accent); }

/* A validation error on a tab that is not showing, marked on the tab itself. */
.tabstrip-tab .chip { padding: 0 6px; font-size: 10px; line-height: 16px; }

/* ==========================================================================
   Style templates. The admin only; what they produce is plain custom
   properties on a page root, so there is nothing here for the public pages.
   ========================================================================== */

.colour-field-row { display: flex; align-items: center; gap: 8px; }
.colour-field-row input[type="text"] { flex: 1 1 auto; min-width: 0; }
.colour-field .validation-message { margin-top: 4px; }

/* The preview renders real components, so it needs the page ground behind them to show what
   the Page colour is doing - a card on white tells you nothing about the page it sits on. */
.style-preview {
  margin-top: 16px; padding: 16px;
  background: var(--grey);
  border: 1px solid var(--line); border-radius: var(--r-lg);
  font-family: var(--font-sans);
  color: var(--ink);
}
.style-preview .business-card { margin-bottom: 12px; }
/* The preview is a page, not a swatch: headings, buttons and chips sit in it because those
   are the things the later options change, and a card alone would not show them. */
.style-preview h3 { margin-top: 0; }
.preview-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }

/* .swatch is sized for a flex row elsewhere; in a table cell it is an inline span, where
   width and height do nothing until it is told to be a box. */
.admin-table .swatch { display: inline-block; width: 14px; height: 14px; }

/* The customer list holds every CRMS client, so the ones that are Directory customers are
   ticked and sorted up; the rest stay legible but recede. */
.admin-table .tick-cell { width: 34px; padding-right: 0; color: var(--success); font-size: 15px; }
.admin-table tr.is-notcustomer td:not(.tick-cell) { color: var(--ink-500); }
.admin-table tr.is-notcustomer a { color: var(--ink-500); }
.admin-table tr.is-clickable { cursor: pointer; }
.admin-table tr.is-clickable:hover { background: var(--grey); }
.admin-table .chip--ok { margin-left: 8px; }

/* A checkbox that cannot be ticked yet should look it. */
.switch-grid label.is-disabled { color: var(--ink-300); }

/* ==========================================================================
   The landing page. The public front door at the root, when there is no embed
   token — an embed always carries one, so nothing that lands here is a
   customer's visitor who has lost their way.

   A dark island: the tokens are redefined on .landing, so the buttons, chips
   and cards below are the same components the product is built from, wearing
   a different set of values. Nothing here is a second design system.
   ========================================================================== */

/* The shell is exactly one viewport tall so an embedded directory can never make its host
   page scroll. The landing page is a document and has to grow past that. The overflow rule
   is the fallback where :has() is unsupported — the shell scrolls rather than clipping. */
.directory-shell { overflow: auto; }
.directory-shell:has(.landing) { height: auto; min-height: 100dvh; overflow: visible; }

.landing {
  --ink: #efedfb;
  --ink-700: #c9c4e9;
  --ink-500: #a49dd0;
  --ink-300: #7d76ab;
  --white: #1a1645;
  --grey: #131034;
  --line: rgba(182,176,221,.17);
  --line-200: rgba(182,176,221,.09);
  --accent: #29cdf7;
  --accent-wash: rgba(41,205,247,.12);
  --accent-tint: rgba(41,205,247,.2);
  --accent-soft: rgba(41,205,247,.34);
  --accent-ink: #56d8fa;
  --on-accent: #08152e;
  --display: 'Archivo', var(--font-sans);
  --edge: clamp(20px, 5vw, 56px);

  background: var(--grey);
  color: var(--ink);
  min-height: 100dvh;
}

.landing h1, .landing h2, .landing h3 {
  font-family: var(--display);
  font-variation-settings: 'wdth' 112;
  font-weight: 600;
  line-height: 1.08;
  letter-spacing: -.015em;
  text-wrap: balance;
  margin: 0;
}
.landing h1 { font-size: clamp(2.3rem, 5.6vw, 4rem); font-variation-settings: 'wdth' 118; }
.landing h2 { font-size: clamp(1.6rem, 3.2vw, 2.4rem); }
.landing h3 { font-size: 1.05rem; font-variation-settings: 'wdth' 104; letter-spacing: 0; }
.landing p { margin: 0; max-width: 62ch; }
.landing a { color: var(--accent-ink); }

/* .landing a beats .btn on specificity, so without this the label is painted in the link
   colour on a button already filled with that colour — a cyan word on a cyan pill. */
.landing a.btn { color: var(--on-accent); }
.landing a.btn-quiet { display: inline-flex; align-items: center; gap: 6px; color: var(--accent-ink); }

.landing-eyebrow {
  display: flex; align-items: center; gap: 10px;
  font-family: var(--font-mono);
  font-size: 11px; font-weight: 500; letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-300);
}
.landing-eyebrow::before { content: ""; width: 22px; height: 1px; background: var(--accent); flex: none; }

.landing-lede { font-size: clamp(1rem, 1.5vw, 1.18rem); color: var(--ink-700); }

/* --- embossed header and footer ------------------------------------------ */
/* Raised off the page rather than drawn on it: a light edge where it catches the light, a
   dark one where it does not, and the page's own shadow underneath. */

.landing-bar {
  position: sticky; top: 0; z-index: 30;
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; flex-wrap: wrap;
  padding: 14px var(--edge);
  background: var(--white);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.08),
    inset 0 -1px 0 rgba(0,0,0,.4),
    0 14px 30px rgba(3,1,18,.32);
}

.landing-mark {
  display: inline-flex; align-items: center; gap: 10px;
  font-family: var(--display); font-variation-settings: 'wdth' 110;
  font-weight: 600; font-size: 15px;
}
.landing-bar-end { display: inline-flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.landing-bar-end .muted { font-size: 12.5px; }

.landing-foot {
  display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap;
  padding: 28px var(--edge) 36px;
  background: var(--white);
  font-size: 13px;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.08),
    inset 0 -1px 0 rgba(0,0,0,.4),
    0 -14px 30px rgba(3,1,18,.28);
}

/* --- sections ------------------------------------------------------------ */

.landing-hero,
.landing-section {
  max-width: 1120px;
  margin-inline: auto;
  padding-inline: var(--edge);
  padding-block: clamp(52px, 8vw, 104px);
  display: flex; flex-direction: column; gap: 22px;
}
.landing-hero { padding-block: clamp(48px, 8vw, 92px) clamp(32px, 5vw, 52px); align-items: flex-start; }
.landing-hero h1 em { font-style: normal; color: var(--accent-ink); }

.landing-cta { display: flex; gap: 16px; flex-wrap: wrap; align-items: center; margin-top: 6px; }

/* --- the showcase: page wide, no frame ----------------------------------- */
/* The map is the product, so it gets the width, rather than sitting inside a browser-chrome
   mock-up pretending to be a screenshot. */

.showcase { padding-block: clamp(8px, 2vw, 24px) clamp(40px, 6vw, 72px); }

/* The labels sit over the SVG in percentages, which only line up while this box keeps the
   viewBox's exact ratio — preserveAspectRatio letterboxes the drawing the moment it does not,
   and every label drifts off its marker. So the height is capped by capping the WIDTH, and
   the ratio is never broken. */
.showcase-stage {
  position: relative;
  width: min(100%, calc(64dvh * 1000 / 720));
  aspect-ratio: 1000 / 720;
  margin-inline: auto;
}

.showcase-panel {
  position: absolute; inset: 0;
  opacity: 0; visibility: hidden;
  transition: opacity .8s ease;
}
.showcase-panel.is-active { opacity: 1; visibility: visible; }
.showcase-panel svg { display: block; width: 100%; height: 100%; }

/* Opacity rather than color-mix(), for the same reason the map labels derive their shades
   in JavaScript: an unsupported colour function does not degrade, it drops the declaration
   and fills the whole municipality solid black. */
.showcase-outline {
  fill: var(--panel-accent); fill-opacity: .09;
  stroke: var(--panel-accent); stroke-opacity: .55;
  stroke-width: 2;
  stroke-linejoin: round;
}
.showcase-pin { fill: var(--panel-accent); fill-opacity: .8; }

.showcase-cluster text {
  font-family: var(--font-mono); font-size: 12px; font-weight: 500;
  fill: var(--on-accent);
}
.showcase-cluster circle { fill: var(--panel-accent); }
.showcase-cluster .showcase-halo { fill: var(--panel-accent); fill-opacity: .16; }

/* The label is the product's own: a bubble with a tail, because a bubble that can sit to
   one side of its marker otherwise names nothing in particular. */
.showcase-label {
  position: absolute;
  width: 128px;
  transform: translate(-50%, -100%);
  pointer-events: none;
}
.showcase-label-bubble {
  height: 33px; overflow: hidden;
  background: var(--white);
  border: 1.5px solid var(--panel-accent);
  border-radius: 9px;
  box-shadow: 0 6px 16px rgba(3,1,18,.4);
}
.showcase-label-track { display: flex; flex-direction: column; transform: translateY(-30px); }
.showcase-label-track.is-sliding { transition: transform .42s cubic-bezier(.32,.72,.3,1); transform: translateY(0); }
.showcase-label-name {
  height: 30px;
  display: flex; align-items: center; justify-content: center;
  padding-inline: 8px;
  font-size: 11px; font-weight: 500; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.showcase-label-tail {
  position: absolute; left: 50%; bottom: -6px;
  width: 10px; height: 10px; margin-left: -5px;
  background: var(--white);
  border-right: 1.5px solid var(--panel-accent);
  border-bottom: 1.5px solid var(--panel-accent);
  transform: rotate(45deg);
}

.showcase-caption {
  max-width: 1120px; margin-inline: auto;
  padding-inline: var(--edge);
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  text-align: center;
}
.showcase-meta { display: none; }
.showcase-meta.is-active { display: block; }
.showcase-meta h2 { font-size: 1.3rem; }
.showcase-meta p { color: var(--ink-500); font-size: 14.5px; margin-inline: auto; }

.showcase-dots { display: flex; gap: 8px; }
.showcase-dot {
  width: 8px; height: 8px; padding: 0;
  border: 0; border-radius: 50%;
  background: var(--line);
  cursor: pointer;
}
.showcase-dot.is-active { background: var(--accent); }

@media (prefers-reduced-motion: reduce) {
  .showcase-panel { transition: none; }
  .showcase-label-track.is-sliding { transition: none; }
}

/* --- sectors, split, features -------------------------------------------- */

.landing-sectors {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 1px; margin-top: 20px;
  background: var(--line-200);
  border: 1px solid var(--line-200);
  border-radius: 16px; overflow: hidden;
}
.landing-sector { display: flex; flex-direction: column; gap: 12px; padding: 28px; background: var(--grey); }
.landing-sector p { font-size: 14.5px; color: var(--ink-500); }
.landing-sector-mark {
  width: 30px; height: 30px; border-radius: 8px;
  display: grid; place-items: center;
  background: var(--accent-wash); color: var(--accent-ink);
}
.landing-sector:nth-child(3) .landing-sector-mark { background: rgba(255,157,61,.14); color: #ff9d3d; }

.landing-split {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, .9fr);
  gap: clamp(28px, 5vw, 64px); align-items: center;
}
.landing-split > div:first-child { display: flex; flex-direction: column; gap: 18px; }

.landing-contrast {
  display: grid; gap: 14px; padding: 26px;
  background: var(--white); border: 1px solid var(--line); border-radius: 16px;
}
.landing-contrast-row { display: flex; gap: 14px; align-items: flex-start; }
.landing-contrast-row p { font-size: 14.5px; }
.landing-contrast-row.is-was p { color: var(--ink-300); }
.landing-contrast-key {
  width: 74px; flex: none; padding-top: 3px;
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-300);
}
.landing-contrast hr { border: 0; border-top: 1px solid var(--line-200); margin: 0; }

.landing-snippet {
  margin-top: 8px;
  background: var(--white); border: 1px solid var(--line);
  border-radius: 14px; overflow: hidden;
}
.landing-snippet-head {
  padding: 11px 16px; border-bottom: 1px solid var(--line-200);
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-300);
}
.landing-snippet pre {
  margin: 0; padding: 18px 16px; overflow-x: auto;
  font-family: var(--font-mono); font-size: 12.5px; line-height: 1.8; color: var(--ink-700);
}

.landing-features {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(232px, 1fr));
  gap: 28px 36px; margin-top: 16px;
}
.landing-feature { display: flex; flex-direction: column; gap: 6px; }
.landing-feature p { font-size: 14.5px; color: var(--ink-500); }

.landing-close {
  display: flex; flex-direction: column; gap: 20px; align-items: flex-start;
  padding: clamp(28px, 5vw, 52px);
  background: var(--white); border: 1px solid var(--line); border-radius: 20px;
}

@media (max-width: 860px) {
  .landing-split { grid-template-columns: 1fr; }
  .showcase-stage { width: 100%; }
}
@media (max-width: 460px) {
  .landing-contrast-row { flex-direction: column; gap: 4px; }
  .landing-contrast-key { width: auto; padding-top: 0; }
  .showcase-label { width: 104px; }
}

/* The customer's real directory, faded in over the drawing once it has loaded. The drawing
   stays underneath rather than being removed: it is what is on screen while the circuit
   connects, and it is what remains if the frame never loads at all. */
.showcase-live {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  border: 0; border-radius: 14px;
  opacity: 0;
  transition: opacity .7s ease;
}
.showcase-panel.is-live .showcase-live { opacity: 1; }
.showcase-panel.is-live > svg,
.showcase-panel.is-live .showcase-label { opacity: 0; transition: opacity .5s ease; }

@media (prefers-reduced-motion: reduce) {
  .showcase-live,
  .showcase-panel.is-live > svg,
  .showcase-panel.is-live .showcase-label { transition: none; }
}
