/* Client area — the ServerDale design system.
   Tokens are lifted verbatim from serverdale.com/static/css/site.css so the
   cabinet reads as the same product. No inline styles anywhere: the content
   security policy forbids them, which is what keeps an injected attribute
   from becoming an injected style. */

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

:root {
  --brand: #fc9400;
  --brand-strong: #ed8406;
  --brand-soft: #fca63f;
  --brand-tint: #fff6e9;
  /* Brand as small text on its own tint: --brand-strong reads 2.48:1 there in
     the light theme (measured 2026-08-23 for the admin, again 2026-09-04 for
     the order page); this darker ink clears 4.5:1 and stays orange. */
  --brand-on-tint: #a85800;

  --bg: #ffffff;
  --bg-alt: #f7f7f8;
  --bg-raised: #ffffff;
  --bg-inset: #f1f1f3;

  --text: #1c1d21;
  --text-muted: #62646c;
  --text-faint: #8b8d96;

  --border: #e3e4e8;
  --border-strong: #cfd1d7;

  --ok: #1a8a4a;
  /* --ok is a component colour: it fills pills and tints their backgrounds,
     where it sits on its own 10 % wash. --ok-text is the same idea as plain
     text on a panel, where it has to clear 4.5:1 on its own — the component
     green does not (4.39:1 on white, 3.86:1 on the dark panel, measured
     2026-08-23). Money in the reader's favour is written in this one. */
  --ok-text: #127a3f;
  --warn: #b26a00;
  --err: #c0392b;
  /* Inks for warning and informational TEXT on a panel — the same idea as
     --ok-text: the component --warn reads 4.24:1 on white and 3.75:1 on its
     own wash (measured 2026-09-05); these clear 4.5:1 in both themes. */
  --warn-text: #8f5600;
  --info-text: #1d5fae;
  /* The one solid destructive fill (the disk wipe's button) keeps the deep
     red in both themes: white on the lifted dark --err is 2.95:1. */
  --err-solid: #c0392b;

  /* the announcement strip's inks (announce.go); each dark block lifts
     them so the icon and border clear 3:1 on the tinted band */
  --ann-info: #2a78d6;
  --ann-warn: #b26a00;
  --ann-crit: #c0392b;

  --radius: 12px;
  --radius-sm: 8px;
  --radius-lg: 20px;

  --shadow-sm: 0 1px 2px rgb(16 18 24 / 6%), 0 1px 3px rgb(16 18 24 / 4%);
  --shadow: 0 4px 12px rgb(16 18 24 / 8%), 0 1px 3px rgb(16 18 24 / 5%);
  --shadow-lg: 0 18px 40px rgb(16 18 24 / 12%), 0 3px 10px rgb(16 18 24 / 6%);

  --wrap: 1180px;

  --font: "Inter var", "Inter", system-ui, -apple-system, "Segoe UI", Roboto,
          "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif;
  --mono: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas,
          "Liberation Mono", monospace;

  --logo-dark: #17181d;
  --logo-brand: #fc9400;

  --glass: rgb(255 255 255 / 88%);

  color-scheme: light;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #101116;
    --bg-alt: #16171d;
    --bg-raised: #1b1c23;
    --bg-inset: #22242c;
    --text: #ecedf1;
    --text-muted: #a3a6b2;
    --text-faint: #767a88;
    --border: #2a2c35;
    --border-strong: #3a3d48;
    --brand-tint: #2a1f10;
    --brand-on-tint: #f5a83c;
    --ok-text: #3fbf7f;
    --warn-text: #e3a544;
    --info-text: #6aa6ee;
    /* The component colours lifted for the dark panel, as the admin has them:
       the light ones read 3.9:1 (ok), 4.0:1 (warn) and 3.1:1 (err) there. */
    --ok: #34c274;
    --warn: #e0a030;
    --err: #ef6f5f;
    --err-solid: #c0392b;
    --logo-dark: #ecedf1;
    --ann-info: #6aa6ee;
    --ann-warn: #e3a544;
    --ann-crit: #ea6f61;
    --shadow-sm: 0 1px 2px rgb(0 0 0 / 40%);
    --shadow: 0 4px 14px rgb(0 0 0 / 45%);
    --shadow-lg: 0 20px 44px rgb(0 0 0 / 55%);
    --glass: rgb(16 17 22 / 82%);
    color-scheme: dark;
  }
}

:root[data-theme="dark"] {
  --bg: #101116;
  --bg-alt: #16171d;
  --bg-raised: #1b1c23;
  --bg-inset: #22242c;
  --text: #ecedf1;
  --text-muted: #a3a6b2;
  --text-faint: #767a88;
  --border: #2a2c35;
  --border-strong: #3a3d48;
  --brand-tint: #2a1f10;
  --brand-on-tint: #f5a83c;
  --ok-text: #3fbf7f;
  --warn-text: #e3a544;
  --info-text: #6aa6ee;
  --ok: #34c274;
  --warn: #e0a030;
  --err: #ef6f5f;
  --err-solid: #c0392b;
  --logo-dark: #ecedf1;
  --ann-info: #6aa6ee;
  --ann-warn: #e3a544;
  --ann-crit: #ea6f61;
  --shadow-sm: 0 1px 2px rgb(0 0 0 / 40%);
  --shadow: 0 4px 14px rgb(0 0 0 / 45%);
  --shadow-lg: 0 20px 44px rgb(0 0 0 / 55%);
  --glass: rgb(16 17 22 / 82%);
  color-scheme: dark;
}

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

*, *::before, *::after { box-sizing: border-box; }
/* The hidden attribute wins over any author display rule — a flex list born
   [hidden] stayed a visible empty line (review 2026-09-06, the finders'
   overflow); admin.css carries the same reset. */
[hidden] { display: none !important; }

/* scrollbar-gutter: the vertical scrollbar takes room only where the OS
   draws classic scrollbars (a mouse on a Mac, Windows), and only on pages
   long enough to scroll — so every centred block sat half a scrollbar
   further left on a long page than on a short one, and the page transition
   cross-faded the two layouts into a ghosted header (owner's recording,
   2026-09-04). A stable gutter keeps the width the same on every page;
   with overlay scrollbars it is zero anyway. Where they are classic, a page
   that does not scroll shows the reserved 15 px band on the right and sits
   7.5 px left of the window's centre — the trade, taken on purpose, for a
   header that never moves. */
html { height: 100%; scrollbar-gutter: stable; }

body {
  min-height: 100%;
  margin: 0;
  background: var(--bg-alt);
  color: var(--text);
  font-family: var(--font);
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  display: flex;
  flex-direction: column;
}

h1, h2, h3 {
  line-height: 1.2;
  letter-spacing: -0.02em;
  text-wrap: balance;
  margin: 0 0 .6em;
}
h1 { font-size: clamp(1.5rem, 1.2rem + 1.4vw, 2rem); }
h2 { font-size: clamp(1.2rem, 1.1rem + .6vw, 1.45rem); }
h3 { font-size: 1.05rem; }

p { margin: 0 0 1em; text-wrap: pretty; }

a { color: var(--brand-strong); text-decoration: none; }
a:hover { text-decoration: underline; }

code, pre, kbd, samp { font-family: var(--mono); font-size: .9em; }

::selection { background: color-mix(in oklab, var(--brand) 30%, transparent); }

:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
  border-radius: 2px;
}

/* The cross-fade between pages, second attempt (owner, 2026-09-04). The
   first one set the duration on the old/new images only: the UA puts its
   0.25 s on the group and the images inherit it, so the transition outlived
   the visible fade by ~70 ms and was torn down on a page that already looked
   finished — Chrome 152 drew the old page once more right there. Now the
   group carries the duration (everything ends together), the header is its
   own group so it stays put while the content fades, and app.js blocks the
   first paint so the fade lands on a complete page. The header's group has
   no animation of its own. The header is sticky at the top, so its place
   only differs between two pages when a banner or the impersonation strip
   sits above it on one of them; the group would then slide the whole chrome
   between the two places in 0.18 s. Without an animation it appears at its
   new place at once — a jump of the strip's height in that rare case — and
   cross-fades there. On the way out to the guest door the header has no
   counterpart and fades alone; accepted. If the old page still flashes in
   the owner's Chrome, drop this block: pages cut cleanly. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }
  ::view-transition-group(*) { animation-duration: .18s; }
  .topbar { view-transition-name: topbar; }
  ::view-transition-group(topbar) { animation-name: none; }
}

.muted { color: var(--text-muted); font-size: .92rem; }
.num { font-variant-numeric: tabular-nums; }
.break { overflow-wrap: anywhere; }
.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }

.eyebrow {
  display: block;
  font-size: .74rem;
  font-weight: 600;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: .35rem;
}

.icon { width: 18px; height: 18px; flex-shrink: 0; }

/* ---------- buttons ---------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .45rem;
  padding: .62rem 1.15rem;
  font: inherit;
  font-size: .95rem;
  font-weight: 600;
  line-height: 1.2;
  color: var(--text);
  background: var(--bg-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  cursor: pointer;
  text-decoration: none;
  transition: background .15s ease, border-color .15s ease, color .15s ease;
}
a.btn:hover { text-decoration: none; }
.btn:hover { background: var(--bg-inset); }
.btn:active { transform: translateY(1px); }

.btn-primary {
  color: #1c1d21;
  background: var(--brand);
  border-color: var(--brand);
}
.btn-primary:hover { background: var(--brand-strong); border-color: var(--brand-strong); }

.btn-ghost {
  background: transparent;
  border-color: transparent;
  color: var(--text-muted);
}
.btn-ghost:hover { background: var(--bg-inset); color: var(--text); }

.btn-danger {
  color: var(--err);
  background: transparent;
  border-color: color-mix(in oklab, var(--err) 40%, transparent);
}
.btn-danger:hover { background: color-mix(in oklab, var(--err) 10%, transparent); }

.btn-sm { padding: .35rem .75rem; font-size: .85rem; }
.btn-block { width: 100%; }

.btn:disabled, .btn[disabled] { opacity: .5; cursor: default; transform: none; }

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  padding: 0;
  color: var(--text-muted);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background .15s ease, color .15s ease;
}
.icon-btn:hover { background: var(--bg-inset); color: var(--text); }

/* theme toggle: default shows the moon (offer dark); flipped when dark wins */
.icon-sun { display: none; }
.icon-moon { display: block; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .icon-sun { display: block; }
  :root:not([data-theme="light"]) .icon-moon { display: none; }
}
:root[data-theme="dark"] .icon-sun { display: block; }
:root[data-theme="dark"] .icon-moon { display: none; }

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

.form { display: flex; flex-direction: column; gap: .9rem; max-width: 26rem; }
.form-wide { max-width: none; }

.field { display: flex; flex-direction: column; gap: .3rem; }
.field > label { font-size: .88rem; font-weight: 600; color: var(--text-muted); }
.field .hint { margin: 0; font-size: .82rem; color: var(--text-faint); }

input[type="email"], input[type="password"], input[type="text"], input[type="search"],
input[type="tel"], input[type="number"], select, textarea {
  width: 100%;
  padding: .6rem .75rem;
  font: inherit;
  color: var(--text);
  background: var(--bg-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  transition: border-color .15s ease, box-shadow .15s ease;
}
input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: none;
  border-color: var(--brand);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--brand) 25%, transparent);
}

input[type="number"] { font-variant-numeric: tabular-nums; }

select {
  appearance: none;
  padding-right: 2.2rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%238b8d96' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right .7rem center;
}

textarea { resize: vertical; min-height: 6rem; }

input[type="checkbox"], input[type="radio"] {
  accent-color: var(--brand);
  width: 1.05rem;
  height: 1.05rem;
}

input[type="file"] { font-size: .9rem; color: var(--text-muted); }
input[type="file"]::file-selector-button {
  margin-right: .7rem;
  padding: .45rem .9rem;
  font: inherit;
  font-size: .85rem;
  font-weight: 600;
  color: var(--text);
  background: var(--bg-inset);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

label.inline {
  display: flex;
  gap: .5rem;
  align-items: center;
  font-size: .95rem;
  cursor: pointer;
}

/* ---------- pills & notices ---------- */

.pill {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  padding: .12rem .6rem;
  font-size: .8rem;
  font-weight: 600;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  color: var(--text-muted);
  background: var(--bg-inset);
  white-space: nowrap;
}
.pill .dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
}

.status-active, .status-paid {
  color: var(--ok);
  background: color-mix(in oklab, var(--ok) 10%, transparent);
  border-color: color-mix(in oklab, var(--ok) 35%, transparent);
}
.status-pending, .status-unpaid, .status-open, .status-customer-reply {
  color: var(--warn);
  background: color-mix(in oklab, var(--warn) 10%, transparent);
  border-color: color-mix(in oklab, var(--warn) 35%, transparent);
}
.status-answered {
  color: var(--brand-on-tint);
  background: var(--brand-tint);
  border-color: color-mix(in oklab, var(--brand) 35%, transparent);
}
.status-suspended {
  color: var(--err);
  background: color-mix(in oklab, var(--err) 10%, transparent);
  border-color: color-mix(in oklab, var(--err) 35%, transparent);
}

.badge {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  padding: .12rem .6rem;
  font-size: .8rem;
  font-weight: 600;
  border-radius: 999px;
  color: var(--brand-on-tint);
  background: var(--brand-tint);
  border: 1px solid color-mix(in oklab, var(--brand) 35%, transparent);
  white-space: nowrap;
}

.notice {
  padding: .75rem 1rem;
  border-radius: var(--radius-sm);
  font-size: .95rem;
  border: 1px solid transparent;
  margin: 0 0 1rem;
}
.notice-error {
  color: var(--err);
  background: color-mix(in oklab, var(--err) 8%, var(--bg-raised));
  border-color: color-mix(in oklab, var(--err) 30%, transparent);
}
.notice-info {
  color: var(--brand-on-tint);
  background: var(--brand-tint);
  border-color: color-mix(in oklab, var(--brand) 30%, transparent);
}
/* The rule existed only as a class name in templates until 2026-08-27: the
   verify-email banner rendered as a bare box. */
.notice-warn {
  color: var(--warn-text);
  background: color-mix(in oklab, var(--warn) 8%, var(--bg-raised));
  border-color: color-mix(in oklab, var(--warn) 30%, transparent);
}

/* The overview's suspended block: each stopped machine on its own line
   with its own way back — the blocking invoice, or the renewal page, or
   the word to talk to support. The cross hides the block for a while; the
   red tile below never hides, so the fact stays on the page. */
.suspended .suspended-head { display: flex; align-items: center; gap: .8rem; margin: 0 0 .2rem; }
.suspended-head form { margin-left: auto; display: flex; }
.suspended-head button {
  display: inline-flex;
  padding: .3rem;
  border: 0;
  background: none;
  color: inherit;
  cursor: pointer;
  border-radius: var(--radius-sm);
}
.suspended-head button:hover { background: color-mix(in oklab, var(--err) 12%, transparent); }
.suspended-list { list-style: none; margin: 0; padding: 0; }
.suspended-list li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .35rem .8rem;
  padding: .45rem 0;
  border-top: 1px solid color-mix(in oklab, var(--err) 15%, transparent);
}
.suspended-list li:first-child { border-top: none; }
a.suspended-note { text-decoration: underline; }
.suspended-list .suspended-name { color: inherit; font-weight: 600; overflow-wrap: anywhere; }
.suspended-list .btn, .suspended-list .suspended-note { margin-left: auto; }
.suspended-note { font-size: .9rem; }
.suspended-more { margin: .45rem 0 0; }
.suspended-more a { color: inherit; text-decoration: underline; }

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

