/* Mapcanto operator console.
   Every colour, space, radius and type value comes from tokens.css, which is
   generated from product/design-tokens.json. No literal hex belongs here. */

@import url("tokens.css");

:root {
  --shell: min(var(--size-content-max-width), 100% - (2 * var(--space-lg)));
}

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

/* Several base rules set display, which would otherwise win over the UA's
   rule for [hidden] and leave a hidden element on screen. */
[hidden] { display: none !important; }

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

body {
  margin: 0;
  min-height: 100vh;
  background: var(--color-surface);
  color: var(--color-text-primary);
  font-family: var(--type-family-sans);
  font-size: var(--type-body-size);
  line-height: var(--type-body-line);
  font-weight: var(--type-body-weight);
}

h1, h2, h3 { margin: 0; text-wrap: balance; }
h1 { font-size: var(--type-title-size); line-height: var(--type-title-line); font-weight: var(--type-title-weight); letter-spacing: -0.01em; }
h2 { font-size: var(--type-headline-size); line-height: var(--type-headline-line); font-weight: var(--type-headline-weight); }
h3 { font-size: var(--type-label-size); line-height: var(--type-label-line); font-weight: var(--type-label-weight); }
p { margin: 0; }
code, pre, .mono { font-family: var(--type-family-mono); font-size: var(--type-code-size); line-height: var(--type-code-line); }
pre { margin: 0; overflow-x: auto; }

.helper { color: var(--color-text-muted); font-size: var(--type-caption-size); line-height: var(--type-caption-line); }
/* access.helper is deliberately the empty string in the contract: the Google
   button says what it does and needs no caption. An empty helper renders
   nothing rather than an empty line. */
.helper:empty { display: none; }
.muted { color: var(--color-text-muted); }
.secondary { color: var(--color-text-secondary); }

a { color: var(--color-primary); text-underline-offset: 3px; }
a:hover { color: var(--color-primary-hover); }

:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: var(--focus-ring-width) solid var(--color-focus);
  outline-offset: var(--focus-ring-offset);
  border-radius: var(--radius-sm);
}

.skip {
  position: absolute;
  left: var(--space-md);
  top: var(--space-md);
  z-index: 30;
  padding: var(--space-sm) var(--space-md);
  background: var(--color-primary);
  color: var(--color-on-primary);
  border-radius: var(--radius-sm);
  font-weight: var(--type-label-weight);
  text-decoration: none;
  transform: translateY(calc(-100% - var(--space-xxl)));
  transition: transform var(--motion-fast-ms) var(--motion-easing);
}
.skip:focus-visible { transform: none; }

.shell { width: var(--shell); margin-inline: auto; }

/* ---------- header + navigation ---------- */

.app-header {
  position: sticky;
  top: 0;
  z-index: 10;
  background: var(--color-surface-raised);
  border-bottom: 1px solid var(--color-border);
}
.app-header > .shell {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm) var(--space-md);
  min-height: var(--size-control-height-large);
  padding-block: var(--space-sm);
}
.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  color: var(--color-text-primary);
  font-weight: var(--type-title-weight);
  font-size: var(--type-label-size);
  letter-spacing: 0.02em;
  text-decoration: none;
  margin-inline-end: auto;
}
.brand small {
  color: var(--color-text-muted);
  font-weight: var(--type-body-weight);
  letter-spacing: 0;
}

/* Who holds this session, when the server knows. A token session names nobody
   and this stays hidden rather than showing a placeholder. */
.operator {
  color: var(--color-text-secondary);
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line);
}
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---------- sign-in ----------
   The only screen an operator sees before a session exists, and so the only one
   with nothing to navigate to: no header bar, no primary nav. It is a single
   card, centred, sitting on the worldbase graticule. Everything scoped to
   .sign-in belongs to that screen alone. */

