/* ============================================================================
   Atlas — dashboard styles.

   ⚠️ Every value lives in tokens.css: here they get composed, not invented. A color
   hand-written in this file would not follow the dark theme.
   ============================================================================ */

/* ---------------------------------------------------------------------------
   The box model.

   🔴 Without this, `width: 100%` means "100% PLUS padding and border": every field
   overflowed its own container. And `min-height: 44px` added itself to the padding
   instead of containing it — a sign-in field came out 70 pixels tall
   (44 + 12 + 12 + 2) and a button 46, so the two never lined up.

   ⚠️ It holds for ::before and ::after too: veils and loading spinners have fixed
   sizes, and under the default model every one of them would end up bigger than the
   number written down.
   --------------------------------------------------------------------------- */

*,
*::before,
*::after {
  box-sizing: border-box;
}


/* ---------------------------------------------------------------------------
   Header
   --------------------------------------------------------------------------- */

.topbar {
  position: sticky;
  top: 0;
  /* The stack: header 199, maximised veil 300, maximised widget 301.
     ⚠️ The widget has to sit ABOVE the header, otherwise it slides under it and its
     controls — the one that closes it included — become unreachable. */
  z-index: 199;
  background: var(--bg-secondary);
  border-bottom: 1px solid var(--border);
  box-shadow: var(--elevation-header);
}

.topbar-inner {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  max-width: 1920px;
  margin: 0 auto;
  padding: var(--space-4) var(--space-6);
}

/* ⚠️ flex-shrink: 0 — the header is a flex container, and on a narrow screen the mark
   is the first thing it would squeeze. A squashed logo is not a logo. */
.topbar-logo {
  width: 28px;
  height: 28px;
  flex-shrink: 0;
}

.topbar-mark {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 1.125rem;
  font-weight: 700;
  color: var(--brand);
  text-decoration: none;
  letter-spacing: -0.02em;
}

.topbar-nav {
  display: flex;
  gap: var(--space-4);
}

.topbar-nav a {
  color: var(--text-secondary);
  text-decoration: none;
  padding: var(--space-2) 0;
}

.topbar-nav a:hover { color: var(--text-primary); }

/* The active item is not set apart by color alone: weight and underline stay readable
   for anyone who cannot pick out the brand green. */
.topbar-nav a[aria-current="page"] {
  color: var(--text-primary);
  font-weight: 600;
  border-bottom: 2px solid var(--brand-light);
}

.topbar-right {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-left: auto;
}

.topbar-account {
  font-size: 0.75rem;
  color: var(--text-secondary);
}

.topbar-switch select {
  font: inherit;
  font-size: 0.75rem;
  color: var(--text-primary);
  background: var(--bg-secondary);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-3);
  /* 44px would be too much for a menu in a dense header, but below 32 the target
     gets awkward even with a mouse. */
  min-height: 32px;
}

.topbar-signout {
  font: inherit;
  font-size: 0.75rem;
  color: var(--text-secondary);
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-3);
  min-height: 32px;
  cursor: pointer;
  transition: color var(--motion-duration) var(--motion-easing),
              border-color var(--motion-duration) var(--motion-easing);
}

.topbar-signout:hover {
  color: var(--text-primary);
  border-color: var(--text-secondary);
}

/* ---------------------------------------------------------------------------
   Page
   --------------------------------------------------------------------------- */

.page {
  max-width: 1920px;
  margin: 0 auto;
  /* Tighter vertically than horizontally: under a header already set apart by a border
     and a shadow, padding matching the side margins was twice the air needed. */
  padding: var(--space-4) var(--space-6);
}

.page-footer {
  max-width: 1920px;
  margin: 0 auto;
  padding: var(--space-4) var(--space-6);
  font-size: 0.75rem;
  color: var(--text-secondary);
}

.page-footer a { color: var(--text-secondary); }

/* Visible to screen readers, not on screen. ⚠️ Not `display: none`: that would take it
   away from them too. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ---------------------------------------------------------------------------
   Dashboard
   --------------------------------------------------------------------------- */

.dash-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-4);
  margin-bottom: var(--space-6);
}

.dash-head h1 {
  font-size: 1.5rem;
  font-weight: 600;
  margin: 0;
}

.dash-controls {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.control {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.control label {
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-secondary);
}

.control select,
.control-apply {
  font: inherit;
  font-size: 0.875rem;
  color: var(--text-primary);
  background: var(--bg-secondary);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-3);
  min-height: 36px;
}

.dash-note {
  font-size: 0.75rem;
  color: var(--text-secondary);
  margin-bottom: var(--space-4);
}

/* ── The widget wall ────────────────────────────────────────────────────────

   🔴 Two separate classes, and the separation is the whole point:

     .widget  says WHERE a panel sits and how wide it is — its place in the grid
              and the spacing.
     .panel   says WHAT it looks like — background, border, radius, shadow, padding.

   They are used together (a panel spanning two columns) or apart (a group of cards
   filling the row without a frame of its own). They used to be fused into three
   different classes repeating the same five properties, and forgetting one of them
   was enough to knock a panel out of shape: that is why the world map had no bottom
   margin.

   ⚠️ There are THREE columns, as in the old Atlas, and a widget's width is expressed
   in columns (--w: 1..3). A widget that does not fit the row wraps on its own: that
   is native CSS grid behavior, no code computes it.
   ────────────────────────────────────────────────────────────────────────── */