.panel {
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: 1.4rem 1.5rem;
}
.panel > h2:first-child, .panel > h3:first-child { margin-top: .1rem; }

.panel-grid {
  display: grid;
  gap: 1rem;
  container-type: inline-size;
}

.stack > * + * { margin-top: 1rem; }

/* ---------- guest (auth) layout ---------- */

body.guest {
  align-items: center;
  justify-content: center;
  padding: 2.5rem 1rem;
  background:
    radial-gradient(60rem 30rem at 50% -12rem,
      color-mix(in oklab, var(--brand) 14%, transparent), transparent 70%),
    radial-gradient(rgb(140 143 155 / 14%) 1px, transparent 1px) 0 0 / 22px 22px,
    var(--bg-alt);
}

.auth-shell {
  width: 100%;
  max-width: 26.5rem;
  display: flex;
  flex-direction: column;
  gap: 1.2rem;
}

.auth-logo {
  display: flex;
  justify-content: center;
}
.auth-logo .wordmark { font-size: 2rem; }

.auth-card {
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: 2rem;
}
.auth-card .form { max-width: none; }
.auth-card h1 { font-size: 1.3rem; margin-bottom: 1.2rem; }

.auth-links {
  display: flex;
  flex-direction: column;
  gap: .3rem;
  margin-top: 1.2rem;
  font-size: .92rem;
}
.auth-links p { margin: 0; }

.separator {
  display: flex;
  align-items: center;
  gap: .8rem;
  margin: 1.1rem 0;
  color: var(--text-faint);
  font-size: .85rem;
}
.separator::before, .separator::after {
  content: "";
  flex: 1;
  border-top: 1px solid var(--border);
}

.guest-theme { position: fixed; top: .9rem; right: .9rem; }

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

/* the administrator-mode strip: impossible to miss, impossible to mistake */
.impersonation {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: .5rem .8rem;
  padding: .45rem 1rem;
  font-size: .88rem;
  font-weight: 600;
  color: #1c1d21;
  background: repeating-linear-gradient(
    -45deg,
    var(--brand), var(--brand) 14px,
    var(--brand-soft) 14px, var(--brand-soft) 28px);
}
.impersonation .icon { width: 16px; height: 16px; }
.impersonation form { display: inline-flex; }
.impersonation button {
  padding: .15rem .7rem;
  font: inherit;
  font-size: .82rem;
  font-weight: 700;
  color: #1c1d21;
  background: rgb(255 255 255 / 55%);
  border: 1px solid rgb(28 29 33 / 35%);
  border-radius: 999px;
  cursor: pointer;
}
.impersonation button:hover { background: rgb(255 255 255 / 75%); }

.topbar {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--glass);
  -webkit-backdrop-filter: saturate(1.8) blur(12px);
  backdrop-filter: saturate(1.8) blur(12px);
  border-bottom: 1px solid var(--border);
}

.topbar-inner {
  max-width: var(--wrap);
  margin: 0 auto;
  padding: 0 1rem;
  display: flex;
  align-items: center;
  gap: 1.2rem;
  min-height: 62px;
}

.brand { display: flex; align-items: center; }
.brand .wordmark { font-size: 1.4rem; }
/* The brand is a link, but not a hyperlink-looking one: with the wordmark
   being real text, the global a:hover underline would draw through it. */
a.brand:hover { text-decoration: none; }

/* The wordmark — the one logo everywhere since 2026-08-14: text, not an
   image, so it follows the theme tokens, costs nothing to load and stays
   crisp at any density. Size is set by the context that places it. */
.wordmark {
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1;
  white-space: nowrap;
  color: var(--logo-dark);
}
.wordmark-dale { color: var(--logo-brand); }

.nav-tabs {
  display: flex;
  gap: .2rem;
  margin-left: .4rem;
  flex: 1;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
}
.nav-tabs::-webkit-scrollbar { display: none; }

/* Without the script the strip is all there is, so it says it scrolls rather
   than pretending everything fits. With it, nothing overflows — and the strip
   stops clipping, which is what lets the overflow menu hang below it. */
.nav-tabs:not(.is-managed) { scrollbar-width: thin; }
.nav-tabs.is-managed { overflow: visible; }
/* The strip is never hidden by CSS while it waits for the script: .js and
   .is-managed are both set by app.js in one task, so a hidden strip could
   only ever be painted by a browser that ignores render blocking — and a
   rule keyed on a class from an earlier script took the navigation away for
   good on every road where app.js failed (adversarial review, 2026-09-04).
   Where the first paint waits for the script the strip is arranged before
   it; elsewhere it is re-arranged a beat after, as it always was. */

/* The menu the tabs that do not fit live in. It is the user menu's own
   component, so it opens and closes without a line of script; only what goes
   inside it is decided by one. */
.nav-more { flex: none; }
.nav-more > summary {
  gap: .3rem;
  padding: .5rem .7rem;
  font-size: .93rem;
  font-weight: 600;
  white-space: nowrap;
}
.nav-more > summary .icon-chevron { width: 15px; height: 15px; }
.nav-more[open] > summary .icon-chevron { transform: rotate(180deg); }
.nav-more > .menu-list { left: 0; right: auto; min-width: 15rem; }
/* On a phone the button sits at the end of a short row, so the panel hangs
   from its right edge instead — anchored left it would run off the screen. */
@media (max-width: 760px) {
  .nav-more > .menu-list { left: auto; right: 0; }
}

/* A tab inside the menu is a menu row, and keeps saying which page it is. */
.menu-list .tab {
  display: flex;
  width: 100%;
  border-radius: var(--radius-sm);
  padding: .5rem .65rem;
}
.menu-list .tab:hover { background: var(--bg-inset); }

/* The mark has to live somewhere: if the page's own tab is inside the menu,
   the button that holds it wears it. */
.nav-more.is-current > summary { color: var(--brand-strong); background: var(--brand-tint); }

.tab {
  display: inline-flex;
  align-items: center;
  /* Never squeezed: a tab that shrinks clips its own label, which is the
     shape the old bar failed in. Too many of them is the strip's problem to
     solve, and it solves it by moving one into the menu. */
  flex: none;
  gap: .45rem;
  padding: .5rem .85rem;
  font-size: .93rem;
  font-weight: 600;
  color: var(--text-muted);
  border-radius: var(--radius-sm);
  white-space: nowrap;
  transition: background .15s ease, color .15s ease;
}
.tab:hover { background: var(--bg-inset); color: var(--text); text-decoration: none; }
.tab[aria-current="page"] {
  color: var(--brand-strong);
  background: var(--brand-tint);
}
.tab .icon { width: 17px; height: 17px; }

.top-actions {
  display: flex;
  align-items: center;
  gap: .3rem;
  margin-left: auto;
}

/* narrow screens: brand and actions share the first row, the tabs take a
   scrollable second one, and the user chip shrinks to its avatar */
@media (max-width: 760px) {
  .topbar-inner { flex-wrap: wrap; row-gap: 0; padding-block: .45rem 0; min-height: 0; }
  .nav-tabs { order: 3; flex-basis: 100%; margin: 0 -1rem; padding: .1rem 1rem .5rem; }
  .menu > summary { padding: .3rem; }
  .menu > summary .menu-label { display: none; }
}

/* user menu: a styled <details>, no script involved */
.menu { position: relative; }
.menu > summary {
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  padding: .42rem .7rem;
  font-size: .9rem;
  font-weight: 600;
  color: var(--text-muted);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
  /* Tight on purpose: the address is identity, not navigation, and the bar is
     capped at the content width. Every pixel this keeps is a pixel the tabs
     do not have — and the whole address is at the top of the menu it opens
     (review 2026-08-19). */
  max-width: 11rem;
}
.menu > summary::-webkit-details-marker { display: none; }
.menu > summary:hover { background: var(--bg-inset); color: var(--text); }
.menu > summary .avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  flex-shrink: 0;
  border-radius: 50%;
  background: var(--brand-tint);
  color: var(--brand-strong);
  font-size: .8rem;
  font-weight: 700;
  text-transform: uppercase;
}
.menu > summary .menu-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* clicking anywhere outside lands on this full-screen extension of summary
   and closes the menu */
.menu[open] > summary::before {
  content: "";
  position: fixed;
  inset: 0;
  cursor: default;
}

.menu-list {
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  z-index: 60;
  min-width: 230px;
  padding: .45rem;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
}

.menu-list .menu-head {
  padding: .5rem .65rem .6rem;
  border-bottom: 1px solid var(--border);
  margin-bottom: .4rem;
}
.menu-list .menu-head strong {
  display: block;
  font-size: .9rem;
  overflow-wrap: anywhere;
}

.menu-item {
  display: flex;
  align-items: center;
  gap: .6rem;
  width: 100%;
  padding: .5rem .65rem;
  font: inherit;
  font-size: .92rem;
  color: var(--text);
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
  text-align: left;
  text-decoration: none;
}
.menu-item:hover { background: var(--bg-inset); text-decoration: none; }
.menu-item .icon { color: var(--text-faint); }

.menu-lang {
  display: flex;
  gap: .3rem;
  padding: .4rem .65rem .25rem;
  border-top: 1px solid var(--border);
  margin-top: .4rem;
}
.menu-lang form { flex: 1; }
.menu-lang button {
  width: 100%;
  padding: .35rem .5rem;
  font: inherit;
  font-size: .85rem;
  font-weight: 600;
  color: var(--text-muted);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.menu-lang button:hover { background: var(--bg-inset); }
.menu-lang button:disabled {
  color: var(--brand-strong);
  background: var(--brand-tint);
  border-color: color-mix(in oklab, var(--brand) 35%, transparent);
  cursor: default;
}

.content {
  flex: 1;
  width: 100%;
  max-width: var(--wrap);
  margin: 0 auto;
  padding: 1.8rem 1rem 3rem;
}

.page-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .8rem 1rem;
  margin-bottom: 1.4rem;
}
.page-head h1 { margin: 0; }
.page-head .page-actions { margin-left: auto; display: flex; gap: .6rem; }

.footer {
  border-top: 1px solid var(--border);
  padding: 1.2rem 1rem 1.6rem;
  margin-top: auto;
}
.footer-inner {
  max-width: var(--wrap);
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .8rem 1.4rem;
  font-size: .85rem;
  color: var(--text-faint);
}
.footer-inner a { color: var(--text-muted); }

.languages { display: flex; gap: .4rem; margin-left: auto; }
.languages button {
  padding: .25rem .65rem;
  font: inherit;
  font-size: .82rem;
  font-weight: 600;
  color: var(--text-muted);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 999px;
  cursor: pointer;
}
.languages button:hover { background: var(--bg-inset); }
.languages button:disabled {
  color: var(--brand-strong);
  background: var(--brand-tint);
  border-color: color-mix(in oklab, var(--brand) 35%, transparent);
  cursor: default;
}
body.guest .languages { margin: 0 auto; }

/* ---------- dashboard ---------- */

.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 1rem;
  margin-bottom: 1.5rem;
}

.stat {
  display: block;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: 1.1rem 1.25rem;
  color: inherit;
  transition: border-color .15s ease, box-shadow .15s ease, translate .15s ease;
}
a.stat:hover {
  text-decoration: none;
  border-color: color-mix(in oklab, var(--brand) 45%, var(--border));
  box-shadow: var(--shadow);
  translate: 0 -2px;
}
.stat .value {
  display: block;
  font-size: 1.7rem;
  font-weight: 700;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}
.stat .value.ok { color: var(--ok); }
.stat .value.warn { color: var(--warn-text); }
.stat .value.err { color: var(--err); }

.dash-grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(21rem, 100%), 1fr));
  align-items: start;
}

/* the wide variant: two columns at most, for panels that hold tables */
.dash-grid.wide {
  grid-template-columns: repeat(auto-fit, minmax(min(27rem, 100%), 1fr));
}

/* The panel that says a machine is not ready yet. Quiet rather than alarming:
   nothing is wrong, the customer simply has a few minutes to wait. */
.notice-panel {
  border-color: color-mix(in oklab, var(--brand) 35%, var(--border));
  background: color-mix(in oklab, var(--brand) 5%, var(--bg-raised));
  margin-bottom: 1rem;
}
.notice-panel h2 { margin-bottom: .4rem; }
.notice-panel p { margin: 0; }

/* The paired variant: never more than two columns, whatever the room.
   A service's panels hold controls rather than prose — a resource's quantity
   beside its price, a plan beside what it costs — and those need width to sit
   on one line. Three columns of a 1180px page leave 324px inside a panel,
   which is narrower than a label and two fields; two leave 518px, which is
   enough for every row here and still short of a line length nobody wants to
   read (owner, 2026-08-18). Below that the auto-fit rule takes over and folds
   to one column on a phone. */