body.sign-in {
  display: flex;
  flex-direction: column;
  /* Meridians and parallels at the contract's cell with every fourth drawn
     heavier, a lit horizon above and the corners taken down — the map this
     console serves, at the contrast of a watermark. It is fixed so the card
     never drags the grid around with it. */
  background-color: var(--color-surface);
  background-image:
    radial-gradient(80% 55% at 50% 0%, var(--color-sign-in-glow), transparent 70%),
    radial-gradient(90% 70% at 50% 50%, transparent 30%, var(--color-sign-in-vignette) 100%),
    repeating-linear-gradient(to right, var(--color-sign-in-graticule-major) 0 var(--size-hairline), transparent var(--size-hairline) calc(4 * var(--size-graticule-cell))),
    repeating-linear-gradient(to bottom, var(--color-sign-in-graticule-major) 0 var(--size-hairline), transparent var(--size-hairline) calc(4 * var(--size-graticule-cell))),
    repeating-linear-gradient(to right, var(--color-sign-in-graticule) 0 var(--size-hairline), transparent var(--size-hairline) var(--size-graticule-cell)),
    repeating-linear-gradient(to bottom, var(--color-sign-in-graticule) 0 var(--size-hairline), transparent var(--size-hairline) var(--size-graticule-cell));
  background-attachment: fixed;
}

.sign-in main {
  display: flex;
  flex: 1 1 auto;
  padding: var(--space-xxl) var(--space-lg);
}

.sign-in-card {
  /* auto margins centre the card on both axes and, unlike align-items: center,
     cannot push its own top off the top of a short viewport. */
  margin: auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
  width: min(var(--size-sign-in-card-width), 100%);
  padding: var(--space-xl);
  border: var(--size-hairline) solid var(--color-border);
  border-radius: var(--radius-lg);
  background: var(--color-sign-in-card);
  box-shadow: var(--shadow-card);
  text-align: center;
}

/* The mark carries the identity on this screen, so it is shown at a size worth
   looking at rather than at the size it takes in a header bar. */
.sign-in-lockup {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-sm);
}
.sign-in-mark {
  display: block;
  width: var(--size-sign-in-mark);
  height: var(--size-sign-in-mark);
}
.wordmark {
  color: var(--color-text-secondary);
  font-size: var(--type-label-size);
  line-height: var(--type-label-line);
  font-weight: var(--type-title-weight);
  letter-spacing: 0.12em;
}

.sign-in-card h1 {
  font-size: var(--type-display-size);
  line-height: var(--type-display-line);
  font-weight: var(--type-display-weight);
}

.sign-in-card .when-signed-out { gap: var(--space-lg); }
.sign-in-card .when-denied { gap: var(--space-md); }

/* One frame per screen. The refusal is a tinted notice inside the card, not a
   second bordered box drawn on top of the first. */
.sign-in-card .message {
  padding: var(--space-md);
  border-radius: var(--radius-md);
  background: var(--color-surface-overlay);
  color: var(--color-text-secondary);
}
.sign-in-card .message[data-tone="error"] {
  border-color: var(--color-danger);
  background: var(--color-danger-surface);
  color: var(--color-danger);
}

/* The Google button is the one control here that does not take its look from
   the console's tokens. Google fixes the surface, text colour, border, corner
   radius, height, type and the 18px mark, and a button restyled to match a
   dark console is a button people stop recognising. Every value below is an
   --identity-* token generated from design-tokens.json#/identity. Do not tune
   them. Resting is flat; hover and focus lift it by Google's own elevation;
   pressed drops the lift and darkens the surface. */
.btn-google {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--identity-google-label-gap);
  align-self: start;
  min-width: var(--identity-google-min-width);
  max-width: 100%;
  min-height: var(--identity-google-height);
  padding-inline: var(--identity-google-padding-x);
  border: 1px solid var(--identity-google-border);
  border-radius: var(--identity-google-radius);
  background: var(--identity-google-surface);
  color: var(--identity-google-text);
  font-family: var(--type-family-sans);
  font-size: var(--identity-google-font-size);
  font-weight: var(--identity-google-font-weight);
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--motion-fast-ms) var(--motion-easing),
              box-shadow var(--motion-fast-ms) var(--motion-easing);
}
.btn-google:hover,
.btn-google:focus-visible {
  background: var(--identity-google-surface-hover);
  color: var(--identity-google-text);
  box-shadow: var(--identity-google-shadow-hover);
}
.btn-google:active {
  background: var(--identity-google-surface-pressed);
  box-shadow: none;
}
.btn-google img {
  width: var(--identity-google-mark-size);
  height: var(--identity-google-mark-size);
  flex: none;
}
/* In the card it is the primary action and takes the card's width. */
.sign-in-card .btn-google { align-self: stretch; }

/* The break-glass path. Findable, and not an offer: plain text, no border, no
   fill, no explanation of what it is for. A hairline above it puts it below
   the primary action rather than beside it. */
