/*
 * Two palettes, kept deliberately separate.
 *
 *   --tsb-*   the Texans for Strong Borders brand board. Identity.
 *   --gold / --green / --orange / --red   the four participation levels.
 *             Meaning, not identity — these must survive a rebrand unchanged,
 *             because a reader who learns the key on one page has to be able
 *             to carry it to the next.
 *
 * Fonts are deliberately not loaded from a CDN: font-src 'self' forbids it,
 * and a third-party request from inside someone else's page is exactly what
 * this map is built to avoid. See README for self-hosting the brand faces.
 */

:root {
  /* --- Texans for Strong Borders brand board ------------------------------ */
  --tsb-black: #241d11; /* brown/black — the board says lean into this */
  --tsb-cream: #efddbb;
  --tsb-red: #c13727;
  --tsb-olive: #443816;
  --tsb-gold: #b8843b;

  --ink: var(--tsb-black);
  --page: var(--tsb-cream);
  --rule: color-mix(in srgb, var(--tsb-black) 22%, var(--tsb-cream) 78%);

  /* --- the four participation levels -------------------------------------- */
  /* Deliberately NOT brand colours: these carry meaning, not identity, and are
     the one part of the palette that must not shift with a rebrand. */
  --green: #2f7d4f;
  /*
   * Gold is a narrow optimum, not a taste call — do not round it off.
   *
   * It sits between orange and the cream page, and moving it in either
   * direction runs into one of them under colour-blind vision. #f2c94c, the
   * obvious tidier value, drops gold/orange under deuteranopia from 14.5 to
   * 12.0; going lighter for more punch closes on the page under tritanopia
   * instead. This value is the brightest yellow whose worst case is still the
   * palette's pre-existing floor — protan green/red, which has nothing to do
   * with gold at all.
   */
  --gold: #f2c954;
  --orange: #ff9900;
  --red: #c2482f;

  /* Absence of a level, not a level. Warmed to sit on the cream page. */
  --no-data: #c9bfa8;

  --outline: var(--tsb-black);
  --focus: var(--tsb-black);

  /*
   * Altivo and Acme Gothic are the brand faces. They are licensed, so they are
   * not bundled — and font-src 'self' rules out a CDN. Self-host the woff2
   * files (see README) or the stack falls back to the closest system geometric
   * and condensed gothic.
   */
  --font-body: "Altivo", "Futura", "Avenir Next", "Century Gothic",
    ui-rounded, system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-display: "Acme Gothic", "Altivo", "Oswald",
    "Franklin Gothic Condensed", "Arial Narrow", var(--font-body);

  /*
   * Texas is nearly square, so an uncapped map is as tall as it is wide: at a
   * 1600px window that is a 1600px-tall map, taller than the screen it is on.
   * Capping the width caps the height.
   *
   * Change this one value to resize the map. If you raise it, raise the
   * max-width on the embed wrapper to match — the embed reserves its height
   * from its own width, and the two have to agree.
   */
  --map-max-width: 760px;

  /*
   * The state-agency column beside the map. Counted here rather than written
   * into the grid, because the page's total width is this plus the map, and
   * the embed has to reserve height from that sum.
   */
  --agency-column: 168px;
  --board-gap: 12px;

  --radius: 6px;
  --shadow: 0 2px 10px color-mix(in srgb, var(--tsb-black) 32%, transparent);
}

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

html,
body {
  margin: 0;
  padding: 0;
  background: var(--page);
  color: var(--ink);
  font-family: var(--font-body);
  /* The iframe is sized by the parent; never scroll inside it. */
  overflow: hidden;
}

.map {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.5rem;
  max-width: var(--map-max-width);
  margin-inline: auto;
}

/*
 * Widened only when there is an agency column to make room for, so the map
 * itself never gives up width — the page gets wider instead.
 *
 * The class is set from JavaScript once the list has something in it, and that
 * is load-bearing rather than cosmetic. `hidden` is `display: none`, which
 * takes the section out of the grid ENTIRELY: with a fixed first column the
 * map would then be placed in it and render at the width of a sidebar. Asking
 * "is there a column?" before drawing one is the only version of this that
 * survives an empty agency table.
 */