.dash-grid {
  /* 🔴 How much room the widget tools take, in one place only: six 26px controls with
     their gaps. .panel-head uses it to reserve the row, and it changes along with the
     buttons — add one tomorrow, update it here, and the title's note does not end up
     underneath them. */
  --wall-tools-width: 11rem;

  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-4);
  /* The space below the grid lives here, in one place only: no widget has to remember
     a margin of its own. */
  margin-bottom: var(--space-6);
}

.widget {
  /* ⚠️ min-width: 0 — without it, wide content (a table, an SVG) stops the column from
     shrinking and blows out the grid. It is the most common CSS grid defect, and it
     raises no error. */
  min-width: 0;
  grid-column: span 1;

  /* The anchor for the tools, which sit in the top-right corner OVER the content and
     not beside it. See .wall-tools. */
  position: relative;
}

@media (min-width: 768px) {
  .dash-grid { grid-template-columns: repeat(2, 1fr); }

  /* On two columns, a three-column widget takes two: there is no third column left
     to take. */
  .widget { grid-column: span min(var(--w, 1), 2); }
}

@media (min-width: 1280px) {
  .dash-grid { grid-template-columns: repeat(3, 1fr); }
  .widget { grid-column: span var(--w, 1); }
}

/* Looks only, no positioning: it can go on anything. */
.panel {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--elevation-card);
  /* --space-4 and not --space-6: a panel is not a waiting room, the content can sit
     closer to the edge without looking cramped. */
  padding: var(--space-4);
}

/* --- Metric cards --------------------------------------------------------- */

/* 🔴 flex and not grid, and the reason is the LAST ROW.
   With `grid-template-columns: repeat(auto-fit, minmax(190px, 1fr))` the columns are
   defined for the whole grid, not row by row: if five of them fit and there are six
   cards, the sixth takes the first track and the other four sit empty to its right.
   auto-fit drops empty tracks only when ALL of them are empty, which in a case like
   this one is never.
   With flex every row splits the space it has: the last one stretches like the rest.

   ⚠️ The price: a card left alone on the last row grows as wide as the panel. It is
   the other side of the same coin — "fill the space" and "keep the same width" are two
   things you cannot have at once. */
.stats {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  /* ⚠️ No margin: the space below comes from the grid that holds the widget. */
}

/* Grows and shrinks from the same basis: 190px is the width below which a number and
   its label start to wrap. */
.stats > * {
  flex: 1 1 190px;

  /* ⚠️ Without it, a card holding a long number would not shrink below the size of its
     content and would blow out the row. */
  min-width: 0;
}

.stat {
  /* The same properties as .panel, but a card is not a panel: it is smaller and has no
     head. They stay here, with the same reduced padding. */
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--elevation-card);
  padding: var(--space-4);
}

.stat-label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-secondary);
  margin-bottom: var(--space-2);
}

.stat-help {
  display: inline-grid;
  place-items: center;
  width: 16px;
  height: 16px;
  font-size: 0.625rem;
  color: var(--text-secondary);
  border: 1px solid var(--border);
  border-radius: 50%;
  cursor: help;
}

.stat-value {
  font-size: 2rem;
  font-weight: 700;
  line-height: 1.25;
  /* Digits stay aligned across neighboring cards even as they change. */
  font-variant-numeric: tabular-nums;
}

/* "Being computed", not zero. See docs/metriche.md, principle 2. */
.stat-pending { color: var(--text-secondary); }

.stat-change {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  margin-top: var(--space-2);
  font-size: 0.75rem;
  color: var(--text-secondary);
}

/* ⚠️ The direction alone cannot pick the color: for bounce rate, going down is an
   improvement. The model decides the class, not the sign. */
.stat-change.is-better { color: var(--success); }
.stat-change.is-worse  { color: var(--error); }

.stat-nochange { color: var(--text-secondary); }

/* --- Chart ---------------------------------------------------------------- */

/* ⚠️ The chart no longer repeats background and border: put .panel next to it. It was
   a fourth copy of the same five properties. */
.chart-body {
  /* Like .chart-plot: the panel keeps its height between a full series and an empty
     one, so the wall does not reflow when the period changes. */
  min-height: 220px;
}

/* 🔴 Explicit height, and it is mandatory. Chart.js with responsive on and
   maintainAspectRatio off measures the CONTAINER: if the container sized itself to the
   canvas, and the canvas to the container, the canvas would grow on every redraw until
   the page was twice as tall. It is the library's best-known defect, and it shows up
   on window resize, not on first load. */
.chart-plot {
  position: relative;
  height: 220px;
}

.chart-canvas {
  display: block;
  width: 100%;
  height: 100%;
}

.chart-empty,
.panel-empty {
  /* ⚠️ Fills the panel instead of being one stray line up top: a recognizable empty box
     reads as "nothing here yet", a single line reads as a failure. */
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 8rem;
  padding: var(--space-4);
  margin: 0;
  font-size: 0.8125rem;
  line-height: 1.5;
  text-align: center;
  text-wrap: balance;
  color: var(--text-secondary);
  background: var(--bg-secondary);
  border: 1px dashed var(--border);
  border-radius: var(--radius-md);
}

/* --- Rankings ------------------------------------------------------------- */

.panel-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-4);

  /* ⚠️ Room for the tools, which are overlaid and would cover the note on the right
     ("by visits"). It is reserved HERE, on a row twenty pixels tall, and not on the
     whole widget: that is the difference between a note with room to breathe and a
     panel with an empty strip running its full height. */
  padding-right: var(--wall-tools-width);
}

.panel-head h2 {
  font-size: 1.125rem;
  font-weight: 600;
  margin: 0;
}