.disclosure > summary {
  /* list-item, so the UA's own disclosure triangle stays: without it this is a
     line of text nobody would think to press. */
  display: list-item;
  width: fit-content;
  color: var(--identity-disclosure-text);
  font-size: var(--type-label-size);
  line-height: var(--type-label-line);
  cursor: pointer;
}
.disclosure > summary:hover { color: var(--identity-disclosure-text-hover); }
.disclosure > summary::marker { color: var(--color-text-muted); }
.disclosure > .stack,
.disclosure > .stack-lg { margin-block-start: var(--space-md); }

.sign-in-card .disclosure {
  padding-block-start: var(--space-lg);
  border-block-start: var(--size-hairline) solid var(--color-border);
}
.sign-in-card .disclosure > summary {
  /* Centred under the button; the marker rides inline so it centres with it. */
  margin-inline: auto;
  list-style-position: inside;
}
/* Revealed, it is a form again: labels, field and message read from the left. */
.sign-in-card .disclosure > .stack {
  margin-block-start: var(--space-lg);
  text-align: start;
}
.sign-in-card .row > .btn { flex: 1 1 auto; }

.sign-in .app-footer {
  border-block-start: 0;
  text-align: center;
}

.primary-nav { border-bottom: 1px solid var(--color-border); background: var(--color-surface); }
.primary-nav ul {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  list-style: none;
  margin: 0;
  padding-block: var(--space-xs);
  padding-inline: 0;
}
.primary-nav a {
  display: inline-block;
  padding: var(--space-xs) var(--space-sm);
  border-radius: var(--radius-sm);
  color: var(--color-text-secondary);
  font-size: var(--type-label-size);
  text-decoration: none;
}
.primary-nav a:hover { background: var(--color-surface-raised); color: var(--color-text-primary); }
.primary-nav a[aria-current="page"] {
  background: var(--color-surface-overlay);
  color: var(--color-text-primary);
  font-weight: var(--type-label-weight);
}

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

main { display: block; padding-block: var(--space-lg) var(--space-xxl); }
.stack { display: flex; flex-direction: column; gap: var(--space-md); }
.stack-lg { display: flex; flex-direction: column; gap: var(--space-lg); }
.row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm); }
.row-between { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-sm); }
.screen-head { display: flex; flex-direction: column; gap: var(--space-xs); }

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

.panel {
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-md);
}
.panel-danger { border-color: var(--color-danger); }

.card-label {
  display: block;
  color: var(--color-text-muted);
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line);
}
.card-value {
  display: block;
  margin-top: var(--space-xxs);
  font-size: var(--type-title-size);
  line-height: var(--type-title-line);
  font-weight: var(--type-title-weight);
  font-variant-numeric: tabular-nums;
}
.card-value.small { font-size: var(--type-headline-size); line-height: var(--type-headline-line); }

/* ---------- definition rows ---------- */

.facts { display: grid; grid-template-columns: max-content 1fr; gap: var(--space-xs) var(--space-md); margin: 0; }
.facts dt { color: var(--color-text-muted); font-size: var(--type-caption-size); line-height: var(--type-body-line); }
.facts dd { margin: 0; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }

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

.table-scroll { overflow-x: auto; border: 1px solid var(--color-border); border-radius: var(--radius-md); }
table { width: 100%; border-collapse: collapse; font-size: var(--type-label-size); }
caption { text-align: left; padding: var(--space-sm) var(--space-md); color: var(--color-text-muted); font-size: var(--type-caption-size); }
th, td { text-align: left; padding: var(--space-sm) var(--space-md); border-bottom: 1px solid var(--color-border); vertical-align: top; }
thead th { color: var(--color-text-muted); font-weight: var(--type-label-weight); font-size: var(--type-caption-size); white-space: nowrap; }
tbody tr:last-child th, tbody tr:last-child td { border-bottom: 0; }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }

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

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  min-height: var(--size-control-height);
  padding-inline: var(--space-md);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  background: var(--color-surface-overlay);
  color: var(--color-text-primary);
  font: inherit;
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  text-decoration: none;
  cursor: pointer;
}
.btn:hover { border-color: var(--color-primary); }
.btn[disabled] { opacity: 0.5; cursor: not-allowed; }
.btn-primary { background: var(--color-primary); border-color: var(--color-primary); color: var(--color-on-primary); }
.btn-primary:hover { background: var(--color-primary-hover); border-color: var(--color-primary-hover); }
.btn-danger { background: var(--color-danger); border-color: var(--color-danger); color: var(--color-on-primary); }
.btn-quiet { background: transparent; }
.btn-small { min-height: auto; padding: var(--space-xxs) var(--space-sm); font-size: var(--type-caption-size); }