.map--agencies {
  max-width: calc(var(--map-max-width) + var(--agency-column) + var(--board-gap));
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

[hidden] {
  display: none !important;
}

/* --- zoom controls --------------------------------------------------------- */

/*
 * On the map, not in the toolbar — see the note in index.html. Bottom-left
 * keeps them clear of the tooltip, which follows the pointer and favours the
 * upper area where most of Texas is.
 */
.zoom {
  position: absolute;
  left: 0.5rem;
  bottom: 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 2px;
  z-index: 2;
}

.zoom__button {
  width: 1.75rem;
  height: 1.75rem;
  display: grid;
  place-items: center;
  padding: 0;
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1;
  color: var(--ink);
  background: color-mix(in srgb, var(--page) 88%, transparent);
  border: 1px solid color-mix(in srgb, var(--ink) 30%, transparent);
  border-radius: 4px;
  cursor: pointer;
  /* The map is dense; a control sitting on it needs an edge of its own. */
  box-shadow: 0 1px 3px color-mix(in srgb, var(--tsb-black) 25%, transparent);
}

.zoom__button:hover {
  background: var(--page);
  border-color: color-mix(in srgb, var(--ink) 55%, transparent);
}

.zoom__button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
}

/* Disabled at the ends of the range, so the range is discoverable. */
.zoom__button[disabled] {
  opacity: 0.4;
  cursor: default;
}

.zoom__button--fit {
  font-size: 0.6875rem;
  letter-spacing: 0.01em;
}

/*
 * Shown for a moment when the pointer is over the map and the wheel is used
 * without a modifier. The page scrolls, as it should; this says why the map
 * did not.
 */
.zoom-hint {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  margin: 0;
  padding: 0.4rem 0.7rem;
  font-size: 0.8125rem;
  color: var(--page);
  background: color-mix(in srgb, var(--tsb-black) 88%, transparent);
  border-radius: var(--radius);
  pointer-events: none;
  z-index: 3;
}

/* Grabbing is the affordance for panning; the shapes keep their own cursors. */
.stage__svg.is-panning {
  cursor: grabbing;
}

@media (prefers-reduced-motion: no-preference) {
  .zoom-hint {
    transition: opacity 150ms ease-out;
  }
}

/* --- toolbar -------------------------------------------------------------- */

/*
 * The toolbar's height must not change between the state view and a drilled-in
 * county. An embed sized by CSS alone (no postMessage listener) reserves one
 * fixed box; if this row wrapped to two lines on a phone when someone drilled
 * in, the legend would be clipped. So: never wrap, and let the county name
 * truncate instead.
 */
.toolbar {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-height: 1.9rem;
  flex-wrap: nowrap;
  overflow: hidden;
}

.toolbar > .button {
  flex: none;
}

/*
 * Who gives up space first, once the row runs out.
 *
 * The switch and the back button are controls and keep their size. Of the two
 * pieces of text, the county name is the one worth keeping — so the status
 * shrinks four times as fast, down to its colour dot if it has to, before the
 * name starts losing characters. At 320px with a long county name there is
 * genuinely not room for both.
 */
/*
 * The floor is stated rather than inherited, because neither automatic answer
 * is the one we want.
 *
 * Left alone, the automatic minimum is min-content — and nowrap text
 * contributes its entire string, so the status refuses to shrink at all and
 * the county name is squeezed out instead. Add min-width:0 or overflow:hidden
 * and the minimum drops to zero, which collapses the box past its own colour
 * dot; the dot is flex:none, so rather than shrink it escapes and widens the
 * row.
 *
 * An explicit floor of exactly the dot's border-box gives the behaviour worth
 * having: the label ellipsizes away, and what survives longest is the colour,
 * which still reads against the legend below.
 */
.toolbar__status {
  flex: 0 4 auto;
  min-width: calc(0.7rem + 2px);
  overflow: hidden;
}

.toolbar__status::before {
  flex: none;
}

