/* ==========================================================================
   Counter — dashboard styles

   DESIGN NOTE
   -----------
   This is an operator's instrument panel for a network of sites, not a
   marketing dashboard. The reference points are plotting charts and signals
   monitoring: a pale chart ground, ink linework, tabular numerals, hairline
   rules, and almost no chrome.

   The one rule that governs the palette: AMBER MEANS LIVE. Nothing that is
   not happening right now is ever amber. Historical data is ink and slate;
   a hit that arrived seconds ago is amber and cools back to ink as it ages.
   Colour encodes recency, so "what is happening now" is readable without
   reading a single timestamp. Do not spend amber on anything else — the
   moment it becomes a generic accent, the signal is gone.

   No build step, no CDN, no webfonts. System stacks only; the typographic
   personality comes from treatment (tabular monospace at display sizes,
   wide-tracked uppercase micro-labels) rather than from a purchased face.
   ========================================================================== */

/* --------------------------------------------------------------- tokens */

:root {
  /* Chart paper */
  --paper:        #eef2f5;
  --paper-raised: #f8fafb;
  --paper-sunk:   #e4eaef;

  /* Ink.

     BUG-0051: --ink-faint was #8ba0ad, which measures 2.41:1 on --paper,
     2.60:1 on --paper-raised and 2.24:1 on the --paper-sunk rail — well under
     the WCAG AA 4.5:1 floor for normal text. It is not a decorative token:
     it colours .label, table th, .delta, td.n.muted, .note, .empty and the
     chart ticks, so the smallest type in the product was also the least
     legible, and the rail's headings sat on the worst background of the three.
     #536b7a measures 4.97 / 5.35 / 4.61 on the same three papers. The quiet
     of the label now comes from its size, tracking and weight rather than
     from being too pale to read. Amber (--signal) is not an option here: it
     means "happening right now" and nothing else. */
  --ink:      #16303f;
  --ink-soft: #5a7183;
  --ink-faint:#536b7a;

  /* Structure */
  --rule:  #cbd8e1;
  --grid:  #dde7ed;

  /* The basemap reads as a MAP, the way StatCounter's does: warm land against
     blue water, with the coastline carried by the colour change rather than by
     a stroke. Sampled off their visitor map — land #e8e5d8, water #94c1e1 —
     then pulled back a little, because theirs is a photographic tile raster
     and this is a flat vector that sits inside a page. */
  --land:        #e6e2d6;
  --ocean:       #b9d5ea;
  --border-line: #b6b1a4;

  /* Pin. Red, like theirs — over a beige-and-blue basemap it is the one hue
     that nothing on the map is already using, which is exactly why every map
     product reaches for it. --signal still owns "just arrived"; this owns
     "a visitor is here". */
  --pin:      #e0453a;
  --pin-edge: #ffffff;

  /* The teardrop, as a mask. Tip at the bottom centre of the viewBox, so the
     anchoring in .dot lines the point up with the city. */
  --pin-shape: url('data:image/svg+xml;utf8,\
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 32">\
<path d="M12 32C12 32 24 18.5 24 11.7 24 5.2 18.6 0 12 0S0 5.2 0 11.7C0 18.5 12 32 12 32Z"/>\
</svg>');

  /* Data series — deliberately not a brand colour, just legible against ink */
  --series:      #2e6b7e;
  --series-soft: rgba(46, 107, 126, 0.14);

  /* LIVE. Reserved. See the note above. */
  --signal:      #cf6a1f;
  --signal-soft: rgba(207, 106, 31, 0.13);

  --up:   #2f6b4f;
  --down: #9c3b2e;

  --display: ui-monospace, "SF Mono", "Cascadia Mono", "JetBrains Mono",
             "Roboto Mono", Menlo, Consolas, "Liberation Mono", monospace;
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
          Arial, sans-serif;

  --rail-width: 232px;
  --gutter: 28px;
  --radius: 2px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --paper:        #0d1c24;
    --paper-raised: #13242e;
    --paper-sunk:   #0a171d;
    --ink:      #dae7ee;
    --ink-soft: #7d96a5;
    /* BUG-0051, dark half. #57707f measured 3.33:1 on --paper, 3.05:1 on the
       raised panels every table header sits inside and 3.49:1 on --paper-sunk;
       #758e9c measures 5.05 / 4.63 / 5.29. Still dimmer than --ink-soft above
       it, so the two-step ink hierarchy survives the lift. */
    --ink-faint:#758e9c;
    --rule:  #23404f;
    --grid:  #1a3140;
    /* Same relationship, dimmed: land still warmer than the water it sits in,
       both dark enough that the pins carry the page. */
    --land:        #3a3a34;
    --ocean:       #16303f;
    --border-line: #55524a;
    --series:      #63aac0;
    --series-soft: rgba(99, 170, 192, 0.16);
    --signal:      #ef8f3f;
    --signal-soft: rgba(239, 143, 63, 0.15);
    --up:   #5aa37c;
    --down: #d0705f;
  }
}

/* ----------------------------------------------------------------- base */

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

html { -webkit-text-size-adjust: 100%; }

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