.panel-note {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 0.75rem;
  color: var(--text-secondary);
}

/* The download link sits next to the note, not among the wall tools: it belongs to the
   ranking — its period, its filters, its dimension — while the tools belong to the widget. */
.panel-download {
  color: var(--text-secondary);
  opacity: 0;
  transition: opacity var(--motion-duration) var(--motion-easing),
              color var(--motion-duration) var(--motion-easing);
}

/* ⚠️ It appears on hover like the wall tools, but never hides from the keyboard: focus-visible
   brings it back, otherwise it would be a control nobody tabbing could reach. */
.widget:hover .panel-download,
.widget:focus-within .panel-download { opacity: 1; }

.panel-download:hover { color: var(--brand); }

.panel-download:focus-visible {
  opacity: 1;
  outline: 2px solid var(--brand);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

.rank {
  margin: 0;
  padding: 0;
  list-style: none;
}

.rank li {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-2);
  border-bottom: 1px solid var(--border);
}

.rank li:last-child { border-bottom: none; }

/* The share bar sits BEHIND the text, not beside it: it reads without stealing width
   from the name, which is the part you actually need to read. */
.rank-bar {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  background: var(--brand-light);
  opacity: 0.12;
  border-radius: var(--radius-sm);
}

/* The browser, OS or device icon.
   ⚠️ position: relative like the rest of the row — without it the icon ends up BEHIND
   the share bar, which is position: absolute over the same space.
   flex-shrink: 0 because it is the first thing flex would squeeze when the name is
   long, and an icon crushed to 4px is worse than no icon at all. */
.rank-icon {
  position: relative;
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  object-fit: contain;
}

.rank-name {
  position: relative;
  flex: 1;
  min-width: 0;
  /* A long path must not widen the card. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* "Everything else": whatever falls outside the ranking.
   ⚠️ It reads as different from the real rows — it is not a value, it is a remainder —
   but stays legible: it gives the measure of how much the ranking does NOT say. */
.rank-rest {
  color: var(--text-secondary);
  font-style: italic;
}

.rank-rest .rank-bar { opacity: 0.06; }

.rank-value {
  position: relative;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* --- Empty state ---------------------------------------------------------- */

/* ⚠️ "No data" is not "zero": two different things, and they have to look different.
   See docs/metriche.md, principle 2.

   🔴 This used to be called `.empty`, like the inline note further down, and the two
   rules overrode each other: the second one won, so this box was never seen — the
   dashboard with no data and the error page both came out looking like a footnote.
   Two different components want two names, which is the same reason .widget and
   .panel are separate. */
.empty-state {
  background: var(--bg-card);
  border: 1px dashed var(--border);
  border-radius: var(--radius-lg);

  /* Generous but not huge: it was 3rem vertically, which on a whole empty page turned
     into a wasteland. */
  padding: var(--space-8) var(--space-6);
  text-align: center;
}

.empty-title {
  font-size: 1.125rem;
  font-weight: 600;
  margin: 0 0 var(--space-2);
}

.empty-hint {
  font-size: 0.875rem;
  color: var(--text-secondary);
  margin: 0;
}

/* ---------------------------------------------------------------------------
   Map
   --------------------------------------------------------------------------- */

/* The map takes the full width: inside one column of the panel grid it would be
   unreadable. */
.map-frame {
  /* The empty-state note is positioned against this box. */
  position: relative;

  /* ⚠️ overflow hidden and not visible: while it is dragged the map runs past its own
     bounds, and without this it would cover the neighboring panels. */
  overflow: hidden;
  border-radius: var(--radius-md);
  background: var(--bg-hover);
  touch-action: none;
}

.map-svg {
  display: block;
  width: 100%;
  height: auto;
}

.map-country {
  /* It used to be --bg-card, that is white: on a light gray world map the borders were
     not visible at all, and the world looked like a single blob. */
  stroke: var(--map-border);
  /* The stroke does not thin out when Panzoom zooms in: without this, at 8× zoom the
     borders would disappear. */
  stroke-width: 0.5;
  vector-effect: non-scaling-stroke;
  transition: fill var(--motion-duration) var(--motion-easing);
}

/* 🔴 The bands are classes, not fill attributes: a color computed on the server would
   stay light inside a page in the dark theme. Here CSS decides the color, with the
   same variables as everything else.

   ⚠️ Band 0 is "no data", not "the lowest value": two different things, and confusing
   them would claim nobody came from there when in fact a few did. */
.map-q0 { fill: var(--border); }
.map-q1 { fill: color-mix(in srgb, var(--brand-light) 18%, var(--bg-card)); }
.map-q2 { fill: color-mix(in srgb, var(--brand-light) 34%, var(--bg-card)); }
.map-q3 { fill: color-mix(in srgb, var(--brand-light) 52%, var(--bg-card)); }
.map-q4 { fill: color-mix(in srgb, var(--brand) 68%, var(--bg-card)); }
.map-q5 { fill: color-mix(in srgb, var(--brand) 84%, var(--bg-card)); }
.map-q6 { fill: var(--brand); }

.map-country:hover {
  fill: var(--warning);
}

.map-legend {
  display: flex;
  align-items: center;
  gap: 2px;
  margin-top: var(--space-4);
}

.map-legend-step {
  width: 28px;
  height: 10px;
  /* The bands are <span>, not <path>: the SVG fill would not color them. */
  background: currentColor;
}

/* The same six bands, but as a background instead of an SVG fill. */
.map-legend .map-q1 { background: color-mix(in srgb, var(--brand-light) 18%, var(--bg-card)); }
.map-legend .map-q2 { background: color-mix(in srgb, var(--brand-light) 34%, var(--bg-card)); }
.map-legend .map-q3 { background: color-mix(in srgb, var(--brand-light) 52%, var(--bg-card)); }
.map-legend .map-q4 { background: color-mix(in srgb, var(--brand) 68%, var(--bg-card)); }
.map-legend .map-q5 { background: color-mix(in srgb, var(--brand) 84%, var(--bg-card)); }
.map-legend .map-q6 { background: var(--brand); }

.map-legend-label {
  font-size: 0.75rem;
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}

.map-legend-label:first-child { margin-right: var(--space-2); }
.map-legend-label:last-child { margin-left: var(--space-2); }

/* ============================================================================
   Management pages: sites, team, profile.

   ⚠️ These are pages for getting work done, not for reading: wide fields, actions you
   can tell apart, none of the dashboard density. Same tokens as everything else.
   ============================================================================ */

.lede {
  color: var(--text-secondary);
  font-size: 0.875rem;
  max-width: 46rem;
  margin-bottom: var(--space-4);
}

/* How much of the license is used up. Always said, not only at the limit. */
.quota {
  display: inline-block;
  padding: var(--space-2) var(--space-3);
  margin-bottom: var(--space-4);
  font-size: 0.75rem;
  color: var(--text-secondary);
  background: var(--bg-secondary);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  font-variant-numeric: tabular-nums;
}

.ok {
  padding: var(--space-3);
  margin-bottom: var(--space-4);
  font-size: 0.8125rem;
  color: var(--success);
  background: color-mix(in srgb, var(--success) 8%, transparent);
  border: 1px solid color-mix(in srgb, var(--success) 30%, transparent);
  border-radius: var(--radius-md);
}

/* The inline note: one line saying why a section is empty or closed — "your plan does
   not allow another site". ⚠️ It is not a whole page's empty state: that one is
   .empty-state. */
.empty {
  padding: var(--space-4);
  font-size: 0.8125rem;
  color: var(--text-secondary);
  background: var(--bg-secondary);
  border: 1px dashed var(--border);
  border-radius: var(--radius-md);
  text-align: center;
}

.cards { list-style: none; padding: 0; margin: 0 0 var(--space-6); }

.card {
  padding: var(--space-4);
  margin-bottom: var(--space-4);
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}

/* A switched-off domain stays readable: it is dimmed, not hidden. */
.card-off { opacity: 0.65; }

.card-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}

.card-head h2 { font-size: 1rem; font-weight: 600; margin: 0; }

.badge {
  padding: 2px var(--space-2);
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--text-secondary);
  background: var(--bg-hover);
  border-radius: var(--radius-md);
}

