/* ═══════════════════════════════════════════════════════════════════════════
   SHEET GRAMMAR — the visual world for GeoPulse v2
   Derived from the 48- and 96-sheet poster: the medium the customer sells.

   A billboard is assembled from paper sheets pasted to a board. It is
   engineered for one thing: a single statement legible at forty metres, with
   everything else subordinate. That is exactly the hierarchy an OOH seller
   needs — the audience read leads, the evidence supports.

   Ground is paper, not screen. The scene decided it: a seller at a desk in a
   daylit office, screen-sharing into a meeting, pasting the result into a media
   pack. Dark chrome photographs badly and prints worse.
   ═══════════════════════════════════════════════════════════════════════════ */

@import url('https://fonts.googleapis.com/css2?family=Anton&family=Archivo:wght@400;500;600;700&family=Archivo+Narrow:wght@400;600&display=swap');

:root {
  /* Paste-up paper, screen-print ink, two pulled colours. Flat: a screen-print
     has no gradient, so neither does this. */
  --paper:      #F2F0EA;
  --paper-deep: #E6E2D8;
  --ink:        #0B0B0C;
  --ink-soft:   #4A4842;
  /* 5.15:1 against paper. The first pass used #E4572E, which measured
     3.23:1 and put the basis line — the single line this product cannot
     afford to have unread — below the floor. */
  --signal:     #B33C18;
  --signal-ink: #E4572E;   /* the brighter cut, for fills and dots only */
  --deep:       #2E5E4E;
  --seam:       rgba(11, 11, 12, 0.14);

  --sheet-unit: 4.6875%;            /* one sheet across a 48-sheet board */
  --rule:       1px;
  --rule-heavy: 3px;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: 'Archivo', system-ui, sans-serif;
  font-size: 16px;
  -webkit-font-smoothing: antialiased;
}

/* ── The board ────────────────────────────────────────────────────────────
   Sheets are pasted in columns. The seams are the material: faint vertical
   rules at sheet pitch, always present, never decorative panels. */
.board {
  position: relative;
  min-height: 100vh;
  background-image: repeating-linear-gradient(
    to right,
    var(--seam) 0 var(--rule),
    transparent var(--rule) var(--sheet-unit)
  );
}

.band {
  border-bottom: var(--rule) solid var(--seam);
  padding: 2.25rem clamp(1.25rem, 4vw, 4rem);
}
.band--tight { padding-top: 1.1rem; padding-bottom: 1.1rem; }

/* ── The statement ────────────────────────────────────────────────────────
   The reason the page exists. One field of colour carrying one sentence at
   poster scale, and directly beneath it the basis that sentence rests on.
   The basis is not a footnote and never a tooltip: this screen ends up in
   front of an advertiser, and a claim without its denominator is the failure
   mode this product has to avoid. */
.statement {
  background: var(--signal);
  color: var(--paper);
  padding: clamp(1.75rem, 5vw, 3.5rem) clamp(1.25rem, 4vw, 4rem) clamp(1.25rem, 3vw, 2rem);
  border-bottom: var(--rule-heavy) solid var(--ink);
}
.statement__read {
  font-family: 'Anton', 'Archivo', sans-serif;
  font-weight: 400;
  text-transform: uppercase;
  line-height: 0.88;
  letter-spacing: -0.02em;
  font-size: clamp(2.5rem, 9.5vw, 6rem);
  margin: 0;
  max-width: 22ch;
  text-wrap: balance;
}
.statement__basis {
  margin: 1.25rem 0 0;
  font-family: 'Archivo Narrow', 'Archivo', sans-serif;
  font-size: clamp(0.82rem, 1.4vw, 0.95rem);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--paper);
  border-top: var(--rule) solid rgba(242, 240, 234, 0.4);
  padding-top: 0.7rem;
  max-width: 70ch;
}
.statement--thin { background: var(--deep); }
.statement--empty { background: var(--paper-deep); color: var(--ink); }
.statement--empty .statement__basis { color: var(--ink-soft); border-top-color: var(--seam); }