label, .field > span { display: block; font-size: var(--type-label-size); font-weight: var(--type-label-weight); }
.field { display: flex; flex-direction: column; gap: var(--space-xs); }
.field-inline { flex-direction: row; align-items: center; gap: var(--space-sm); }
.field-inline label { font-weight: var(--type-body-weight); }

input[type="text"], input[type="password"], input[type="number"], input[type="date"], select, textarea {
  width: 100%;
  min-height: var(--size-control-height);
  padding: var(--space-xs) var(--space-sm);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text-primary);
  font: inherit;
}
textarea { min-height: calc(4 * var(--size-control-height)); resize: vertical; font-family: var(--type-family-mono); font-size: var(--type-code-size); }
input::placeholder, textarea::placeholder { color: var(--color-text-muted); }
.checks { display: flex; flex-direction: column; gap: var(--space-xs); }
.checks label { display: flex; align-items: baseline; gap: var(--space-sm); font-weight: var(--type-body-weight); }
.checks label > span { min-width: 0; overflow-wrap: anywhere; }
.checks input { flex: none; }
.form-grid { display: grid; gap: var(--space-md); grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
/* A fieldset is min-content wide by default, so one long tileset slug would
   push it out of a narrow dialog instead of wrapping. */
fieldset { margin: 0; padding: var(--space-md); border: 1px solid var(--color-border); border-radius: var(--radius-md); min-inline-size: 0; }
legend { padding-inline: var(--space-xs); font-size: var(--type-label-size); font-weight: var(--type-label-weight); }

/* ---------- addresses ---------- */

.address {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
}
.address code {
  flex: 1 1 min(100%, 320px);
  padding: var(--space-sm);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  overflow-wrap: anywhere;
}
.secret {
  display: block;
  padding: var(--space-md);
  border: 1px solid var(--color-primary);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  overflow-wrap: anywhere;
}

/* ---------- badges ---------- */

.badge {
  display: inline-block;
  padding: var(--space-xxs) var(--space-sm);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-pill);
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line);
  white-space: nowrap;
}
.badge[data-tone="ready"], .badge[data-tone="active"], .badge[data-tone="synced"], .badge[data-tone="valid"] { border-color: var(--color-success); color: var(--color-success); }
.badge[data-tone="warning"], .badge[data-tone="stale"], .badge[data-tone="expiringSoon"], .badge[data-tone="degraded"], .badge[data-tone="restricted"] { border-color: var(--color-warning); color: var(--color-warning); }
.badge[data-tone="danger"], .badge[data-tone="revoked"], .badge[data-tone="expired"], .badge[data-tone="missing"], .badge[data-tone="unavailable"] { border-color: var(--color-danger); color: var(--color-danger); }
.badge[data-tone="info"], .badge[data-tone="public"] { border-color: var(--color-info); color: var(--color-info); }

/* ---------- meters ---------- */

.meter {
  display: block;
  height: var(--space-sm);
  border-radius: var(--radius-pill);
  background: var(--color-surface-overlay);
  overflow: hidden;
}
.meter > span { display: block; height: 100%; background: var(--color-primary); }
.meter[data-over="true"] > span { background: var(--color-danger); }

/* A bar chart drawn from the usage rows. No chart library, no canvas. */
.bars { display: flex; align-items: flex-end; gap: var(--space-xxs); height: 140px; padding-top: var(--space-sm); }
.bars > div { flex: 1 1 0; display: flex; flex-direction: column; justify-content: flex-end; height: 100%; min-width: 2px; }
.bars > div > span { display: block; background: var(--color-primary); border-radius: var(--radius-sm) var(--radius-sm) 0 0; min-height: 1px; }

/* ---------- messages ---------- */

.message {
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface-raised);
  color: var(--color-text-secondary);
}
.message[data-tone="error"] { border-color: var(--color-danger); color: var(--color-danger); }
.message[data-tone="warning"] { border-color: var(--color-warning); color: var(--color-warning); }
.message[data-tone="success"] { border-color: var(--color-success); color: var(--color-success); }
.message .btn { margin-inline-start: var(--space-sm); }

