/* Clave web app — "Soft workspace". One stylesheet, no build step, no
   framework: the whole surface is a dozen pages and this is smaller than
   the config a bundler would need.

   Warm paper, plum accent, rounded cards, generous spacing. It is a
   workspace rather than a shop window, so the landing page keeps its own
   voice and this one keeps the tokens below.

   Two rules govern everything here, and both were paid for in production:

   - A missing CSP directive fails silently and the page still returns
     200. `style-src 'self'` covers inline style attributes as well as
     stylesheets, so no template may carry one. Geometry therefore goes in
     SVG presentation attributes — widths of meters, points of charts —
     and colour goes in a class. `TestNoTemplateUsesAStyleAttribute`
     holds the line.
   - A class a template names and no rule matches renders as nothing, and
     nothing catches it. `TestEveryTemplateClassIsStyled` holds that one.

   Only the latin subset of each face is vendored, so a character outside
   it falls back mid-sentence. Every arrow, tick and glyph on this surface
   is drawn as inline SVG for that reason. */

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

:root {
  color-scheme: light dark;

  /* Surfaces, from the page up. */
  --page: #f3ede6;
  --panel: #fbf8f4;
  --card: #ffffff;
  --line: #e7ded4;
  --line-strong: #d2c6b9;

  /* Text, from most to least. Faint is for tertiary labels only: it
     does not meet 4.5:1 and must never carry a sentence a reader has to
     read. */
  --ink: #2b2327;
  --muted: #6f6369;
  --faint: #9d9196;

  --accent: #6b2d5c;
  --accent-hover: #5c2750;
  --on-accent: #fffefc;
  --accent-tint: #f2e6ef;

  --ok: #2f7d4a;
  --ok-tint: #e3f1e7;
  --wait: #9a6a12;
  --wait-tint: #f8ecd2;
  --bad: #b8392f;
  --bad-tint: #f9e3e0;

  --shadow-card: 0 1px 2px rgba(43, 35, 39, .05), 0 8px 24px -16px rgba(43, 35, 39, .18);
  --shadow-raised: 0 1px 2px rgba(43, 35, 39, .06);
  --shadow-lifted: 0 1px 2px rgba(43, 35, 39, .05), 0 20px 40px -24px rgba(43, 35, 39, .25);

  --text: 'Outfit', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
  --mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
}

/* The same roles, not a second palette. Shadows move to neutral black,
   because a warm shadow on a dark ground reads as a brown smear. */
@media (prefers-color-scheme: dark) {
  :root {
    --page: #1a1417;
    --panel: #231c21;
    --card: #2c242a;
    --line: #3a3037;
    --line-strong: #4c4049;

    --ink: #f4edf1;
    --muted: #b5a8af;
    --faint: #7f737a;

    --accent: #d29ac4;
    --accent-hover: #e0b0d3;
    --on-accent: #1a1417;
    --accent-tint: #3d2a38;

    --ok: #7fcf98;
    --ok-tint: #1f3a29;
    --wait: #e2b357;
    --wait-tint: #3b2f16;
    --bad: #ef8a80;
    --bad-tint: #3e1f1c;

    --shadow-card: 0 1px 2px rgba(0, 0, 0, .2), 0 8px 24px -16px rgba(0, 0, 0, .5);
    --shadow-raised: 0 1px 2px rgba(0, 0, 0, .25);
    --shadow-lifted: 0 1px 2px rgba(0, 0, 0, .2), 0 20px 40px -24px rgba(0, 0, 0, .6);
  }
}

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

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

body {
  margin: 0;
  min-height: 100vh;
  background: var(--page);
  color: var(--ink);
  font-family: var(--text);
  font-size: 15px;
  font-weight: 400;
  line-height: 1.5;
  /* Every native control the browser draws itself — the one checkbox on
     this surface and the review page's radios. Set here rather than on
     each of them, because a control this misses is drawn in the
     browser's own blue, and that blue reads as a page that lost its
     stylesheet. It is inherited, so it follows the scheme. */
  accent-color: var(--accent);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3 { margin: 0; font-weight: 500; }
h1 { font-size: 28px; letter-spacing: -.025em; }
h2 { font-size: 16px; }
h3 { font-size: 14px; }
p { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }

a { color: inherit; text-decoration: none; }

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

/* The ring follows the shape of the thing it rings, so this radius is
   only for an element that has no shape of its own — a link, a summary,
   a row. `:where()` carries no specificity, so anything that does have a
   shape keeps it: a field stays a pill while somebody types in it, and a
   button stays a button. Written plainly, this rule outranked the fields
   and squared off whichever one had the focus. */
:where(:focus-visible) { border-radius: 4px; }

/* Colour and background only. A layout property here would animate a
   reflow, and nothing on this surface is worth that. */
a, button, .nav-item, .row-hover {
  transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease;
}

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; }
}

/* ----------------------------------------------------------------- shell */

/* Two columns at the designed width. The sidebar is a fixed 216px and
   the main column takes what is left; `minmax(0, 1fr)` rather than `1fr`
   so a wide table inside it scrolls rather than pushing the grid out. */
.app {
  display: grid;
  grid-template-columns: 216px minmax(0, 1fr);
  min-height: 100vh;
}

/* The sidebar is its own viewport-tall pane, and that is what keeps the
   account row at the foot of the *window* rather than at the foot of
   whichever page is beside it. A grid item stretches to the row, the row
   is as tall as the longest column, and `margin-top: auto` on
   .sidebar-foot then pushes the account row to the bottom of a column
   that is a whole page long — so a long page put the account link below
   the fold and an owner had to scroll the page to reach it.

   `align-self: start` gives the column its own height instead of the
   row's, `height: 100vh` makes that height the window, and `sticky`
   holds it there while the page beside it scrolls. The sidebar scrolls
   inside itself when an account holds more sites than the window fits,
   so nothing here is reachable only by scrolling the main column. */
.sidebar {
  padding: 22px 14px 16px;
  display: flex;
  flex-direction: column;
  gap: 26px;
  min-width: 0;
  position: sticky;
  top: 0;
  align-self: start;
  height: 100vh;
  overflow-y: auto;
}

/* The wordmark: the name in Newsreader, and one plum dot on the baseline
   after the "e" like a full stop. There is no symbol.

   Newsreader rather than Outfit, which sets the rest of this surface.
   One mark on both browser surfaces is worth more than type consistency
   inside one of them: an owner who arrives from the landing page must
   see the same name, and a second cut of a wordmark is the same kind of
   second copy as a second stylesheet — it drifts, and only in a browser.
   The page's own type stays Outfit.

   Real text and one empty span rather than an image, for the reason
   every glyph on this surface is drawn rather than typed: it follows
   the palette in both schemes, it stays crisp at any size, and a reader
   can select it. The accessible name is the word "clave"; the dot is
   decorative. */