/* ── Survey type ──────────────────────────────────────────────────────────
   Below the statement the register changes completely: this is the data book,
   set narrow and dense, the way an audience-measurement report is set. */
.survey-title {
  font-family: 'Archivo Narrow', sans-serif;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  font-size: 0.78rem;
  color: var(--ink-soft);
  margin: 0 0 1.1rem;
}
.survey-note {
  font-family: 'Archivo Narrow', sans-serif;
  font-size: 0.8rem;
  color: var(--ink-soft);
  margin: 0.85rem 0 0;
  max-width: 68ch;
}

/* Measured rows. Tabular figures because these are quantities being compared,
   which is measurement, not a monospace costume. */
.rows { display: grid; gap: 0.45rem; }
.row {
  display: grid;
  grid-template-columns: minmax(6.5rem, 13rem) 1fr auto;
  align-items: baseline;
  gap: 0.9rem;
}
.row__label {
  font-size: 0.9rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.row__bar { position: relative; height: 0.7rem; background: var(--paper-deep); }
.row__fill { position: absolute; inset: 0 auto 0 0; background: var(--ink); }
.row__fill--signal { background: var(--signal-ink); }
.row__fill--deep   { background: var(--deep); }
.row__value {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  font-size: 0.9rem;
  min-width: 4.5rem;
  text-align: right;
}

.cols {
  display: grid;
  gap: clamp(1.5rem, 4vw, 3rem);
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
}

/* ── Controls ─────────────────────────────────────────────────────────────
   Stamped, not styled: a control looks like something pressed onto the sheet. */
.stamp {
  font-family: 'Archivo Narrow', sans-serif;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-size: 0.78rem;
  padding: 0.5rem 0.9rem;
  background: transparent;
  color: var(--ink);
  border: var(--rule) solid var(--ink);
  cursor: pointer;
  transition: background 180ms cubic-bezier(0.16, 1, 0.3, 1),
              color 180ms cubic-bezier(0.16, 1, 0.3, 1);
}
.stamp:hover  { background: var(--ink); color: var(--paper); }
.stamp:focus-visible { outline: var(--rule-heavy) solid var(--signal); outline-offset: 2px; }
.stamp[aria-pressed="true"] { background: var(--ink); color: var(--paper); }
.stamp[disabled] { opacity: 0.4; cursor: not-allowed; }
.stamp__count {
  font-variant-numeric: tabular-nums;
  opacity: 0.62;
  margin-left: 0.45rem;
}
.stamp--signal[aria-pressed="true"] { background: var(--signal); border-color: var(--signal); color: var(--paper); }

.chips { display: flex; flex-wrap: wrap; gap: 0.4rem; }

.field {
  font-family: 'Archivo Narrow', sans-serif;
  font-size: 0.95rem;
  padding: 0.5rem 0.7rem;
  background: var(--paper);
  color: var(--ink);
  border: var(--rule) solid var(--ink);
  font-variant-numeric: tabular-nums;
}
.field:focus-visible { outline: var(--rule-heavy) solid var(--signal); outline-offset: 2px; }

/* ── Masthead ─────────────────────────────────────────────────────────────
   A poster site carries its own panel identification. So does this. */
.masthead {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 1.5rem;
  align-items: baseline;
  justify-content: space-between;
  border-bottom: var(--rule-heavy) solid var(--ink);
  padding: 0.9rem clamp(1.25rem, 4vw, 4rem);
}
.masthead__mark {
  font-family: 'Anton', sans-serif;
  text-transform: uppercase;
  letter-spacing: -0.01em;
  font-size: 1.35rem;
  margin: 0;
}
.masthead__nav { display: flex; gap: 1.1rem; flex-wrap: wrap; }
.masthead__nav a {
  font-family: 'Archivo Narrow', sans-serif;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-size: 0.76rem;
  color: var(--ink-soft);
  text-decoration: none;
  padding-bottom: 2px;
  border-bottom: 2px solid transparent;
}
.masthead__nav a:hover,
.masthead__nav a[aria-current="page"] { color: var(--ink); border-bottom-color: var(--signal); }

/* ── Map ──────────────────────────────────────────────────────────────────
   Flat and paper-toned, so it reads as a plotted survey map rather than a
   product's dark-mode basemap. */
.map { height: clamp(19rem, 52vh, 34rem); border-bottom: var(--rule) solid var(--seam); }
.leaflet-container { background: var(--paper-deep); font-family: 'Archivo Narrow', sans-serif; }
.leaflet-tile { filter: grayscale(1) contrast(0.86) brightness(1.06); }

/* ── The authored moment ──────────────────────────────────────────────────
   A sheet is pasted up: it sweeps down over the board and settles. One moment,
   on the element that changed, not an entrance on every section — and not the
   whole-board opacity dip this file previously described but did not build.
   The content stays visible throughout; the mask reveals, it does not hide. */
@keyframes paste-up {
  from { clip-path: inset(0 0 100% 0); }
  to   { clip-path: inset(0 0 0 0); }
}
.pasted { animation: paste-up 420ms cubic-bezier(0.16, 1, 0.3, 1); }
.is-loading { opacity: 0.55; transition: opacity 160ms cubic-bezier(0.16, 1, 0.3, 1); }
.empty {
  font-family: 'Archivo Narrow', sans-serif;
  color: var(--ink-soft);
  font-size: 0.9rem;
  padding: 0.6rem 0;
}

.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  * { transition-duration: 1ms !important; animation-duration: 1ms !important; }
}