.muted { font-size: 0.75rem; color: var(--text-secondary); }

/* The failure code: it gets read out over the phone, so it has to be visible. */
.error-code {
  margin: var(--space-4) 0 var(--space-2);
  font-size: 0.875rem;
  color: var(--text-primary);
}

.error-code code {
  padding: var(--space-1) var(--space-2);
  font-size: 0.875rem;
  background: var(--bg-secondary);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

.card-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
}

/* The key gets copied: monospace, and selected on click. */
.key {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.8125rem;
}

.snippet {
  padding: var(--space-3);
  margin: 0;
  font-size: 0.75rem;
  background: var(--bg-hover);
  border-radius: var(--radius-md);
  /* A snippet wider than the screen scrolls inside its box, it does not push the page. */
  overflow-x: auto;
}

/* --- Field to copy: tracking key and snippet ------------------------------ */

/* ⚠️ The value stays selectable by hand anyway (onclick="this.select()" on the key):
   the button is a convenience, not the only way to copy it. */
.field-copy {
  display: flex;
  align-items: stretch;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}

.field-copy .key { flex: 1; min-width: 0; }
.field-copy .snippet { flex: 1; min-width: 0; }

.copy-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  /* 44px: below this height the target is awkward to tap. */
  width: 44px;
  min-height: 44px;
  color: var(--text-secondary);
  background: var(--bg-secondary);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: color var(--motion-duration) var(--motion-easing),
              border-color var(--motion-duration) var(--motion-easing);
}

.copy-btn:hover {
  color: var(--text-primary);
  border-color: var(--text-secondary);
}

.copy-btn-icon-check { display: none; }

/* The button text changes along with the icon (via JavaScript, aria-label and title
   included): green alone would not be enough for anyone who cannot tell it apart. */
.copy-btn.is-copied {
  color: var(--success);
  border-color: color-mix(in srgb, var(--success) 40%, transparent);
}

.copy-btn.is-copied .copy-btn-icon-copy  { display: none; }
.copy-btn.is-copied .copy-btn-icon-check { display: block; }

.row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: flex-end;
  margin-bottom: var(--space-4);
}

/* ⚠️ Without a max-width, just two fields on a wide row split the space in half each:
   on a full-width card a text field ended up as wide as half the page. 20rem is past
   the useful limit for an input or a select. */
.row > div { flex: 1 1 12rem; max-width: 20rem; }

/* 🔴 A button in a .row is not a field: it does not grow like a `<div>`. It used to
   match NO flex rule at all — not being a div, it kept the default basis — and with no
   limit on the neighboring fields the row wrapped, leaving it alone at the bottom of
   the card instead of next to the inputs. */
.row > button { flex: 0 0 auto; }

.stack { max-width: 26rem; }