.toast-region {
  position: fixed;
  inset-block-end: var(--space-lg);
  inset-inline: 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-sm);
  pointer-events: none;
}
.toast {
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-success);
  border-radius: var(--radius-pill);
  background: var(--color-surface-overlay);
  color: var(--color-text-primary);
  box-shadow: var(--shadow-raised);
  font-size: var(--type-label-size);
}
.toast[data-tone="error"] { border-color: var(--color-danger); }

/* ---------- state machine ----------
   Every screen carries the states declared for it in product/flows.json on
   [data-state]. The stylesheet decides what shows; the scripts only set it.

   These rules only ever HIDE. Toggling a block back on with `display: revert`
   would revert the whole author origin, quietly throwing away the `display:
   grid` its own component rule sets — so a shown block simply has no display
   declaration from here at all.

   Exclusive blocks — .when-<state> — appear in exactly one state. Three
   persistent classes carry the parts of a screen that outlive a state change:

     .when-ready-persist  a form, hidden only while the screen is still loading
     .when-present        loaded content that must survive a failed action
     .when-loaded         data, hidden whenever there is none to show */

[data-state]:not([data-state="loading"]) .when-loading,
[data-state]:not([data-state="empty"]) .when-empty,
[data-state]:not([data-state="error"]) .when-error,
[data-state]:not([data-state="ready"]) .when-ready,
[data-state]:not([data-state="submitting"]) .when-submitting,
[data-state]:not([data-state="success"]) .when-success,
[data-state]:not([data-state="copied"]) .when-copied,
[data-state]:not([data-state="revoked"]) .when-revoked,
[data-state]:not([data-state="quotaExceeded"]) .when-quota-exceeded,
[data-state]:not([data-state="needsKey"]) .when-needs-key,
[data-state]:not([data-state="previewFailed"]) .when-preview-failed,
[data-state]:not([data-state="noClearPass"]) .when-no-clear-pass,
[data-state]:not([data-state="dateUnavailable"]) .when-date-unavailable,
[data-state]:not([data-state="restricted"]) .when-restricted,
[data-state]:not([data-state="degraded"]) .when-degraded,
[data-state]:not([data-state="pending"]) .when-pending,
[data-state]:not([data-state="rendering"]) .when-rendering,
[data-state]:not([data-state="verifying"]) .when-verifying,
[data-state]:not([data-state="activationBlocked"]) .when-activation-blocked,
[data-state]:not([data-state="denied"]) .when-denied,
[data-state]:not([data-state="redirecting"]) .when-redirecting { display: none; }

/* .when-signed-out is the access screen's sign-in controls: gone while the
   screen is still working out what this deployment can do, gone once the
   browser is leaving for Google, and gone behind a refusal, which carries its
   own way to try again. */
[data-state="loading"] .when-signed-out,
[data-state="redirecting"] .when-signed-out,
[data-state="denied"] .when-signed-out { display: none; }

[data-state="loading"] .when-ready-persist,
[data-state="loading"] .when-present,
[data-state="empty"] .when-present,
[data-state="noClearPass"] .when-present,
[data-state="loading"] .when-loaded,
[data-state="empty"] .when-loaded,
[data-state="error"] .when-loaded { display: none; }

[data-state="submitting"] button[type="submit"] { opacity: 0.5; pointer-events: none; }

.skeleton {
  border-radius: var(--radius-sm);
  background: var(--color-surface-overlay);
  min-height: var(--size-control-height-large);
  animation: pulse 1.4s var(--motion-easing) infinite;
}

@keyframes pulse { 0%, 100% { opacity: 0.45; } 50% { opacity: 0.8; } }

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

.map-frame {
  position: relative;
  height: clamp(320px, 52vh, 560px);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--color-map-water);
}
.map-frame .maplibregl-map { position: absolute; inset: 0; }
/* The map surface's canvas is the screen, not an illustration beside one, so it
   gets the height a map is actually read at. */