a { color: inherit; text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 2px; }

/* Audit #21 (BUG-0042). --series, not --signal. Amber is reserved for things
   happening right now, and a focus ring is the single most frequently drawn
   accent in the interface — every Tab press painted "live" onto whatever the
   keyboard happened to be resting on. A colour that means everything means
   nothing, and it is the arrivals feed and the presence dot that pay. */
:focus-visible {
  outline: 2px solid var(--series);
  outline-offset: 2px;
}

/* Every numeral in this interface is tabular. Live figures update in place,
   and proportional digits make them jitter as the values change. */
.num, .metric-value, table td.n, .feed-time {
  font-family: var(--display);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* The recurring micro-label: wide-tracked, uppercase, quiet. */
.label {
  font-family: var(--sans);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* --------------------------------------------------------------- layout */

.shell {
  display: grid;
  grid-template-columns: var(--rail-width) minmax(0, 1fr);
  min-height: 100vh;
}

/* The network rail. This service exists to watch many sites at once, so the
   whole network stays on screen rather than hiding behind a dropdown. */
.rail {
  background: var(--paper-sunk);
  border-right: 1px solid var(--rule);
  padding: 20px 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.rail-brand {
  font-family: var(--display);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  padding: 0 20px 18px;
  color: var(--ink);
}

.rail-brand small {
  display: block;
  font-family: var(--sans);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.08em;
  color: var(--ink-faint);
  margin-top: 3px;
}

.rail-heading { padding: 14px 20px 6px; }

.site-link {
  display: flex;
  align-items: baseline;
  gap: 9px;
  padding: 7px 20px;
  border-left: 2px solid transparent;
  color: var(--ink-soft);
  font-size: 13px;
}

.site-link:hover { background: var(--paper); text-decoration: none; color: var(--ink); }

/* Audit #24 (BUG-0043). --series, not --signal. "Which site am I looking at"
   is navigation state, not liveness, and this marker sits inches from the
   presence dot that uses amber to mean exactly one thing. Two amber marks
   side by side, one meaning "selected" and one meaning "live", is how the
   reader stops trusting either. It also makes the .live-pill comment below
   true again — that really is now the only always-amber element. */
.site-link[aria-current="page"] {
  background: var(--paper);
  border-left-color: var(--series);
  color: var(--ink);
  font-weight: 600;
}

/* Presence dot: amber when the site has traffic in the last five minutes. */
.presence {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--ink-faint);
  flex: none;
  align-self: center;
}
.presence.is-live { background: var(--signal); }

.site-link .count {
  margin-left: auto;
  font-family: var(--display);
  font-variant-numeric: tabular-nums;
  font-size: 12px;
  color: var(--ink-faint);
}

.main { min-width: 0; display: flex; flex-direction: column; }

/* The [hidden] attribute only sets `display: none` through the user-agent
   stylesheet, which any class-based `display` rule outranks. Without this,
   every element the JavaScript hides with `node.hidden = true` stays on
   screen — which is exactly how the live pill ended up reading "0 now".
   Declared once, globally, so the trap cannot be re-sprung per component. */
[hidden] { display: none !important; }

/* --------------------------------------------------------------- header */

.topbar {
  display: flex;
  align-items: center;
  gap: 18px;
  padding: 18px var(--gutter) 0;
  /* Wrap rather than overflow: on a phone the title plus the live pill plus
     the range select do not fit on one line, and an un-wrapped flex row
     pushes the range control off the right edge where it cannot be reached. */
  flex-wrap: wrap;
}

.topbar h1 {
  margin: 0;
  font-size: 19px;
  font-weight: 650;
  letter-spacing: -0.01em;
}

.topbar .domain {
  font-family: var(--display);
  font-size: 12px;
  color: var(--ink-faint);
}

.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 10px; }

/* The live pill. The only always-amber element on the page — it is by
   definition about now. */
.live-pill {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 4px 10px;
  border: 1px solid var(--signal);
  border-radius: 999px;
  color: var(--signal);
  font-family: var(--display);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

.live-pill .dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--signal);
  animation: pulse 2.4s ease-in-out infinite;
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.25; }
}

select, .btn {
  font-family: var(--sans);
  font-size: 13px;
  color: var(--ink);
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 5px 9px;
  cursor: pointer;
}

.btn:hover { border-color: var(--ink-soft); text-decoration: none; }

/* ----------------------------------------------------------------- tabs */

.tabs {
  display: flex;
  gap: 0;
  padding: 0 var(--gutter);
  border-bottom: 1px solid var(--rule);
  margin-top: 14px;
  overflow-x: auto;
  scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }

.tab {
  padding: 9px 14px;
  font-size: 13px;
  color: var(--ink-soft);
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  white-space: nowrap;
}
.tab:hover { color: var(--ink); text-decoration: none; }
.tab[aria-current="page"] {
  color: var(--ink);
  font-weight: 600;
  border-bottom-color: var(--ink);
}

.content { padding: var(--gutter); flex: 1; }

/* ------------------------------------------------- hero: map + arrivals */