/* A field that takes the whole row: the exclusion lists are one rule per line, and at
   the 20rem of a normal field a CIDR wraps. */
.row > .field-wide {
  flex: 1 1 100%;
  max-width: none;
}

/* ⚠️ Monospace and no resize handle on the horizontal axis: these are addresses and
   paths, they line up when the digits do, and a textarea dragged wider than its card
   breaks the card layout. */
.page textarea {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  line-height: 1.5;
  resize: vertical;
}

/* Stacked fields (profile): with no margin of their own, the bottom of one field
   touches the next one's label — in grid forms (.row) the gap gives the space. */
.stack input,
.stack select {
  margin-bottom: var(--space-3);
}

/* A table wider than the screen scrolls inside its own box: Italian and Japanese
   stretch the cells further than English does, and without this they would push the
   whole page sideways. */
.table-scroll {
  overflow-x: auto;
  margin-bottom: var(--space-3);
}

.table-scroll .grid { min-width: 32rem; margin-bottom: 0; }

.grid { width: 100%; border-collapse: collapse; margin-bottom: var(--space-3); }

.grid th {
  padding: var(--space-2);
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-secondary);
  text-align: left;
  border-bottom: 1px solid var(--border);
}

.grid td {
  padding: var(--space-3) var(--space-2);
  font-size: 0.8125rem;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}

.grid td strong { display: block; }

.inline { display: flex; gap: var(--space-2); align-items: center; }

/* The sites a person can reach: checkboxes stacked inside the cell. */
.sites fieldset {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: 0;
  margin: 0;
  border: 0;
}

.check {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  font-size: 0.8125rem;
  /* ⚠️ The whole label is the click area: a 13px checkbox on its own is too small a
     target, and on a phone you miss it. */
  cursor: pointer;
}

.check input { margin: 0; }

.hint {
  margin: var(--space-2) 0;
  font-size: 0.75rem;
  color: var(--text-secondary);
}

/* Screen readers only: a legend for whoever listens and not for whoever looks, because
   the column header already says the same thing. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Fields and buttons on the management pages. */
.page input[type="text"],
.page input[type="email"],
.page textarea,
.page select {
  width: 100%;
  /* 44px: below this height the field is awkward to tap on mobile. */
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  font: inherit;
  font-size: 0.8125rem;
  color: var(--text-primary);
  background: var(--bg-secondary);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  transition: border-color var(--motion-duration) var(--motion-easing);
}

.page input[type="text"]:focus,
.page input[type="email"]:focus,
.page textarea:focus,
.page select:focus {
  border-color: var(--brand-light);
}

.page input:disabled, .page input[readonly] {
  color: var(--text-secondary);
  background: var(--bg-hover);
}

.page label {
  display: block;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-secondary);
  margin-bottom: var(--space-1);
}

.page button[type="submit"] {
  /* 44px: below this height the target is awkward to tap. */
  min-height: 44px;
  padding: 0 var(--space-4);
  font: inherit;
  font-size: 0.8125rem;
  font-weight: 600;
  color: #fff;
  background: var(--brand);
  border: none;
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: background var(--motion-duration) var(--motion-easing);
}

.page button[type="submit"]:hover  { background: var(--brand-dark); }
.page button[type="submit"]:active { background: var(--brand-dark); }

/* Actions you cannot undo look different: color and border, not color alone. */
.page button.danger {
  color: var(--error);
  background: transparent;
  border: 1px solid color-mix(in srgb, var(--error) 40%, transparent);
  transition: background var(--motion-duration) var(--motion-easing),
              border-color var(--motion-duration) var(--motion-easing);
}

.page button.danger:hover {
  background: color-mix(in srgb, var(--error) 8%, transparent);
  border-color: var(--error);
}

.link {
  display: inline-block;
  margin-top: var(--space-4);
  font-size: 0.8125rem;
  color: var(--brand);
}

.link:hover { text-decoration: underline; }

/* --- Developer note (error page) ------------------------------------------ */

/* Collapsed on purpose: it is a technical detail for whoever develops, not the first
   message someone who simply ran into an error has to read. */
.dev-note {
  margin-top: var(--space-4);
  padding: var(--space-4);
  font-size: 0.8125rem;
  color: var(--text-secondary);
  background: var(--bg-secondary);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}

.dev-note summary {
  font-weight: 600;
  color: var(--text-primary);
  cursor: pointer;
}

.dev-note p { margin: var(--space-2) 0 0; }

/* ============================================================================
   Signed-out layout: sign-in, invite, password reset, sign-up.

   Moved here from a <style> block inlined in _AuthLayout.cshtml — same rule as
   everything else: tokens from tokens.css, no hand-written color.
   ============================================================================ */

.auth-shell {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: var(--space-4);
}

.auth-card {
  width: 100%;
  max-width: 26rem;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--elevation-card);
  /* --space-6 and not --space-8: .auth-shell already isolates the card in the middle
     of the screen, it does not also need full-page padding. */
  padding: var(--space-6);
}

/* The full logo on the signed-out pages: there is room for it there. */
.auth-logo {
  display: block;
  width: 132px;
  height: auto;
  margin: 0 auto var(--space-2);
}

.auth-lede {
  color: var(--text-secondary);
  font-size: 0.75rem;
  margin-bottom: var(--space-4);
}

.auth-card h1 {
  font-size: 1.125rem;
  font-weight: 600;
  margin-bottom: var(--space-3);
}

.auth-card label {
  display: block;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-secondary);
  margin-bottom: var(--space-1);
}