/* ── Narrow ───────────────────────────────────────────────────────────────
   The row is a three-track grid built for a wide sheet. Below 600px the label
   track alone took most of the width and the bar collapsed to a stub, which
   destroys the only comparison the row exists to make. Stack instead. */
@media (max-width: 600px) {
  .row {
    grid-template-columns: 1fr auto;
    grid-template-areas: "label value" "bar bar";
    row-gap: 0.3rem;
  }
  .row__label { grid-area: label; white-space: normal; }
  .row__value { grid-area: value; }
  .row__bar   { grid-area: bar; }

  /* The page must lead with who. On a phone the filter stack pushed the
     statement below the fold. Order it above the controls but never above the
     masthead — both contracts state the first viewport as "masthead, then …",
     and the first pass hoisted the statement over the mark and the nav. */
  .board--lead-statement { display: flex; flex-direction: column; }
  .board--lead-statement > .masthead  { order: 0; }
  .board--lead-statement > .statement { order: 1; }
  .board--lead-statement > *          { order: 2; }
}

/* A row inside a multi-column block has no width to spare for three tracks:
   the label and value alone consumed the column and the bar vanished, which
   turned the composition — the one block that says who — into a numeric list. */
.cols .row {
  grid-template-columns: 1fr auto;
  grid-template-areas: "label value" "bar bar";
  row-gap: 0.25rem;
}
.cols .row__label { grid-area: label; }
.cols .row__value { grid-area: value; }
.cols .row__bar   { grid-area: bar; height: 0.5rem; }

/* ═══════════════════════════════════════════════════════════════════════════
   ONE SCREEN
   The board is a poster, and a poster is not scrolled. Everything sits inside
   the viewport: the statement leads at the top of a left column, the map holds
   the right, and the evidence sits under the statement in its own scroll well
   so a long list cannot push the claim off screen.

   Only the wells scroll, never the page. A screenshot of this board is the
   whole board, which is the point — this ends up in a media pack.
   ═══════════════════════════════════════════════════════════════════════════ */
.board--screen {
  height: 100dvh;
  min-height: 0;
  display: grid;
  grid-template-rows: auto 1fr;
  overflow: hidden;
}
/* Map on the left, the read on the right. The names say panel and map rather
   than left and right, so a future flip cannot make them lie. */