/* The thesis of the overview. Most analytics products bury geography in a
   tab; for a visitor counter, "someone in Osaka is reading this right now"
   is the whole feeling, so it opens the page. */
.hero {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 290px;
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  margin-bottom: 26px;
}

/* The Map tab, where the map is the content rather than the backdrop. Giving
   it the full width is also the only correct way to make it taller: the
   viewport's height is derived from its width by the 2:1 above. */
.hero.is-wide { grid-template-columns: minmax(0, 1fr); }

.hero > * { background: var(--paper-raised); }

.map-panel { padding: 16px 18px 10px; min-width: 0; }

/* Title left, state right — the same head grammar as .panel-head, so the map
   reads as a titled region rather than a picture with a caption. The state is
   live copy: which sites are showing, how many cities, what the zoom is
   scoped to. */
.map-head {
  display: flex;
  align-items: baseline;
  gap: 12px;
  margin-bottom: 6px;
}

.map-head h2 { margin: 0; font-size: 13px; font-weight: 650; }

/* Second child onward, NOT `.label` — the share page's head is two labels with
   no heading, and keying on the class put `margin-left:auto` on the title as
   well, which let the first item absorb the free space and shunted the whole
   head to the right edge. */
.map-head > * + * { margin-left: auto; text-align: right; }

/* overflow:hidden is the backstop for BUG-0013 — nothing positioned against
   the map may escape its box onto the page. */
.map-wrap { position: relative; overflow: hidden; }

/* The window onto the map.
   THE 2:1 IS LOAD-BEARING — DO NOT GIVE THIS A DIFFERENT ASPECT RATIO.
   The basemap's viewBox is 1000x500 and it is drawn with
   preserveAspectRatio="xMidYMid meet", so in any box that is not 2:1 the SVG
   letterboxes itself and draws smaller than its element. The dot layer is
   positioned in percentages of this box, so the moment the two disagree every
   dot slides off the coastline by up to half the letterbox. Want a bigger
   map? Give it more width (see .hero.is-wide) — the height follows. */
.map-viewport {
  position: relative;
  overflow: hidden;
  aspect-ratio: 2 / 1;
  cursor: grab;
  touch-action: pan-y;   /* one finger still scrolls the page; see map.js */
  /* The water. The basemap draws land only, so everything the coastlines do
     not cover is ocean — which is what makes this read as a map rather than as
     a diagram of some continents. */
  background: var(--ocean);
}

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

.map-viewport.is-dragging { cursor: grabbing; }

/* Everything that moves. One transform, written by map.js.
   Deliberately NOT will-change:transform. Promoting this to its own
   compositor layer makes the browser rasterise the basemap once at 1x and
   then scale that bitmap, so at 16x every coastline is a soft grey smear
   instead of a vector edge. Re-rasterising per zoom is the correct trade:
   the map is a few hundred paths, and a blurry map is not a map. */
.map-canvas {
  position: absolute;
  inset: 0;
  transform-origin: 0 0;
}

.worldmap {
  display: block;
  width: 100%;
  height: 100%;
  color: var(--land);
}

/* Land is a quiet ground, not the subject. The dots are the subject. */
.worldmap-land { fill: currentColor; }

/* The 50m countries replace the 110m silhouette rather than sitting on top of
   it — the same land drawn twice at two resolutions fringes every coastline. */
.map-viewport.has-detail .worldmap-land { display: none; }

/* vector-effect keeps every stroke a hairline on screen instead of thickening
   with the transform — without it a 16x view is all border and no country.
   IT MUST BE SET ON THE PATHS, NOT THE GROUP: fill and stroke inherit, but
   vector-effect does not, so a rule on the <g> silently does nothing. */
.worldmap-countries path,
.worldmap-states path,
.worldmap-graticule path { vector-effect: non-scaling-stroke; }

/* Borders are drawn IN a border colour now, not knocked out in the panel
   background. Knocking them out only works while the land sits on the panel;
   over water it drew white canals down every frontier. */
.worldmap-countries {
  fill: currentColor;
  stroke: var(--border-line);
  stroke-width: 0.6px;
  stroke-linejoin: round;
}

.worldmap-states {
  fill: none;
  stroke: var(--border-line);
  stroke-width: 0.5px;
  stroke-linejoin: round;
  opacity: 0;
  transition: opacity 140ms linear;
}

/* has-states means "fetched"; show-states means "zoomed in far enough to
   want them". Both are needed — see the note in map.js. */
.map-viewport.has-states.show-states .worldmap-states { opacity: 0.7; }

/* Latitude/longitude every 30°. Under the land, and quiet enough to read as
   graph paper rather than as data. */
.worldmap-graticule {
  fill: none;
  stroke: var(--grid);
  stroke-width: 1px;
  opacity: 0;
  transition: opacity 160ms linear;
}

.map-viewport.is-zoomed .worldmap-graticule { opacity: 0.7; }

/* The tile layer. Sits under everything else inside .map-canvas, so the one
   transform that pans and zooms the map carries the tiles along with the dots
   for free — no per-frame repositioning. */