.auth-card input[type="email"],
.auth-card input[type="password"] {
  width: 100%;
  /* 44px: below this height the field is awkward to tap on mobile. With box-sizing:
     border-box that is the REAL height, padding included. */
  min-height: 44px;

  /* ⚠️ The same padding as the management fields: two different shapes for the same
     object show up as soon as you move from one page to the next. */
  padding: var(--space-2) var(--space-3);
  margin-bottom: var(--space-3);
  font: inherit;
  color: var(--text-primary);
  background: var(--bg-secondary);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  transition: border-color var(--motion-duration) var(--motion-easing);
}

.auth-card input:disabled {
  color: var(--text-secondary);
  background: var(--bg-hover);
}

.auth-card input:focus {
  border-color: var(--brand-light);
}

.auth-submit {
  width: 100%;
  /* 44px: below this height the target is awkward to tap on mobile. */
  min-height: 44px;
  padding: var(--space-3) var(--space-4);
  font: inherit;
  font-weight: 600;
  color: #fff;
  background: var(--brand);
  border: none;
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: background var(--motion-duration) var(--motion-easing);
}

.auth-submit:hover  { background: var(--brand-dark); }
.auth-submit:active { background: var(--brand-dark); }

/* 🔴 The error message is not just red: it carries an icon and a border too. Color on
   its own is not readable for anyone who cannot tell it apart. */
.auth-error {
  display: flex;
  gap: var(--space-2);
  align-items: flex-start;
  padding: var(--space-3);
  margin-bottom: var(--space-4);
  font-size: 0.75rem;
  color: var(--error);
  background: color-mix(in srgb, var(--error) 8%, transparent);
  border: 1px solid color-mix(in srgb, var(--error) 30%, transparent);
  border-radius: var(--radius-md);
}

.auth-error .icon { margin-top: 1px; }

.auth-check {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
  font-size: 0.75rem;
  color: var(--text-secondary);
  /* The whole row is clickable: a 13px checkbox is an awkward target. */
  cursor: pointer;
  /* A 44px row even though the checkbox itself is smaller: the touch target is the
     whole row, not the little square. */
  min-height: 44px;
}

.auth-check input { min-width: 16px; min-height: 16px; margin: 0; }

.auth-link {
  display: inline-block;
  margin-top: var(--space-4);
  font-size: 0.75rem;
  color: var(--brand);
  text-decoration: none;
}

.auth-link:hover { text-decoration: underline; }

.auth-note {
  font-size: 0.75rem;
  color: var(--text-secondary);
  margin-bottom: var(--space-4);
}

/* A side note after the form, not before: here the margin goes on top, not below. */
.auth-note-after {
  margin-top: var(--space-4);
  margin-bottom: 0;
}

/* A secondary link rendered as a solid button (the "Address confirmed" page): same
   height and radius as .auth-submit, but it is an <a>, not a <button>. */
.auth-submit-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: auto;
  text-decoration: none;
}


/* ── Map with no data ───────────────────────────────────────────────────────
   The world shows up anyway: dimmed, with a note over it. Replacing it with a line of
   text made a working panel look broken. */
.map-frame-empty .map-svg { opacity: 0.45; }

.map-overlay {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  margin: 0;
  padding: var(--space-2) var(--space-4);
  font-size: 0.8125rem;
  color: var(--text-secondary);
  background: color-mix(in srgb, var(--bg-card) 88%, transparent);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  /* The note must not steal the pointer from the map, which stays explorable. */
  pointer-events: none;
}

/* ── Cross filters ──────────────────────────────────────────────────────────
   A ranking row is a link: clicking it filters the whole dashboard. Active filters
   show up as chips, and every chip is the link that removes it. */

.rank-row .rank-name {
  color: var(--text-primary);
  text-decoration: none;
  /* The link fills the row: the target is the whole row, not the words. */
  display: block;
}

.rank-row:hover { background: var(--bg-hover); }
.rank-row:hover .rank-name { color: var(--brand); }

.rank-row .rank-name:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
  border-radius: var(--radius-md);
}

/* ── Inside a country ───────────────────────────────────────────────────────
   The server-side viewBox does the zooming: all that is left here is telling the
   framed country apart from what surrounds it. */

/* ⚠️ The neighbors stay visible, muted. Hiding them would leave a polygon floating in
   white that nobody recognizes: the borders around it are half the information a map
   carries. */
.map-outside {
  opacity: 0.35;
}

/* The path of the country you entered stands out from the rest. */
.map-svg.is-focused .map-country:not(.map-outside) {
  stroke: var(--text-primary);
  stroke-width: 0.6;
  /* The stroke width is in canvas units, and the canvas is scaled up: without this the
     border would grow with the zoom until it ate the country. */
  vector-effect: non-scaling-stroke;
}

/* The insets for detached territories: bottom right, lining up leftwards.
   ⚠️ aria-hidden and no tooltip: the main map already announces the country, and these
   are a graphic detail of it — not a second item to read out. */