.logo {
  display: inline-flex;
  align-items: baseline;
  gap: .05em;
  margin: 0 12px;
  font-family: 'Newsreader', Georgia, 'Times New Roman', serif;
  font-size: 21px;
  font-weight: 500;
  letter-spacing: -.03em;
  line-height: 1;
}

/* align-items: baseline puts the bottom edge of an empty span on the
   text baseline, which is what makes the dot read as punctuation rather
   than as a bullet beside the word.

   It is --accent, the same plum as the dot that says a site is up, and
   it is the only coloured element in the mark. */
.logo-dot {
  width: .22em;
  height: .22em;
  border-radius: 50%;
  background: var(--accent);
  flex: none;
}

.nav { display: grid; gap: 3px; }

.nav-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  border-radius: 10px;
  font-size: 15px;
  padding: 8px 12px;
  color: var(--muted);
  min-width: 0;
}

.nav-item:hover { background: var(--panel); }

.nav-item.is-here {
  font-weight: 500;
  color: var(--ink);
  background: var(--panel);
  box-shadow: var(--shadow-raised);
}

/* A site or a domain under its section. Two indents, because a draft
   waiting for an answer sits under the site it belongs to. */
.nav-item.nav-sub {
  font-size: 14px;
  padding: 6px 12px 6px 36px;
}

.nav-item.nav-sub-sub {
  font-size: 14px;
  padding: 6px 12px 6px 50px;
  color: var(--accent);
}

.nav-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The count of open errands. Plum rather than faint, because it is the
   one number on this surface that asks for something. */
.nav-badge {
  font-size: 12px;
  font-weight: 600;
  color: var(--on-accent);
  background: var(--accent);
  border-radius: 999px;
  padding: 0 7px;
  line-height: 18px;
  flex: none;
}

.nav-count {
  font-size: 13px;
  color: var(--faint);
  flex: none;
}

.sidebar-foot {
  margin-top: auto;
  display: grid;
  gap: 10px;
}

.plan-meter {
  padding: 12px 14px;
  border-radius: 12px;
  background: var(--panel);
  display: grid;
  gap: 6px;
}

.plan-meter-head {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  font-size: 13px;
  color: var(--muted);
}

.plan-meter-head .plan-upgrade { color: var(--accent); font-weight: 500; }
.plan-meter-head .plan-upgrade:hover { text-decoration: underline; }

.plan-rows {
  display: grid;
  gap: 4px;
  font-size: 12px;
  color: var(--muted);
}

.plan-row { display: flex; justify-content: space-between; gap: 8px; }

/* The bar. Its length is data, and data cannot go in a style attribute
   on this surface, so the fill is a rect whose width is a presentation
   attribute out of 100. The track keeps the radius and clips. */
.meter {
  height: 4px;
  border-radius: 2px;
  background: var(--line);
  overflow: hidden;
}

.meter-bar { display: block; width: 100%; height: 4px; }
.meter-fill { fill: var(--accent); }

.account-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  border-radius: 12px;
  min-width: 0;
}

.account-row:hover { background: var(--panel); }

.account-who {
  display: grid;
  min-width: 0;
}

.account-name {
  font-weight: 500;
  font-size: 14px;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.account-what { font-size: 12px; color: var(--muted); }

.main {
  min-width: 0;
  padding: 32px 36px 36px;
  display: grid;
  gap: 28px;
  align-content: start;
}

/* A page whose sections sit closer together than Home's. */
.main.tight { gap: 22px; }

/* Below the designed width the sidebar stops being a column and becomes
   the top of the page. Not designed; this is the plainest thing that
   keeps every control reachable on a narrow screen.

   It sits *after* the rules it narrows, and that position is the whole
   of it: a media query adds no specificity, so each of these loses to a
   plain `.sidebar` or `.main` rule written below it. It was written
   above them once, and every line of it but the border was dead — the
   phone kept the desktop's gap, its padding and `margin-top: auto`. */
@media (max-width: 1023px) {
  .app { grid-template-columns: minmax(0, 1fr); }
  .sidebar {
    border-bottom: 1px solid var(--line);
    gap: 16px;
    /* A column no longer, so it takes its own height back: a 100vh
       sticky block at the top of a phone screen would be the whole
       screen and the page would start below it. */
    position: static;
    height: auto;
    overflow: visible;
  }
  .sidebar-foot { margin-top: 16px; }
  .main { padding: 24px 20px 28px; }
}

/* ------------------------------------------------------- page furniture */

.page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}

.page-head-titles { display: grid; gap: 2px; min-width: 0; }
.page-kicker { font-size: 14px; color: var(--muted); }
.page-sub { font-size: 14px; color: var(--muted); }
.page-actions { display: flex; align-items: center; gap: 10px; }

.section { display: grid; gap: 14px; }

.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}

.section-note { font-size: 14px; color: var(--muted); }

/* ------------------------------------------------------------- surfaces */

.card {
  background: var(--card);
  border-radius: 18px;
  box-shadow: var(--shadow-card);
}

/* A card whose own rows carry the padding, so a divider can run the full
   width of it. */
.card.flush { overflow: hidden; }

.card-pad { padding: 20px 22px; }

.card-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
  padding: 18px 22px 14px;
}

.card-head-spread { justify-content: space-between; }

.card-note { font-size: 14px; color: var(--muted); }

.panel-tile {
  background: var(--panel);
  border-radius: 14px;
  padding: 16px 18px;
}

.sunk-tile {
  background: var(--page);
  border-radius: 12px;
  padding: 12px 14px;
}

/* Empty, not-live and irreversible all read as "this is an outline, not a
   thing yet". One border, three uses. */
.dashed {
  border: 1.5px dashed var(--line-strong);
  border-radius: 18px;
  background: var(--panel);
}

.dashed.plain { background: transparent; }

.divided > * + * { border-top: 1px solid var(--line); }

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

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font-family: inherit;
  font-size: 14px;
  font-weight: 500;
  line-height: 1.35;
  padding: 9px 16px;
  border-radius: 999px;
  border: 1px solid transparent;
  background: none;
  color: var(--ink);
  cursor: pointer;
  white-space: nowrap;
  text-align: center;
}

.button.primary {
  background: var(--accent);
  color: var(--on-accent);
}

.button.primary:hover { background: var(--accent-hover); }

.button.secondary {
  background: var(--panel);
  border-color: var(--line);
}