.map-frame-tall {
  --map-frame-height: clamp(420px, 64vh, 760px);
  height: var(--map-frame-height);
}
/* A frame cut to the shape of its corpus. Cape May's bounding box is all but
   square in Web Mercator, so a frame as wide as the shell fitted the county by
   height and left a third of its width as empty water either side. The ratio is
   written on by the screen from the tileset's own bounds -- never a number
   here, because the next corpus is a different shape -- and the frame takes the
   smaller of the width that ratio asks for and the width it is given. Below
   that width the height comes down with it, so the picture fills the frame at
   every size rather than letterboxing at one end or the other. */
.map-frame-fit {
  /* Square until the screen says otherwise. imagery.js writes the corpus's own
     ratio onto the element, which is an inline style and wins over this. */
  --frame-aspect: 1;
  width: min(100%, calc(var(--frame-aspect) * var(--map-frame-height)));
  height: auto;
  aspect-ratio: var(--frame-aspect);
  max-height: var(--map-frame-height);
  margin-inline: auto;
}
.attribution-note { color: var(--color-text-muted); font-size: var(--type-caption-size); }

/* The two scrubbers. A label, a track and a value on each -- there is nothing
   else on either to style, because there is nothing else on either to say. The
   tracks are given the full width they can get: forty stops across a narrow
   input puts two pictures inside one pixel of travel, and the archive is on its
   way to a thousand.

   One grid rather than two rows, because only the date row carries the toggle
   and two independent flex rows would start their tracks at different x. The
   rows share four columns -- toggle, label, track, value -- and a row with no
   toggle leaves the first one empty instead of reserving a spacer element to
   do the same job with an extra node. */
.scrubbers {
  display: grid;
  grid-template-columns: auto auto minmax(0, 1fr) auto;
  align-items: center;
  column-gap: var(--space-md);
  row-gap: var(--space-sm);
}
.scrubber {
  display: contents;
}
.scrubber > .btn { grid-column: 1; }
.scrubber > label { grid-column: 2; }
.scrubber > input[type="range"] { grid-column: 3; }
/* The ticks are painted on the track, so the list belongs in the track's own
   cell. It is zero-height and draws nothing itself, so sharing the cell with
   the input it decorates costs nothing; auto-placing it would push it onto a
   row of its own and pull the cloud track down a line. */
.scrubber > datalist { grid-column: 3; }
.scrubber > .scrubber-value { grid-column: 4; }
/* Placed explicitly. Auto-placement puts the datalist and the track it shares a
   column with on different rows. */
.scrubbers > .scrubber:first-child > * { grid-row: 1; }
.scrubbers > .scrubber:last-child > * { grid-row: 2; }
/* The toggle keeps the width of its wider word, so swapping Play for Pause does
   not shove the track sideways under the handle mid-playback. */
.scrubber .btn {
  flex: none;
  min-width: 7ch;
}
.scrubber label {
  font-size: var(--type-label-size);
  line-height: var(--type-label-line);
  font-weight: var(--type-label-weight);
  color: var(--color-text-secondary);
  flex: none;
}
.scrubber input[type="range"] {
  flex: 1 1 auto;
  min-width: 0;
  height: var(--size-control-height);
  margin: 0;
  background: transparent;
  accent-color: var(--color-primary);
  cursor: pointer;
}
.scrubber input[type="range"]:disabled { cursor: default; opacity: 0.5; }
.scrubber input[type="range"]:focus-visible {
  outline: var(--focus-ring-width) solid var(--color-focus);
  outline-offset: var(--focus-ring-offset);
  border-radius: var(--radius-sm);
}
/* The datalist ticks are drawn by the browser on the track. Firefox and Chrome
   disagree about the height they reserve for them, so the track is given a
   fixed one and the ticks are left to sit in it. */
.scrubber datalist {
  display: block;
  writing-mode: horizontal-tb;
  height: 0;
}
/* The value beside each handle: a date on one row, a percentage on the other,
   and nothing else on either. Tabular figures and a reserved width, so neither
   readout shuffles its track sideways as its handle moves between a one- and a
   two-digit day or a two- and a three-digit percent. Both rows reserve the same
   width because they are one grid column. */
.scrubber-value {
  flex: none;
  min-width: 10ch;
  text-align: right;
  font-size: var(--type-body-size);
  line-height: var(--type-body-line);
  color: var(--color-text-primary);
  font-variant-numeric: tabular-nums;
}
/* Narrow: each track takes its own line under its label, so the columns the
   grid existed to align have nothing left to align and the rows go back to
   being rows. */