@media (min-width: 64rem) {
  .dash-grid.paired { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.account-list { margin: 0; padding: 0; list-style: none; }
.account-list li {
  display: flex;
  gap: .6rem;
  align-items: center;
  padding: .55rem 0;
  border-bottom: 1px solid var(--border);
}
.account-list li:last-child { border-bottom: none; }
.account-list .spacer { flex: 1; }

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

.table-wrap { overflow-x: auto; }

.table {
  width: 100%;
  border-collapse: collapse;
  font-size: .95rem;
}
.table th {
  text-align: left;
  font-size: .76rem;
  font-weight: 600;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--text-faint);
  padding: .55rem .8rem;
  border-bottom: 1px solid var(--border);
}
.table td {
  padding: .7rem .8rem;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}
.table tbody tr { transition: background .12s ease; }
.table tbody tr:hover { background: var(--bg-inset); }
.table tbody tr:last-child td { border-bottom: none; }
.table .num { text-align: right; font-variant-numeric: tabular-nums; }
.table .credit-in { color: var(--ok-text); }
.table .credit-out { color: var(--warn-text); }
.table td.primary a { font-weight: 600; }
/* A hostname may be one long unbreakable word; without this its widest row
   dictates the column's minimum and pushes the table into sideways
   scrolling on anything narrower than a desktop. */
.table .mono { overflow-wrap: anywhere; }
/* The services list keeps its hostname under the product name, one cell
   telling one story: what this is and where it lives. fit-content keeps the
   quiet zone the size of the words: past the hostname's last letter the row
   is a link again (review 2026-08-26). */
.table td.primary .sub { font-weight: 400; font-size: .85rem; color: var(--text-muted); margin-top: .1rem; width: fit-content; }
/* Some cells say the same fact twice: once for the columned table, once in
   the words a card needs. Only one of the two is ever shown. */
.card-only { display: none; }

/* Row-link lists (services, invoices, tickets): the whole row is one click
   target for the row's own page. The only anchor stays on the name cell and
   its ::after stretches over the row, so keyboard, middle-click and screen
   readers all see a plain short link. transform+clip-path exist for Safari,
   which gives no containing block to position:relative on a table row: the
   transform makes one, the clip keeps the stretched area inside the row. */
.table tr.rowlink {
  position: relative;
  transform: translate(0);
  clip-path: inset(0);
  cursor: pointer;
}
.table tr.rowlink td.primary a::after { content: ""; position: absolute; inset: 0; }
/* Copyable text stays above the stretched link: selecting or clicking a
   quiet element never navigates. */
.table tr.rowlink .quiet, .table tr.rowlink a.tk-chip { position: relative; z-index: 1; }
.table tr.rowlink:focus-within { outline: 2px solid var(--brand); outline-offset: -2px; }
/* The row's ring replaces the anchor's own; the underline stays as the
   indicator of last resort for a browser that will not draw an outline on
   a table row (Safari is lawless around table-row boxes — see above). */
.table tr.rowlink td.primary a:focus-visible { outline: none; text-decoration: underline; }
/* The chevron takes no clicks of its own: hovered, its transform lifts it
   above the stretched link, and on cards it is positioned above it — either
   way a click on the icon must reach the link underneath (owner, 2026-09-05). */
.table .cell-go { width: 2rem; padding-left: 0; text-align: right; color: var(--text-faint); pointer-events: none; }
.table td.cell-go .icon { width: 16px; height: 16px; vertical-align: -3px; }
.table tr.rowlink:hover td.cell-go { color: var(--brand-strong); }
@media (prefers-reduced-motion: no-preference) {
  .table td.cell-go .icon { transition: transform .12s ease; }
  .table tr.rowlink:hover td.cell-go .icon { transform: translateX(2px); }
}

/* narrow screens: each row becomes a small card, headers move inline */
@media (max-width: 640px) {
  .table thead { display: none; }
  .table, .table tbody, .table tr, .table td { display: block; }
  .table tr {
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    margin-bottom: .7rem;
    padding: .4rem .2rem;
    background: var(--bg-raised);
  }
  .table td {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 1rem;
    border-bottom: none;
    padding: .3rem .8rem;
  }
  .table td[data-label]::before {
    content: attr(data-label);
    font-size: .76rem;
    font-weight: 600;
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--text-faint);
  }
  .table .num { text-align: left; }
  /* A cell that carries nothing is not a line of the card: the held renewal
     row has no amount, and an empty flex row read as a figure that failed to
     load (review 2026-08-24). It keeps the column on wider screens. */
  .table td.is-empty { display: none; }
  /* On cards the whole card is the target; the chevron leaves the flow and
     sits mid-right, and the card's padding makes room for it. */
  .table tr.rowlink { padding-right: 1.5rem; }
  .table tr.rowlink td.cell-go {
    position: absolute;
    top: 50%;
    right: .4rem;
    width: auto;
    padding: 0;
    transform: translateY(-50%);
  }
  /* The services card tells its story in three lines — name and status,
     hostname, then price and term — instead of six label-and-value rows.
     Flex with an explicit break after the status pill, not a grid: column
     tracks would reserve the pill's width across every line and squeeze
     the term into it; here a meta line that does not fit simply continues
     under the price, at the left edge, like text. */
  .svc-table tbody tr.rowlink {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .2rem .8rem;
    padding: .65rem 1.8rem .7rem .8rem;
  }
  .svc-table td { display: block; padding: 0; }
  .svc-table td[data-label]::before { content: none; }
  /* The identity owns the card's whole width and the pill joins the money
     line: beside the pill the name had 118 px of a 293 px card and broke
     at every hyphen (research 2026-09-09) — the picker's own lesson,
     app.css "the name takes the whole row". */
  .svc-table td.primary { flex: 1 1 100%; min-width: 0; padding-right: 0; }
  .svc-table td.primary a { font-size: 1rem; }
  .svc-table td.svc-status { order: 2; margin-left: auto; }
  .svc-table tbody tr.rowlink::after { content: ""; width: 100%; order: 1; }
  .svc-table td.svc-amount, .svc-table td.svc-due {
    order: 2;
    color: var(--text-muted);
    font-size: .92rem;
  }
  .svc-table .card-only { display: inline; }
  .svc-table .wide-only { display: none; }
}
/* The address column (research 2026-09-09): content-wide, mono, muted
   (5.9:1 on white, 7.0:1 in dark — never --text-faint). Below 960 px the
   column would squeeze the price onto two lines, so it folds into the
   identity block as a second quiet line; the two are never shown together. */
.svc-table th.svc-ip-h, .svc-table td.svc-ip { width: 1%; white-space: nowrap; }
.svc-table td.svc-ip { font-family: var(--mono); color: var(--text-muted); }
.svc-table .sub-ip { display: none; }
/* The search box reuses the picker's .tk-find rules whole (the wrapper
   carries both classes); only the "nothing found" row and the reset are
   the list's own. */
.svc-table tr.svc-empty td { color: var(--text-muted); padding: .9rem .8rem; }
.svc-find .btn-link { background: none; border: 0; padding: 0; font: inherit; font-size: .88rem; color: var(--brand-strong); text-decoration: underline; cursor: pointer; }
.svc-find .btn-link:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* While a filter runs: the match is marked in the brand's tint, the outline
   walks with the arrows, the last VISIBLE row drops its border (the DOM's
   last is hidden), and with nothing matching the headings step aside and
   the empty row, alone, drops its border too. */
.svc-table mark { background: color-mix(in oklab, var(--brand) 30%, transparent); color: inherit; border-radius: 2px; }
.svc-table tr.rowlink.is-current { outline: 2px solid var(--brand); outline-offset: -2px; }
.svc-table.is-filtered tbody tr.is-last td { border-bottom: none; }
.svc-table.is-none thead { display: none; }
.svc-table.is-none tr.svc-empty td { border-bottom: none; }
@media (max-width: 960px) {
  .svc-table th.svc-ip-h, .svc-table td.svc-ip { display: none; }
  .svc-table .sub-ip { display: block; }
}

/* ---------- detail lists ---------- */

dl.details {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: .45rem 1.6rem;
  margin: 0;
}
dl.details dt { color: var(--text-muted); font-size: .92rem; }
dl.details dd { margin: 0; overflow-wrap: anywhere; }

@media (max-width: 480px) {
  dl.details { grid-template-columns: 1fr; gap: .1rem; }
  dl.details dt { margin-top: .6rem; }
}

.kv-chip {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  padding: .3rem .7rem;
  font-family: var(--mono);
  font-size: .92rem;
  background: var(--bg-inset);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow-wrap: anywhere;
}

/* ---------- tickets ---------- */

.messages { display: flex; flex-direction: column; gap: .8rem; margin: 1.2rem 0; }

.message {
  max-width: 46rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  border-bottom-left-radius: 4px;
  background: var(--bg-raised);
  padding: .8rem 1rem;
  box-shadow: var(--shadow-sm);
}
.message-staff {
  align-self: flex-end;
  border-bottom-left-radius: var(--radius);
  border-bottom-right-radius: 4px;
  background: color-mix(in oklab, var(--brand-tint) 55%, var(--bg-raised));
  border-color: color-mix(in oklab, var(--brand) 25%, var(--border));
}
.message-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  margin-bottom: .35rem;
  font-size: .88rem;
}
.message-head strong { font-size: .92rem; }
/* Plain text with its line breaks kept: no user-supplied markup exists here,
   html/template escaped everything, and pre-wrap does what <br> would. */
.message-body { white-space: pre-wrap; overflow-wrap: anywhere; }
/* the one markup a body gets: its http(s) addresses as anchors (stage 5), the
   address itself as the text, so a long one may break anywhere */
.message-body a { color: var(--brand-on-tint); text-decoration: underline; text-underline-offset: .12em; overflow-wrap: anywhere; } /* the readable ink: --brand-strong is 2.66:1 on white (review 2026-09-07) */
/* Attachments as a tidy grid of uniform cards: images wear a fixed-frame
   thumbnail (object-fit keeps any aspect ratio inside it), everything else
   wears its extension. The caption below links to the download. */
.message-files {
  margin-top: .75rem; padding-top: .75rem;
  border-top: 1px solid var(--border);
  display: flex; flex-wrap: wrap; align-items: flex-start; gap: .75rem;
}
.attach-card { margin: 0; width: 148px; display: flex; flex-direction: column; gap: .35rem; }
.attach-thumb, .attach-ext {
  display: block; width: 148px; height: 100px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--bg-inset); overflow: hidden;
}
.attach-thumb { cursor: zoom-in; }
.attach-thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
.attach-thumb:hover, .attach-ext:hover { border-color: var(--border-strong); }
.attach-ext {
  display: flex; align-items: center; justify-content: center;
  text-decoration: none;
}
a.attach-ext:hover { text-decoration: none; }
.attach-ext span {
  font-family: var(--mono); font-size: .8rem; font-weight: 700;
  letter-spacing: .08em; color: var(--text-faint);
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  padding: .25rem .5rem;
}
.attach-card figcaption { min-width: 0; }
.attach-name {
  display: block; max-width: 148px; font-size: .8rem;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* The file picker: the native control cannot be styled or translated, so it
   hides (still focusable, still openable through its label without JS) and
   a localized button fronts it; app.js echoes the chosen names. */
.file-picker { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; }
/* Present to a screen reader, absent to the eye. The two hidden controls
   below spell this out themselves for historical reasons; new markup uses
   the class. */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

.file-input {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}
/* Unlike the truly ghost buttons, the picker must read as a control at
   first glance: it keeps a visible outline. */
.file-btn { margin: 0; border-color: var(--border-strong); font-size: .88rem; padding: .45rem .9rem; }
.file-input:focus-visible + .file-btn { outline: 2px solid var(--brand); outline-offset: 2px; }
.file-chosen { font-size: .85rem; color: var(--text-faint); min-width: 0; }

/* One row for what the conversation offers: answer on the left, the quieter
   close on the right. The close form itself is empty and lives outside; its
   button sits here through the form attribute. */
.ticket-actions { justify-content: space-between; }

/* ---- the grouped list: whose move it is (ticket redesign stage 1) ---- */
.tk-group { margin-bottom: 1.6rem; }
.tk-group-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: .6rem; margin: 0 0 .6rem .2rem; }
.tk-group-head h2 { margin: 0; font-size: 1.05rem; }
.tk-group-head .count {
  font-size: .8rem; font-weight: 600; color: var(--text-muted);
  padding: .05rem .5rem; border-radius: 999px; background: var(--bg-inset);
  font-variant-numeric: tabular-nums;
}
.tk-group-head .lead { font-size: .88rem; color: var(--text-muted); }
.tk-group-you .tk-group-head .count { color: var(--brand-on-tint); background: var(--brand-tint); }
.tk-group-us .tk-group-head .count { color: var(--info-text); background: color-mix(in oklab, var(--info-text) 10%, transparent); }
/* the closed group is quieter: its rows are history, not work */
.tk-group-closed td.primary a { font-weight: 500; }
.tk-group-more { margin: .8rem 0 0; font-size: .9rem; }
/* the department chip: only finance wears one, support is the default */
.tk-chip {
  display: inline-flex; align-items: center; gap: .3rem;
  padding: .08rem .5rem; border-radius: 6px;
  background: var(--bg-inset); color: var(--text-muted);
  font-size: .78rem; font-weight: 500; white-space: nowrap;
}
.tk-when { white-space: nowrap; color: var(--text-muted); font-variant-numeric: tabular-nums; }
/* customer-side pills: "Є відповідь" asks for the reader's action and wears
   the brand; "Розглядаємо" is calm blue — the info ink, not a warning; a
   notice and "Закрито" are the default grey pill */
.pill.status-you {
  color: var(--brand-on-tint); background: var(--brand-tint);
  border-color: color-mix(in oklab, var(--brand) 35%, transparent);
}
.pill.status-us {
  color: var(--info-text);
  background: color-mix(in oklab, var(--info-text) 10%, transparent);
  border-color: color-mix(in oklab, var(--info-text) 35%, transparent);
}
/* the overview tile when something waits on the reader */
.stat .value.attn { color: var(--brand-on-tint); }
@media (max-width: 640px) {
  .tk-table td.tk-blank { display: none; }
}

/* The lightbox: the preview grown to the whole viewport. Click or Esc puts
   it away; app.js builds it, and without JS the thumbnail link opens the
   image in a new tab instead. */
/* The lightbox is a modal <dialog> (report 1.6): the browser traps focus
   and answers Esc; the backdrop and the zoom-out cursor are ours. */
dialog.lightbox {
  border: 0; padding: 0; background: transparent;
  max-width: 100vw; max-height: 100vh; cursor: zoom-out;
}
dialog.lightbox::backdrop { background: rgba(10, 11, 14, .88); }
dialog.lightbox img {
  display: block; max-width: 92vw; max-height: 92vh;
  border-radius: var(--radius-sm); box-shadow: var(--shadow);
}

/* Landing on a message (report 1.5): #m{id} scrolls under the sticky
   topbar, and the target wears a brand ring — a still one under reduced
   motion, a fading one otherwise. */
html { scroll-padding-top: 5rem; }
.message:target { box-shadow: 0 0 0 3px color-mix(in oklab, var(--brand) 55%, transparent); }
@media (prefers-reduced-motion: no-preference) {
  .message:target { animation: tk-land 2.4s ease-out both; }
  @keyframes tk-land {
    0%, 60% { box-shadow: 0 0 0 3px color-mix(in oklab, var(--brand) 55%, transparent); }
    100% { box-shadow: 0 0 0 3px transparent; }
  }
}
.message-author { margin: 0; font-size: .92rem; font-weight: 600; }
.messages { list-style: none; padding: 0; }
/* the wrappers vanish so .message-staff can still align itself; the day
   separator is the one li that keeps its own box (review 2026-09-05:
   (0,1,1) beat .tk-day's (0,1,0) and the rule drew as three rows) */
.messages > li:not(.tk-day) { display: contents; }
/* a day separator: a quiet word on a line */
.tk-day {
  display: flex; align-items: center; gap: .8rem;
  margin: .4rem 0 0; font-size: .8rem; color: var(--text-muted);
}
.tk-day::before, .tk-day::after { content: ""; flex: 1; border-top: 1px solid var(--border); }
.tk-day span { white-space: nowrap; }

/* the unread mark (report 2.1): a brand dot before the subject, a bold
   subject, a faint wash on the row; the dot is an element with an sr-only
   word beside it, never colour alone */
.tk-dot {
  display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  background: var(--brand); margin-right: .45rem; vertical-align: 1px;
}
.tk-table tr.is-unread { background: color-mix(in oklab, var(--brand) 4%, transparent); }
.tk-table tr.is-unread:hover { background: color-mix(in oklab, var(--brand) 8%, transparent); }
.tk-table tr.is-unread td.primary a { font-weight: 700; color: var(--text); }
/* the tab's badge: the count of answers not yet opened */
.nav-tabs .tab-count {
  display: inline-block; min-width: 1.3rem; padding: 0 .4rem; margin-left: .2rem;
  border-radius: 999px; font-size: .74rem; font-weight: 700; line-height: 1.3rem; text-align: center;
  color: var(--brand-on-tint); background: var(--brand-tint); font-variant-numeric: tabular-nums;
}

/* the storyline card: one sentence about where things stand (report 2.2) */
.tk-story {
  display: flex; align-items: flex-start; flex-wrap: wrap; gap: .8rem;
  padding: .9rem 1.1rem; margin: 0 0 1.4rem;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--bg-raised); box-shadow: var(--shadow-sm);
}
.tk-story-icon {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 2.1rem; height: 2.1rem; border-radius: 50%;
  background: var(--bg-inset); color: var(--text-muted);
}
.tk-story-icon .icon { width: 18px; height: 18px; }
.tk-story-main { flex: 1 1 18rem; min-width: 0; }
.tk-story-line { margin: 0; font-weight: 600; font-size: 1rem; line-height: 1.35; }
.tk-story-line .sep { color: var(--text-faint); font-weight: 400; margin: 0 .3rem; }
.tk-story-sub { margin: .25rem 0 0; font-size: .9rem; color: var(--text-muted); max-width: 64ch; }
.tk-story-actions { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin-left: auto; }
.tk-story-actions form { display: contents; }
.tk-story-you {
  border-color: color-mix(in oklab, var(--brand) 35%, var(--border));
  background: color-mix(in oklab, var(--brand-tint) 55%, var(--bg-raised));
}
.tk-story-you .tk-story-icon { background: var(--brand-tint); color: var(--brand-on-tint); }
.tk-story-us .tk-story-icon { background: color-mix(in oklab, var(--info-text) 12%, transparent); color: var(--info-text); }
.tk-story-received .tk-story-icon { background: color-mix(in oklab, var(--ok) 12%, transparent); color: var(--ok-text); }
@media (max-width: 640px) {
  .tk-story-actions { margin-left: 0; width: 100%; }
  .tk-story-actions .btn { flex: 1 1 auto; }
}