.button.secondary:hover { background: var(--line); }

/* The same button inside a white card, where a filled one would read as
   a second surface. */
.button.outline {
  background: none;
  border-color: var(--line-strong);
}

.button.outline:hover { background: var(--panel); }

.button.danger {
  background: var(--bad);
  color: var(--on-accent);
  border-color: transparent;
}

.button.danger:hover { filter: brightness(.94); }

/* A page-level action, one step larger than a row's. */
.button.big { padding: 10px 18px; }

/* Offered but not yet possible: it says what it is waiting for rather
   than vanishing, so nobody wonders where the control went. */
.button[disabled],
.button.waiting {
  background: var(--panel);
  border-color: var(--line);
  color: var(--faint);
  cursor: default;
  box-shadow: none;
}

.button.waiting:hover { background: var(--panel); }

.link {
  color: var(--accent);
  font-size: 14px;
  font-weight: 500;
}

.link:hover { text-decoration: underline; }

/* The same minimum, for a link that is an action rather than a word in a
   sentence: Manage and Everything beside a heading, Versions and Review
   in a row of state, and the one under a card that opens another page.
   The class is the one prose uses as well, so this names the places a
   link stands on its own — every one of them a direct child, which is
   what keeps a link inside a paragraph out of it. An inline-flex link in
   a sentence could not wrap on to a second line. */
.section-head > .link,
.card-head > .link,
.state-row > .link,
.card > .link {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
}

.link.quiet {
  color: var(--muted);
  font-weight: 400;
}

/* A submit button that has to read as a link, because the action is a
   POST and a link cannot carry one. */
.linkish {
  font: inherit;
  font-size: 14px;
  font-weight: 500;
  color: var(--accent);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  /* A box a finger can hit. 14px text on a 21px line is under the 24px
     a touch target needs, and every one of these is a control of its
     own inside its own form rather than a word in a sentence — the case
     the rule makes an exception for. The words keep their size and the
     box grows around them. The gap is what the flex box takes away:
     whitespace between the words and a glyph is dropped once they are
     flex items. */
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 24px;
}

.linkish:hover { text-decoration: underline; }

.linkish.quiet { color: var(--muted); font-weight: 400; }

/* A chip is a control small enough to sit inside a row. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 500;
  color: var(--ink);
  background: var(--page);
  border: 0;
  border-radius: 8px;
  padding: 5px 10px;
  cursor: pointer;
  white-space: nowrap;
}

.chip:hover { background: var(--line); }

.chip.on-sunk {
  background: var(--card);
  color: var(--accent);
}

.chip.danger { color: var(--bad); }

/* ------------------------------------------------------------- labelling */

.pill {
  display: inline-flex;
  align-items: center;
  font-size: 12px;
  font-weight: 600;
  border-radius: 999px;
  padding: 3px 10px;
  white-space: nowrap;
  color: var(--muted);
  background: var(--panel);
}

.pill.plum { color: var(--accent); background: var(--accent-tint); }
.pill.green { color: var(--ok); background: var(--ok-tint); }
.pill.amber { color: var(--wait); background: var(--wait-tint); }
.pill.red { color: var(--bad); background: var(--bad-tint); }

/* Live or not. Never wraps: a status that breaks across two lines reads
   as two statuses. */
.state {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  font-weight: 500;
  white-space: nowrap;
  color: var(--muted);
}

.state.live { color: var(--ok); }

.dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--line-strong);
  flex: none;
}

.dot.green { background: var(--ok); }
.dot.amber { background: var(--wait); }
.dot.red { background: var(--bad); }
.dot.plum { background: var(--accent); }

/* Not live is a hollow ring rather than a grey disc: absent reads
   differently from off. */
.dot.hollow {
  background: none;
  border: 1.5px solid var(--line-strong);
}

.avatar {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--accent-tint);
  color: var(--accent);
  display: grid;
  place-items: center;
  font-weight: 600;
  font-size: 12px;
  flex: none;
}

.avatar.big { width: 34px; height: 34px; font-size: 13px; }
.avatar.amber { background: var(--wait-tint); color: var(--wait); }

.mono { font-family: var(--mono); font-size: .95em; }

.muted { color: var(--muted); }
.faint { color: var(--faint); }
.plum { color: var(--accent); }

.fineprint { font-size: 13px; color: var(--faint); line-height: 1.5; }

/* ------------------------------------------------------------ switching */

/* One control, two jobs: a range of days, a filter of sites. Each option
   is a link with a query parameter, so it works with no script at all. */
.segmented {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  background: var(--page);
  border-radius: 999px;
  padding: 3px;
  font-size: 13px;
}

.segmented > * {
  padding: 5px 12px;
  border-radius: 999px;
  color: var(--muted);
  white-space: nowrap;
}

.segmented > .is-here {
  background: var(--card);
  color: var(--ink);
  font-weight: 500;
  box-shadow: var(--shadow-raised);
}

.tabs {
  display: flex;
  gap: 4px;
  border-bottom: 1px solid var(--line);
  overflow-x: auto;
}

.tab {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 14px;
  padding: 8px 12px;
  color: var(--muted);
  white-space: nowrap;
}

.tab:hover { color: var(--ink); }

.tab.is-here {
  color: var(--ink);
  font-weight: 500;
  box-shadow: inset 0 -2px 0 var(--accent);
}

.tab-count { font-size: 13px; color: var(--faint); }

/* ------------------------------------------------------------- timeline */

/* One rule down the left, a dot per entry sitting on it. The dot's own
   border is the card colour, so the rule appears to pass behind it. */
.timeline { display: grid; gap: 16px; }

.timeline-row {
  display: grid;
  grid-template-columns: 12px minmax(0, 1fr) auto;
  gap: 0 12px;
  position: relative;
}

.timeline-row::before {
  content: "";
  position: absolute;
  left: 5px;
  top: 14px;
  bottom: -16px;
  width: 2px;
  background: var(--line);
}

.timeline-row:last-child::before { display: none; }

.timeline-dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  border: 2px solid var(--card);
  background: var(--line-strong);
  margin-top: 3px;
  position: relative;
  z-index: 1;
}

.timeline-dot.plum { background: var(--accent); }
.timeline-dot.green { background: var(--ok); }
.timeline-dot.red { background: var(--bad); }

.timeline-body { display: grid; gap: 2px; min-width: 0; }
.timeline-what { font-size: 14px; }
.timeline-meta { font-size: 13px; color: var(--muted); }
.timeline-when { font-size: 13px; color: var(--faint); white-space: nowrap; }

/* ---------------------------------------------------------------- forms */