.map-inset {
  position: absolute;
  bottom: var(--space-2);
  right: var(--space-2);
  width: 21%;
  /* The same proportions as the world map canvas, so a territory does not get squashed
     on its way from the big map into the inset. */
  aspect-ratio: 2 / 1;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

/* The second one goes beside the first, not on top of it. */
.map-inset + .map-inset { right: calc(21% + var(--space-2) * 2); }

.map-back {
  font-size: 0.75rem;
  color: var(--brand);
  text-decoration: none;
}

.map-back:hover { text-decoration: underline; }

.map-back:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* The clickable countries on the map: the cursor says so, the border confirms it. */
.map-clickable { cursor: pointer; }
.map-svg a:hover .map-clickable { stroke: var(--text-primary); stroke-width: 0.8; }
.map-svg a:focus-visible .map-clickable { stroke: var(--brand-dark); stroke-width: 1.2; }

.filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

.filters-label {
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-secondary);
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  /* 32px: shorter than the 44px of the primary targets, but this is a secondary action
     and always next to others. */
  min-height: 32px;
  padding: var(--space-1) var(--space-3);
  font-size: 0.75rem;
  color: var(--text-primary);
  text-decoration: none;
  background: var(--bg-card);
  border: 1px solid var(--brand-light);
  border-radius: 999px;
}

.chip:hover { background: var(--bg-hover); }

.chip:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* The dimension before the value: "Country Italy" reads, "Italy" on its own does not. */
.chip-dim { color: var(--text-secondary); }
.chip-value { font-weight: 600; }

.chip-x {
  font-size: 1rem;
  line-height: 1;
  color: var(--text-secondary);
}

.chip:hover .chip-x { color: var(--error); }

.filters-clear {
  font-size: 0.75rem;
  color: var(--text-secondary);
}

/* ── The wall tools ─────────────────────────────────────────────────────────
   They show up on hover and stay visible while anything inside the widget holds focus:
   whoever navigates by keyboard has to be able to reach them. */

.wall-tools {
  /* 🔴 Overlaid, not in the flow. They used to be `float: right`, and a float does NOT
     overlap anything that opens a formatting context of its own: the card group is a
     flex container, so it shrank to make room — and the metrics showed an empty strip
     on the right, as wide as the tools, even while the tools were invisible.

     ⚠️ The space is reserved where it costs nothing: on the title row (.panel-head),
     not across the whole width of the widget. */
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);

  /* Above the panel content: these are controls, and they must stay clickable. */
  z-index: 2;

  display: flex;
  align-items: center;
  gap: var(--space-1);
  opacity: 0;
  transition: opacity var(--motion-duration) var(--motion-easing);
}

.widget:hover .wall-tools,
.widget:focus-within .wall-tools { opacity: 1; }

.wall-tools button {
  /* ⚠️ inline-flex centered on both axes: an icon inside a button does not center
     itself — the text baseline pushes it down, and that is why they all used to sit
     crooked. */
  display: inline-flex;
  align-items: center;
  justify-content: center;

  /* Small: they are secondary tools, and they live in a corner of the panel. */
  width: 26px;
  height: 26px;
  padding: 0;
  color: var(--text-secondary);
  background: var(--bg-secondary);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  cursor: pointer;
}

/* ── The icons ──────────────────────────────────────────────────────────────
   They come from the sprite at /icons/ui.svg. Whoever uses them sets the size, the
   color they inherit from the text: they follow the theme with no extra line. */
.icon {
  width: 14px;
  height: 14px;
  /* Kills the baseline: without it the icon aligns to the text and drops. */
  display: block;
  flex: none;

  /* 🔴 The click passes through the icon and reaches the button holding it. Without
     this, the click target was the <path> inside the <use>: an icon taken from an
     EXTERNAL sprite lives in a shadow tree belonging to another document, and the
     event does not always bubble up to the button. The result was a button that
     answered on its transparent edges and stayed silent if you aimed at the icon —
     which is exactly where everyone clicks. */
  pointer-events: none;
}

.wall-tools button:hover { color: var(--text-primary); background: var(--bg-hover); }

.wall-tools button:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 1px;
  opacity: 1;
}

.wall-grip { cursor: grab; }
.wall-grip:active { cursor: grabbing; }

.wall-width {
  min-width: 1ch;
  font-size: 0.6875rem;
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}

/* The widget being dragged, and the one that would receive it. */
.wall-dragging { opacity: 0.4; }

.wall-over {
  outline: 2px dashed var(--brand-light);
  outline-offset: 2px;
}

/* ⚠️ A failed save gets said, not hidden: the layout on screen stays the one that was
   picked, but next time the page opens it will be back as it was. */
.wall-unsaved { outline: 2px solid var(--warning); outline-offset: var(--space-2); }

.wall-reset { margin-bottom: var(--space-6); }

.wall-reset button {
  font: inherit;
  font-size: 0.75rem;
  color: var(--text-secondary);
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  text-decoration: underline;
}

/* ── Maximising a widget ────────────────────────────────────────────────────
   An overlay: the panel lifts off the page, with a margin around it and a dark veil
   behind. A temporary state — it is not saved in the preferences, and Esc closes it. */

.widget.is-maximised {
  position: fixed;

  /* The margin that lifts it off the screen edges. On narrow screens it shrinks: 3em
     on a phone would be nearly all the space there is. */
  inset: 1em;
  z-index: 301;

  margin: 0;
  overflow: auto;

  /* ⚠️ Widgets without .panel — the metrics group — need a solid background here too:
     the page is underneath, and without one it would show through. */
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--elevation-card);
  /* The same padding as .panel: here the widget BEHAVES as a panel, and has to have
     the same inner air, not one of its own. */
  padding: var(--space-4);
}

@media (min-width: 768px) {
  .widget.is-maximised { inset: 3em; }
}

/* A focus outline on a full-screen panel would be a frame around the screen: here
   focus only exists to give the keyboard a starting point. */
.widget.is-maximised:focus-visible { outline: none; }