/* the inbox line (report 2.4): who said the newest thing, then its start */
.tk-snip {
  display: block; margin-top: .15rem;
  font-size: .85rem; color: var(--text-muted); font-weight: 400;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 34rem;
}
.tk-snip .who { font-weight: 600; }
.tk-table tr.is-unread .tk-snip { color: var(--text); }
.tk-table tr.is-unread .tk-snip .who { color: var(--brand-on-tint); }
.tk-group-closed .tk-snip { color: var(--text-faint); }
.tk-chips { display: flex; flex-wrap: wrap; gap: .3rem; }
.tk-chip .icon { width: 13px; height: 13px; color: var(--text-faint); }
a.tk-chip { text-decoration: none; }
a.tk-chip:hover { color: var(--brand-on-tint); background: var(--brand-tint); }
.tk-chip .mono { font-family: var(--mono); font-size: .76rem; }
@media (max-width: 640px) {
  .tk-snip { white-space: normal; max-width: none; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
  /* the stacked row's subject cell: its label on a line of its own, then the dot
     and the title, then the inbox line — not the four of them across one row */
  .tk-table td.primary { flex-wrap: wrap; justify-content: flex-start; gap: .15rem .45rem; }
  .tk-table td.primary::before, .tk-table td.primary .tk-snip { flex-basis: 100%; }
  .tk-table td.primary > a { flex: 1 1 0; min-width: 0; } /* shares the line with the dot */
}

/* long messages fold behind a button, only where the script runs (2.12) */
.tk-unfold { display: none; }
.js .tk-unfold {
  display: inline-block; margin-top: .4rem; padding: .25rem .7rem;
  border: 1px solid var(--border-strong); border-radius: 999px;
  background: var(--bg-raised); color: var(--text-muted); font: inherit; font-size: .82rem; cursor: pointer;
}
.js .message.is-long:not(.is-open) .message-body {
  max-height: 14rem; overflow: hidden;
  -webkit-mask-image: linear-gradient(to bottom, #000 70%, transparent);
  mask-image: linear-gradient(to bottom, #000 70%, transparent);
}

/* the phone's way to the box: a bar at the bottom while the conversation is open */
.tk-bar { display: none; }
@media (max-width: 640px) {
  .tk-bar {
    position: fixed; left: 1rem; right: 1rem; bottom: .9rem; z-index: 40;
    display: flex; justify-content: center; padding: .8rem 1rem; border-radius: 999px;
    background: var(--brand); color: #1c1d21; font-weight: 700; text-decoration: none; box-shadow: var(--shadow-lg);
  }
  .tk-bar:hover { text-decoration: none; }
  body.app:has(.tk-bar) .content { padding-bottom: 4.5rem; }
}

/* the guest's room (2.10): a wide shell, and the two panels under the thread */
.auth-shell.auth-wide { max-width: 64rem; }
.tk-guest, .tk-guest-account { display: flex; flex-wrap: wrap; gap: .8rem 1.2rem; align-items: center; justify-content: space-between; }
.tk-guest p, .tk-guest-account p { margin: 0; flex: 1 1 24rem; color: var(--text-muted); }
.tk-guest-account { margin-top: 1rem; }

/* the composer's drop zone and file chips (report 2.8) */
.tk-drop { position: relative; padding: .6rem; border: 1px dashed var(--border-strong); border-radius: var(--radius-sm); }
.tk-drop.is-over { border-color: var(--brand); background: var(--brand-tint); }
.tk-drop-hint { flex: 1 1 100%; margin: .2rem 0 0; font-size: .82rem; color: var(--text-faint); display: none; }
.js .tk-drop-hint { display: block; }
.tk-camera, .tk-camera-input { display: none; }
@media (pointer: coarse) {
  .file-picker.has-file-js .tk-camera { display: inline-flex; }
}
.tk-files { flex: 1 1 100%; list-style: none; margin: .3rem 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: .4rem; }
.tk-file {
  display: inline-flex; align-items: center; gap: .4rem; max-width: 100%;
  padding: .2rem .35rem .2rem .6rem; border-radius: 999px;
  border: 1px solid var(--border-strong); background: var(--bg-raised); font-size: .82rem;
}
.tk-file-name { max-width: 14rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tk-file-meta { color: var(--text-faint); white-space: nowrap; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.tk-file.is-bad { border-color: color-mix(in oklab, var(--err) 50%, transparent); background: color-mix(in oklab, var(--err) 7%, var(--bg-raised)); flex-wrap: wrap; }
/* the refusal is a sentence: it wraps inside the chip instead of pushing the composer off a phone */
.tk-file.is-bad .tk-file-meta { color: var(--err); white-space: normal; overflow: visible; overflow-wrap: anywhere; }
.tk-file-remove {
  border: 0; background: transparent; color: var(--text-muted); font: inherit; font-size: 1rem; line-height: 1;
  padding: 0 .3rem; cursor: pointer; border-radius: 50%;
}
.tk-file-remove:hover { color: var(--err); background: color-mix(in oklab, var(--err) 10%, transparent); }
.tk-kbd { display: none; margin-left: .45rem; font: inherit; font-size: .72rem; opacity: .75; }
.js .btn .tk-kbd { display: inline; }
@media (pointer: coarse) { .js .btn .tk-kbd { display: none; } }

/* the request form (report 2.5): what happened, which machine, tell us */
.tk-lead { margin: -.6rem 0 1rem; color: var(--text-muted); max-width: 64ch; }
.tk-form .tk-block + .tk-block { margin-top: 1.4rem; padding-top: 1.2rem; border-top: 1px solid var(--border); }
.tk-form fieldset { border: 0; padding: 0; margin: 0; min-width: 0; }
.tk-form legend { padding: 0; float: left; width: 100%; }
.tk-form legend + * { clear: both; }
.tk-intents { display: flex; flex-wrap: wrap; gap: .5rem; }
.tk-intent { position: relative; cursor: pointer; padding: .5rem .9rem; }
.tk-intent input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.tk-intent:has(input:checked) { border-color: var(--brand); background: var(--brand-tint); color: var(--brand-on-tint); }
.tk-intent:has(input:focus-visible) { outline: 2px solid var(--brand); outline-offset: 2px; }
/* the department question exists only for "something else" */
.tk-dept { display: flex; margin-top: .8rem; gap: 1.2rem; flex-wrap: wrap; }
@supports selector(:has(a)) {
  .tk-form:has(input[name="intent"]:checked:not([value="other"])) .tk-dept { display: none; }
}
/* service cards: one radio per machine, the "no server" card last */
.tk-svcs { display: grid; gap: .6rem; grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr)); }
.tk-svc {
  position: relative; display: flex; flex-direction: column; gap: .25rem; min-width: 0;
  padding: .75rem .9rem; margin: 0; border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  background: var(--bg-raised); cursor: pointer;
}
.tk-svc input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.tk-svc:hover { border-color: var(--brand); }
.tk-svc:has(input:checked) { border-color: var(--brand); background: var(--brand-tint); box-shadow: inset 0 -3px 0 var(--brand); }
.tk-svc:has(input:focus-visible) { outline: 2px solid var(--brand); outline-offset: 2px; }
.tk-svc-head { display: flex; align-items: center; gap: .5rem; min-width: 0; }
/* the name is the hostname — the one string a customer knows a machine by
   (the product repeats thirty times in a big fleet); mono, like the services
   list. On a desk it ellipsises; on a phone it wraps at label boundaries
   only (<wbr> before each dot, switched on below). */
.tk-svc-name { font-family: var(--mono); font-size: .95rem; font-weight: 700; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tk-svc-name wbr { display: none; }
.tk-svc-head .pill { font-size: .72rem; padding: .05rem .5rem; }
.tk-svc-meta { font-size: .85rem; color: var(--text-muted); overflow-wrap: anywhere; }
.tk-svc-ip { font-family: var(--mono); }
/* on a phone the <wbr> is the only break the name gets: nowrap stops the
   browser breaking at a hyphen inside a label (Chromium and WebKit break at a
   <wbr> under nowrap; a browser that does not just ellipsises the name) */
@media (max-width: 640px) {
  .tk-svc-name wbr { display: inline; }
}
.tk-svc-live { display: flex; align-items: center; gap: .4rem; font-size: .82rem; color: var(--text-muted); margin-top: .1rem; }
.tk-svc-live .dot-pulse { flex: none; }
.tk-svc-live.is-busy { color: var(--brand-on-tint); }
.tk-svc-live.is-warn { color: var(--warn-text); }
.tk-svc-live.is-err { color: var(--err); }
.tk-svc-none { border-style: dashed; justify-content: center; align-items: center; color: var(--text-muted); font-weight: 600; }
.tk-svc-none:has(input:checked) { color: var(--brand-on-tint); }
/* helpers: shown when the form's state asks for them; without :has(), all of them.
   The hide rule names the form too: .callout sets display later in this file at
   the same specificity and would win otherwise (every helper showed at once). */
.tk-form .tk-help { grid-column: 1 / -1; margin: 0; }
.tk-form .tk-help-intent { margin-top: .9rem; }
.tk-help .btn { margin-top: .6rem; }
.tk-help .tk-help-note { font-size: .82rem; color: var(--text-faint); }
@supports selector(:has(a)) {
  .tk-form .tk-help { display: none; }
  .tk-form:has(input[name="intent"][value="network"]:checked) .tk-help-intent[data-intent="network"],
  .tk-form:has(input[name="intent"][value="billing"]:checked) .tk-help-intent[data-intent="billing"] { display: flex; }
  /* a machine's helpers follow its card in a fixed order: [down] access reinstall;
     adjacency chains pick the chosen card's own (a ~ sibling would light every
     later machine's, and :nth-of-type counts the whole grid). */
  .tk-form:has(input[name="intent"][value="down"]:checked) .tk-svc:has(input:checked) + .tk-help[data-intent="down"],
  .tk-form:has(input[name="intent"][value="access"]:checked) .tk-svc:has(input:checked) + .tk-help[data-intent="access"],
  .tk-form:has(input[name="intent"][value="access"]:checked) .tk-svc:has(input:checked) + .tk-help[data-intent="down"] + .tk-help[data-intent="access"],
  .tk-form:has(input[name="intent"][value="reinstall"]:checked) .tk-svc:has(input:checked) + .tk-help[data-intent="access"] + .tk-help[data-intent="reinstall"],
  .tk-form:has(input[name="intent"][value="reinstall"]:checked) .tk-svc:has(input:checked) + .tk-help[data-intent="down"] + .tk-help[data-intent="access"] + .tk-help[data-intent="reinstall"] { display: flex; }
}
.tk-urgent { margin: .2rem 0 1rem; font-weight: 600; }

/* The request form's desk (design round 2026-09-06). The cabinet's 26rem form
   width is a login box's, so this form is .form-wide, and the width buys: a
   receipt column carrying the one Send button, six chips in one row, a full
   band for a machine's helper under the chosen card, a message box the width
   of the column. DOM order is reading order at every width — chips, picker,
   composer, receipt — and only the grid moves the receipt to the side. */
.tk-desk { display: grid; gap: 0 2rem; align-items: start; }
.tk-desk > * { min-width: 0; }
.tk-recap { margin-top: 1.4rem; }
@media (min-width: 68rem) {
  .tk-desk { grid-template-columns: minmax(0, 1fr) 23rem; }
  .tk-b1 { grid-column: 1 / -1; }
  .tk-pick, .tk-write { grid-column: 1; }
  .tk-recap {
    grid-column: 2; grid-row: 2 / span 2; position: sticky; top: calc(62px + 1rem);
    padding: 1rem 1.1rem; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-alt);
  }
}
/* the receipt: the house .cfg-receipt rows say the form's state back. The
   what/whom/urgent rows are filled by :has() from the controls themselves;
   the server row is the script's — html:not(.js) outranks .cfg-rows .row,
   which .js-only alone does not. Labels on --text-muted, never --text-faint. */
.tk-recap .eyebrow, .tk-recap.cfg-receipt .hint { color: var(--text-muted); } /* .cfg-receipt .hint sits later at the same weight */
.tk-recap .cfg-rows .row .v { white-space: normal; text-align: right; overflow-wrap: anywhere; }
.tk-recap-what .v > span, .tk-recap-dept .v > span { display: none; }
.tk-recap .cfg-rows .tk-recap-urgent { display: none; }
html:not(.js) .tk-recap-server { display: none; }
@supports selector(:has(a)) {
  .tk-form:has(input[name="intent"][value="down"]:checked) .tk-recap-what [data-intent="down"],
  .tk-form:has(input[name="intent"][value="access"]:checked) .tk-recap-what [data-intent="access"],
  .tk-form:has(input[name="intent"][value="network"]:checked) .tk-recap-what [data-intent="network"],
  .tk-form:has(input[name="intent"][value="reinstall"]:checked) .tk-recap-what [data-intent="reinstall"],
  .tk-form:has(input[name="intent"][value="billing"]:checked) .tk-recap-what [data-intent="billing"],
  .tk-form:has(input[name="intent"][value="other"]:checked) .tk-recap-what [data-intent="other"],
  .tk-form:has(input[name="intent"][value="billing"]:checked) .tk-recap-dept [data-dept="billing"],
  .tk-form:has(input[name="intent"]:checked:not([value="other"]):not([value="billing"])) .tk-recap-dept [data-dept="support"],
  .tk-form:has(input[name="intent"][value="other"]:checked):has(input[name="department"][value="support"]:checked) .tk-recap-dept [data-dept="support"],
  .tk-form:has(input[name="intent"][value="other"]:checked):has(input[name="department"][value="billing"]:checked) .tk-recap-dept [data-dept="billing"] { display: inline; }
  .tk-form:has(input[name="urgent"]:checked) .cfg-rows .tk-recap-urgent { display: flex; }
}
@supports not selector(:has(a)) {
  .tk-recap .cfg-rows .tk-recap-what, .tk-recap .cfg-rows .tk-recap-dept { display: none; }
}
/* below the desk the receipt is its button, at the end of the form */
@media (max-width: 67.99rem) {
  .tk-recap .eyebrow, .tk-recap .cfg-rows, .tk-recap .hint { display: none; }
  .tk-recap.cfg-receipt .btn-block { margin-top: 0; }
}

/* the picker of a big fleet: the count line, the search, the zones. The
   legend floats (above), so the first lines clear it. */
.tk-fleet { clear: both; margin: -.4rem 0 .6rem; font-size: .88rem; color: var(--text-muted); }
.tk-truncated { margin: 0 0 .6rem; }
.tk-find { clear: both; display: flex; flex-wrap: wrap; align-items: center; gap: .5rem 1rem; margin: 0 0 .9rem; }
.tk-find input[type="search"] { flex: 1 1 16rem; max-width: 26rem; font-size: 1rem; }
.tk-find-status { margin: 0; font-size: .88rem; color: var(--text-muted); }
.tk-find-empty { margin: 0 0 .9rem; padding: .8rem 1rem; border: 1px dashed var(--border-strong); border-radius: var(--radius-sm); color: var(--text-muted); }
.tk-find-empty p { margin: 0 0 .6rem; }
.tk-zones { display: flex; flex-direction: column; }
.tk-zone-h { margin: 0 0 .5rem; font-size: .74rem; font-weight: 600; letter-spacing: .09em; text-transform: uppercase; color: var(--text-muted); }
.tk-zone-n { font-weight: 500; }
/* the gap above a zone follows a VISIBLE predecessor, not DOM position: the
   script hides whole zones while filtering, and the first shown one is flush */
.tk-zone:not([hidden]) ~ .tk-zone .tk-zone-h, .tk-zone:not([hidden]) ~ .tk-zone .tk-more { margin-top: .9rem; }
.tk-svcs-none { margin-top: .9rem; }
.tk-more > summary .tk-zone-n { color: var(--text-muted); font-weight: 500; }
/* a row is a card compressed by its container: the same markup, so the
   helper chains above need no second copy */
.tk-rows { display: flex; flex-direction: column; gap: .35rem; }
.tk-rows .tk-svc {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; grid-template-areas: "name meta pill" "live live live";
  column-gap: .8rem; row-gap: .1rem; align-items: center; padding: .45rem .8rem;
}
.tk-rows .tk-svc-head { display: contents; }
.tk-rows .tk-svc-name { grid-area: name; }
.tk-rows .tk-svc-head .pill { grid-area: pill; }
.tk-rows .tk-svc-meta { grid-area: meta; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tk-rows .tk-svc-live { grid-area: live; margin: 0; }
/* in the row form a live line that only restates the pill («Призупинений»
   under «призупинена») goes; the one carrying an operation's time stays */
.tk-rows .tk-svc-live:not(.is-busy) { display: none; }
/* on a phone the attention zone takes the row form too: as cards the page
   for a client with eleven machines came out longer than the one this desk
   replaced (measured 3 302 px against 3 115 at 390 × 844) */
@media (max-width: 640px) {
  .tk-att { display: flex; flex-direction: column; gap: .35rem; }
  /* the name takes the whole row and the pill joins the meta line: beside
     the pill a name had half the width and broke at every dot */
  .tk-rows .tk-svc, .tk-att .tk-svc {
    display: grid; grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "name name" "meta pill" "live live";
    column-gap: .8rem; row-gap: .1rem; align-items: center; padding: .45rem .8rem; min-height: 44px;
  }
  .tk-rows .tk-svc-head .pill, .tk-att .tk-svc-head .pill { justify-self: end; }
  .tk-att .tk-svc-head { display: contents; }
  .tk-att .tk-svc-name { grid-area: name; }
  .tk-att .tk-svc-head .pill { grid-area: pill; }
  .tk-att .tk-svc-meta { grid-area: meta; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .tk-att .tk-svc-live { grid-area: live; margin: 0; }
  .tk-att .tk-svc-live:not(.is-busy) { display: none; }
}
/* while the script filters: the structural counts step aside so the status
   line is the only number on the page; the keyboard's current match wears a
   solid outline (the dashed one is the "no server" card's) */
.tk-pick.is-filtered .tk-fleet, .tk-pick.is-filtered .tk-truncated, .tk-pick.is-filtered .tk-zone-n { visibility: hidden; }
.tk-svc.is-current { outline: 2px solid var(--brand); outline-offset: 2px; }
/* the match's fill must show on the checked card too, whose ground is already the brand tint */
.tk-svc mark { background: color-mix(in oklab, var(--brand) 30%, transparent); color: inherit; border-radius: 2px; padding: 0 .05em; }
/* without :has() the machines' helpers cannot follow the chosen card: one
   line points at the machine pages instead of every helper of every machine */
.tk-nohas { display: none; margin: .8rem 0 0; }
@supports not selector(:has(a)) {
  .tk-form .tk-help:not(.tk-help-intent) { display: none; }
  .tk-nohas { display: block; }
}

/* the closed conversation's line and its door (stage 2) */
.tk-event { margin: -.4rem 0 1rem; font-size: .9rem; color: var(--text-muted); }
.tk-reopen > summary { display: inline-flex; list-style: none; cursor: pointer; }
.tk-reopen > summary::-webkit-details-marker { display: none; }
.tk-reopen[open] > summary { margin-bottom: .8rem; }
.tk-reopen .hint { margin: 0 0 .8rem; }

/* the seen line under the thread and the hours line above the box */
.tk-seen { margin: -.4rem 0 1rem; font-size: .85rem; color: var(--text-muted); }
.tk-hours { margin: 0 0 .8rem; }
.page-head .page-sub.tk-hours { margin: .25rem 0 0; }

/* the skip link: first in the page, visible only when it has focus */
.skip-link {
  position: absolute; left: .75rem; top: -3rem; z-index: 100;
  padding: .5rem .9rem; border-radius: var(--radius-sm);
  background: var(--bg-raised); color: var(--text); box-shadow: var(--shadow);
  text-decoration: none; font-weight: 600;
}
.skip-link:focus { top: .75rem; }
main:focus { outline: none; }

/* forced colours: what colour alone said, a border says too */
@media (forced-colors: active) {
  .pill, .tk-chip, .message, .notice { border: 1px solid CanvasText; }
  .pill .dot { background: CanvasText; }
  .message:target { outline: 3px solid Highlight; }
}

/* ---------- invoices ---------- */

.invoice-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem 1.2rem;
  color: var(--text-muted);
  font-size: .92rem;
  margin-bottom: 1.2rem;
}

.invoice-items { width: 100%; border-collapse: collapse; margin: 1rem 0; font-size: .95rem; }
.invoice-items th {
  text-align: left;
  font-size: .76rem;
  font-weight: 600;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--text-faint);
  padding: .5rem .7rem;
  border-bottom: 1px solid var(--border-strong);
}
.invoice-items td { padding: .55rem .7rem; border-bottom: 1px solid var(--border); }
.invoice-items .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* The totals live outside the scrolling item table, so a narrow screen keeps
   the amounts in view while the wide table scrolls on its own. */
.invoice-totals { margin: .8rem 0 1rem auto; max-width: 22rem; font-size: .95rem; }
.invoice-totals .row {
  display: flex;
  justify-content: space-between;
  gap: 1.5rem;
  padding: .35rem .7rem;
}
.invoice-totals .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.invoice-totals .row.grand {
  font-weight: 700;
  font-size: 1.02rem;
  border-top: 2px solid var(--border-strong);
  padding-top: .6rem;
}
@media (max-width: 640px) {
  .invoice-totals { max-width: none; }
}

.pay { display: flex; flex-wrap: wrap; gap: .6rem; margin: 1.2rem 0; }

/* ---------- order ---------- */

/* ---------- the shop window (stage 8.6) ----------
   Scenario chips, the term switch, then the plans as cards whose rows line
   up across the grid (subgrid), so the eye compares down a column the way it
   would in a table. One price per card for the chosen term; the badge is a
   ribbon on the card's top edge. All of it is links and text — the page
   reads the same without a script. */

.scenarios { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin: 1.6rem 0 .9rem; }
.scenarios .lbl { font-size: .85rem; color: var(--text-muted); margin-right: .2rem; }
.chip {
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .4rem .8rem; border-radius: 999px;
  border: 1px solid var(--border-strong); background: var(--bg-raised);
  color: var(--text); font-size: .88rem; font-weight: 600;
  transition: border-color .15s ease, background .15s ease;
}
.chip:hover { text-decoration: none; border-color: var(--brand); background: var(--brand-tint); }
.chip.is-on { border-color: var(--brand); background: var(--brand-tint); color: var(--brand-on-tint); }
.scenario-note {
  margin: 0 0 1.2rem; padding: .55rem .8rem; max-width: 70ch;
  border: 1px solid var(--border); border-left: 3px solid var(--brand); border-radius: var(--radius-sm);
  background: color-mix(in oklab, var(--brand) 8%, var(--bg-raised));
  color: var(--text-muted); font-size: .9rem;
}

.shop-group { margin-bottom: 1.6rem; }
.shop-group-title { font-size: 1.05rem; color: var(--text-muted); margin: 0 0 .7rem; }
.shop-grid {
  display: grid; gap: .8rem; padding-top: .5rem;
  grid-template-columns: repeat(auto-fit, minmax(min(12.5rem, 100%), 1fr));
}
.shop-card {
  position: relative;
  display: grid; grid-template-rows: subgrid; grid-row: span 5; gap: .4rem;
  padding: .95rem 1rem 1rem;
  background: var(--bg-raised); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  transition: border-color .15s ease, box-shadow .15s ease, translate .15s ease;
}
.shop-card:hover { border-color: color-mix(in oklab, var(--brand) 45%, var(--border)); box-shadow: var(--shadow); translate: 0 -2px; }
.shop-card.is-marked { border-color: var(--brand); box-shadow: 0 0 0 3px color-mix(in oklab, var(--brand) 22%, transparent); }
.shop-card.is-off { color: var(--text-muted); }
.shop-badge {
  position: absolute; top: -.7rem; left: 50%; translate: -50% 0; z-index: 1;
  font-size: .7rem; letter-spacing: .04em; text-transform: uppercase;
}
.shop-name { margin: 0; font-size: 1.1rem; }
.shop-fit { margin: 0; font-size: .85rem; color: var(--text-muted); line-height: 1.35; min-height: 2.3em; }
.shop-mark { display: inline-block; font-size: .7rem; font-weight: 700; padding: .05rem .45rem; border-radius: 999px; color: var(--ok-text); background: color-mix(in oklab, var(--ok) 12%, transparent); vertical-align: middle; }
.shop-spec { list-style: none; margin: 0; padding: 0; display: grid; gap: .3rem; font-size: .9rem; font-variant-numeric: tabular-nums; }
.shop-spec li { display: flex; align-items: center; gap: .5rem; }
.shop-spec .icon { width: 16px; height: 16px; color: var(--text-faint); }
.shop-price { display: flex; flex-direction: column; align-items: flex-start; text-align: left; gap: .1rem; margin-top: .3rem; padding-top: .7rem; border-top: 1px solid var(--border); font-variant-numeric: tabular-nums; }
.shop-price .amt { font-size: 1.2rem; font-weight: 800; letter-spacing: -.02em; line-height: 1.2; }
.shop-price .per { font-size: .8rem; color: var(--text-muted); }
.shop-price .sub { font-size: .8rem; color: var(--text-muted); margin-top: .25rem; }
.shop-price .sub.ok { color: var(--ok-text); }
.shop-cta { margin-top: .5rem; align-self: end; }
.included { margin: .4rem 0 0; font-size: .85rem; color: var(--text-muted); max-width: 80ch; }

/* ---------- phone verification page ---------- */
.verify-wrap { display: flex; justify-content: center; }
.verify-card { width: 100%; max-width: 460px; }
.verify-card h1 { margin-top: 0; }
.hint { font-size: .85rem; }
.phone-line {
  display: flex; align-items: baseline; gap: .75rem; flex-wrap: wrap;
  margin: .3rem 0 .2rem; font-size: 1.3rem;
}
.otp-input {
  font-size: 1.6rem; letter-spacing: .45em; text-align: center;
  font-variant-numeric: tabular-nums;
}
.resend-row { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .85rem; }
.trouble { margin-top: 1.25rem; }

/* The SMS-or-call picker: two cards, the chosen one lights up. */
fieldset.choice { border: 0; padding: 0; margin: 0 0 1rem; min-width: 0; }
fieldset.choice legend { font-size: .85rem; font-weight: 600; color: var(--text-muted); margin-bottom: .4rem; }
label.choice-card {
  display: flex; gap: .65rem; align-items: flex-start;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: .65rem .9rem; margin-bottom: .5rem; cursor: pointer;
  transition: border-color .15s ease, background .15s ease;
}
label.choice-card input[type="radio"] { margin-top: .2rem; }
label.choice-card:has(input:checked) {
  border-color: var(--brand);
  background: color-mix(in oklab, var(--brand-tint) 45%, var(--bg-raised));
}

/* ---------- the Users page (stage 6.95) ---------- */

/* The access form: two radio cards, and the halves that only apply to one
   choice fold away with it. Pure CSS: with :has() unsupported everything
   stays visible, which is only verbose, never wrong. */
/* Wider than the 26rem form default: the picker rows carry a hostname, an
   address and a level select side by side, and the cards want to align
   with the email field above them. */
/* A fieldset carries a browser min-inline-size of min-content, so it refuses
   to shrink below its widest row and drags the form out of the card. */
.access-form { max-width: 42rem; }
.access-form fieldset { min-width: 0; }
.access-form .access-level,
.access-form .svc-picker { margin: -.2rem 0 .9rem 2.1rem; }
.access-form:has(input[name="scope"][value="selected"]:checked) .access-level { display: none; }
.access-form:has(input[name="scope"][value="all"]:checked) .svc-picker { display: none; }

.access-level { display: flex; gap: 1.2rem; }
.choice-inline { display: inline-flex; gap: .45rem; align-items: center; cursor: pointer; }

.svc-picker {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: .3rem .8rem;
  max-height: 16rem;
  overflow-y: auto;
}
.svc-row {
  display: flex; align-items: center; gap: .8rem;
  padding: .45rem 0;
  border-bottom: 1px solid var(--border);
  min-width: 0;
}
.svc-row:last-child { border-bottom: none; }
.svc-name { display: inline-flex; gap: .45rem; align-items: center; cursor: pointer; flex: 1; min-width: 0; }
/* The ellipsis needs a floor to shrink to: a flex item defaults to
   min-width:auto and never goes below its content, so an ordinary
   "IP · hostname" pushed the whole picker — and with it the page — sideways
   (review 2026-08-20, measured at 375px and at 1280px). */
.svc-name span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.svc-row select { width: auto; }

.access-flags label.choice-card { margin-bottom: .5rem; }
.choice-card span small { display: block; margin-top: .15rem; }

/* Sortable list headers (owner, 2026-08-27): the header is the control on
   a wide screen, the chips below the page head take over where the table
   collapses into cards and its header row is gone. One arrow, on the one
   column that currently orders the list. */
.table th a.th-sort {
  color: inherit;
  display: inline-flex;
  align-items: center;
  gap: .25rem;
  text-decoration: none;
}
.table th a.th-sort:hover { color: var(--text); text-decoration: none; }
.table th[aria-sort] a.th-sort { color: var(--text); }
.sort-icon { width: 13px; height: 13px; }

.sort-chips { display: none; }
@media (max-width: 640px) {
  .sort-chips {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .45rem;
    margin: 0 0 .9rem;
    font-size: .85rem;
    color: var(--text-faint);
  }
  .sort-chip {
    display: inline-flex;
    align-items: center;
    gap: .25rem;
    padding: .25rem .7rem;
    border: 1px solid var(--border-strong);
    border-radius: 999px;
    color: var(--text-muted);
    font-weight: 600;
  }
  .sort-chip:hover { text-decoration: none; }
  .sort-chip.active {
    color: var(--brand-strong);
    background: var(--brand-tint);
    border-color: color-mix(in oklab, var(--brand) 35%, transparent);
  }
  .sort-chip .sort-icon { width: 12px; height: 12px; }
}

/* The History fold under the services list. */
.history-fold { margin-top: 1rem; }

/* The paid invoice's "what next" block (owner, 2026-08-28): where the
   money went and the door to it, so a receipt is never a dead end. */
.next-panel { border-left: 3px solid var(--ok); }
.next-row {
  display: flex;
  align-items: center;
  gap: .5rem .9rem;
  flex-wrap: wrap;
  padding: .3rem 0;
}
.next-row .btn { margin-left: auto; }
.next-mail { margin: .5rem 0 0; }

/* The build timeline on a pending machine's card: real states, told in
   order — a check for what happened, a breathing dot for what is
   happening, quiet for what is still to come. */
.build-steps { list-style: none; margin: .4rem 0 0; padding: 0; }
.build-steps li { position: relative; padding: 0 0 1.05rem 2rem; }
.build-steps li::before {
  content: "";
  position: absolute;
  left: .55rem;
  top: 1.35rem;
  bottom: -.1rem;
  width: 2px;
  background: var(--border);
}
.build-steps li:last-child { padding-bottom: .2rem; }
.build-steps li:last-child::before { content: none; }
.build-steps .mark {
  position: absolute;
  left: 0;
  top: .15rem;
  width: 1.25rem;
  height: 1.25rem;
  border-radius: 50%;
  border: 2px solid var(--border-strong);
  background: var(--bg-raised);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.build-steps .done .mark { border-color: var(--ok); background: color-mix(in oklab, var(--ok) 12%, var(--bg-raised)); }
.build-steps .done .mark::after { content: "✓"; font-size: .7rem; font-weight: 700; color: var(--ok); }
.build-steps .now .mark { border-color: var(--brand); }
.build-steps .now .mark::after {
  content: "";
  width: .5rem;
  height: .5rem;
  border-radius: 50%;
  background: var(--brand);
}
@media (prefers-reduced-motion: no-preference) {
  .build-steps .now .mark::after { animation: build-pulse 1.6s ease-in-out infinite; }
  @keyframes build-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
}
.build-steps .todo { color: var(--text-faint); }
.build-steps .t { font-weight: 600; }
.build-steps .todo .t { font-weight: 500; }
.build-steps .sub { display: block; font-size: .88rem; color: var(--text-muted); margin-top: .1rem; }
.build-steps .now .sub.live { color: var(--brand-strong); }

/* A quiet compact button for table rows. */
.btn-small { padding: .3rem .7rem; font-size: .82rem; }
.cell-actions { text-align: right; white-space: nowrap; }
.cell-actions form.inline { display: inline-block; }
.account-list form.inline { display: inline-block; margin-left: .6rem; }
.account-current { background: color-mix(in oklab, var(--brand-tint) 40%, transparent); }
/* The word beside the tint, so the current account is distinguishable
   without relying on colour (WCAG 1.4.1). */
.badge-current { background: color-mix(in oklab, var(--brand) 14%, transparent); }

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

details.fold {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: .7rem .95rem;
}
details.fold > summary {
  cursor: pointer;
  font-weight: 600;
  font-size: .95rem;
  color: var(--text-muted);
}
details.fold[open] { padding-bottom: 1rem; }
details.fold[open] > summary { margin-bottom: .8rem; color: var(--text); }

details.danger-zone {
  border: 1px solid color-mix(in oklab, var(--err) 35%, transparent);
  border-radius: var(--radius-sm);
  padding: .7rem .95rem;
}
details.danger-zone > summary { cursor: pointer; font-weight: 600; color: var(--err); }
details.danger-zone[open] { padding-bottom: 1rem; }
details.danger-zone[open] > summary { margin-bottom: .8rem; }

.recovery-codes {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: .4rem;
  margin: .8rem 0;
  padding: 0;
  list-style: none;
}
.recovery-codes li {
  padding: .4rem .6rem;
  text-align: center;
  background: var(--bg-inset);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-sm);
}

/* Two-factor enrolment (owner, 2026-08-21). One markup, two readings: a
   device that can hover is a desktop, and its phone scans the QR code; a
   touch device cannot scan its own screen, so it opens the code app with
   the otpauth link and the QR code steps back as "for another device".
   The order comes from flex, the switch from the pointer media query —
   no user-agent sniffing, no script. */
.enrol { display: flex; flex-direction: column; gap: .9rem; margin-bottom: 1.2rem; }
.enrol-step { min-width: 0; }
.enrol-step > p { margin: 0 0 .5rem; }
.enrol-qr .small { margin: .6rem 0 0; font-size: .82rem; max-width: 30rem; }
.enrol-open { display: none; }
.enrol-open .btn { width: 100%; max-width: 28rem; padding: .8rem 1rem; font-size: 1rem; }
.enrol-open .hint { margin: .5rem 0 0; }
.enrol-qr { order: 1; }
.enrol-manual { order: 2; }
.enrol-other { display: none; }

/* The QR code sits on a white tile whatever the theme: a camera reads
   dark modules on white, not the other way round. */
.qr-tile {
  display: inline-block;
  padding: .6rem;
  background: #ffffff;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  line-height: 0;
}
.qr { display: block; width: 220px; height: 220px; image-rendering: pixelated; }

/* The secret and its copy button on one line: the secret wraps inside its
   box, the button stays beside it. The button is hidden without the
   script, as every copy button is. */
.secret-row { display: flex; align-items: center; gap: .5rem; }
.secret-row .secret { flex: 1 1 auto; min-width: 0; margin: 0; }

@media (hover: none) and (pointer: coarse) {
  .enrol-open { display: block; order: 0; }
  .enrol-manual { order: 1; }
  .enrol-qr { order: 2; }
  .enrol-qr .enrol-scan { display: none; }
  .enrol-other { display: block; }
  .enrol-qr .qr { width: 150px; height: 150px; }
  .enrol-qr .qr-tile { padding: .45rem; }
  .secret-row .secret { letter-spacing: .05em; font-size: .98rem; }
}

.secret {
  font-family: var(--mono);
  font-size: 1.05rem;
  letter-spacing: .1em;
  padding: .6rem .8rem;
  background: var(--bg-inset);
  border-radius: var(--radius-sm);
  overflow-wrap: anywhere;
}

.empty {
  text-align: center;
  color: var(--text-faint);
  padding: 2.5rem 1rem;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
}
.empty p { margin: 0 0 .8rem; }

.pager-nav {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  margin-top: 1rem;
}

/* The resources a service carries: what each one is, what a unit of it costs,
   and the two controls that change it.

   Laid out against this list's own width rather than the window's. The same
   panel is half of a wide page and the whole width of a phone, and a row that
   fits the window can still overflow a column a third its width — which is how
   the save buttons ended up outside the card (owner, 2026-08-18). So the parts
   stack by default and take one line only where the container has room. */

/* Fixed tracks rather than a flex row, so the fields of neighbouring rows line
   up in a column: the figure a customer types is the same distance from the
   edge whether or not that resource has a total to state beside it. */

.actions { display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; }

/* ---------- profile ---------- */

/* paired fields (name + surname, city + region) sit side by side where the
   panel is wide enough, and stack on phones like everything else */
.field-row { display: grid; gap: .9rem; }
@media (min-width: 560px) {
  .field-row { grid-template-columns: 1fr 1fr; }
}

/* a quiet separator between the blocks of one panel */
.rule { border: 0; border-top: 1px solid var(--border); margin: 1.2rem 0; }

/* label: value lines for the read-only facts (email, currency, phone) */
.kv-line { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin: .35rem 0; }

/* ---------- print ---------- */

.print-page { display: block; background: #fff; color: #1c1d21; }
.print-page main { max-width: 48rem; margin: 0 auto; padding: 2rem 1rem; }
.print-page .brand .wordmark { font-size: 1.6rem; }

@media print {
  .no-print { display: none !important; }
  .print-page main { padding: 0; }
  body { background: #fff; }
}

/* ---------- the plan-change confirmation ---------- */

/* The move itself, before any argument about it: where the machine is, where
   it would go, and the one number the decision turns on. */
.crumbs { margin: 0 0 .8rem; font-size: .9rem; }
.crumbs a { color: var(--text-muted); }

.plan-move {
  display: flex;
  align-items: center;
  gap: 1.2rem;
  flex-wrap: wrap;
}
.plan-side { display: flex; flex-direction: column; gap: .15rem; }
.plan-label {
  font-size: .72rem;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--text-muted);
}
.plan-side b { font-size: 1.05rem; }
.plan-disk { color: var(--text-muted); font-size: .9rem; font-variant-numeric: tabular-nums; }
.plan-arrow { color: var(--text-faint); font-size: 1.3rem; }
/* The price belongs to something — a plan the customer would move to, or an
   option's own line — and an unlabelled number in this corner has been read as
   both. It carries its label like the two sides do (review 2026-08-18). */
.plan-price {
  margin-left: auto;
  display: flex;
  flex-direction: column;
  gap: .15rem;
  align-items: flex-end;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.plan-per { font-weight: 400; font-size: .85rem; color: var(--text-muted); }

/* What an option change is about, since both sides of the strip are the same
   option and only the size differs. */
.disk-subject { margin: -.4rem 0 1rem; }

/* What the quantity beside it comes to on the machine. Quiet: it is the
   answer to a question the customer did not have to ask. */

/* A panel that carries a consequence. The colour is the warning; the words do
   the work, so the tint stays quiet enough to read a paragraph on. */
.panel-danger {
  border-color: color-mix(in oklab, var(--err) 40%, var(--border));
  background: color-mix(in oklab, var(--err) 4%, var(--bg-raised));
}
.panel-danger > h2:first-child { color: var(--err); }

.disk-figure {
  font-variant-numeric: tabular-nums;
  padding: .7rem .9rem;
  border-radius: var(--radius-sm);
  background: color-mix(in oklab, var(--err) 7%, transparent);
}

/* The way out, offered before the way through. */
.disk-alternative {
  margin: 1.2rem 0;
  padding: 1rem 1.1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg);
}
.disk-alternative p { margin-bottom: .7rem; }

/* The destructive path is one deliberate tick away, never one stray click. */
.disk-consent {
  margin-top: 1.4rem;
  padding-top: 1.2rem;
  border-top: 1px solid var(--border);
}
.check { display: flex; align-items: flex-start; gap: .6rem; margin-bottom: 1rem; }
.check input { margin-top: .2rem; flex-shrink: 0; }
.check span { font-size: .95rem; }

/* The one button on the site that destroys something reads like it. The
   outlined .btn-danger elsewhere refuses an action; this one carries it out,
   and the difference should not rest on the reader noticing a border. */
.disk-consent .btn-danger {
  color: #fff;
  background: var(--err-solid);
  border-color: var(--err-solid);
}
.disk-consent .btn-danger:hover {
  background: color-mix(in oklab, var(--err-solid) 85%, black);
}

/* A page that is waiting for a node says so with something that moves; the
   one that has an answer says nothing of the kind. */
.dot-pulse {
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--brand);
  animation: dot-pulse 1.6s ease-in-out infinite;
}
@keyframes dot-pulse { 50% { opacity: .25; } }
@media (prefers-reduced-motion: reduce) { .dot-pulse { animation: none; } }

/* ---------- access details: the page a customer comes to in order to copy ---
   Three credentials, each a row you can take in at a glance and copy without
   selecting anything. The password is masked until asked for: this page is
   opened in offices and shared screens, and copying does not need it visible.
   Everything is built from the tokens the rest of the cabinet uses. */

.access-grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  align-items: start;
}
@media (max-width: 860px) {
  .access-grid { grid-template-columns: 1fr; }
}

.cred-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  /* the first row's rule is the panel's own top edge */
  margin-top: -.35rem;
}

.cred {
  display: grid;
  grid-template-columns: 7rem minmax(0, 1fr) auto;
  align-items: center;
  gap: 1rem;
  padding: 1rem 0;
  border-top: 1px solid var(--border);
}
.cred:first-child { border-top: 0; }

/* Narrow: the label goes over the value, the buttons stay beside it. A fixed
   label column and a monospace address do not both fit on a phone, and the
   address is the half that must not wrap. */
@media (max-width: 560px) {
  .cred {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "label label"
      "value actions";
    gap: .3rem .75rem;
  }
  .cred-label  { grid-area: label; }
  .cred-value  { grid-area: value; }
  .cred-actions { grid-area: actions; align-self: center; }
}

.cred-label {
  font-size: .78rem;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.cred-value {
  font-family: var(--mono);
  font-size: 1.05rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: .01em;
  overflow-wrap: anywhere;
  user-select: all;
}

/* A little smaller than the header's icon buttons: a column of them beside
   the values reads as one object. */
.cred-actions .icon-btn { width: 34px; height: 34px; }
.cred-actions .icon-btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* The button that asks for the password. Outlined rather than filled — the
   page's own call to action is the instructions — and outlined rather than
   ghost, so that it reads as a control at first glance. */
.cred-reveal { margin: 0; }
.btn-reveal { padding: .4rem .8rem; font-size: .88rem; }

/* The copy buttons are an enhancement and appear only once app.js has run.
   Without it every value is still on the page to select by hand, and nothing
   on the page does nothing. */
.cred-actions { display: none; }
.js .cred-actions { display: inline-flex; gap: .25rem; }

/* Copied: the tick replaces the sheets for a moment. No text swaps in beside
   it — a confirmation that moves the button under the pointer is a
   confirmation that gets clicked twice. */
.copy-done { display: none; }
.icon-btn.is-done .copy-idle { display: none; }
.icon-btn.is-done .copy-done { display: block; color: var(--ok); }

/* The word is for anybody who cannot see the tick. Kept out of the layout so
   that confirming a copy never moves the buttons under the pointer. */
.cred-live {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* The one warning on the page, given the weight of one. */
.callout {
  display: flex;
  gap: .75rem;
  align-items: flex-start;
  padding: .9rem 1.1rem;
  margin-top: 1.1rem;
  font-size: .92rem;
  line-height: 1.5;
  color: var(--text);
  background: color-mix(in oklab, var(--brand) 6%, var(--bg-raised));
  border: 1px solid color-mix(in oklab, var(--brand) 30%, var(--border));
  border-radius: var(--radius-sm);
}
/* What follows the callout on the access page - the reset's disclosure, its
   "in flight" notice, the sentences about the account - keeps its distance:
   neither box carries a top margin of its own (review 41, C93). */
.callout + details.danger-zone,
.callout + .notice,
.callout ~ .hint { margin-top: 1rem; }
/* And the "last reset failed" notice after the disclosure itself: the two
   bordered boxes used to touch (review 42, C75). */
details.danger-zone + .notice { margin-top: 1rem; }
.callout .icon { color: var(--brand-strong); margin-top: .15rem; }
.callout-body { min-width: 0; }
.callout-body p { margin: 0; }
/* Both lines carry the same weight: the second one — how long a new password
   has to be — is the more actionable of the two, and muting it would say the
   opposite. */
.callout-body p + p { margin-top: .5rem; }

.access-aside h2 { margin-top: .1rem; }
.access-aside p { color: var(--text-muted); font-size: .94rem; }
.access-aside .btn { width: 100%; }

.access-back { margin-top: 1.2rem; }

/* ---------- renewing several services at once (owner, 2026-08-21) ----------
   The list on the left, the running total on the right — sticky on a wide
   screen, folded into a bar along the bottom of a phone, the way a cart
   behaves. The figures are the server's; the script only keeps them in step
   with the ticks while somebody is choosing. */

.page-sub { margin: -.9rem 0 1.4rem; color: var(--text-muted); max-width: 60ch; }

/* The account's standing discount, said once per page that prices things
   (stage 6.97). A quiet strip rather than a banner: it explains figures the
   reader is already looking at, and it is on every visit for the accounts
   that have one — a loud box would be shouting the same thing forever.
   It is --bg-raised, not --bg-alt: the page's own background IS --bg-alt, so
   the strip was invisible and its padding read as a 15px hang against every
   heading and panel on the page (visual review 2026-08-23). */
.discount-note {
  margin: 1rem 0 1.4rem;
  padding: .5rem .75rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  border-left: 3px solid var(--ok);
  /* Its own tint, not a lighter ground: --bg-raised is invisible on the pages
     whose ground IS --bg-raised — the transfer card, where the note ended up
     reading as a mis-rendered row of the detail list above it (review
     2026-08-24). A wash of the same green as the left rule separates it from
     every ground this cabinet has. */
  background: color-mix(in oklab, var(--ok) 10%, var(--bg-raised));
  color: var(--text-muted);
  font-size: .9rem;
  max-width: 60ch;
}
/* Under a page head it closes the same gap .page-sub closes; anywhere else
   it keeps its own air (margin-top above), because the pull-up was tuned for
   a heading and landed flush against a paragraph on the disk-change page —
   and flush against a detail list on the transfer card, which brings no
   margin of its own (review 2026-08-24). */
.page-head + .discount-note,
.page-sub + .discount-note { margin-top: -.9rem; }

.renew-grid { display: grid; gap: 1rem; align-items: start; }
/* Two columns only when the list keeps room for its four: at 64rem the
   summary squeezed the table into sideways scrolling, which on a Mac shows no
   scrollbar and reads as the amount column cut off (owner, 2026-08-22). */
@media (min-width: 68rem) {
  .renew-grid { grid-template-columns: minmax(0, 1fr) 18rem; }
  .renew-summary { position: sticky; top: calc(62px + 1rem); }
}

.renew-table th, .renew-table td { padding-left: .6rem; padding-right: .6rem; }
.renew-table td.primary { min-width: 12.5rem; }
.renew-pick { display: flex; align-items: flex-start; gap: .7rem; cursor: pointer; min-width: 0; }
.renew-pick input { margin-top: .2rem; flex: none; }
.renew-pick > span { min-width: 0; }
.renew-pick .name { font-weight: 600; }
/* A hostname has no spaces to wrap at; unbroken it set the column's width and
   pushed the amount column out of the panel. */
.renew-pick .host { display: block; font-family: var(--mono); font-size: .85rem; color: var(--text-muted); font-weight: 400; overflow-wrap: anywhere; }
.renew-table tbody tr.is-on { background: color-mix(in oklab, var(--brand) 7%, transparent); }
.renew-table tbody tr.is-on:hover { background: color-mix(in oklab, var(--brand) 11%, transparent); }
.renew-table tbody tr.is-held { color: var(--text-muted); }
.renew-table select { width: auto; max-width: 100%; padding-top: .42rem; padding-bottom: .42rem; font-size: .92rem; }
.renew-table .due, .renew-table .amount { display: flex; flex-direction: column; align-items: flex-start; gap: .1rem; }
.renew-table .amount { align-items: flex-end; }
.renew-table .date-line { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; }
.renew-table .sub { display: block; font-size: .8rem; color: var(--text-faint); }
.renew-table .sub.soon { color: var(--warn-text); }
.renew-table .sub.past { color: var(--err); }
.renew-table .price { font-weight: 600; white-space: nowrap; }
.renew-table th.check-all label { display: flex; gap: .5rem; align-items: center; text-transform: none; letter-spacing: 0; font-size: .82rem; cursor: pointer; }
.held-link { font-size: .88rem; }

.renew-totals { display: flex; flex-direction: column; }
.renew-totals .row { display: flex; justify-content: space-between; gap: 1rem; padding: .38rem 0; color: var(--text-muted); }
.renew-totals .row .num { font-variant-numeric: tabular-nums; color: var(--text); }
.renew-totals .row.credit .num { color: var(--ok-text); }
.renew-totals .row.grand { margin-top: .4rem; padding-top: .8rem; border-top: 1px solid var(--border); color: var(--text); font-weight: 700; white-space: nowrap; }
.renew-totals .row.grand .num { font-size: 1.15rem; letter-spacing: -.02em; }
.renew-summary .btn-block { margin-top: 1rem; white-space: nowrap; }
.renew-summary .hint { margin: .8rem 0 0; color: var(--text-faint); font-size: .84rem; }
.renew-others { margin: .9rem 0 0; font-size: .9rem; }

/* The select-all above the table: the one place it survives on a phone, where
   the table header is a stack of cards and has no header row to live in. It is
   the same control as the header checkbox — the script drives every
   [data-renew-all] — so the two never disagree. */
.renew-toolbar { display: none; margin: 0 0 .8rem; }
@media (max-width: 640px) {
  .renew-toolbar { display: flex; }
  .renew-table th.check-all label { display: none; }
}

/* the phone: the summary folds into a bar that stays at the bottom of the screen */
.renew-bar { display: none; }
@media (max-width: 67.99rem) {
  .renew-bar {
    display: flex; align-items: center; gap: 1rem;
    position: sticky; bottom: 0; z-index: 40;
    margin: 1rem -1rem -3rem; padding: .8rem 1rem calc(.8rem + env(safe-area-inset-bottom));
    background: var(--glass); -webkit-backdrop-filter: saturate(1.8) blur(12px); backdrop-filter: saturate(1.8) blur(12px);
    border-top: 1px solid var(--border);
  }
  .renew-bar .sum { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
  .renew-bar .sum .count { font-size: .8rem; color: var(--text-muted); }
  .renew-bar .sum .total { font-weight: 700; font-size: 1.15rem; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
  .renew-bar .sum .credit { font-size: .78rem; color: var(--ok-text); font-variant-numeric: tabular-nums; }
  .renew-bar .btn { margin-left: auto; flex: none; white-space: nowrap; }
  .renew-summary { display: none; }
}
@media (max-width: 640px) {
  .renew-table td.primary { min-width: 0; }
  .renew-table td.primary::before { display: none; }
  .renew-table select { width: 100%; }
  .renew-table td.cycle { display: block; }
}

/* an invoice over several machines heads each one's lines with its name */
.invoice-items tr.group th {
  padding: .9rem .8rem .3rem;
  font-family: var(--mono);
  font-size: .82rem;
  font-weight: 600;
  text-align: left;
  text-transform: none;
  letter-spacing: 0;
  color: var(--text-muted);
  border-bottom: none;
}

/* the account's standing discount, on the line under what it comes off
   (stage 6.97): quiet on the left, because the description repeats the line
   above it, and in the "good news" colour on the right, because it is the
   one figure on an invoice that is in the reader's favour */
.invoice-items tr.discount td { color: var(--text-muted); }
.invoice-items tr.discount td.num { color: var(--ok-text); }

/* ---------- a switch (owner, 2026-08-22) ----------
   A checkbox wearing a track and a knob. The input stays in the tree and
   keyboard-reachable; only its box is hidden, so the focus ring lands on the
   track and the label still toggles it. */
.switch { display: inline-flex; align-items: center; gap: .65rem; cursor: pointer; }
.switch input {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.switch-track {
  position: relative; flex: none;
  width: 2.4rem; height: 1.35rem;
  border-radius: 999px;
  /* --border-strong left the off track at 1.53:1 against the panel, with a
     white knob at the same ratio against the track: you could not tell there
     was a knob, nor which side it sat on (review 2026-08-22). */
  background: var(--text-faint);
  transition: background .15s ease;
}
.switch-track::after {
  content: "";
  position: absolute; top: .15rem; left: .15rem;
  width: 1.05rem; height: 1.05rem;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 2px rgb(16 18 24 / 30%);
  transition: translate .15s ease;
}
.switch input:checked + .switch-track { background: var(--brand); }
.switch input:checked + .switch-track::after { translate: 1.05rem 0; }
/* A solid outline, not a tinted glow: the native box is clipped away, and a
   25%-alpha ring is 1.23:1 against the panel — invisible, and the same hue as
   the track when the switch is on (review 2026-08-22). This is the shape the
   file already uses for the file picker and the credential buttons. */
.switch input:focus-visible + .switch-track { outline: 2px solid var(--brand); outline-offset: 2px; }
.switch input:disabled + .switch-track { opacity: .55; }
.switch input:disabled ~ .switch-label { color: var(--text-muted); }
.switch:has(input:disabled) { cursor: default; }
.switch-label { font-weight: 600; font-size: .95rem; }
/* The fact line and the folded rule under the switch. The summary is a
   quiet dotted link, the way a "?" would read, without the tooltip's
   blindness to touch screens. */
/* Three classes, deliberately: `.auto-renew .hint` further down is two, and
   an equal count would let the later rule win on order alone — which is how
   the fact line ended up in --text-faint at 3.31:1, under AA and quieter than
   the folded rule it is supposed to outrank (review 2026-08-22). */
.auto-renew .hint.auto-renew-fact { margin: .5rem 0 0; font-size: .9rem; color: var(--text); }
.auto-renew-how { margin-top: .3rem; }
.auto-renew-how > summary {
  display: inline-block; cursor: pointer; list-style: none;
  font-size: .82rem; color: var(--text-muted);
  text-decoration: underline dotted; text-underline-offset: 3px;
}
.auto-renew-how > summary::-webkit-details-marker { display: none; }
.auto-renew-how > summary:hover, .auto-renew-how[open] > summary { color: var(--brand-strong); }
.auto-renew-how > summary:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 2px; }
.auto-renew-how > .hint { margin: .4rem 0 0; color: var(--text-faint); }
@media (prefers-reduced-motion: reduce) {
  .switch-track, .switch-track::after { transition: none; }
}

/* the standing order under the renewal form */
.auto-renew { margin-top: 1.1rem; padding-top: 1rem; border-top: 1px solid var(--border); }
.auto-renew .hint { margin: .45rem 0 0; color: var(--text-faint); font-size: .84rem; }
.auto-renew .btn-small { margin-top: .7rem; }
.js .js-hidden { display: none; }

/* The announcement strip (announce.go): one tinted band per live notice,
   gravest first, above the topbar. The ink token is set by the severity
   class and everything else derives from it, so a new tier is one line. */
.announce {
  --ann-ink: var(--ann-info);
  display: flex; align-items: flex-start; gap: .6rem;
  padding: .55rem clamp(1rem, 4vw, 2rem);
  font-size: .92rem; color: var(--text);
  background: color-mix(in oklab, var(--ann-ink) 9%, var(--bg-raised));
  border-bottom: 1px solid color-mix(in oklab, var(--ann-ink) 28%, var(--border));
}
.announce-warning { --ann-ink: var(--ann-warn); }
.announce-critical {
  --ann-ink: var(--ann-crit);
  background: color-mix(in oklab, var(--ann-ink) 13%, var(--bg-raised));
}
.announce > .icon { flex: none; width: 18px; height: 18px; color: var(--ann-ink); }
.announce-main { flex: 1 1 auto; min-width: 0; }
.announce-text strong { font-weight: 600; }
.announce-when { color: var(--text-muted); }
/* The body waits behind a fold, so the band keeps its one line — the same
   dotted-summary language as the auto-renew explainer. */
.announce-more > summary {
  display: inline-block; cursor: pointer; list-style: none;
  font-size: .82rem; color: var(--text-muted);
  text-decoration: underline dotted; text-underline-offset: 3px;
}
.announce-more > summary::-webkit-details-marker { display: none; }
.announce-more > summary:hover, .announce-more[open] > summary { color: var(--text); }
.announce-more > summary:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 2px; }
.announce-body { max-width: 60rem; }
.announce-body p { margin: .45rem 0 0; font-size: .9rem; color: var(--text); }
.announce form { margin-left: auto; }
.announce-hide {
  border: 0; background: none; cursor: pointer;
  color: var(--text-muted); font-size: 1.15rem; line-height: 1;
  padding: .15rem .45rem; border-radius: var(--radius-sm);
}
.announce-hide:hover {
  color: var(--text);
  background: color-mix(in oklab, var(--ann-ink) 14%, transparent);
}
.announce-hide:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* on the guest door the strip becomes a card above the sign-in card */
.auth-shell .announce {
  width: 100%; margin-bottom: 1rem;
  border: 1px solid color-mix(in oklab, var(--ann-ink) 30%, var(--border));
  border-radius: var(--radius);
}

/* Passkeys (security page + login door). The controls are born [hidden] in
   the templates: the ceremony is JavaScript or nothing, so app.js reveals
   them only after the browser proves it can run one — no dead buttons with
   script or without. */
.passkey-list { list-style: none; margin: 0 0 1rem; padding: 0; display: grid; gap: .8rem; }
.passkey { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: .75rem .9rem; }
.passkey-head { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.passkey-head strong { overflow-wrap: anywhere; }
.passkey-meta { margin: .25rem 0 0; font-size: .82rem; }
.passkey-manage { margin-top: .45rem; }
.passkey-manage > summary { cursor: pointer; font-size: .88rem; color: var(--text-muted); }
.passkey-manage[open] > summary { margin-bottom: .8rem; }
.passkey-manage .form { margin-bottom: 1rem; }
.passkey-signin { margin-top: .8rem; }
.passkey-add .notice, .passkey-signin .notice { margin-top: .8rem; }

/* ---------- the configurator (stage 8.6.4) ----------
   Five blocks down the page, the receipt beside them — sticky on a wide
   screen, a bar along the bottom of a phone, as the renewal cart does it.
   The slider is laid over the number field and shown only with a script
   (.js-only); without one the page is the number fields the server posts.
   The track's fill is a custom property the script sets through the CSSOM,
   which the content security policy does not forbid — a style attribute in
   the markup it would. */

.cfg-grid { display: grid; gap: 1.2rem; align-items: start; }
@media (min-width: 68rem) {
  .cfg-grid { grid-template-columns: minmax(0, 1fr) 21rem; }
  .cfg-receipt { position: sticky; top: calc(62px + 1rem); }
}
.cfg-main { display: flex; flex-direction: column; gap: 1.2rem; min-width: 0; }
.cfg-block fieldset { border: 0; padding: 0; margin: 0; min-width: 0; }
.cfg-block legend { padding: 0; }
.cfg-head { display: flex; align-items: center; gap: .7rem; margin: 0 0 .9rem; flex-wrap: wrap; }
.cfg-head h2 { margin: 0; font-size: 1.1rem; }
.cfg-no {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  width: 1.6rem; height: 1.6rem; border-radius: 50%;
  background: var(--text); color: var(--bg); font-size: .78rem; font-weight: 700;
}
.cfg-sub { font-size: .85rem; color: var(--text-muted); margin-left: auto; }
.cfg-sub-line { margin: -.6rem 0 .9rem; }
a.cfg-sub { color: var(--brand-strong); }
@media (max-width: 640px) { .cfg-sub { flex-basis: 100%; margin-left: 0; padding-left: 2.3rem; margin-top: -.5rem; } }

.cfg-plan { display: flex; flex-wrap: wrap; gap: .6rem 2rem; align-items: flex-start; }
.cfg-plan-name { font-weight: 700; font-size: 1.15rem; display: flex; flex-direction: column; gap: .15rem; flex: 1 1 14rem; }
.cfg-plan-fit { font-weight: 400; font-size: .9rem; color: var(--text-muted); }
.cfg-plan .shop-spec { flex: 1 1 12rem; }

.cfg-terms {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(9rem, 100%), 1fr));
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm); overflow: hidden; background: var(--bg-inset);
  gap: 1px;
}
.cfg-term {
  position: relative; display: flex; flex-direction: column; gap: .1rem; min-height: 4.6rem;
  padding: .65rem .8rem; background: var(--bg-raised); cursor: pointer; transition: background .15s ease;
}
.cfg-term input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.cfg-term .t { font-weight: 600; font-size: .9rem; }
.cfg-term .p { font-size: .9rem; }
.cfg-term .pm { font-size: .8rem; color: var(--text-muted); }
.cfg-term:hover { background: var(--brand-tint); }
.cfg-term:has(input:checked) { background: var(--brand-tint); box-shadow: inset 0 -3px 0 var(--brand); }
.cfg-term:has(input:checked) .t { color: var(--brand-on-tint); }
.cfg-term:has(input:focus-visible) { outline: 2px solid var(--brand); outline-offset: -2px; }
.cfg-recalc { margin-top: .8rem; }

.cfg-res-list { display: flex; flex-direction: column; }
/* The separator hangs off a VISIBLE preceding row, not off :first-child: a
   hidden first row is still the first child and would take the separator
   with it (review 2026-09-06, wave 5). */
.cfg-res { padding: 0 0 1rem; border-top: 0; display: grid; gap: .5rem; min-width: 0; }
.cfg-res:not([hidden]) ~ .cfg-res { padding-top: 1rem; border-top: 1px solid var(--border); }
fieldset.cfg-res { border: 0; margin: 0; }
fieldset.cfg-res:not([hidden]) ~ fieldset.cfg-res { border-top: 1px solid var(--border); }
.cfg-res-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: .3rem 1rem; }
.cfg-res-name { font-weight: 600; }
.cfg-res-rate { font-size: .85rem; color: var(--text-muted); margin-left: auto; }
.cfg-read { margin: 0; display: flex; flex-wrap: wrap; align-items: baseline; gap: .35rem .5rem; }
.cfg-read .base { color: var(--text-muted); }
.cfg-read .arrow { color: var(--text-faint); }
.cfg-read .new { font-weight: 700; font-size: 1.1rem; letter-spacing: -.01em; }
.cfg-read .delta { font-size: .9rem; color: var(--text-muted); }
.cfg-read .delta b { color: var(--ok-text); font-weight: 600; }
.cfg-res:not(.is-changed) .cfg-read .base,
.cfg-res:not(.is-changed) .cfg-read .arrow,
.cfg-res:not(.is-changed) .cfg-read .delta { display: none; }
.cfg-res:not(.is-changed) .cfg-read .new { font-weight: 600; font-size: 1rem; color: var(--text-muted); }
.cfg-ctl { display: grid; grid-template-columns: auto; gap: .5rem; align-items: center; }
.js .cfg-ctl { grid-template-columns: 2.4rem minmax(0, 1fr) 2.4rem auto; }
@media (max-width: 520px) {
  .js .cfg-ctl { grid-template-columns: 2.4rem minmax(0, 1fr) 2.4rem; }
  .js .cfg-qty-wrap { grid-column: 1 / -1; }
}
.js-only { display: none; }
.js .js-only { display: revert; }
.js .cfg-chips { display: flex; }
.cfg-step {
  width: 2.4rem; height: 2.4rem; border-radius: var(--radius-sm); font: inherit; font-size: 1.1rem; font-weight: 600;
  border: 1px solid var(--border-strong); background: var(--bg-raised); color: var(--text); cursor: pointer;
}
.cfg-step:hover { border-color: var(--brand); background: var(--brand-tint); }
.cfg-qty-wrap { display: inline-flex; align-items: center; gap: .4rem; }
input.cfg-qty { width: 4.6rem; text-align: center; }
.cfg-unit { font-size: .8rem; color: var(--text-muted); white-space: nowrap; }
.cfg-slider { width: 100%; height: 2.4rem; margin: 0; background: transparent; accent-color: var(--brand); cursor: pointer; }
.js .cfg-slider { -webkit-appearance: none; appearance: none; }
.js .cfg-slider::-webkit-slider-runnable-track { height: 6px; border-radius: 999px; border: 1px solid var(--border); background: linear-gradient(to right, var(--brand) calc(var(--fill, 0) * 1%), var(--bg-inset) 0); }
.js .cfg-slider::-moz-range-track { height: 6px; border-radius: 999px; border: 1px solid var(--border); background: linear-gradient(to right, var(--brand) calc(var(--fill, 0) * 1%), var(--bg-inset) 0); }
.js .cfg-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 24px; height: 24px; margin-top: -10px; border-radius: 50%; background: var(--bg-raised); border: 2px solid var(--brand); box-shadow: var(--shadow-sm); }
.js .cfg-slider::-moz-range-thumb { width: 20px; height: 20px; border-radius: 50%; background: var(--bg-raised); border: 2px solid var(--brand); box-shadow: var(--shadow-sm); }
.js .cfg-slider:hover::-webkit-slider-thumb, .js .cfg-slider:active::-webkit-slider-thumb { box-shadow: 0 0 0 6px color-mix(in oklab, var(--brand) 18%, transparent); }
.js .cfg-slider:focus-visible { outline: none; }
.js .cfg-slider:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 4px var(--bg-raised), 0 0 0 6px var(--brand); }
.js .cfg-slider:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 4px var(--bg-raised), 0 0 0 6px var(--brand); }
.cfg-chips { flex-wrap: wrap; gap: .4rem; }
.cfg-chips .chip { cursor: pointer; font-family: inherit; }
.cfg-chips .chip-reset { color: var(--text-muted); border-style: dashed; }
.cfg-res-ip { padding: .7rem 0; gap: .5rem; }
@media (max-width: 520px) {
  .cfg-ipbar { border: 0; border-radius: 0; background: transparent; gap: .35rem; }
  .cfg-pill { border: 1px solid var(--border-strong); border-radius: 999px; }
  .cfg-pill:has(input:checked) { border-color: var(--brand); box-shadow: none; }
}
.cfg-ipbar { display: inline-flex; flex-wrap: wrap; border: 1px solid var(--border-strong); border-radius: 999px; overflow: hidden; background: var(--bg-raised); }
.cfg-pill { position: relative; padding: .3rem .8rem; font-size: .85rem; font-weight: 600; color: var(--text-muted); cursor: pointer; border-right: 1px solid var(--border-strong); transition: background .15s ease, color .15s ease; }
.cfg-pill:last-child { border-right: 0; }
.cfg-pill input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.cfg-pill:hover { background: var(--bg-inset); color: var(--text); }
.cfg-pill:has(input:checked) { background: var(--brand-tint); color: var(--brand-on-tint); box-shadow: inset 0 -3px 0 var(--brand); }
.cfg-pill:has(input:focus-visible) { outline: 2px solid var(--brand); outline-offset: -2px; }