.screen-body {
  display: grid;
  grid-template-columns: 1fr minmax(22rem, 42%);
  min-height: 0;
  overflow: hidden;
}
.screen-map   { min-height: 0; display: grid; }
.screen-panel { display: grid; grid-template-rows: auto 1fr; min-height: 0; overflow: hidden; }
.screen-map .map { height: 100%; border-bottom: 0; border-right: var(--rule) solid var(--seam); }

/* The one place scrolling is allowed, and it is a well inside the sheet, not
   the sheet itself. */
.well { min-height: 0; overflow-y: auto; padding: 1.1rem clamp(1rem, 2.5vw, 2rem) 1.4rem; }
.well::-webkit-scrollbar { width: 6px; }
.well::-webkit-scrollbar-thumb { background: var(--seam); }

/* Poster scale has to yield to the screen budget: the statement can no longer
   take half the viewport, so it is capped in vh as well as vw. */
.board--screen .statement { padding: clamp(1rem, 2.2vh, 1.9rem) clamp(1rem, 2.5vw, 2rem) clamp(.8rem, 1.6vh, 1.2rem); }
.board--screen .statement__read { font-size: clamp(1.6rem, 3.4vw + 0.6vh, 3.4rem); }
.board--screen .statement__basis { margin-top: .7rem; padding-top: .55rem; font-size: .76rem; letter-spacing: .05em; }
.board--screen .band { padding: .8rem clamp(1rem, 2.5vw, 2rem); }
.board--screen .masthead { padding: .55rem clamp(1rem, 2.5vw, 2rem); }
.board--screen .survey-title { margin-bottom: .6rem; }

/* Below the split the two columns stack, and the page still does not scroll:
   the map takes a fixed slice and the well takes what is left. */
@media (max-width: 860px) {
  /* Stacked, the read still comes first: the statement, then the map, then the
     well. A phone has no left and right to flip. */
  .screen-body { grid-template-columns: 1fr; grid-template-rows: auto 25vh 1fr; }
  .screen-panel { display: contents; }
  .board--screen .screen-panel > .statement { order: 1; }
  .screen-map { order: 2; }
  .board--screen .screen-panel > .well { order: 3; min-height: 0; }
  .screen-map .map { border-right: 0; border-bottom: var(--rule) solid var(--seam); }
}

/* The panel must not scroll either: the whole board is one page in both axes.
   Chips and rows tighten rather than the content being cut. */
.board--screen .chips { gap: .3rem; }
.board--screen .stamp { padding: .35rem .6rem; font-size: .72rem; }
.board--screen .rows { gap: .3rem; }
.board--screen .row__bar { height: 0.55rem; }
.board--screen .row__label,
.board--screen .row__value { font-size: .82rem; }
.board--screen .survey-title { font-size: .72rem; margin-bottom: .45rem; }
.board--screen .well { padding: .8rem clamp(.9rem, 2vw, 1.5rem) 1rem; }

/* ── Tabs (phone only) ────────────────────────────────────────────────────
   A 390px screen cannot hold composition, elsewhere and the venue list at
   once, and the requirement is that nothing scrolls. Desktop has the height,
   so there the strip disappears and every panel shows — one markup, two
   behaviours, no duplicated content. */
.tabs { display: none; gap: .3rem; margin-bottom: .2rem; }
.tabs .stamp[aria-selected="true"] { background: var(--ink); color: var(--paper); }

@media (max-width: 860px) {
  .tabs { display: flex; }
  [data-panel] { display: none; }
  [data-panel].is-active { display: block; }
}

/* On a desktop browser, scrolling down is expected and fine — the user said so
   after a two-column well broke the layout sideways. The map holds its full
   height on the left, the panel scrolls on the right. Only the phone, where a
   thin map plus four blocks genuinely cannot coexist, uses tabs. */
@media (min-width: 861px) {
  .board--screen .well { overflow-y: auto; }
}