/* Sibling of .map-canvas, NOT a child of it — see the long note in map.js.
   Positioned in screen pixels and repositioned per frame, because a tile
   inside a 30,000x transform has a sub-pixel layout box and rasterises to
   nothing. Inserted before the canvas so the pins paint on top. */
.map-tiles {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}

.map-tile {
  position: absolute;
  display: block;
  /* The browser resamples while a gesture is mid-flight and before the next
     zoom level's tiles arrive; smooth beats the blocky nearest-neighbour some
     engines pick for upscaled images. */
  image-rendering: auto;
}

/* Once real tiles are down, the vendored silhouette underneath them is just a
   beige shape showing through every gap while they load. */
.map-viewport.has-tiles .worldmap { display: none; }

.map-dots,
.map-labels {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* A map PIN, the shape StatCounter uses and the shape anyone reading a map
   goes looking for. The teardrop is a mask rather than a background image so
   the fill stays a CSS colour — a pin still has to go amber the moment a hit
   lands, and a data-URI <svg> cannot inherit currentColor.

   transform-origin is the TIP, and the pin is lifted by its own full height,
   so the point of the teardrop sits exactly on the city and stays there
   through the counter-scale. Anchored at the centre, as the old round dot was,
   a pin would drift half its height out to sea as it scaled. */
.dot {
  position: absolute;
  background: var(--pin);
  /* --dot-k is 1/zoom, set by map.js on the canvas. Without it every pin
     would inflate with the map and a zoomed view would be one solid blob. */
  transform: translate(-50%, -100%) scale(var(--dot-k, 1));
  transform-origin: 50% 100%;
  -webkit-mask: var(--pin-shape) no-repeat center / contain;
  mask: var(--pin-shape) no-repeat center / contain;
  /* Solid. Over photographic tiles a translucent marker picks up whatever
     coastline is behind it and stops reading as one object. */
  opacity: 1;
}

/* Several cities merged because they were within a few pixels of each other.
   Ringed rather than recoloured — a cluster is still the same kind of thing,
   and colour here is spoken for. */
/* A mask clips box-shadow, so the ring the old round dot used is not available
   here. A cluster is a deeper red instead — still one kind of thing, just more
   of it, which is what the ring was saying. */
.dot.is-cluster { background: #a82c23; }

/* BUG-0052. The dot layer above is pointer-events: none, which also suppresses
   the browser's native tooltip on a dot's title attribute. In the dashboard
   that costs nothing — map.js hit-tests on the viewport's own mousemove and
   draws .map-tip itself, and letting a dot swallow the pointer would only risk
   a native tooltip fighting the custom one. The public share report is the
   opposite case: templates/share/view.html is a flat, JavaScript-free render
   whose dots carry title="City — visitors" and nothing else, so with the layer
   inert every dot there was an anonymous red pin with no way to find out which
   city it marked. Re-enabled for that view only, keyed on .map-wrap, which is
   the share view's container — the dashboard's .map-viewport/.map-canvas never
   matches. */
.map-wrap .map-dots .dot { pointer-events: auto; }

/* The rest of the network, while one site's footprint is in focus.
   Weight, not hue: the same series colour dropped to a ground. Recolouring it
   would invent a second series, and the one colour with a meaning free to take
   is amber — which is spoken for and would read as "these cities are live".
   Uniform and small by design (see GHOST_PX in map.js): this layer says where
   the network is, and the layer on top says how much. */
.dot.is-muted {
  opacity: 0.32;
  box-shadow: none;
}

@media (prefers-color-scheme: dark) {
  /* A pale dot on a dark ground loses more to a low alpha than a dark dot on
     paper does. Same apparent weight, different number. */
  .dot.is-muted { opacity: 0.42; }
}

/* Screen space, not canvas space — see the note in site.html. map.js writes
   left/top in pixels whenever the transform moves. */
.map-label {
  position: absolute;
  transform: translateY(-50%);
  font: 600 11px/1 var(--sans);
  color: var(--ink);
  white-space: nowrap;
  /* A halo rather than a plate: the label has to stay readable over land,
     over water and over another country's fill without boxing the map in. */
  text-shadow:
     0  1px 2px var(--paper-raised),  0 -1px 2px var(--paper-raised),
     1px  0   2px var(--paper-raised), -1px  0   2px var(--paper-raised),
     1px  1px 0   var(--paper-raised), -1px  1px 0   var(--paper-raised),
     1px -1px 0   var(--paper-raised), -1px -1px 0   var(--paper-raised);
}

/* -------------------------------------------------------- map: chrome */

/* ZOOM BUTTONS ARE BACK, deliberately, and the note that used to sit here
   arguing against them is gone. It said a control cluster "just covers the
   part of the world it is sitting on" — true, and irrelevant next to the fact
   that a map with no visible controls reads as a picture. Every map anyone
   uses has +/- in a corner; StatCounter's has exactly this stack, bottom
   right, and people go looking for it. The wheel and drag still work, and the
   keyboard still gets arrows, +/- and 0. */

.map-controls {
  position: absolute;
  right: 10px;
  bottom: 10px;
  z-index: 3;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: 0 1px 4px rgb(0 0 0 / 0.18);
}

.map-btn {
  width: 27px;
  height: 27px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--paper-raised);
  color: var(--ink);
  border: 0;
  border-radius: 0;
  font: 600 15px/1 var(--sans);
  cursor: pointer;
}

.map-btn + .map-btn { border-top: 1px solid var(--rule); }
.map-btn:hover { background: var(--paper-sunk); }
.map-btn:focus-visible { outline: 2px solid var(--series); outline-offset: -2px; }
.map-btn[disabled] { color: var(--ink-faint); cursor: default; background: var(--paper-raised); }

/* Reset sits under the two zoom keys, as the third key in the same stack. */
.map-btn svg { width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 1.8; }

/* Expand, top right — the corner everything else puts it in. */
.map-expand {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 3;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: 0 1px 4px rgb(0 0 0 / 0.18);
}

/* Expanded. NOT stretched to the window — the dot layer is positioned in
   percentages of this box, so breaking the 2:1 slides every pin off its city
   (see the note on .map-viewport). So it becomes the largest 2:1 that fits,
   centred over a backdrop. */
.map-viewport.is-full {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(96vw, 186vh);
  height: auto;
  z-index: 61;
  box-shadow: 0 8px 40px rgb(0 0 0 / 0.35);
}

.map-backdrop {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: rgb(8 22 30 / 0.72);
}

/* Required by the tile licence, and it has to stay legible over both water and
   land at every zoom, so it gets a plate rather than a halo. */
.map-credit {
  position: absolute;
  left: 0;
  bottom: 0;
  z-index: 3;
  padding: 2px 6px;
  background: rgb(255 255 255 / 0.72);
  color: #29323a;
  font: 400 10px/1.4 var(--sans);
  border-top-right-radius: var(--radius);
}

.map-credit a { color: inherit; text-decoration: underline; }

/* Stacked, not a sentence: place, then figures, then whatever the page adds.
   One long line made the eye read prose to reach a number, and left nowhere to
   put a third fact — which the network map needs, to say who is behind a dot. */
.map-tip {
  position: absolute;
  z-index: 2;
  padding: 6px 9px;
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  font: 500 11px/1.45 var(--sans);
  color: var(--ink);
  white-space: nowrap;
  pointer-events: none;
  box-shadow: 0 2px 8px rgb(0 0 0 / 0.12);
}

.map-tip span { display: block; }
.map-tip-place { font-weight: 650; }

.map-tip-counts {
  font-family: var(--display);
  font-variant-numeric: tabular-nums;
  font-size: 10.5px;
  color: var(--ink-soft);
}

/* Ruled off rather than just spaced: the line above is this city's own totals,
   the line below is how they split. */
.map-tip-detail {
  margin-top: 4px;
  padding-top: 4px;
  border-top: 1px solid var(--grid);
  font-size: 10.5px;
  color: var(--ink-soft);
}

/* A ranked row that the map can frame. */
tr.is-mappable { cursor: zoom-in; }
tr.is-mappable:hover .bar-label,
tr.is-mappable:focus-visible .bar-label { color: var(--series); }

/* A dot for a hit that just landed. It fades from amber back to the series
   colour over 20s — the same cooling as the arrivals feed, so the two read
   as one system. */
/* The pulse ring is gone with the round dot — a mask clips box-shadow. The
   cooling colour is the part that carried the meaning anyway, so a fresh pin
   lands amber and cools to the resting pin red over the same 20s as the feed.
   It is also drawn a little larger, so an arrival reads before its colour
   does. */
.dot.is-fresh {
  background: var(--signal);
  animation: land 20s linear forwards;
}

@keyframes land {
  0%   { background: var(--signal); }
  100% { background: var(--pin); }
}

/* -------------------------------------------------- signature: arrivals */

/* The one bold element. A ticker of hits as they land, newest at the top,
   each cooling from amber to ink over 20 seconds so age is legible at a
   glance without a "3s ago" column. */
.arrivals { display: flex; flex-direction: column; min-height: 0; }

.arrivals-head {
  padding: 14px 16px 8px;
  border-bottom: 1px solid var(--rule);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
}

.feed {
  list-style: none;
  margin: 0;
  padding: 4px 0;
  overflow-y: auto;
  max-height: 340px;
  font-family: var(--display);
  font-size: 11.5px;
}

/* Audit #22 (BUG-0041). No animation on the base row. The cooling belongs to
   .is-entering alone, which the JS applies only once the feed is primed —
   i.e. to hits that genuinely just landed, never to the ~50 backfilled rows
   painted on load. Animating every row meant a page refresh lit the whole
   feed amber and cooled it, announcing "fifty visitors just arrived" about
   traffic that was hours old. That is the recency indicator lying, which is
   worse than not having one. */
.feed li {
  display: flex;
  gap: 9px;
  padding: 3px 16px;
  color: var(--ink-soft);
  white-space: nowrap;
}

@keyframes cool {
  0%   { color: var(--signal); background: var(--signal-soft); }
  12%  { background: transparent; }
  100% { color: var(--ink-soft); background: transparent; }
}

.feed li.is-entering { animation: slide-in 220ms ease-out, cool 20s linear forwards; }

@keyframes slide-in {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}

.feed-time { flex: none; opacity: 0.75; }
.feed-path { overflow: hidden; text-overflow: ellipsis; }
.feed-geo { margin-left: auto; flex: none; opacity: 0.8; }

.feed-empty { padding: 22px 16px; color: var(--ink-faint); font-size: 12px; }

/* ----------------------------------------------------- signature: reach */

/* The network map's companion, and the thing that makes a combined map worth
   drawing at all. A per-site map already answers "where are my visitors"; only
   the network view can answer "which property owns which part of the world",
   and a map whose dots are all the same colour cannot answer it either.

   So the panel is the legend AND the control: one row per site, and pointing at
   a row isolates that site's cities on the map while the rest of the network
   drops to a ground behind it (.dot.is-muted). Sweeping the list plays each
   property's footprint over the shape of the whole.

   Deliberately NOT a colour key. Twelve sites is twelve hues, none of them
   distinguishable at 4px, all of them competing with the one colour in this
   interface that carries meaning. One footprint at a time, in the series
   colour, over a ghost, says the same thing with nothing spent. */
/* THE MAP SETS THE HEIGHT OF THIS ROW, NEVER THIS COLUMN.
   The hero is a grid, so the row is as tall as its tallest child. The map's
   height is derived from its width by the 2:1, and this column's is driven by
   how many sites and shared cities there are — the two only agree at one
   window width. Left to grow, this column won the row at ordinary laptop
   widths and the map panel got a half-panel of white space under the map.
   So the lists below are given `flex-basis: 0` and `min-height: 0`: they
   contribute nothing to the column's minimum height, then expand into whatever
   the map leaves and scroll past that. */
/* BUG-0074. The map, not this column, decides how tall the hero row is.
   .hero is a grid, so the row is as tall as its tallest child. The map's
   height is derived from its width by the load-bearing 2:1, while this
   column's came from however many sites and shared cities there happened to
   be — the two agreed at exactly one window width, and below roughly 1500px
   this column won and left a band of empty panel under the map.

   `height: 0` is what fixes it: a grid item with a definite height
   contributes nothing to an auto row's size, so the row collapses onto the
   map. `min-height: 100%` then resolves against that now-definite row height
   and stretches this column back to fill it. The order matters — take the
   height away first, then claim the result. */
.reach {
  display: flex;
  flex-direction: column;
  height: 0;
  min-height: 100%;
  overflow: hidden;
}

/* display:flex above beats the [hidden] default, and network.js hides this
   whole column when there is only one site to talk about. */
.reach[hidden] { display: none; }

.reach-head {
  padding: 14px 16px 8px;
  border-bottom: 1px solid var(--rule);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}

/* Shrinkable, but it gets what it needs before Shared ground does — the site
   roster is the point of the panel and is short; the city list is the filler. */
.reach-list {
  list-style: none;
  margin: 0;
  padding: 2px 0;
  flex: 0 1 auto;
  min-height: 0;
  max-height: 296px;
  overflow-y: auto;
}

/* A button because it does something, and because the isolate has to be
   reachable from the keyboard — focus isolates, exactly as hover does. */
.reach-row {
  position: relative;
  display: block;
  width: 100%;
  padding: 7px 16px 8px;
  background: none;
  border: 0;
  border-radius: 0;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

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

/* Share of the network's located visitors.
   NOT the bar-behind-the-label the ranked tables use. Behind a two-line row a
   part-width block stops reading as a measure and starts reading as a
   half-highlighted row — it looks like a selection, which is a state this panel
   genuinely has and needs to keep for itself. So the share is stated explicitly
   instead: a rule at the foot of the row, filled to its share of the leader. */
.reach-meter {
  display: block;
  height: 2px;
  margin-top: 6px;
  background: var(--grid);
}

.reach-meter i {
  display: block;
  height: 100%;
  background: var(--series);
  opacity: 0.55;
}

.reach-line { display: flex; align-items: center; gap: 7px; }

.reach-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12.5px;
  font-weight: 600;
}

.reach-visitors {
  margin-left: auto;
  padding-left: 10px;
  font-size: 11.5px;
  color: var(--ink-soft);
}

.reach-sub { display: block; margin-top: 2px; padding-left: 13px; }

/* The row background is reserved for state, which is why the share is a rule
   and not a block behind the text. */
.reach-row:hover,
.reach-row:focus-visible { background: var(--paper-sunk); }
.reach-row:hover .reach-name,
.reach-row:focus-visible .reach-name { color: var(--series); }
.reach-row:hover .reach-meter i,
.reach-row:focus-visible .reach-meter i { opacity: 1; }

/* Locked on. The rail marks the current site the same way, so a held selection
   looks the same wherever it is held. */
.reach-row[aria-pressed="true"] { box-shadow: inset 2px 0 0 var(--series); }
.reach-row[aria-pressed="true"] .reach-name { color: var(--series); }
.reach-row[aria-pressed="true"] .reach-meter i { opacity: 1; }

/* ------------------------------------------------------- shared ground */

/* Where the network doubles up. The reach rows above say what each property
   covers; this says where they land on top of each other, which is the other
   half of the same question and the thing an operator acts on — two sites
   competing for one city is a decision, one site alone in a city is not.

   It also does honest work for the layout: the map is 2:1 and half a page tall,
   so the column beside it is tall and narrow. Left at one short list it was
   four hundred pixels of empty panel. */
/* Takes the column's slack rather than a fixed height: the map is 2:1, so how
   tall this column is depends on how wide the window is, and any cap picked
   here is wrong at some width — too short and it scrolls with rows to spare
   below, too tall and it pushes the footer off. */
.shared {
  display: flex;
  flex-direction: column;
  /* basis 0, so a long city list cannot push the column past the map */
  flex: 1 1 0;
  min-height: 0;
  border-top: 1px solid var(--rule);
}

.shared-head {
  padding: 12px 16px 7px;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}

.shared-list {
  list-style: none;
  margin: 0;
  padding: 0 0 6px;
  flex: 1 1 0;
  min-height: 0;
  overflow-y: auto;
  font-size: 12px;
}

/* Framing a city is the same gesture the ranked tables offer, so it gets the
   same cursor. */
.shared-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  width: 100%;
  padding: 3px 16px;
  background: none;
  border: 0;
  color: var(--ink-soft);
  font: inherit;
  text-align: left;
  cursor: zoom-in;
}