.cfg-os-grid { display: grid; gap: .5rem; grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr)); }
.cfg-os {
  display: flex; align-items: center; gap: .55rem; min-height: 3.1rem; cursor: pointer;
  border: 1px solid var(--border); border-radius: var(--radius-sm); padding: .6rem .9rem; margin: 0;
  background: var(--bg-raised); transition: border-color .15s ease, background .15s ease;
}
.cfg-os:hover { border-color: color-mix(in oklab, var(--brand) 45%, var(--border)); }
.cfg-os:has(input:checked) { border-color: var(--brand); background: color-mix(in oklab, var(--brand-tint) 45%, var(--bg-raised)); }
.cfg-os:has(input:focus-visible) { outline: 2px solid var(--brand); outline-offset: 2px; }
.cfg-os input { margin: 0; flex: none; accent-color: var(--brand); }
.cfg-os .t { flex: 1; min-width: 0; font-weight: 600; font-size: .95rem; }
.cfg-os-tags { display: inline-flex; gap: .3rem; flex: none; }
.cfg-os-lang { font-family: var(--mono); font-size: .7rem; padding: .05rem .4rem; border-radius: 999px; border: 1px solid var(--border-strong); color: var(--text-muted); }
.cfg-os-rec { font-size: .7rem; font-weight: 700; padding: .05rem .45rem; border-radius: 999px; color: var(--ok-text); background: color-mix(in oklab, var(--ok) 12%, transparent); white-space: nowrap; }
/* the software strip under the grid: one per image that has any, the chosen
   image's shown by the script; without one all show, each named */