@media (max-width: 860px) {
  /* The phone budget is tight enough that the statement takes one line of
     poster scale rather than two, and the rows sit closer. Measured: the Who
     panel plus controls overran by 141px before this. */
  .board--screen .statement { padding: .7rem 1rem .6rem; }
  .board--screen .statement__read { font-size: clamp(1.35rem, 6.4vw, 1.9rem); }
  .board--screen .well { padding: .6rem 1rem .55rem; }
  .board--screen .rows { gap: .22rem; }
  .board--screen .row__bar { height: .45rem; }
  .board--screen .survey-title { margin-bottom: .3rem; }
  #pin-readout { font-size: .72rem; }
}

/* ── Overlay inks ─────────────────────────────────────────────────────────
   A multi-series overlay needs more than the two pulled colours the rest of
   the world runs on. Five is a real screen-print run, not a palette invented
   for a chart: the fills overprint at low alpha, so where two interests both
   concentrate the ink darkens the way lapped screens do. That overlap is the
   finding, not a rendering artefact. */
:root {
  --ink-1: #E4572E;   /* signal — the first series, the world's own accent */
  --ink-2: #2E5E4E;   /* deep */
  --ink-3: #C9861A;   /* ochre */
  --ink-4: #33526E;   /* slate */
}

.legend { display: flex; flex-wrap: wrap; gap: .5rem 1rem; margin: 0 0 .9rem; padding: 0; list-style: none; }
.legend li { display: flex; align-items: center; gap: .4rem; font-family: 'Archivo Narrow', sans-serif;
             font-size: .78rem; letter-spacing: .04em; text-transform: uppercase; color: var(--ink-soft); }
.legend .swatch { width: 1.1rem; height: .72rem; border: var(--rule) solid var(--ink); flex: none; }

/* Lift bars: the district list is read as a ranking, so the bar is the rank. */
.liftrow { display: grid; grid-template-columns: 1fr auto; gap: .15rem .8rem; padding: .5rem 0;
           border-bottom: var(--rule) solid var(--seam); }
.liftrow__name { font-weight: 600; font-size: .92rem; }
.liftrow__val  { font-family: 'Archivo Narrow', sans-serif; font-variant-numeric: tabular-nums;
                 font-weight: 600; font-size: .92rem; }
.liftrow__bar  { grid-column: 1 / -1; height: .34rem; background: var(--paper-deep); }
.liftrow__fill { height: 100%; display: block; }
.liftrow__note { grid-column: 1 / -1; font-family: 'Archivo Narrow', sans-serif; font-size: .72rem;
                 letter-spacing: .04em; text-transform: uppercase; color: var(--ink-soft); }
.liftrow--thin .liftrow__name { color: var(--ink-soft); }
.stamp[disabled] { opacity: .38; cursor: not-allowed; }
.stamp[disabled]:hover { background: transparent; color: var(--ink); }

/* Section headings, against the h3 block labels that share .survey-title.
   A heavier weight at caption size still reads as a caption, so these take the
   world's own title voice instead: Anton, the display face the statement uses,
   at a size that breaks out of the 0.78rem survey scale — and the heavy rule
   above, which is the paste-up device already in this grammar. The rule is
   what does most of the work: it says a new sheet starts here. */
.survey-title--section {
  /* Archivo, not Anton: Anton is a condensed face and cannot be widened
     without synthesising, which smears it. Archivo at 700 is the world's other
     voice, already loaded, and carries the same weight of statement at a normal
     width. Tracking goes slightly negative — uppercase at this size needs less
     letter-spacing than the 0.14em the small survey labels use, not more. */
  font-family: 'Archivo', system-ui, sans-serif;
  font-weight: 700;
  font-size: 1.62rem;
  line-height: 1.14;
  letter-spacing: -0.012em;
  text-transform: uppercase;
  text-wrap: balance;
  color: var(--ink);
  border-top: var(--rule-heavy) solid var(--ink);
  padding-top: 0.6rem;
  margin: 1.8rem 0 0.95rem;
}
/* The first one needs no rule above it — the statement already closed. */
.well > .survey-title--section:first-of-type { margin-top: 0.6rem; }