label {
  display: block;
  font-size: 14px;
  font-weight: 500;
  color: var(--ink);
  margin-bottom: 6px;
}

/* Every field, named by what it is not.

   It named `text` and `email` once, and the types nobody listed fell
   through it in silence. Two did, on the account page alone: the Stripe
   key fields are `password`, so they drew as the browser's own grey box
   beside these pills; and the token name carries no `type` at all, so it
   was set in the browser's font rather than in Outfit. Each renders as
   valid markup, returns 200 and passes every test — the family this
   surface keeps meeting.

   `:where()` carries no specificity, so the whole selector weighs what
   `input` alone weighs. That is what lets a single class still dress one
   field differently: .token-name is a borderless name in a row, and it
   must not be given the shape of a form field. */
input:where(:not([type="checkbox"], [type="radio"], [type="file"],
                 [type="submit"], [type="button"], [type="hidden"])),
select,
textarea {
  font-family: inherit;
  font-size: 15px;
  color: var(--ink);
  background: var(--card);
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  padding: 10px 16px;
  width: 100%;
  max-width: 420px;
}

textarea {
  border-radius: 14px;
  max-width: 100%;
  line-height: 1.5;
}

input:focus-visible, select:focus-visible, textarea:focus-visible {
  border-color: var(--accent);
}

.field { display: grid; gap: 0; margin: 0; }
.hint { font-size: 13px; color: var(--muted); margin-top: 6px; }
.actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 14px; }

/* A field that is not a field: the shape of an input, holding the words
   that would be in one, on a screen where the thing is not writable yet. */
.placeholder-input {
  background: var(--page);
  border-radius: 999px;
  padding: 10px 16px;
  color: var(--faint);
  font-size: 15px;
}

.error {
  font-size: 14px;
  color: var(--bad);
}

.banner {
  border-radius: 14px;
  padding: 14px 18px;
  font-size: 14px;
  display: flex;
  align-items: flex-start;
  gap: 10px;
}

.banner.green { background: var(--ok-tint); color: var(--ok); }
.banner.amber { background: var(--wait-tint); color: var(--wait); }
.banner.red { background: var(--bad-tint); color: var(--bad); }

/* A step that cannot be undone asks twice, and the second ask is inline
   rather than a dialog: this page works with JavaScript switched off,
   and the irreversible button is the wrong one to make depend on it. */
.confirm {
  border: 1.5px dashed var(--bad);
  border-radius: 14px;
  background: var(--bad-tint);
  padding: 16px 18px;
  display: grid;
  gap: 12px;
  font-size: 14px;
  color: var(--ink);
}

/* --------------------------------------------------------------- glyphs */

/* Drawn, not typed. Every one of these lives outside the vendored latin
   subset, so the character itself would fall back to a system face
   mid-line. */
.glyph {
  flex: none;
  vertical-align: -1px;
}

.glyph-round {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  flex: none;
}

.glyph-round.green { background: var(--ok-tint); color: var(--ok); }
.glyph-round.red { background: var(--bad-tint); color: var(--bad); }
.glyph-round.plum { background: var(--accent-tint); color: var(--accent); }

.glyph-round.big { width: 44px; height: 44px; }

/* ---------------------------------------------------- site thumbnails */

/* A wireframe, not a picture. Nothing on this platform takes a
   screenshot of a live site, so a tile pretending to be one would be the
   worse kind of placeholder — it would look like a feature. This says
   "a site" and no more. */
.thumb {
  margin: 10px 10px 0;
  aspect-ratio: 16 / 9;
  border-radius: 12px;
  background: var(--page);
  overflow: hidden;
  display: block;
  width: calc(100% - 20px);
}

.thumb-line { fill: var(--line); }
.thumb-line-strong { fill: var(--line-strong); }

/* The tile for a site with no page to sketch. Not dashed: the card
   around it already is, and two dashed edges read as two empty things. */
.thumb.empty {
  display: grid;
  place-items: center;
  align-content: center;
  gap: 2px;
  padding: 16px;
  text-align: center;
  font-size: 14px;
  line-height: 1.4;
  color: var(--muted);
}

.thumb-hint { font-size: 13px; color: var(--faint); }

/* ------------------------------------------------------------ bare frame */

/* Every page somebody reaches from a link in an inbox: sign-in, the
   agent's grant, an upload link, an invitation, an outcome. No sidebar,
   because the person answering may not have an account here at all, and
   a list of somebody's sites is the wrong thing to put in front of them. */
.bare {
  min-height: 100vh;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  align-content: start;
}

.bare-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 20px 28px;
}

.bare-who {
  display: flex;
  align-items: center;
  gap: 12px;
  font-size: 14px;
}

.bare-main {
  padding: 28px 28px 48px;
  display: grid;
  gap: 18px;
  justify-items: center;
  align-content: start;
}

/* The one measure every bare page keeps. 500px is the grant card's, and
   the rest follow it so two links from the same inbox do not open two
   differently shaped pages. */
.bare-main > * { width: 100%; max-width: 500px; }

/* A page that is a list rather than a decision — the upload receipt,
   the assets grid — needs more than a card's width. */
.bare-main.wide > * { max-width: 760px; }

.back {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  justify-self: start;
}

/* ------------------------------------------------------------------ home */

/* Two columns, the left one wider. The reading matter is on the left and
   the reference is on the right, which is also the order they collapse
   into on a narrow screen. */
.split {
  display: grid;
  grid-template-columns: minmax(0, 1.45fr) minmax(0, 1fr);
  gap: 14px;
  align-items: start;
}

@media (max-width: 1023px) {
  .split { grid-template-columns: minmax(0, 1fr); }
}

.stack-tight { display: grid; gap: 12px; align-content: start; }

/* One row of the "Waiting for you" card: a pill, what to do, a button.
   The pill's column is fixed so three rows read as one list rather than
   as three sentences at three indents. */
.errand {
  display: grid;
  grid-template-columns: 128px minmax(0, 1fr) auto;
  gap: 0 20px;
  align-items: center;
  padding: 14px 22px;
}

.errand > .pill { justify-self: start; }
.errand-say { display: grid; gap: 1px; min-width: 0; }
.errand-title { font-weight: 500; }
.errand-detail { font-size: 14px; }

@media (max-width: 767px) {
  .errand {
    grid-template-columns: minmax(0, 1fr);
    gap: 10px;
    justify-items: start;
  }
}

/* Three across, and three whatever the count. A grid that reflows to
   fill the row makes one site's card the width of the page, which reads
   as a different kind of thing from the same card in a row of three. */
.site-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 14px;
}