.cfg-soft .os-soft { display: flex; flex-wrap: wrap; gap: .3rem 1rem; align-items: center; margin-top: .6rem; padding: .5rem .8rem; border: 1px dashed var(--border-strong); border-radius: var(--radius-sm); font-size: .9rem; }
.js .cfg-soft .os-soft { display: none; }
.js .cfg-soft .os-soft.is-on { display: flex; }

.cfg-receipt { display: flex; flex-direction: column; }
.cfg-receipt .eyebrow { margin-bottom: .5rem; }
.cfg-rows { display: flex; flex-direction: column; }
.cfg-rows .row { display: flex; justify-content: space-between; gap: 1rem; padding: .36rem 0; color: var(--text-muted); font-size: .95rem; border-radius: calc(var(--radius-sm) / 2); transition: background .4s ease; }
.cfg-rows .row .k { min-width: 0; }
.cfg-rows .row .k small { display: block; color: var(--text-faint); font-size: .8rem; }
.cfg-rows .row .v { color: var(--text); white-space: nowrap; }
.cfg-rows .row.discount .v, .cfg-rows .row.credit .v { color: var(--ok-text); }
.cfg-rows .row.flash { background: color-mix(in oklab, var(--brand) 14%, transparent); }
.cfg-rows .row.grand { margin-top: .4rem; padding-top: .8rem; border-top: 1px solid var(--border); color: var(--text); font-weight: 700; align-items: baseline; }
.cfg-rows .row.grand .v { font-size: 1.35rem; letter-spacing: -.02em; font-weight: 800; }
.cfg-rows .row.permo { padding-top: 0; justify-content: flex-end; font-size: .85rem; }
.cfg-rows .row.permo .v { color: var(--text-faint); font-weight: 500; }
.cfg-rows .row.less { padding-top: 0; font-size: .85rem; }
.cfg-rows .row.less .k { color: var(--ok-text); }
.cfg-next { margin: .7rem 0 0; font-size: .85rem; color: var(--text-muted); line-height: 1.4; }
.cfg-receipt .btn-block { margin-top: 1rem; width: 100%; white-space: nowrap; }
.cfg-receipt .hint { margin: .8rem 0 0; color: var(--text-faint); font-size: .85rem; }