.shared-row:hover { background: var(--paper-sunk); color: var(--ink); }
.shared-row:focus-visible { outline: 2px solid var(--series); outline-offset: -2px; }

.shared-city {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* How many of the network's sites are in this city. The count is the ranking
   key, so it is the figure that gets the emphasis, not the visitor total. */
.shared-sites {
  margin-left: auto;
  flex: none;
  font-family: var(--display);
  font-variant-numeric: tabular-nums;
  font-size: 11px;
  color: var(--ink);
}

.shared-visitors {
  flex: none;
  width: 46px;
  text-align: right;
  font-family: var(--display);
  font-variant-numeric: tabular-nums;
  font-size: 11px;
  color: var(--ink-faint);
}

/* Not a metric tile. It is one sentence, because overlap is a fact about the
   network rather than a figure to track. */
.reach-foot {
  margin-top: auto;
  padding: 10px 16px 12px;
  border-top: 1px solid var(--rule);
  color: var(--ink-faint);
  font-size: 11.5px;
  line-height: 1.4;
}

.reach-empty { padding: 22px 16px; color: var(--ink-faint); font-size: 12px; }

/* ------------------------------------------------------ metric readout */

/* An instrument readout, not a row of cards. Values share one baseline so
   the eye can scan them as a single line of telemetry. */
.readout {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(126px, 1fr));
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  margin-bottom: 26px;
}