@media (max-width: 640px) {
  .scrubbers { display: block; }
  .scrubber { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-md); }
  .scrubber + .scrubber { margin-block-start: var(--space-sm); }
  .scrubber input[type="range"] { flex-basis: 100%; order: 3; }
}

/* Canvas first, then what the colours mean and where to fetch them. The side
   column has a width the addresses fit in; under that it stacks. */
.map-layout {
  display: grid;
  gap: var(--space-md);
  grid-template-columns: minmax(0, 1fr) minmax(280px, 360px);
  align-items: start;
}
@media (max-width: 959px) {
  .map-layout { grid-template-columns: minmax(0, 1fr); }
}
.map-side { min-width: 0; }

/* The legend reads as a list of swatch/meaning pairs. The swatch colour is set
   from the corpus's own theme at runtime, which is why there is no colour here:
   a value typed in this file would be a second opinion about what the renderer
   drew. */
.legend { display: grid; grid-template-columns: max-content 1fr; gap: var(--space-xxs) var(--space-sm); margin: 0; }
.legend dd { margin: 0; font-size: var(--type-caption-size); line-height: var(--type-body-line); }
.legend dt { display: flex; align-items: center; min-height: var(--type-body-line); }
.swatch {
  display: inline-block;
  width: var(--space-md);
  height: var(--space-md);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
}
/* A ramp that names what it is measuring. The heading spans both columns by
   ending its own row, so the swatches under it stay aligned. */
.legend-heading {
  grid-column: 1 / -1;
  color: var(--color-text-muted);
  font-size: var(--type-caption-size);
  margin-block-start: var(--space-xs);
}
.legend-heading + .legend-heading-spacer { display: none; }

/* ---------- the key table's row control ---------- */

/* Opening a key's detail opens a dialog on this screen, not a navigation, so
   the control is a button. It is drawn as the link it replaced: the row still
   reads as somewhere to go. */
.link-button {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: var(--color-primary);
  text-align: left;
  cursor: pointer;
  text-underline-offset: 3px;
}
.link-button:hover { color: var(--color-primary-hover); text-decoration: underline; }

/* ---------- dialogs ---------- */

dialog {
  max-width: min(var(--size-content-max-width), 100% - (2 * var(--space-lg)));
  padding: var(--space-lg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface-raised);
  color: var(--color-text-primary);
}
dialog::backdrop { background: var(--color-surface); opacity: 0.72; }

/* createKey and keyDetail. The dialog itself is the scroller, so a short
   viewport scrolls the form inside it rather than the page behind, and the
   action row is pinned to its bottom edge so it never scrolls out of reach.
   Its padding lives on the parts, because a sticky row in a padded scroller
   floats a padding's height above the edge it is meant to sit on. */
.modal {
  width: min(var(--size-content-max-width), 100% - (2 * var(--space-lg)));
  max-height: calc(100dvh - (2 * var(--space-lg)));
  padding: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--color-surface);
}
.modal-narrow { width: min(var(--size-reading-max-width), 100% - (2 * var(--space-lg))); }
.modal-body { padding: var(--space-lg); }
.modal-actions {
  position: sticky;
  bottom: 0;
  padding: var(--space-md) var(--space-lg);
  border-top: 1px solid var(--color-border);
  background: var(--color-surface);
}
/* The page behind a modal holds still. */
html:has(dialog:modal) { overflow: hidden; }

/* ---------- footer ---------- */

.app-footer {
  border-top: 1px solid var(--color-border);
  padding-block: var(--space-lg);
  color: var(--color-text-muted);
  font-size: var(--type-caption-size);
}
.app-footer p { margin-block: var(--space-xxs); }

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

@media (max-width: 719px) {
  .facts { grid-template-columns: 1fr; gap: var(--space-xxs) 0; }
  .facts dd { margin-bottom: var(--space-sm); }
  /* The card keeps its width and gives back the gutter instead, so the heading
     still sits on one line at the narrowest viewport worth drawing. */
  .sign-in main { padding: var(--space-xl) var(--space-md); }
  .sign-in-card { padding: var(--space-lg); }
  .modal-body { padding: var(--space-md); }
  .modal-actions { padding-inline: var(--space-md); }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: var(--motion-reduced-motion-ms) !important;
    animation-iteration-count: 1 !important;
    transition-duration: var(--motion-reduced-motion-ms) !important;
  }
}