.toolbar__status-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.toolbar__title {
  margin: 0;
  min-width: 0;
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 700;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/*
 * Drilled in, the county is almost entirely covered by its own cities — in
 * Harris County, Houston alone hides most of it. Without this the county's
 * status is invisible at exactly the moment someone is looking closest.
 */
.toolbar__status {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  margin: 0;
  font-size: 0.8125rem;
  color: color-mix(in srgb, var(--ink) 75%, var(--page) 25%);
}

/*
 * The swatch takes its own custom property rather than currentColor.
 *
 * Sharing one `color` between the dot and the label meant every status colour
 * had to be legible as small text on cream, which none of them are — the
 * status classes were being darkened toward ink to compensate, and still only
 * reached 2.2:1. Splitting them lets the dot show the true map colour while
 * the label keeps the body contrast.
 */
.toolbar__status::before {
  content: "";
  width: 0.7rem;
  height: 0.7rem;
  border-radius: 3px;
  border: 1px solid color-mix(in srgb, var(--ink) 25%, transparent);
  background: var(--swatch, var(--no-data));
}

.toolbar__status.is-gold {
  --swatch: var(--gold);
}
.toolbar__status.is-green {
  --swatch: var(--green);
}
.toolbar__status.is-orange {
  --swatch: var(--orange);
}
.toolbar__status.is-red {
  --swatch: var(--red);
}
.toolbar__status.is-none {
  --swatch: var(--no-data);
}

/* --- counties / cities switch --------------------------------------------- */

.switch {
  display: flex;
  flex: none;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  overflow: hidden;
}

.switch__option {
  padding: 0.3rem 0.7rem;
  border: 0;
  background: color-mix(in srgb, var(--tsb-cream) 55%, white 45%);
  color: inherit;
  font: inherit;
  font-size: 0.8125rem;
  line-height: 1.25;
  white-space: nowrap;
  cursor: pointer;
}

.switch__option + .switch__option {
  border-left: 1px solid var(--rule);
}

.switch__option:hover[aria-pressed="false"] {
  background: white;
}

.switch__option[aria-pressed="true"] {
  background: var(--ink);
  color: var(--page);
  font-weight: 600;
  cursor: default;
}

.switch__option:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: -2px;
}

.button {
  padding: 0.3rem 0.7rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--tsb-cream) 55%, white 45%);
  color: inherit;
  font: inherit;
  font-size: 0.8125rem;
  cursor: pointer;
}

.button:hover {
  border-color: var(--ink);
  background: white;
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/*
 * On a narrow embed the toolbar cannot fit the back label, the county name and
 * the status. The county name is the one worth keeping, so the back button
 * collapses to its arrow first — its aria-label still reads in full.
 */
@media (max-width: 34rem) {
  .button__label {
    display: none;
  }
}

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

/*
 * The agency list sits beside the map on anything wide enough, and above it
 * on a phone. The map keeps `min-width: 0` so it is the part that gives way —
 * an SVG in a grid track will happily refuse to shrink otherwise.
 */
.board {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--board-gap);
  align-items: start;
}

.map--agencies .board {
  grid-template-columns: var(--agency-column) minmax(0, 1fr);
}

@media (max-width: 34rem) {
  .map--agencies .board {
    grid-template-columns: minmax(0, 1fr);
  }
}

.agencies {
  min-width: 0;
}