.cfg-bar { display: none; }
@media (max-width: 67.99rem) {
  .cfg-receipt { order: 2; }
  .cfg-bar {
    display: flex; align-items: center; gap: 1rem; order: 3;
    position: sticky; bottom: 0; z-index: 40;
    margin: 1rem -1rem -3rem; padding: .7rem 1rem calc(.7rem + env(safe-area-inset-bottom));
    background: var(--glass); -webkit-backdrop-filter: saturate(1.8) blur(12px); backdrop-filter: saturate(1.8) blur(12px);
    border-top: 1px solid var(--border);
  }
  .cfg-bar .sum { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
  .cfg-bar-plan { font-size: .8rem; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .cfg-bar .total { font-weight: 800; font-size: 1.15rem; letter-spacing: -.02em; }
  .cfg-bar .btn { margin-left: auto; flex: none; white-space: nowrap; }
  .cfg-bar.is-away { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .shop-card, .chip, .cfg-rows .row, .cfg-term, .cfg-pill, .cfg-os, .svc-plan { transition: none; }
  .shop-card:hover { translate: none; }
}

/* ---------- the service page's plan cards and resource rows (stage 8.6.5) ---------- */
.svc-plans { display: grid; gap: .6rem; grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), 1fr)); }
.svc-plan {
  position: relative; display: flex; flex-direction: column; gap: .35rem; padding: .8rem .9rem;
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm); background: var(--bg-raised); cursor: pointer;
  transition: border-color .15s ease, background .15s ease;
}
.svc-plan input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.svc-plan:hover { border-color: var(--brand); }
.svc-plan:has(input:checked) { border-color: var(--brand); background: var(--brand-tint); box-shadow: inset 0 -3px 0 var(--brand); }
.svc-plan:has(input:focus-visible) { outline: 2px solid var(--brand); outline-offset: 2px; }
.svc-plan.is-current { cursor: default; border-style: dashed; background: var(--bg-alt); }
.svc-plan-tag { font-size: .7rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted); }
.svc-plan-name { font-weight: 700; }
.svc-plan .shop-spec { font-size: .85rem; }
.svc-plan-price { font-weight: 700; margin-top: .2rem; }
.svc-plan-price small { font-weight: 500; color: var(--text-muted); font-size: .8rem; }
.cfg-res-solo { padding: 1rem 0; }
.cfg-res-solo .cfg-save { display: flex; justify-content: flex-end; }