@media (max-width: 1279px) {
  .site-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 767px) {
  .site-grid { grid-template-columns: minmax(0, 1fr); }
}

.site-card {
  display: grid;
  align-content: start;
  background: var(--card);
  border-radius: 18px;
  overflow: hidden;
  box-shadow: var(--shadow-card);
}

/* A site that is not live is the shape of a site rather than a site, so
   it carries the dashed border and no shadow. */
.site-card.dashed {
  box-shadow: none;
  background: var(--panel);
}

.site-card-body {
  padding: 14px 18px 18px;
  display: grid;
  gap: 4px;
  min-width: 0;
}

.site-card-top {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

.site-card-name {
  font-weight: 500;
  font-size: 16px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.site-card-host {
  font-size: 14px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.site-card-foot {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  margin-top: 10px;
  padding-top: 12px;
  border-top: 1px solid var(--line);
  font-size: 14px;
}

.site-card-where { font-weight: 500; }

.timeline-verb { font-weight: 500; }

.agent-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  padding-top: 14px;
}

.agent-row:first-child { padding-top: 0; }
.agent-who { display: grid; gap: 1px; min-width: 0; }

.agent-name {
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.agent-used { font-size: 14px; }

.agent-foot {
  padding-top: 14px;
  border-top: 1px solid var(--line);
  font-size: 14px;
}

/* ----------------------------------------------------------- first run */

.first-run {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 380px;
  gap: 28px;
  padding: 28px 30px;
  /* Each column as tall as what is in it. Without this the aside is a
     grid item stretched to the three steps beside it, so the first
     screen a new account opens carries a panel with about 180px of
     nothing under two lines and a button. */
  align-items: start;
}

@media (max-width: 1023px) {
  .first-run { grid-template-columns: minmax(0, 1fr); }
}

.first-run-say { display: grid; gap: 14px; align-content: start; }

.big-title { font-size: 22px; letter-spacing: -.02em; line-height: 1.25; }

.first-run-aside {
  background: var(--page);
  border-radius: 14px;
  padding: 20px 22px;
  display: grid;
  gap: 10px;
  justify-items: start;
}

.first-run-none { font-size: 15px; font-weight: 500; }

.steps { display: grid; gap: 14px; margin-top: 4px; }

.step {
  display: grid;
  grid-template-columns: 30px minmax(0, 1fr);
  gap: 0 14px;
  align-items: start;
}

/* The one filled circle is where the reader is, and the outlined ones
   are what comes after. Numbers in a circle rather than a list marker,
   because a browser's own marker cannot be given a ring. */
.step-number {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: 13px;
  font-weight: 600;
  border: 1.5px solid var(--line-strong);
  color: var(--muted);
}

.step-number.filled {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}

.step-body { display: grid; gap: 2px; padding-top: 3px; }
.step-title { font-weight: 500; }
.step-note { font-size: 14px; }

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

.empty-card {
  padding: 18px 20px;
  display: grid;
  gap: 6px;
  align-content: start;
}

.empty-card-title { font-size: 15px; font-weight: 500; }

/* -------------------------------------------------------------- enquiries */

/* The list on the left, the message on the right. The list is a fixed
   360px because it holds one line of somebody's words and a wider column
   would only hold more of a line nobody reads in full. */
.inbox {
  display: grid;
  grid-template-columns: 360px minmax(0, 1fr);
  gap: 14px;
  align-items: start;
}

@media (max-width: 1023px) {
  .inbox { grid-template-columns: minmax(0, 1fr); }
}

.message-list { display: grid; gap: 6px; align-content: start; }

.message-row {
  background: var(--panel);
  border-radius: 14px;
  padding: 14px 16px;
  display: grid;
  gap: 4px;
  min-width: 0;
}

.message-row:hover { background: var(--line); }

/* The one that is open: white, and ringed rather than bordered, so the
   row does not change size when it is chosen. */
.message-row.is-here {
  background: var(--card);
  box-shadow: 0 0 0 2px var(--accent);
}

.message-row.is-here:hover { background: var(--card); }

.message-row-top {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
}

.message-who {
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.message-when { font-size: 13px; color: var(--faint); white-space: nowrap; }

/* One line of it. What a visitor wrote is a paragraph, and a list is not
   where a paragraph belongs. */
.message-preview {
  font-size: 14px;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.message-flag { justify-self: start; padding: 1px 8px; }

.inbox-detail { display: grid; gap: 14px; align-content: start; }

.message-card { padding: 24px 26px; display: grid; gap: 18px; }

.message-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 12px;
  flex-wrap: wrap;
}

.message-head-who { display: grid; gap: 2px; min-width: 0; }
.message-name { font-size: 20px; font-weight: 500; letter-spacing: -.01em; }
.message-meta { font-size: 14px; color: var(--muted); }

/* Larger than the page's body text and measured, because this is the one
   block on the surface somebody reads rather than scans. */
.message-body {
  font-size: 17px;
  line-height: 1.55;
  max-width: 56ch;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.message-rest {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 6px 16px;
  margin: 0;
  font-size: 14px;
}

.message-field { color: var(--muted); }
.message-value { margin: 0; overflow-wrap: anywhere; }

.chips { display: flex; gap: 8px; flex-wrap: wrap; font-size: 13px; }

/* A chip that says something rather than doing something. Same shape as
   the control, no hover, no pointer. */
.chip.static {
  border-radius: 999px;
  padding: 4px 10px;
  color: var(--muted);
  cursor: default;
  font-weight: 400;
}

.chip.static:hover { background: var(--page); }
.chip.static.amber-chip { background: var(--wait-tint); color: var(--wait); }

/* Three facts across, one under the other on a narrow screen. */
.facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 12px;
  font-size: 14px;
}

.fact { display: grid; gap: 1px; min-width: 0; }
.fact-label { font-size: 13px; }
.fact-value { overflow-wrap: anywhere; }
.warn-text { color: var(--wait); }

/* The segmented control on a page whose background is the page colour,
   where the track needs an edge of its own to read as a track. */
.segmented.framed {
  background: var(--panel);
  border: 1px solid var(--line);
}

.empty-state {
  padding: 56px 32px;
  display: grid;
  justify-items: center;
  gap: 8px;
  text-align: center;
  background: none;
}

.empty-state-title { font-size: 18px; font-weight: 500; }
.empty-state-say { max-width: 44ch; }
.empty-state-do { margin-top: 8px; }

.fault { display: grid; gap: 6px; }
.fault-form { font-size: 13px; color: var(--faint); }
.fault-lead { font-size: 14px; color: var(--wait); }

.fault-reasons { display: grid; gap: 4px; font-size: 14px; }

.fault-reasons li {
  display: grid;
  grid-template-columns: 3ch minmax(0, 1fr);
  gap: 10px;
}

.fault-count { font-weight: 500; text-align: right; }

/* The word that tells a site somebody shared from a site you own. */
.site-card-shared { margin-right: auto; }

/* ------------------------------------------------------------------ site */

.stack { display: grid; gap: 14px; align-content: start; }

/* A page that is one column of decisions rather than two of readings:
   people, settings. Wide enough for a sentence and no wider. */
.narrow-column {
  display: grid;
  gap: 14px;
  align-content: start;
  max-width: 620px;
}

.site-title {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}

/* The state beside a 28px heading inherits its tracking otherwise, and
   the dot's label is 13px, where -.025em is a squeeze. */
.site-title .state { letter-spacing: 0; }

/* A page inside a site names itself under the site's own heading, so its
   title is a heading rather than the heading. */
.sub-heading { font-size: 20px; letter-spacing: -.02em; }

.card-title { margin-bottom: 10px; }

/* Rows inside a card, divided by a rule that runs to the card's padding
   rather than to the page. */
.rows > * { border-top: 1px solid var(--line); }

/* What the public sees, what is saved since, what is waiting. The label
   column is fixed so the three read as one answer. */
.state-row {
  display: grid;
  grid-template-columns: 150px minmax(0, 1fr) auto;
  gap: 0 16px;
  align-items: center;
  padding: 12px 0;
}

.state-row.is-waiting { color: var(--accent); }
.state-row.is-waiting .link { color: inherit; }
.state-label { color: var(--muted); font-size: 14px; }
.state-row.is-waiting .state-label { color: inherit; }
.state-say { min-width: 0; overflow-wrap: anywhere; }
.state-strong { font-weight: 500; }

@media (max-width: 767px) {
  .state-row {
    grid-template-columns: minmax(0, 1fr);
    gap: 4px;
    justify-items: start;
  }
}

.visitors { display: grid; gap: 16px; }

.figures { display: flex; gap: 40px; align-items: flex-end; flex-wrap: wrap; }
.figure { display: grid; gap: 2px; }
.figure-big { font-size: 34px; font-weight: 500; letter-spacing: -.03em; line-height: 1; }
.figure-small { font-size: 22px; font-weight: 500; letter-spacing: -.02em; line-height: 1; }
.figure-label { font-size: 14px; }

/* The chart is scaled to whatever width it is given, which is why the
   line carries vector-effect: a non-uniform scale would otherwise
   stretch a 2px stroke into a wedge. */
.chart { display: block; width: 100%; height: 72px; }

.chart-line { stroke: var(--accent); }
.chart-peak { fill: var(--accent); }

/* stop-color is a CSS property, so the wash follows the theme rather
   than being painted into the markup. */
.wash-top { stop-color: var(--accent); stop-opacity: .22; }
.wash-foot { stop-color: var(--accent); stop-opacity: 0; }

.chart-ticks {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  font-size: 13px;
  color: var(--faint);
}

.ranked { display: grid; font-size: 14px; }

.ranked-row {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 8px 0;
  border-top: 1px solid var(--line);
}

.version-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 9px 0;
  font-size: 14px;
}

.version-id { font-weight: 500; flex: none; }
.version-when { flex: 1; color: var(--muted); }

/* The same row on its own tab, where it has room for a button and for
   the confirmation under it. */
.version-full {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: 8px 14px;
  align-items: center;
  padding: 14px 22px;
  font-size: 14px;
}

.version-confirm { grid-column: 1 / -1; }

.faces { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.faces .avatar { margin-right: -14px; }
.faces .avatar:last-of-type { margin-right: 0; }
.faces-say { font-size: 14px; margin-left: 6px; }

.person {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: 12px;
  align-items: center;
  padding-top: 14px;
}

.person:first-child { padding-top: 0; }
.person-who { display: grid; gap: 1px; min-width: 0; }

.person-name {
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.person-email { font-size: 14px; }

/* The one row with no avatar beside it: an invitation is an address and
   not yet a person. */
.person:not(:has(.avatar)) { grid-template-columns: minmax(0, 1fr) auto; }

/* The wash under the line. Its fill is the gradient, named in the
   markup, so this carries only the rule that it draws no edge. */
.chart-area { stroke: none; }

/* A timeline in the narrow column, where there is no room for a time on
   the right, so the time goes into the line under the sentence. */
.timeline-row.narrow { grid-template-columns: 12px minmax(0, 1fr); }

/* Drawn in nothing. It exists to carry a bucket's name to a pointer and
   to a screen reader, which an area chart has no other place for. */
.chart-hit { fill: transparent; }

/* A list of rows rather than a column of decisions: versions. Wider
   than the settings measure, because a row here carries an id, a date
   and a control, and narrower than the page, because three rows across
   a thousand pixels read as a page that failed to load. */
.wide-column {
  display: grid;
  gap: 14px;
  align-content: start;
  max-width: 820px;
}

/* ---------------------------------------------------------------- domain */

/* Four equal boxes. The reader's own step is filled, the ones behind it
   are ticked, and the ones ahead are outlined — so the page says which
   stage is theirs before it says what to do. */
.stepper {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 8px;
}

@media (max-width: 1023px) {
  .stepper { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 599px) {
  .stepper { grid-template-columns: minmax(0, 1fr); }
}

.step-box {
  display: grid;
  grid-template-columns: 26px minmax(0, 1fr);
  gap: 0 10px;
  align-items: center;
  padding: 12px 14px;
  border-radius: 14px;
  background: var(--panel);
  color: var(--muted);
  min-width: 0;
}

.step-box.is-done {
  background: var(--card);
  color: var(--ink);
  box-shadow: var(--shadow-raised);
}

.step-box.is-now {
  background: var(--accent);
  color: var(--on-accent);
}

.step-mark {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: 12px;
  font-weight: 600;
  border: 1.5px solid currentColor;
}

.step-box.is-done .step-mark {
  border-color: var(--ok);
  color: var(--ok);
}

.step-box.is-now .step-mark {
  border-color: currentColor;
}

.step-box-say { display: grid; gap: 1px; min-width: 0; }
.step-box-title { font-size: 14px; font-weight: 500; }
.step-box-note { font-size: 13px; opacity: .8; }

/* A value somebody has to copy into another company's website. Sunk into
   the page colour so it reads as a field, with the button in the card
   colour beside it. */
.values { display: grid; gap: 8px; }

.value-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  background: var(--page);
  border-radius: 12px;
  padding: 10px 12px;
}

.value-text { font-size: 14px; overflow-wrap: anywhere; }

.records { display: grid; }

.record {
  display: grid;
  grid-template-columns: 70px 130px minmax(0, 1fr);
  gap: 0 12px;
  align-items: baseline;
  padding: 10px 0;
  border-top: 1px solid var(--line);
  font-size: 14px;
}

@media (max-width: 767px) {
  .record { grid-template-columns: 70px minmax(0, 1fr); }
  .record .record-content { grid-column: 1 / -1; }
}

.record-type { color: var(--muted); }
.record-name { overflow-wrap: anywhere; }
.record-content { overflow-wrap: anywhere; color: var(--muted); }

/* The records this page exists to make somebody look at. */
.record.is-mail .record-content { color: var(--ink); }

.domain-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 14px;
}

.domain-card { display: grid; gap: 6px; align-content: start; }

.domain-card-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  min-width: 0;
}

.domain-name {
  font-size: 16px;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.domain-card-say { font-size: 14px; }

/* Two ways to look after a domain, offered by what each one means. The
   radio is the whole row, so the words are the target. */
.choices {
  display: grid;
  gap: 10px;
  border: 0;
  padding: 0;
  margin: 16px 0 0;
}

.choices legend {
  padding: 0;
  font-size: 14px;
  font-weight: 500;
  margin-bottom: 4px;
}

.choice {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 12px;
  align-items: start;
  padding: 14px 16px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--panel);
  font-weight: 400;
  margin: 0;
  cursor: pointer;
}

.choice:hover { border-color: var(--line-strong); }
.choice:has(input:checked) { border-color: var(--accent); background: var(--card); }
.choice input { margin: 3px 0 0; }
.choice-main { display: grid; gap: 4px; min-width: 0; }
.choice-name { font-weight: 500; }
.choice-why { font-size: 14px; color: var(--muted); }

/* The label on a value row, for a record with three fields to enter
   rather than one value to paste. */
.value-label {
  font-size: 13px;
  color: var(--muted);
  width: 72px;
  flex: none;
}

/* ---------------------------------------------------------------- review */

/* The approval page is reached from a chat window as often as from this
   app, so it keeps the bare frame and its own measure. */
.review { display: grid; gap: 18px; align-content: start; }

/* The list of pages on the left, the draft itself on the right. The
   right side is the wider one here, because it holds two rendered pages
   and the left holds a column of routes. */
.review-split {
  display: grid;
  grid-template-columns: 300px minmax(0, 1fr);
  gap: 14px;
  align-items: start;
}

@media (max-width: 1023px) {
  .review-split { grid-template-columns: minmax(0, 1fr); }
}

/* What the agent said, in its own words, above everything this page
   worked out by comparing. */
.agent-note {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 14px;
  align-items: start;
}

.note-face { background: var(--accent); color: var(--on-accent); }
.note-say { display: grid; gap: 4px; min-width: 0; }
.note-who { font-size: 13px; color: var(--muted); }
.note-words { font-size: 15px; line-height: 1.55; }

.page-rows { display: grid; }

.page-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 22px;
  border-top: 1px solid var(--line);
  font-size: 14px;
}

.page-route { overflow-wrap: anywhere; }

/* Changed, new and the same are three different weights of news, so
   they are three colours and the least interesting is the faintest. */
.page-status {
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
  color: var(--faint);
}

.page-status.is-modified, .page-status.is-changed { color: var(--wait); }
.page-status.is-added, .page-status.is-new { color: var(--ok); }
.page-status.is-removed, .page-status.is-deleted { color: var(--bad); }

/* The lines the agent wrote. Removed struck through as well as tinted,
   because a reader scanning two colours should not have to remember
   which is which. */
.diff { display: grid; gap: 6px; }

.diff-file {
  font-size: 13px;
  color: var(--muted);
  cursor: pointer;
}

.diff-lines {
  display: grid;
  border-radius: 10px;
  overflow: hidden;
  font-family: var(--mono);
  font-size: 12.5px;
  line-height: 1.5;
}

.diff-line {
  padding: 2px 10px;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.diff-line.is-removed {
  background: var(--bad-tint);
  color: var(--bad);
  text-decoration: line-through;
}

.diff-line.is-added {
  background: var(--ok-tint);
  color: var(--ok);
}

.pair { display: grid; gap: 14px; }
.pair.is-both { grid-template-columns: repeat(2, minmax(0, 1fr)); }

@media (max-width: 1279px) {
  .pair.is-both { grid-template-columns: minmax(0, 1fr); }
}

/* One page, at the width a phone gives it. A frame narrower than its
   own content is what makes "it is wrong on my phone" something an owner
   can check here. */
.pair.is-phone .frame { max-width: 390px; }

.frame { margin: 0; display: grid; gap: 8px; }

.frame-cap {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  font-size: 13px;
}

.frame-cap-draft { font-weight: 500; }

.frame-box {
  aspect-ratio: 4 / 3;
  border-radius: 14px;
  overflow: hidden;
  background: var(--card);
  box-shadow: var(--shadow-card);
}

/* The draft is ringed rather than shadowed, which is the same mark the
   selected row in a list carries: this is the one of the two you are
   deciding about. */
.frame-box.is-draft { box-shadow: 0 0 0 2px var(--accent); }

.frame-box iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}

.frame-box.empty {
  display: grid;
  place-content: center;
  gap: 6px;
  padding: 24px;
  text-align: center;
  background: var(--panel);
  border: 1.5px dashed var(--line-strong);
  box-shadow: none;
  color: var(--muted);
}

.source-files { display: grid; gap: 6px; }
.source-files summary { cursor: pointer; }

/* ---------------------------------------------------------------- sheets */

/* One decision on a page of its own: sign in, the agent's grant, a
   connector's consent, an invitation, an upload link, an outcome.
   One measure and one shape, because two links from the same inbox
   should not open two differently proportioned pages. */
.sheet {
  border-radius: 20px;
  padding: 32px;
  display: grid;
  gap: 22px;
  align-content: start;
  box-shadow: var(--shadow-lifted);
}

.sheet-head { display: grid; gap: 8px; justify-items: start; }

.sheet-title {
  font-size: 26px;
  font-weight: 500;
  letter-spacing: -.02em;
  line-height: 1.2;
}

.sheet-form { display: grid; gap: 6px; }

/* The code the reader compares against their own session. It is the part
   that decides the page, so it is the largest thing on it. */
.code-panel {
  background: var(--page);
  border-radius: 14px;
  padding: 20px;
  display: grid;
  gap: 6px;
  text-align: center;
}

.code-label { font-size: 13px; color: var(--muted); }

.code {
  font-size: 36px;
  font-weight: 600;
  letter-spacing: .04em;
  line-height: 1.1;
  overflow-wrap: anywhere;
}

/* A second field under the first, where the label would otherwise sit
   flush against the input above it. */
.second-label { margin-top: 14px; }

.uploads { display: grid; gap: 6px; }

.upload {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 14px;
  border-radius: 12px;
  font-size: 14px;
  background: var(--panel);
}

.upload.ok { background: var(--ok-tint); color: var(--ok); }
.upload .path { font-weight: 500; overflow-wrap: anywhere; }
.upload .note { white-space: nowrap; }

/* A refused file carries a sentence where an accepted one carries a
   size, and a sentence is not a value in a row. `nowrap` was written for
   the size and burst this row open on the other: the reason ran on one
   line far past the card, the page scrolled sideways, and the name
   beside it was squeezed to one character a line. So a refusal stacks,
   and its reason wraps. */
.upload.bad {
  display: grid;
  gap: 4px;
  background: var(--bad-tint);
  color: var(--bad);
}

.upload.bad .note { white-space: normal; }

input[type="file"] {
  font: inherit;
  font-size: 14px;
  padding: 10px 0;
  width: 100%;
}

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

/* Three parts: the name, which is a field; what it is and when it was
   used; and the way to take it back. Two rows on a narrow card, because
   a credential's name is free text and can be long. */
.token {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 4px 12px;
  align-items: center;
  padding-top: 16px;
}

.token:first-child { padding-top: 0; }

.token-rename {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

/* The name is the field. A borderless input that takes its edge on
   hover, so the row reads as a name rather than as a form. */
.token-name {
  font-size: 15px;
  font-weight: 500;
  background: none;
  border: 1px solid transparent;
  border-radius: 10px;
  padding: 4px 8px;
  max-width: 100%;
  min-width: 0;
  flex: 1;
}

.token-name:hover { border-color: var(--line); }
.token-name:focus-visible { border-color: var(--accent); background: var(--panel); }

.token-meta {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  font-size: 13px;
  color: var(--muted);
  padding-left: 9px;
}

/* One place the owner takes money into. A block rather than a row: the
   sentence under the facts is the part that has to be read, because
   "taking money" and "ready" are a bank detail apart. */
.payout {
  display: grid;
  gap: 8px;
  padding: 16px 0;
  border-top: 1px solid var(--line);
}

.payout:first-of-type { border-top: 0; padding-top: 0; }
/* The row of buttons under the facts sits close to them, and that is
   what this is for. A form's own buttons are not that row: they follow a
   field and need the gap every other form on this surface has, so this
   reaches the direct child alone. */
.payout > .actions { margin-top: 0; }

/* Help that is read once, and shut until it is. The words under it are
   what somebody making a key at another company needs in front of them,
   and they are longer than everything else in the panel — open by
   default they bury the field they are about. */
.help { font-size: 13px; color: var(--faint); line-height: 1.5; }
.help summary { cursor: pointer; color: var(--muted); }
.help ul { display: grid; gap: 6px; margin-top: 8px; }

.token-revoke { grid-row: 1; grid-column: 2; }
.danger-link { color: var(--bad); }

/* Read by a screen reader, drawn nowhere. The field above it is the
   whole control, so a visible label would say the same word twice. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------------------------------------------------------------- assets */

.asset-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 14px;
}

.asset { display: grid; align-content: start; overflow: hidden; }

.asset-frame, .asset-doc {
  display: block;
  aspect-ratio: 4 / 3;
  background: var(--page);
  overflow: hidden;
}

.asset-thumb {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* No thumbnail: nothing here can draw a PDF, and a broken image frame
   would read as a file that failed to arrive. */
.asset-doc { display: grid; place-items: center; }

.asset-doc-mark {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .04em;
  color: var(--muted);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 6px 10px;
}

.asset-facts {
  padding: 12px 14px 14px;
  display: grid;
  gap: 2px;
  min-width: 0;
}

.asset-name { font-size: 14px; font-weight: 500; overflow-wrap: anywhere; }
.asset-path { font-size: 12px; color: var(--muted); overflow-wrap: anywhere; }
.asset-size { font-size: 13px; color: var(--muted); }
.asset-url { font-size: 12px; color: var(--muted); overflow-wrap: anywhere; }
.asset-credit { font-size: 12px; color: var(--faint); }

/* A plan somebody could move to: the button and why, on one line. */
.buy { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.buy-why { font-size: 14px; }

.account-row.is-here {
  background: var(--panel);
  box-shadow: var(--shadow-raised);
}

/* Beside the name field rather than under it, and quiet: submitting an
   unchanged name is a no-op, so this is a confirmation and not a step. */
.token-save { flex: none; }

/* --- Bookings: what visitors booked, as receipts. --- */

/* A heading over a run of cards, where the page has two lists and each
   needs a word. Quieter than an h1, because the page already has one. */
.section-heading {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0;
}

.booking { display: grid; gap: 12px; }

/* The time and the state on one line, because those are the two things
   an owner reads before anything else. */
.booking-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}

.booking-when { display: grid; gap: 2px; }
.booking-what { font-size: 14px; color: var(--muted); }

/* Who, how to reach them, and where. A definition list rather than a
   table: there are three of them and a table of three rows is furniture. */
.booking-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  gap: 10px 16px;
  margin: 0;
}

.booking-facts div { display: grid; gap: 2px; }
.booking-facts dt { font-size: 12px; color: var(--faint); }
.booking-facts dd { margin: 0; font-size: 14px; overflow-wrap: anywhere; }

.booking-actions {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}

/* A box and its words on one line, so the two are one target. */
.checkbox {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
}

/* --- passkeys ---------------------------------------------------------

   The panel reuses .token for its rows, because a passkey and an agent
   credential are the same shape on the page: a name you can change, when
   it last worked, and the way to take it away. Two classes are its own.

   .needs-webauthn is the control the script reveals. It carries the
   hidden attribute in the markup, so a browser without WebAuthn — or
   with JavaScript off — never sees a button that cannot reach an
   authenticator. The rule is what the reveal restores it to: display
   is set here rather than left to the attribute alone, because
   [hidden] is display:none and a grid or flex child needs a value to
   come back to. */
.needs-webauthn {
  display: grid;
  gap: 8px;
}

.needs-webauthn[hidden] { display: none; }

/* The panel's own message line, which the script writes into and the
   server renders on a full page. It carries .error for its colour and
   this for the hook, so the two cannot disagree about the words. */
.passkey-error { margin: 0; }

.passkey-error[hidden] { display: none; }