/* 🔴 Full screen the ranking reaches fifty rows: it scrolls INSIDE the panel instead
   of stretching it. The title and the dimension picker stay put at the top, which is
   the whole point of enlarging — if the panel scrolled as a whole, after ten rows you
   would no longer know what you are looking at. */
.widget.is-maximised .rank {
  max-height: calc(100vh - 16em);
  overflow-y: auto;
}

/* The tools stay reachable while maximised too, but only the ones that make sense
   here. */
.widget.is-maximised .wall-tools { opacity: 1; }

/* 🔴 Full screen, only the button that shrinks it back survives.
   The X goes because clicking it is the instinctive way to close an overlay, and that
   one hides the widget: you would leave full screen and find the panel gone, with no
   idea why.
   The grip and the two width buttons go for a related reason: here they would have no
   visible effect, yet they would still change the saved layout — a control that does
   something you cannot see. */
.widget.is-maximised .wall-hide,
.widget.is-maximised .wall-grip,
.widget.is-maximised .wall-narrow,
.widget.is-maximised .wall-wide,
.widget.is-maximised .wall-width { display: none; }

/* The veil: it says whatever lies underneath is not active. It sits on body and not
   inside the grid, because it has to cover the header too. */
.wall-locked { overflow: hidden; }

.wall-locked::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 300;
  background: rgb(9 35 40 / 55%);
}

@media (prefers-reduced-motion: no-preference) {
  .wall-locked::before { animation: wall-veil var(--motion-duration) var(--motion-easing); }

  @keyframes wall-veil {
    from { opacity: 0; }
    to { opacity: 1; }
  }
}

/* ── The hidden widgets ─────────────────────────────────────────────────────
   🔴 Without this bar, hiding a widget would simply make it vanish for good.
   It shows up only when there is at least one. */

.wall-shelf {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-6);
  padding: var(--space-3);
  background: var(--bg-secondary);
  border: 1px dashed var(--border);
  border-radius: var(--radius-md);
}

.wall-shelf-label {
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-secondary);
}

.wall-shelf-item {
  min-height: 32px;
  padding: var(--space-1) var(--space-3);
  font: inherit;
  font-size: 0.75rem;
  color: var(--text-primary);
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 999px;
  cursor: pointer;
}

.wall-shelf-item:hover { border-color: var(--brand-light); background: var(--bg-hover); }

.wall-shelf-item:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* ── The dimension picker inside a widget ───────────────────────────────────
   One widget per family — geography, technology — and this is where you pick the level
   to look at it. It is a <details>: it opens and closes without JavaScript, and the
   items stay links, so the choice ends up in the URL. */

.dim-picker {
  position: relative;
  margin-bottom: var(--space-4);
  /* 🔴 Above the ranking below it: the menu leaves the flow, and without this it would
     end up behind the widget rows. */
  z-index: 2;
}

.dim-current {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-2) var(--space-1) var(--space-3);
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-primary);
  background: var(--bg-hover);
  border-radius: var(--radius-md);
  cursor: pointer;
  /* Drops the native triangle: the caret from the sprite takes its place. */
  list-style: none;
}

.dim-current::-webkit-details-marker { display: none; }

.dim-current:hover { background: var(--bg-card); box-shadow: var(--elevation-card); }

.dim-current:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 1px;
}

.dim-caret {
  width: 14px;
  height: 14px;
  color: var(--text-secondary);
  transition: transform 0.15s ease;
}

.dim-picker[open] .dim-caret { transform: rotate(180deg); }

.dim-menu {
  position: absolute;
  top: calc(100% + var(--space-1));
  left: 0;
  min-width: 100%;
  /* ⚠️ With many dimensions the dropdown scrolls instead of spilling out of the box. */
  max-height: 15rem;
  overflow-y: auto;
  margin: 0;
  padding: var(--space-1);
  list-style: none;
  white-space: nowrap;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--elevation-card);
}

.dim-option {
  display: block;
  padding: var(--space-1) var(--space-3);
  font-size: 0.75rem;
  color: var(--text-secondary);
  text-decoration: none;
  border-radius: calc(var(--radius-md) - 2px);
}

.dim-option:hover { color: var(--text-primary); background: var(--bg-hover); }

/* ⚠️ The current item is not set apart by color alone: it has the filled background
   and the text weight, and carries aria-current for whoever sees neither. */
.dim-option.is-current {
  color: var(--text-primary);
  font-weight: 600;
  background: var(--bg-hover);
}

.dim-option:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 1px;
}

/* ── A widget being refreshed ───────────────────────────────────────────────
   ⚠️ The old content stays visible, dimmed, instead of being swapped for a skeleton:
   the panel keeps its height, the page does not jump, and whoever was reading a number
   can still see it. */

.widget.is-loading { position: relative; }

.widget.is-loading > *:not(.wall-tools) { opacity: 0.4; }

.widget.is-loading::after {
  content: "";
  position: absolute;
  /* The same offset as .panel's padding: the spinner lands in the corner of the
     content, not at some point picked on its own. */
  top: var(--space-4);
  right: var(--space-4);
  width: 14px;
  height: 14px;
  border: 2px solid var(--border);
  border-top-color: var(--brand);
  border-radius: 50%;
  animation: widget-spin 0.7s linear infinite;
}

@keyframes widget-spin {
  to { transform: rotate(360deg); }
}

/* Whoever asked for less motion sees a still dot, not a spinning wheel. */
@media (prefers-reduced-motion: reduce) {
  .widget.is-loading::after { animation: none; border-top-color: var(--brand); }
}