.metric {
  background: var(--paper-raised);
  padding: 14px 16px 13px;
}

.metric-value {
  font-size: 25px;
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.02em;
  display: block;
}

.metric .label { margin-top: 5px; display: block; }

.delta {
  font-family: var(--display);
  font-size: 11px;
  margin-top: 3px;
  display: block;
  color: var(--ink-faint);
}
.delta.up   { color: var(--up); }
.delta.down { color: var(--down); }

/* ---------------------------------------------------------------- panels */

.panel {
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  margin-bottom: 22px;
}

.panel-head {
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 13px 16px;
  border-bottom: 1px solid var(--rule);
}

.panel-head h2 { margin: 0; font-size: 13px; font-weight: 650; }
.panel-head .spacer { margin-left: auto; }
.panel-body { padding: 14px 16px; }

.grid-2 {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 22px;
  /* Panels size to their content. Without this, grid stretches every panel in
     a row to match the tallest, so an 8-row table next to a 15-row one gets
     half a screen of dead space under it. */
  align-items: start;
}

/* ----------------------------------------------------------------- chart */

.chart { width: 100%; height: 200px; display: block; overflow: visible; }
.chart .axis   { stroke: var(--grid); stroke-width: 1; }
.chart .series-line { fill: none; stroke: var(--series); stroke-width: 1.75; }
.chart .series-area { fill: var(--series-soft); }
.chart .tick   { font-family: var(--display); font-size: 9.5px; fill: var(--ink-faint); }
.chart .marker { fill: var(--series); }