/* ---------- the browser console (stage 8.21.2, SPEC §4.11; owner D9) ------
   The console is the ALTERNATIVE way in: «Дані доступу» keeps the primary,
   the console stands beside it as a plain button, and one grey line under
   the head says why the RDP client is the recommended way. The click has a
   side effect and spends a one-shot ticket, so the control is a form; the
   form has to sit in the row like the buttons beside it. */
.page-actions .console-form,
.access-aside .console-form { display: flex; margin: 0; }
.console-form .btn { width: 100%; }

/* The standing advisory under the head, the same sentence in the aside and
   on the refusal card: one class, three homes. .page-sub / .access-aside p
   already supply the muted ink and the measure; the only thing missing is a
   readable link — the cabinet's own link colour measures 2.48:1 on the page
   ground in the light theme, which is exactly why .message-body already
   switches to --brand-on-tint. */
.console-note a { color: var(--brand-on-tint); text-decoration: underline; text-underline-offset: .12em; }

/* The head's row wraps rather than squeezing three labels (a longer
   translation moves to a second row); on a phone the RECOMMENDED way takes
   the first row on its own at the platform's 44 px, and «Написати в
   підтримку» and the console share the row below. Scoped with :has() so
   /services keeps its own two-button row. */
.page-head .page-actions:has(.console-form) { flex-wrap: wrap; }
@media (max-width: 640px) {
  .page-head .page-actions:has(.console-form) { width: 100%; gap: .5rem; }
  .page-head .page-actions:has(.console-form) > * { flex: 1 1 0; }
  .page-head .page-actions:has(.console-form) > .btn-primary { flex: 1 1 100%; order: -1; min-height: 44px; }
}

/* The aside: the guide first, a hairline, then the other way in. The rule is
   the whole "Recommended / Alternative" label — no badges, no new words. */
.access-aside .access-or { margin: 1.1rem 0 .6rem; padding-top: 1rem; border-top: 1px solid var(--border); }
.access-aside .console-note { margin: .6rem 0 0; }

/* The console's cards — the connection check, the hop while the gateway
   mints the session, the refusal — share the guest shell of every hop. */
.hop-card { text-align: center; }
/* .auth-card h1 (0,1,1) outranks a bare .hop-title (0,1,0). */
.auth-card .hop-title { font-size: 1.25rem; margin: 0 0 .4rem; }
.hop-target { color: var(--text-muted); font-size: .95rem; margin: 0 0 1rem; }
.hop-spinner { display: flex; justify-content: center; gap: .45rem; margin: 0 0 1.1rem; }
.hop-spinner .dot-pulse:nth-child(2) { animation-delay: .2s; }
.hop-spinner .dot-pulse:nth-child(3) { animation-delay: .4s; }
/* .hint sets a size and no colour. */
.hop-card .hint { color: var(--text-muted); margin: 0 0 .5rem; }
.hop-go { margin: 1.2rem 0 0; }
.hop-reopen { margin: 0 0 .5rem; }
.hop-card .notice { text-align: left; }
/* Only on the refusal card: when the alternative fails, the recommended way
   is the customer's way out. The link is atomic, so it moves to its own
   line instead of splitting mid-phrase in the centred card at 390 px. */
.hop-alt { margin: 1.1rem 0 0; font-size: .92rem; color: var(--text-muted); }
.hop-alt a { display: inline-block; }