.agencies__title {
  margin: 0 0 0.4rem;
  font-family: var(--font-display);
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.agencies__list {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 0.75rem;
  line-height: 1.3;
}

/*
 * Same shape as a legend row, because it is answering the same question — what
 * does this colour mean here — and a reader should not have to learn a second
 * pattern halfway down the page.
 */
.agencies__item {
  display: flex;
  align-items: start;
  gap: 0.4rem;
}

.agencies__swatch {
  width: 0.8rem;
  height: 0.8rem;
  margin-top: 0.15rem;
  border-radius: 3px;
  border: 1px solid color-mix(in srgb, var(--ink) 25%, transparent);
  flex: none;
  background: currentColor;
  color: var(--no-data);
}

.agencies__swatch.is-gold {
  color: var(--gold);
}
.agencies__swatch.is-green {
  color: var(--green);
}
.agencies__swatch.is-orange {
  color: var(--orange);
}
.agencies__swatch.is-red {
  color: var(--red);
}

.agencies__name {
  min-width: 0;
}

/*
 * Shown instead of the list for a county with none of these. The column keeps
 * its place rather than collapsing, because the grid column disappearing on
 * every drill-down would resize the embedding iframe each way.
 */
.agencies__empty {
  margin: 0;
  font-size: 0.75rem;
  line-height: 1.3;
  color: color-mix(in srgb, var(--ink) 65%, transparent);
}

/*
 * Stacked on a phone the list becomes a wrapping row rather than a column, so
 * it costs a couple of lines above the map instead of a block of them. The
 * embed reserves its height from width alone, so every step in that height has
 * to be paid for in slack.
 *
 * After the base rules, not with the other breakpoint above: same specificity,
 * so source order is what decides, and up there the column below would win.
 */
@media (max-width: 34rem) {
  .agencies__list {
    flex-direction: row;
    flex-wrap: wrap;
    gap: 0.25rem 0.9rem;
  }
}

.stage {
  position: relative;
  width: 100%;
}

.stage__svg {
  display: block;
  width: 100%;
  height: auto;
  /* Clicking a shape must never start a text selection or flash a grey tap
     highlight on mobile. */
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
  /*
   * One finger still scrolls the page. This map lives inside somebody else's
   * article, and a full-width element that swallows a vertical swipe is a
   * reader stuck against it — so panning by touch is two fingers, and the
   * browser keeps the one-finger gesture.
   */
  touch-action: pan-y;
}

.county,
.city {
  stroke-linejoin: round;
  transition: fill 120ms ease-out;
}

.county {
  stroke: color-mix(in srgb, var(--page) 55%, var(--ink) 45%);
  stroke-width: 0.6;
  cursor: pointer;
}

/*
 * City strokes are the one place a scaling stroke does not work.
 *
 * stroke-width is in USER units, so it multiplies by the zoom: the same 1.4
 * that reads as a hairline across the whole state renders at 7px once the
 * view is a rectangle around one county, which on a small municipality is
 * thicker than the shape. non-scaling-stroke pins these to screen pixels
 * instead, so a hover outline is the same weight at every zoom and the widths
 * below can be read as what they are.
 *
 * Counties keep scaling strokes on purpose — they are large enough that the
 * growth reads as emphasis rather than damage, and the focused-county outline
 * in the drill-down depends on it.
 */
.city {
  stroke: color-mix(in srgb, var(--ink) 45%, transparent);
  stroke-width: 0.75px;
  vector-effect: non-scaling-stroke;
  cursor: default;
}

/* Status fills. Colour is never the only signal — the tooltip and the hidden
   list both carry the label as text. */
.is-gold {
  fill: var(--gold);
}
.is-green {
  fill: var(--green);
}
.is-orange {
  fill: var(--orange);
}
.is-red {
  fill: var(--red);
}
.is-none {
  fill: var(--no-data);
}

/*
 * Hover must NOT touch fill. The status classes set `fill`, not `color`, so
 * anything built on currentColor here resolves to the page text colour and
 * paints every hovered shape near-black — losing the one thing the map is for.
 * Brighten, and outline in ink so the affordance still reads on the lighter
 * fills.
 */
.county:hover,
.city:hover {
  filter: brightness(1.12) saturate(1.15);
  stroke: var(--ink);
  stroke-width: 1.4;
}

/* Screen pixels, per the note above — heavier than resting, never swamping. */
.city:hover {
  stroke-width: 2px;
}

.county:focus-visible,
.city:focus-visible {
  outline: none;
  stroke: var(--focus);
  stroke-width: 2.5;
  filter: brightness(1.12);
}

/* Focus stays clearly heavier than hover; a keyboard user has no pointer to
   confirm what is selected. */
.city:focus-visible {
  stroke-width: 3px;
}

/*
 * Once zoomed the counties are a backdrop and the cities are the subject —
 * with one exception. The county being read has to be findable at a glance,
 * because the view is a rectangle around it and its neighbours fill most of
 * the frame. Without this every county in shot looked identical and the reader
 * had to infer which one was the subject from where the cities happened to be.
 */
.is-zoomed .layer--counties .county {
  opacity: 0.18;
}

/*
 * Held down rather than lit up. Its cities are drawn on top and they are what
 * carries meaning here — a county at full saturation would put two coloured
 * layers in front of one legend, which is the thing the cities view below goes
 * out of its way to avoid. So the fill stays muted and the OUTLINE does the
 * work.
 */
.is-zoomed .layer--counties .county--focus {
  opacity: 0.55;
  stroke: var(--ink);
  stroke-width: 1.1;
}

/*
 * Hover and focus must still outrank it. They come earlier in the file, so
 * without this the county being read would swallow its own hover state and the
 * one shape the reader is pointing at would stop responding.
 */
.is-zoomed .layer--counties .county--focus:hover {
  opacity: 0.7;
  stroke-width: 1.6;
}

.is-zoomed .layer--counties .county--focus:focus-visible {
  opacity: 0.7;
  stroke: var(--focus);
  stroke-width: 2.5;
}

/*
 * Statewide cities view. The counties stay on as a locator — Texas cities are
 * small, and a scatter of shapes with nothing behind them is unreadable — but
 * they give up their status colours to do it.
 *
 * Two coloured layers at once would leave the reader guessing which one the
 * legend is describing, and the answer would be "both, differently". So in
 * this view exactly one thing carries meaning. The selector outranks the
 * .is-gold / .is-green / … fills by specificity, which is what lets a plain
 * class override them without !important.
 */
.is-cities .layer--counties .county {
  fill: var(--rule);
  opacity: 0.45;
  cursor: default;
  /* Not just decoration: hover and tooltips must not fire on the backdrop. */
  pointer-events: none;
}

/* The city outline is what separates neighbours at this scale. */
.is-cities .layer--cities .city {
  stroke-width: 0.5px;
}

/*
 * Hover, stated rather than left to specificity.
 *
 * The rule above is three classes and .city:hover is one class plus a
 * pseudo-class, so the resting width was quietly winning and hovering a city
 * in this view changed its brightness but never its outline. Whether that was
 * the intent or not, it should be a decision.
 *
 * It stays fine here on purpose. Statewide most cities are a few pixels
 * across, and the 2px used in the drill-down would swallow them whole — so
 * this is just enough to read as an outline change.
 */
.is-cities .layer--cities .city:hover {
  stroke-width: 1px;
}

.is-zoomed .state-outline {
  opacity: 0.35;
}

.state-outline {
  fill: none;
  stroke: var(--outline);
  stroke-width: 1.6;
  stroke-linejoin: round;
  pointer-events: none;
}

/* --- tooltip -------------------------------------------------------------- */

.tooltip {
  position: absolute;
  z-index: 2;
  max-width: min(18rem, 75%);
  padding: 0.4rem 0.6rem;
  border-radius: var(--radius);
  background: var(--ink);
  color: white;
  box-shadow: var(--shadow);
  pointer-events: none;
  font-size: 0.8125rem;
  line-height: 1.35;
  transform: translate(-50%, calc(-100% - 0.75rem));
}

.tooltip__name {
  margin: 0;
  font-weight: 700;
}

.tooltip__status {
  margin: 0.1rem 0 0;
  font-size: 0.75rem;
}

/* A small dot of the status colour, so the tooltip ties back to the shape. */
.tooltip__status::before {
  content: "";
  display: inline-block;
  width: 0.55rem;
  height: 0.55rem;
  margin-right: 0.35rem;
  border-radius: 50%;
  background: currentColor;
  vertical-align: baseline;
}

.tooltip__status.is-gold {
  color: color-mix(in srgb, var(--gold) 70%, white 30%);
}
.tooltip__status.is-green {
  color: color-mix(in srgb, var(--green) 60%, white 40%);
}
.tooltip__status.is-orange {
  color: var(--orange);
}
.tooltip__status.is-red {
  color: color-mix(in srgb, var(--red) 60%, white 40%);
}
.tooltip__status.is-none {
  color: color-mix(in srgb, var(--no-data) 80%, white 20%);
}

.tooltip__note {
  margin: 0.15rem 0 0;
  color: color-mix(in srgb, white 78%, var(--ink) 22%);
}

.tooltip__note:empty {
  display: none;
}

/* --- legend and status ---------------------------------------------------- */

.legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 1rem;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 0.8125rem;
}

.legend__item {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.legend__swatch {
  width: 0.85rem;
  height: 0.85rem;
  border-radius: 3px;
  border: 1px solid color-mix(in srgb, var(--ink) 25%, transparent);
  flex: none;
  /* The swatch reuses the status classes, which set fill for SVG; a span
     needs background instead. */
  background: currentColor;
}

.legend__swatch.is-gold {
  color: var(--gold);
}
.legend__swatch.is-green {
  color: var(--green);
}
.legend__swatch.is-orange {
  color: var(--orange);
}
.legend__swatch.is-red {
  color: var(--red);
}
.legend__swatch.is-none {
  color: var(--no-data);
}

.status {
  margin: 0;
  font-size: 0.8125rem;
  color: var(--red);
}

.status--visible {
  display: block;
}

@media (prefers-reduced-motion: reduce) {
  .county,
  .city {
    transition: none;
  }
}