/* ---------------------------------------------------------------- tables */

/* Ranked lists carry an inline bar behind the label. The bar is the
   comparison; the number is the detail. */
table { width: 100%; border-collapse: collapse; font-size: 13px; }

table th {
  text-align: left;
  font-family: var(--sans);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-faint);
  padding: 0 8px 7px 0;
  border-bottom: 1px solid var(--rule);
}

table td {
  padding: 6px 8px 6px 0;
  border-bottom: 1px solid var(--grid);
  vertical-align: middle;
}

table tr:last-child td { border-bottom: 0; }
table td.n { text-align: right; white-space: nowrap; width: 1%; padding-left: 14px; }
table td.n.muted { color: var(--ink-faint); }

.bar-cell { position: relative; min-width: 0; }

.bar {
  position: absolute;
  left: 0; top: 50%;
  transform: translateY(-50%);
  height: 17px;
  background: var(--series-soft);
  border-radius: 1px;
  z-index: 0;
}

.bar-label {
  position: relative;
  z-index: 1;
  display: block;
  padding: 0 6px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.empty {
  padding: 26px 4px;
  color: var(--ink-faint);
  font-size: 13px;
  text-align: center;
}

/* ------------------------------------------------------------ goal form */

.goal-form {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 12px;
  align-items: end;
  margin-top: 20px;
  padding-top: 18px;
  border-top: 1px solid var(--rule);
}

.goal-form .field { margin-bottom: 0; }

.goal-form input,
.goal-form select {
  width: 100%;
  padding: 6px 9px;
  font-family: var(--sans);
  font-size: 13px;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}

.goal-form input:focus,
.goal-form select:focus { border-color: var(--series); outline: none; }

.goal-form button { height: 32px; }

/* Errors span the whole grid rather than squeezing into one column. */
.goal-form [data-goal-error] {
  grid-column: 1 / -1;
  margin: 0;
  color: var(--down);
}

/* ------------------------------------------------------------------ misc */

.flash {
  padding: 9px 12px;
  border-left: 2px solid var(--down);
  background: var(--paper-raised);
  font-size: 13px;
  margin-bottom: 14px;
}

.note {
  font-size: 12px;
  color: var(--ink-faint);
  margin-top: 8px;
}

code, .mono { font-family: var(--display); font-size: 12px; }

.snippet {
  background: var(--paper-sunk);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 11px 13px;
  font-family: var(--display);
  font-size: 12px;
  overflow-x: auto;
  white-space: pre;
  color: var(--ink);
}

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

.auth {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 24px;
}

.auth-card {
  width: 100%;
  max-width: 336px;
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 28px;
}

.auth-brand {
  font-family: var(--display);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  margin-bottom: 22px;
}

.field { margin-bottom: 14px; }
.field label { display: block; margin-bottom: 5px; }

.field input {
  width: 100%;
  padding: 8px 10px;
  font-family: var(--sans);
  font-size: 14px;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}

.field input:focus { border-color: var(--series); outline: none; }

.btn-primary {
  width: 100%;
  padding: 9px;
  background: var(--ink);
  color: var(--paper-raised);
  border: 1px solid var(--ink);
  font-weight: 600;
}
.btn-primary:hover { opacity: 0.9; }

/* ----------------------------------------------------------- responsive */

@media (max-width: 1080px) {
  .hero { grid-template-columns: minmax(0, 1fr); }
  .arrivals, .reach { border-top: 1px solid var(--rule); }
  .feed { max-height: 220px; }
  .reach-list { max-height: 220px; }

  /* Stacked, there is no map beside this column to take a height from, so the
     `flex-basis: 0` that keeps it from outgrowing the map upstairs would
     collapse it to its headings. Back to sizing themselves. */
  .reach { overflow: visible; }
  .shared, .shared-list { flex: 0 0 auto; }
}

@media (max-width: 760px) {
  :root { --gutter: 16px; }
  .shell { grid-template-columns: minmax(0, 1fr); }
  .rail {
    flex-direction: row;
    overflow-x: auto;
    padding: 10px 0;
    border-right: 0;
    border-bottom: 1px solid var(--rule);
    gap: 0;
  }
  .rail-brand, .rail-heading { display: none; }
  .site-link {
    border-left: 0;
    border-bottom: 2px solid transparent;
    white-space: nowrap;
  }
  .site-link[aria-current="page"] {
    border-left: 0;
    /* Audit #24 (BUG-0043): the narrow-viewport half of the same marker. */
    border-bottom-color: var(--series);
  }
  .site-link .count { display: none; }
  .metric-value { font-size: 21px; }
  .topbar h1 { font-size: 17px; }
  /* Keep the controls together on their own line instead of splitting the
     pill and the select across the wrap. */
  .topbar-right { margin-left: 0; width: 100%; }
}

/* Reduced motion: the cooling colour still communicates recency, but nothing
   moves, pulses or slides. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  /* Audit #23 (BUG-0044). This fallback used to target `.feed li.is-fresh`, a
     class nothing in the feed ever sets — only map dots use `is-fresh`. So the
     comment above was aspirational: with the animation flattened to 0.01ms the
     cool keyframes' `forwards` fill snapped straight to the resting colour and
     reduced-motion users got no recency cue at all.

     `animation: none` is required, not merely tidy: an animation's fill state
     outranks normal declarations in the cascade, so without it the colour
     below would still lose to the flattened keyframe. `:first-child` bounds
     the amber to the single newest arrival — the feed prepends, and every
     row keeps `is-entering` for the life of the page, so without it the whole
     list would end up amber and mean nothing. */
  .feed li { color: var(--ink-soft); }
  .feed li.is-entering { animation: none; }
  .feed li.is-entering:first-child { color: var(--signal); }
}
