/* ============================================================================
   jira-clockify-avanti console — light admin theme.

   Hand-written rather than pulled from a CDN: the pages run behind a strict
   egress policy and an IP allow-list, so every byte has to ship with the image.
   The palette is the NeverHack scheme shared with the jira-jira dashboard:
   deep navy sidebar, blue-grey canvas with a dotted texture, white cards. The
   two consoles should look like one product.
   ========================================================================== */

/* Figtree, served from this deployment. Two variable files carry every weight
   from 300 to 900, upright and italic, in 125 KB — the closest free face to the
   one the company site sets, and free is what makes it shippable at all.

   `swap`: the text is readable at once in the system face and reflows when the
   file arrives, rather than a blank page for the first moments. */
@font-face {
  font-family: Figtree;
  src: url("../fonts/figtree.ttf") format("truetype");
  font-weight: 300 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: Figtree;
  src: url("../fonts/figtree-italic.ttf") format("truetype");
  font-weight: 300 900;
  font-style: italic;
  font-display: swap;
}

:root {
  /* Figtree, and the company site's own monospace stack character for
     character. The site itself sets Roobert, which is licensed; this is the
     nearest free face — the same geometric sans with a tall x-height, so the
     two read as related rather than identical. Both fall back to system-ui,
     which is Segoe UI on Windows and San Francisco on a Mac.

     Nothing here is fetched from anywhere. The faces are served by this
     deployment, from assets/fonts/ — a webfont service would be one more thing
     between an operator and a page that has to work when the network is the
     thing that is broken. */
  --font-sans: Figtree, system-ui, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
    "Liberation Mono", "Courier New", monospace;

  --primary: #16386e;          /* NeverHack navy */
  --primary-dark: #0e2a4f;
  --secondary: #2f6fed;        /* brighter blue accent */
  --success: #0ab39c;
  --info: #2f6fed;
  --warning: #f7b84b;
  --danger: #f06548;

  --body-bg: #eef1f7;
  --card-bg: #ffffff;
  /* Two neutral surfaces so table headers, quiet buttons and chips stop being
     hardcoded greys that drift when the palette changes. */
  --surface: #f4f6fa;
  --surface-hover: #f9fafd;
  --border: #e2e7f1;
  --text: #4a5568;
  --heading: #15233f;
  --muted: #8a93a6;
  --field-border: #cfd7e6;

  --sidebar-bg: #0e2a4f;
  --sidebar-fg: rgba(255, 255, 255, 0.72);
  --sidebar-active: #ffffff;
  --sidebar-active-bg: rgba(255, 255, 255, 0.10);
  --sidebar-w: 250px;
  --topbar-h: 60px;

  --radius: 0.4rem;
  --shadow: 0 1px 1px rgba(21, 35, 63, .05), 0 2px 6px -1px rgba(21, 35, 63, .07);
  --shadow-lift: 0 1px 1px rgba(21, 35, 63, .05), 0 10px 22px -6px rgba(21, 35, 63, .16);

  /* Ink on a coloured ground: white on the navy sidebar and on a primary
     button, dark on the amber one. Named rather than written out, because a
     dark theme keeps the ground and changes everything around it. */
  --on-dark: #ffffff;
  --on-warning: #5b4708;

  /* The darker end of the two gradients, and the border a card lifts to. */
  --sidebar-top: #143458;
  --hero-bottom: #0b223f;
  --border-strong: #b9c4d8;

  /* Text on the soft badges and alerts. One per tone, two shades apart: the
     badge sits on a 12% wash, the alert on a 10% one with a border. */
  --success-ink: #0a9382;
  --success-ink-strong: #0a7d70;
  --danger-ink: #d54d33;
  --danger-ink-strong: #c0432c;
  --warning-ink: #b8860b;
  --warning-ink-strong: #9a6f0d;
  --info-ink: #1f4f9e;
  --info-ink-strong: #1b4a94;

  /* Two washes and one soft ink, for the panels that are a whole state rather
     than a line: the field-values preview, and the danger zone. */
  --info-wash: #eef3fe;
  --danger-wash: #fffafa;
  --danger-ink-soft: #9b5c63;
}

/* ── The same console at night ────────────────────────────────────────────
   One block, not two: the choice is written onto <html> as data-theme before
   the first paint (see the inline script in layouts/app.blade.php), whether it
   came from the switch in the topbar or from the operating system. So the
   stylesheet has a single dark palette and no @media copy of it to keep in
   step.

   The brand navy stays where it belongs — the sidebar and the hero — and the
   page around it goes to the dark end of the same blue rather than to grey:
   a neutral charcoal under a navy rail reads as two designs. The accent has to
   move, though. #16386e is a foreground colour on paper and a hole in the page
   at night, so the brighter blue this palette already carries becomes the
   primary, and the navy is kept for the rail behind it. */
:root[data-theme="dark"] {
  color-scheme: dark;

  --primary: #2f6fed;
  --primary-dark: #2559c9;
  --secondary: #5d93ff;
  --success: #12c2a8;
  --info: #5d93ff;
  --warning: #f0b64a;
  --danger: #f2705a;

  --body-bg: #0b1626;
  --card-bg: #111f33;
  --surface: #16263c;
  --surface-hover: #1b2d46;
  --border: #223349;
  --text: #c3cddd;
  --heading: #eaf0f8;
  --muted: #8795ac;
  --field-border: #2c405e;

  --sidebar-bg: #0a1626;
  --sidebar-top: #0d1c2f;
  --sidebar-fg: rgba(255, 255, 255, 0.68);
  --hero-bottom: #081321;
  --border-strong: #35496b;

  /* The inks lift off the dark washes; the same tones, brought up until they
     read on a navy ground instead of on white. */
  --success-ink: #4fd6c2;
  --success-ink-strong: #6ee0cf;
  --danger-ink: #ff9280;
  --danger-ink-strong: #ffab9c;
  --warning-ink: #f5c96f;
  --warning-ink-strong: #f8d78f;
  --info-ink: #8ab4ff;
  --info-ink-strong: #a6c6ff;
  --on-warning: #2a2003;

  --info-wash: #14263f;
  --danger-wash: #2a1720;
  --danger-ink-soft: #d99aa2;

  --shadow: 0 1px 1px rgba(0, 0, 0, .25), 0 2px 6px -1px rgba(0, 0, 0, .35);
  --shadow-lift: 0 1px 1px rgba(0, 0, 0, .25), 0 10px 22px -6px rgba(0, 0, 0, .5);
}


* { box-sizing: border-box; }

/* An author `display` beats the browser's own `[hidden] { display: none }`, so
   any styled element with the attribute stayed visible — a button that presses
   and does nothing, because the script that was going to wire it up had not.
   Stated once, globally, rather than remembered per component. */
[hidden] { display: none !important; }

body {
  margin: 0;
  color: var(--text);
  /* The dotted texture from neverhack.ee. Painted, not an image: nothing is
     fetched, and it survives the strict egress policy. */
  background-color: var(--body-bg);
  background-image: radial-gradient(rgba(22, 56, 110, 0.06) 1px, transparent 1px);
  background-size: 22px 22px;
  font-family: var(--font-sans);
  font-size: 14px;
  line-height: 1.5;
}

a { color: var(--secondary); text-decoration: none; }
a:hover { text-decoration: underline; }
h1, h2, h3, h4 { color: var(--heading); margin: 0; }
code, .mono { font-family: var(--font-mono); }

/* Somebody who asked their system for less motion is not asking for a duller
   console — the depth stays, the movement goes. */
@media (prefers-reduced-motion: reduce) {
  .card, .tile, .card:hover, .tile:hover,
  .card.is-linked:hover, button.tile.is-clickable:hover, a.tile:hover {
    transition: none; transform: none;
  }
}

/* ── Sidebar ─────────────────────────────────────────────────────────── */
.sidebar {
  position: fixed; inset: 0 auto 0 0; width: var(--sidebar-w);
  background: linear-gradient(180deg, var(--sidebar-top), var(--sidebar-bg));
  color: var(--sidebar-fg);
  display: flex; flex-direction: column; z-index: 20; overflow-y: auto;
}
.sidebar .brand {
  height: var(--topbar-h); display: flex; align-items: center; gap: .65rem;
  padding: 0 1.15rem; color: var(--on-dark); font-weight: 600; font-size: .95rem;
  border-bottom: 1px solid rgba(255, 255, 255, 0.1); flex: 0 0 auto;
}

/* ── Brand mark ──────────────────────────────────────────────────────
   Three rings overlapping into one core. The rings inherit the surface's
   colour so the same markup sits on the navy sidebar and the navy login
   panel; only the core is painted, and the descending opacities keep the
   three readable at 26px where three equal strokes would just look like a
   smudge. */
.brand-mark { flex: 0 0 auto; overflow: visible; }
.brand-mark .ring {
  fill: none; stroke: currentColor; stroke-width: 1.9;
  vector-effect: non-scaling-stroke;
}
.brand-mark .ring-1 { opacity: 1; }
.brand-mark .ring-2 { opacity: .68; }
.brand-mark .ring-3 { opacity: .44; }
.brand-mark .core { fill: var(--success); }

/* ── Wordmark ────────────────────────────────────────────────────────
   The separators carry meaning — three systems — but they should not
   compete with the names for attention. */
.wordmark { white-space: nowrap; }
.wordmark-sep { opacity: .38; margin: 0 .34em; font-weight: 400; }
.wordmark-also { opacity: .55; font-weight: 400; font-size: .88em; }

/* The tagline is a caption, not a second title: small caps, wide tracking and
   low contrast so it explains the name without competing with it. */
.brand-text { display: flex; flex-direction: column; gap: .1rem; line-height: 1.2; min-width: 0; }
.brand-tagline {
  font-size: .58rem; font-weight: 500; letter-spacing: .13em; text-transform: uppercase;
  color: rgba(255, 255, 255, .5); white-space: nowrap;
}
.menu { list-style: none; margin: 0; padding: 1rem .75rem; display: flex; flex-direction: column; gap: .1rem; }
.menu .label {
  color: rgba(255, 255, 255, .5); font-size: .68rem; text-transform: uppercase;
  letter-spacing: .06em; padding: .8rem .75rem .35rem;
}
.menu a {
  display: flex; align-items: center; gap: .65rem; color: var(--sidebar-fg);
  padding: .6rem .75rem; border-radius: var(--radius); font-size: .88rem;
}
.menu a:hover { color: var(--on-dark); background: rgba(255, 255, 255, .07); text-decoration: none; }
.menu a.active { color: var(--sidebar-active); background: var(--sidebar-active-bg); font-weight: 500; }
.menu a .ico { width: 1.15rem; text-align: center; opacity: .9; }
/* How many are behind a menu entry, before it is opened. The menu is already
   the navigation between systems, so the count belongs on it rather than on a
   second row of tabs repeating the same three links. */
.nav-count {
  margin-left: auto; min-width: 1.15rem; padding: 0 .32rem; border-radius: 999px;
  background: rgba(255, 255, 255, .12); color: var(--sidebar-fg);
  font-size: .68rem; font-weight: 600; text-align: center; line-height: 1.15rem;
}
.menu a.active .nav-count { background: rgba(255, 255, 255, .22); color: var(--sidebar-active); }
/* A menu entry that opens. The summary is styled as the link it replaces, and
   the marker is drawn by hand so it points the way it turns — the native one
   sits on the wrong side and cannot be coloured. */
.nav-group > summary {
  display: flex; align-items: center; gap: .65rem; color: var(--sidebar-fg);
  padding: .6rem .75rem; border-radius: var(--radius); font-size: .88rem;
  cursor: pointer; list-style: none;
}
.nav-group > summary::-webkit-details-marker { display: none; }
.nav-group > summary:hover { color: var(--on-dark); background: rgba(255, 255, 255, .07); }
.nav-group > summary::after {
  content: "›"; margin-left: auto; opacity: .55;
  transition: transform .16s ease; transform: rotate(0deg);
}
.nav-group[open] > summary::after { transform: rotate(90deg); }
/* Indented under their heading, and hung on a rail so the group reads as one
   thing rather than as four entries that happen to be adjacent. */
.nav-group > ul {
  list-style: none; margin: .1rem 0 .35rem; padding: 0 0 0 1.5rem;
  border-left: 1px solid rgba(255, 255, 255, .1); margin-left: 1.05rem;
}
.nav-group > ul a { padding: .42rem .6rem; font-size: .84rem; }
@media (prefers-reduced-motion: reduce) {
  .nav-group > summary::after { transition: none; }
}
/* "soon" beside a system with no connector yet: present, but clearly not the
   same weight as the ones that work. */
.menu-tag {
  margin-left: auto; font-size: .58rem; font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; padding: .05rem .3rem; border-radius: 2px;
  background: rgba(255, 255, 255, .12); color: rgba(255, 255, 255, .6);
}

/* ── A page on its way ───────────────────────────────────────────────── */
html.is-leaving { cursor: progress; }
html.is-leaving::before {
  content: ""; position: fixed; top: 0; left: 0; z-index: 200;
  height: 3px; width: 28%; background: var(--primary); border-radius: 0 2px 2px 0;
  animation: leaving 1.1s ease-in-out infinite;
}
@keyframes leaving {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(360%); }
}
@media (prefers-reduced-motion: reduce) {
  html.is-leaving::before { animation: none; width: 100%; opacity: .6; }
}

/* ── Topbar ──────────────────────────────────────────────────────────── */
.topbar {
  position: sticky; top: 0; z-index: 10; height: var(--topbar-h);
  margin-left: var(--sidebar-w);
  display: flex; align-items: center; gap: .75rem;
  padding: 0 1.5rem; background: var(--card-bg);
  border-bottom: 1px solid var(--border);
}
.page-title { font-size: 1.02rem; font-weight: 600; color: var(--heading); }
.page-sub { color: var(--muted); font-size: .78rem; }
.topbar .right { margin-left: auto; display: flex; align-items: center; gap: .6rem; }
.whoami { text-align: right; line-height: 1.25; }
.whoami .n { font-weight: 600; color: var(--heading); font-size: .85rem; }
.whoami .r { color: var(--muted); font-size: .72rem; }
.whoami-source {
  display: inline-block; margin-left: .25rem; padding: .04rem .32rem;
  border-radius: 2px; font-size: .62rem; font-weight: 600; letter-spacing: .03em;
  background: rgba(22, 56, 110, .09); color: var(--primary);
  cursor: help;
}

/* ── Content ─────────────────────────────────────────────────────────── */
.content { margin-left: var(--sidebar-w); padding: 1.5rem; }
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.1rem; }
.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.1rem; }
.span2 { grid-column: span 2; }
.span3 { grid-column: 1 / -1; }
.stack { display: flex; flex-direction: column; gap: 1.1rem; }
.row { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.row-end { margin-left: auto; }

/* Prose with one action pinned to its end.
   Not `.row`: flex decides to wrap on an item's natural width, not on the width
   it could shrink to, so a long description drops the button onto its own line
   however much room it would have had after shrinking. */
.item-row { display: flex; align-items: flex-start; gap: .75rem; flex-wrap: nowrap; }
.item-row > .item-main { flex: 1 1 auto; min-width: 0; }
.item-row > .item-act { flex: 0 0 auto; }
/* One row per instance with something to throw away. */
.cleanup-row { padding: .55rem 0; border-bottom: 1px solid var(--border); align-items: center; }
.cleanup-row:last-of-type { border-bottom: 0; }
.cleanup-acts { display: flex; gap: .4rem; flex-wrap: wrap; justify-content: flex-end; }
.mt { margin-top: 1.1rem; }

/* ── Cards ───────────────────────────────────────────────────────────── */
.card {
  background: var(--card-bg); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow);
  transition: box-shadow .16s ease, transform .16s ease, border-color .16s ease;
}
/* A millimetre, and only the shadow really moves. Enough that the page feels
   made of layers rather than printed on one; not enough to claim the card does
   something when clicked — that promise is `cursor: pointer` and a tinted
   border, which only `.is-linked` below makes. */
/* Only the ones you can actually press. A panel that rises because the pointer
   crossed it is the page twitching at somebody reading a table — movement has
   to mean "this answers to you", or it means nothing. */
a.card:hover, button.card:hover, .card.is-linked:hover {
  box-shadow: var(--shadow-lift); transform: translateY(-1px);
}

/* The head is a band, not just a line: a hairline of white above the border
   catches the light and gives the card an edge instead of a seam. */
.card-head {
  display: flex; align-items: center; gap: .6rem;
  padding: .85rem 1.1rem; border-bottom: 1px solid var(--border);
  background: linear-gradient(180deg, var(--card-bg), var(--surface));
  border-radius: calc(var(--radius) - 1px) calc(var(--radius) - 1px) 0 0;
}
.card-head h2 { font-size: .9rem; font-weight: 600; }
/* A heading that leads somewhere keeps the heading's colour and picks the
   underline up on hover, so it reads as a title first and a link second. */
.card-head h2 a { color: inherit; text-decoration: none; }
.card-head h2 a:hover { text-decoration: underline; }

/* A card you can click anywhere on. The link stays a real link — it is only
   its hit area that is stretched over the card — so the keyboard, the status
   bar and "open in new tab" all behave. Text stays selectable, which a card
   wrapped in an <a> would lose. */
.card.is-linked { position: relative; cursor: pointer; }
/* Higher than a plain card, and tinted: this one is going somewhere. */
.card.is-linked:hover { border-color: var(--border-strong); transform: translateY(-2px); }
.card.is-linked .card-head h2 a::after { content: ''; position: absolute; inset: 0; }
.card.is-linked:hover .card-head h2 a { text-decoration: underline; }
.card.is-linked:focus-within { border-color: var(--info); }
/* Anything the card links to itself must sit above the stretched area, or the
   card's link swallows the click. */
.card.is-linked .card-body a, .card.is-linked .card-body button { position: relative; z-index: 1; }
.card-head .row-end { margin-left: auto; }
.card-body { padding: 1.1rem; }
.card-body.tight { padding: 0; }

/* ── Stat tiles ──────────────────────────────────────────────────────── */
.tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1.1rem; }
.tile { background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius);
        box-shadow: var(--shadow); padding: 1.1rem;
        transition: box-shadow .16s ease, transform .16s ease, border-color .16s ease; }
a.tile:hover, button.tile.is-clickable:hover {
  box-shadow: var(--shadow-lift); transform: translateY(-1px);
}
/* A tile that goes nowhere should not behave as though it does. Its own links
   still work; the box around them is not one. */
.tile-static, .tile-static:hover { box-shadow: var(--shadow); transform: none; cursor: default; }
.tile-static .sub a { color: var(--muted); text-decoration: none; border-bottom: 1px dotted var(--border); }
.tile-static .sub a:hover { color: var(--primary); border-bottom-color: currentColor; }
.tile .lbl { color: var(--muted); font-size: .72rem; text-transform: uppercase; letter-spacing: .04em; }
.tile .num { font-size: 1.7rem; font-weight: 700; color: var(--heading); line-height: 1.3; }
.tile .sub { color: var(--muted); font-size: .78rem; }
.tile.bad .num { color: var(--danger); }

/* ── Health rows ─────────────────────────────────────────────────────── */
.health { display: flex; align-items: flex-start; gap: .75rem; }
.health .dot { margin-top: .35rem; }
.health .h-name { font-weight: 600; color: var(--heading); }
.health .h-detail { color: var(--muted); font-size: .82rem; word-break: break-word; }
.dot { width: 10px; height: 10px; border-radius: 50%; background: var(--muted); display: inline-block; flex: 0 0 auto; }
.dot-ok { background: var(--success); box-shadow: 0 0 0 3px rgba(10, 179, 156, .16); }
.dot-bad { background: var(--danger); box-shadow: 0 0 0 3px rgba(240, 101, 72, .16); }
.dot-warn { background: var(--warning); box-shadow: 0 0 0 3px rgba(247, 184, 75, .18); }
.dot-na { background: var(--muted); box-shadow: 0 0 0 3px rgba(135, 138, 153, .14); }

/* ── Badges / pills ──────────────────────────────────────────────────── */
.badge {
  display: inline-block; font-size: .68rem; font-weight: 600; letter-spacing: .02em;
  padding: .22rem .5rem; border-radius: .25rem; text-transform: uppercase; white-space: nowrap;
}
.pill { display: inline-block; font-size: .74rem; font-weight: 600; padding: .25rem .6rem; border-radius: 1rem; white-space: nowrap; }
.soft-success { background: rgba(10, 179, 156, .12); color: var(--success-ink); }
.soft-danger  { background: rgba(240, 101, 72, .12); color: var(--danger-ink); }
.soft-warning { background: rgba(247, 184, 75, .16); color: var(--warning-ink); }
.soft-info    { background: rgba(47, 111, 237, .12); color: var(--info-ink); }
.soft-primary { background: rgba(22, 56, 110, .09); color: var(--primary); }
.soft-muted   { background: var(--surface); color: var(--muted); }

/* In-flight and just-finished states for a per-row action. The pulse is what
   tells you a re-test of an already-OK instance actually ran — without it the
   cell would end up looking exactly as it did before you pressed the button. */
.badge.is-testing { animation: badge-pulse 1.1s ease-in-out infinite; }
.badge.is-flash   { animation: badge-pop .32s ease-out; }
@keyframes badge-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }
@keyframes badge-pop {
  0%   { transform: scale(.9); opacity: .4; }
  60%  { transform: scale(1.04); opacity: 1; }
  100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .badge.is-testing, .badge.is-flash { animation: none; }
}

/* A name that opens what is known about that session. Underlined on hover only:
   in a table of names, six permanently underlined ones read as six links to six
   pages, which is not what these are. */
.is-openable { cursor: pointer; }
.is-openable:hover { text-decoration: underline; text-underline-offset: 2px; }
.is-openable:focus-visible { outline: 2px solid var(--info); outline-offset: 2px; border-radius: 2px; }

/* ── A restart in progress ───────────────────────────────────────────────
   No percentage is honestly available — the console cannot watch itself being
   replaced — so the bar says "something is happening", and the words above it
   say what has actually completed. An indeterminate bar that pretended to be a
   percentage would be a lie with a number on it. */
.restart-bar {
  display: block; height: 3px; margin-top: .6rem; border-radius: 2px;
  background: rgba(47, 111, 237, .16); overflow: hidden;
}
.restart-bar > span {
  display: block; width: 35%; height: 100%; border-radius: 2px;
  background: var(--info); animation: restart-sweep 1.4s ease-in-out infinite;
}
@keyframes restart-sweep {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(300%); }
}
/* Done: the bar fills and stops, so the end state is a full bar rather than one
   still running under a sentence saying it has finished. */
.alert[data-restart].is-done .restart-bar > span { width: 100%; animation: none; }
@media (prefers-reduced-motion: reduce) {
  .restart-bar > span { width: 100%; animation: none; opacity: .55; }
}

/* ── The status strip ────────────────────────────────────────────────────
   The platform's state, in the topbar, on every page. Smaller and quieter than
   a .badge — it is read at a glance and must not compete with the page title
   beside it — and mixed case, because these are words rather than labels.

   Colour comes from the same soft-* classes as everything else, so a green here
   is the green a card uses for the same fact. */
.status-strip { display: flex; align-items: center; gap: .4rem; }
.status-badge {
  display: inline-flex; align-items: center; gap: .35rem;
  font-size: .72rem; font-weight: 600; white-space: nowrap;
  padding: .22rem .55rem; border-radius: 1rem;
  font-variant-numeric: tabular-nums;
}
.status-badge[hidden] { display: none; }

/* Take it back. Present in the markup — a screen reader and the keyboard both
   reach it — but out of the way until the pointer is on the badge it belongs
   to, because it is an escape hatch and not a control anybody uses daily.
   Width, not display, so revealing it does not shift the badges beside it more
   than the letter it adds. */
.status-undo { display: inline-flex; margin-left: .1rem; }
.status-undo[hidden] { display: none; }
.status-undo > button {
  all: unset; cursor: pointer; width: 0; overflow: hidden;
  font-size: .82rem; line-height: 1; opacity: 0; color: inherit;
  transition: width .12s ease, opacity .12s ease;
}
.status-badge:hover .status-undo > button,
.status-undo > button:focus-visible { width: .8rem; opacity: .75; }
.status-undo > button:hover { opacity: 1; }
.status-undo > button:focus-visible { outline: 2px solid currentColor; outline-offset: 1px; }
@media (prefers-reduced-motion: reduce) {
  .status-undo > button { transition: none; }
}
/* Nothing has been read for a while: the numbers are still the last true ones,
   so they stay, but the strip stops claiming to be current. Hover says why. */
.status-strip.is-stale { opacity: .5; }
.status-dot {
  width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto;
  background: currentColor; opacity: .75;
}

/* The one moving thing on the page, and it means something: the switch is on
   AND a worker is answering. A dot that pulsed on the strength of the switch
   alone would beat away over a fleet of nothing.

   The ring is drawn with box-shadow rather than a second element, so the pulse
   costs one composited property and no layout. */
.status-dot.is-live { opacity: 1; animation: status-pulse 2s ease-in-out infinite; }
/* The ring travels outwards and fades, and is already gone at 100% — it never
   shrinks back in. Written as three stops rather than two for exactly that
   reason: `0%, 100%` on one line plays the second half of every cycle
   backwards, and a ring that returns to the dot reads as a wobble. */
@keyframes status-pulse {
  0%   { box-shadow: 0 0 0 0 currentColor; opacity: 1; }
  70%  { box-shadow: 0 0 0 4px transparent; opacity: .6; }
  100% { box-shadow: 0 0 0 0 transparent; opacity: 1; }
}
/* Asked for less motion: the dot stays lit, it just stops beating. Colour and
   the word beside it carry the state on their own. */
@media (prefers-reduced-motion: reduce) {
  .status-dot.is-live { animation: none; }
}

/* Narrow: the page title and who you are win. What survives is the state that
   would make somebody stop what they are doing — so what goes is the badge in
   its CALM tone. "no workers" is red and stays; "2 workers" is grey and does
   not need the room it is taking. */
@media (max-width: 900px) {
  .status-badge[data-status="workers"].soft-muted { display: none; }
}
@media (max-width: 640px) {
  .status-badge[data-status="busy"] { display: none; }
  /* Only the calm state gives up its word and shrinks to the dot. A red dot
     with nothing beside it is the one badge nobody should have to guess at, so
     "Stopped" keeps its word however narrow the screen gets. */
  .status-badge[data-status="platform"].soft-success span:not(.status-dot) { display: none; }
  .status-badge[data-status="platform"].soft-success { padding: .3rem; }
}

/* ── Buttons ─────────────────────────────────────────────────────────── */
.btn {
  display: inline-flex; align-items: center; gap: .4rem; border: 1px solid transparent;
  border-radius: var(--radius); padding: .45rem .85rem; font-size: .84rem; font-weight: 500;
  cursor: pointer; color: var(--on-dark); background: var(--primary); font-family: inherit;
  /* A hairline of light along the top edge and a shadow the colour of the
     button itself: the button reads as raised without a grey drop shadow, which
     on a tinted surface always looks like dirt. */
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .16), 0 1px 2px rgba(15, 35, 63, .16);
  transition: transform .12s ease, box-shadow .16s ease, filter .16s ease, background .16s ease;
}
.btn:hover {
  filter: brightness(1.07); text-decoration: none; color: var(--on-dark);
  transform: translateY(-1px);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .2), 0 4px 10px rgba(15, 35, 63, .18);
}
/* Pressing a button that opens a dialog can take a beat on a long page; without
   this the click looks like it was dropped. */
.btn:active {
  transform: translateY(1px); filter: brightness(.94);
  box-shadow: inset 0 2px 4px rgba(15, 35, 63, .2);
  transition-duration: .04s;
}
.btn:focus-visible {
  outline: none;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .16), 0 0 0 3px rgba(47, 111, 237, .35);
}
/* Nobody asked for the page to move under them. */
@media (prefers-reduced-motion: reduce) {
  .btn, .btn:hover, .btn:active { transform: none; transition: none; }
}
.btn.is-busy { opacity: .65; pointer-events: none; }
.btn-success { background: var(--success); }
.btn-info    { background: var(--info); }
.btn-warning { background: var(--warning); color: var(--on-warning); }
.btn-danger  { background: var(--danger); }
.btn-light   { background: var(--surface); color: var(--text); border-color: var(--border); box-shadow: 0 1px 1px rgba(15, 35, 63, .06); }
.btn-light:hover { color: var(--heading); border-color: var(--muted); box-shadow: 0 3px 8px rgba(15, 35, 63, .1); }
.btn-sm { padding: .28rem .6rem; font-size: .76rem; }
.btn:disabled, .btn.is-disabled {
  opacity: .55; cursor: not-allowed; filter: none;
  transform: none; box-shadow: none;
}

/* ── Alerts ──────────────────────────────────────────────────────────── */
.alert { border-radius: var(--radius); padding: .8rem 1rem; font-size: .86rem; margin-bottom: 1.1rem; border: 1px solid transparent; }
.alert-success { background: rgba(10, 179, 156, .1); border-color: rgba(10, 179, 156, .25); color: var(--success-ink-strong); }
.alert-danger  { background: rgba(240, 101, 72, .1); border-color: rgba(240, 101, 72, .25); color: var(--danger-ink-strong); }
.alert-warning { background: rgba(247, 184, 75, .12); border-color: rgba(247, 184, 75, .3); color: var(--warning-ink-strong); }
.alert-info    { background: rgba(47, 111, 237, .09); border-color: rgba(47, 111, 237, .22); color: var(--info-ink-strong); }

/* ── Tables ──────────────────────────────────────────────────────────── */
.table-wrap { overflow-x: auto; }
/* A long table inside a dialog: the box scrolls, not the dialog, so the column
   heads have something to stick to and the dialog's own head stays above. */
.table-tall { max-height: 60vh; overflow: auto; }
.table-tall table.tbl thead th { position: sticky; top: 0; z-index: 1; }
table.tbl { width: 100%; border-collapse: collapse; font-size: .85rem; }
table.tbl th {
  text-align: left; color: var(--muted); font-weight: 600; font-size: .7rem;
  text-transform: uppercase; letter-spacing: .04em; white-space: nowrap;
  padding: .6rem .9rem; background: var(--surface); border-bottom: 1px solid var(--border);
}
/* A heading that sorts. It inherits the heading's own type rather than a
   button's, so the row still reads as a row of headings; the caret is drawn
   from aria-sort, which is what a screen reader announces too. */
table.tbl th .th-sort {
  all: unset; cursor: pointer; display: inline-flex; align-items: center; gap: .3rem;
  font: inherit; color: inherit; letter-spacing: inherit; text-transform: inherit;
}
table.tbl th .th-sort:focus-visible { outline: 2px solid var(--secondary); outline-offset: 2px; }
table.tbl th .th-sort:hover { color: var(--secondary); }
/* The caret only appears once a column is sorted, so an unsorted heading gives
   no hint that it can be. A ghost caret on hover does, without adding a fourth
   glyph to every heading at rest. */
table.tbl th .th-sort::after { transition: opacity .15s ease; }
table.tbl th:not([aria-sort="ascending"]):not([aria-sort="descending"]) .th-sort:hover::after {
  content: "▲"; font-size: .6em; opacity: .4;
}
table.tbl th[aria-sort="ascending"] .th-sort::after { content: "▲"; font-size: .6em; }
table.tbl th[aria-sort="descending"] .th-sort::after { content: "▼"; font-size: .6em; }
table.tbl td { padding: .65rem .9rem; border-bottom: 1px solid var(--border); vertical-align: middle; }
table.tbl tbody tr:last-child td { border-bottom: 0; }
table.tbl tbody tr:hover { background: var(--surface-hover); }
.num-cell { text-align: right; font-variant-numeric: tabular-nums; }
/* `table.tbl th` outranks a bare class, so a numeric header kept the default
   left alignment while its column sat right — the header and its numbers
   pointed at different places. */
table.tbl th.num-cell { text-align: right; }
.empty { color: var(--muted); text-align: center; padding: 1.6rem; font-size: .88rem; }
.nowrap { white-space: nowrap; }

/* A run that changed something ----------------------------------------------
   Most runs find nothing to do, so a column of zeros is the normal state and
   every row looks like every other one. Two quiet marks fix that without
   turning the table into a traffic light: the zeros step back, and the row that
   did something carries a rail down its left edge.

   The rail is an inset shadow on the first cell rather than a border, so it
   does not move the text by a pixel and rows stay aligned with the header. */
.tbl td.is-nil { color: var(--muted); }
.tbl td.is-bad { color: var(--danger); font-weight: 600; }
.tbl tr.did-something td:first-child { box-shadow: inset 2px 0 0 var(--info); }
.tbl tr.did-something td.num-cell:not(.is-nil) { font-weight: 600; color: var(--heading); }
/* The name a row is about, kept on one line. "NEVERHACK Jira" broken after the
   first word reads as two entries, and a list is scanned down the first column.
   Only the name itself: the badges and the description under it still wrap, and
   the table scrolls sideways in .table-wrap, so nothing is pushed off the page.
   Not applied to every first column — a helper's name is a sentence long and
   would drag the whole table sideways. */
.name-cell > a { white-space: nowrap; }

/* ── Definition lists ────────────────────────────────────────────────── */
.kv { display: grid; grid-template-columns: minmax(9rem, auto) 1fr; gap: .55rem 1.2rem; margin: 0; }
.kv dt { color: var(--muted); font-size: .82rem; }
.kv dd { margin: 0; color: var(--heading); font-size: .85rem; word-break: break-word; }

/* ── Forms ───────────────────────────────────────────────────────────── */
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.1rem; }
.field { display: flex; flex-direction: column; gap: .3rem; }
.field.full { grid-column: 1 / -1; }
.field label, .field .field-label { font-size: .82rem; font-weight: 600; color: var(--heading); }
/* A checkbox's own label is the action, not the heading: normal weight, so
   the two lines do not compete. */
.field .check label { font-weight: 400; color: var(--text); }
.field .help { color: var(--muted); font-size: .78rem; }
.field .err { color: var(--danger); font-size: .78rem; }
input[type="text"], input[type="password"], input[type="email"], input[type="url"],
input[type="number"], input[type="search"], select, textarea {
  width: 100%; padding: .5rem .7rem; font-size: .86rem; font-family: inherit; color: var(--text);
  background: var(--card-bg); border: 1px solid var(--field-border); border-radius: var(--radius);
}
input:focus, select:focus, textarea:focus {
  outline: none; border-color: var(--secondary); box-shadow: 0 0 0 3px rgba(47, 111, 237, .14);
}
input[readonly], input:disabled, select:disabled { background: var(--surface); color: var(--muted); }
textarea { min-height: 5rem; resize: vertical; }
/* A search box, with the magnifier inside it. The icon is decoration — the
   field has a label — so it is out of the way of the pointer and of anything
   reading the page aloud. */
.search-field { position: relative; display: block; }
.search-field .search-icon {
  position: absolute; left: .62rem; top: 50%; transform: translateY(-50%);
  width: .82rem; height: .82rem; pointer-events: none;
  fill: none; stroke: var(--muted); stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}
.search-field input[type="search"] { padding-left: 2rem; border-radius: 999px; }
/* The magnifier follows the field: grey at rest, the accent while typing. */
.search-field input[type="search"]:focus ~ .search-icon,
.search-field:focus-within .search-icon { stroke: var(--secondary); }
/* Chrome draws its own X; it sits closer to the edge than a pill's curve. */
.search-field input[type="search"]::-webkit-search-cancel-button { margin-right: .15rem; }

.check { display: flex; align-items: flex-start; gap: .5rem; }
.field .field-label { display: block; }
.check input { margin-top: .25rem; }
.check label { font-weight: 500; }
/* The five words, and the answers under References. A dictionary voice: the
   line that defines the thing stays upright so it can be scanned, and the
   sentences around it lean, the way a reference book sets its glosses. */
.glossary dd,
.guide-ref-body p {
  font-style: italic;
}
.glossary dd strong,
.guide-ref-body p strong,
.guide-ref-body p .mono,
.guide-ref-body p code {
  font-style: normal;
}
/* Slightly quieter than body text, and a touch more room between entries:
   these are read one at a time, not straight through. */
.glossary dd { color: var(--text); line-height: 1.65; }
.glossary dt { padding-top: .15rem; }

/* A preview of somebody else's screen. Their layout, not ours: a day band with
   its total, then one line per entry — description, where it is booked, the
   tag, the two times and the length. Recognisable at a glance is the whole
   point; a table of the same values would not be. */
.modal-wide { max-width: 62rem; width: 92vw; }
.clockify-preview { display: flex; flex-direction: column; gap: .35rem; }
.cp-day {
  display: flex; align-items: center; justify-content: space-between;
  margin-top: .6rem; padding: .35rem .6rem; border-radius: var(--radius);
  background: var(--surface); font-size: .76rem; font-weight: 600; color: var(--muted);
}
.cp-day:first-child { margin-top: 0; }
.cp-total { font-variant-numeric: tabular-nums; color: var(--text); }
.cp-entry {
  display: grid; align-items: center; gap: .5rem;
  grid-template-columns: minmax(8rem, 1.4fr) minmax(8rem, 1.2fr) auto auto auto minmax(6rem, .8fr);
  padding: .5rem .6rem; border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--card-bg); font-size: .8rem;
}
.cp-said { font-weight: 600; color: var(--text); }
.cp-where { color: var(--secondary); font-size: .76rem; }
.cp-workspace { color: var(--muted); }
.cp-dot {
  display: inline-block; width: .45rem; height: .45rem; border-radius: 50%;
  background: var(--secondary); margin-right: .3rem; vertical-align: middle;
}
.cp-tag {
  justify-self: start; padding: .05rem .4rem; border: 1px solid var(--field-border);
  border-radius: var(--radius); font-size: .7rem; color: var(--muted);
}
.cp-times, .cp-length { font-variant-numeric: tabular-nums; color: var(--muted); font-size: .76rem; }
.cp-length { font-weight: 600; color: var(--text); }
.cp-who { color: var(--muted); font-size: .76rem; text-align: right; }
@media (max-width: 800px) {
  .cp-entry { grid-template-columns: 1fr auto; }
  .cp-where, .cp-tag, .cp-who { grid-column: 1 / -1; }
}

/* The dry version of a summary. Four cards and three coloured banners said the
   same thing and took a screen doing it; this is a readout, meant to be read
   once on the way to the table. */
/* The link out of the tab strip, pushed to its end: what a kind of absence
   becomes in Clockify is set elsewhere, and the tabs are the only other place
   kinds are named. Quiet — it is a way out, not one of the tabs. */
.tabs-aside {
  margin-left: auto; align-self: center; padding: 0 .2rem .55rem;
  font-size: .78rem; color: var(--muted); border-bottom: 0;
}
.tabs-aside:hover { color: var(--secondary); }

/* The switch at the head of a mapping row.

   A button rather than a checkbox — it submits, so it works with no script —
   but it wears the same slider as every other toggle in the console. Sharing
   the LOOK and not the class: `.switch` belongs to the toggle component, and a
   second rule under that name squeezed every one of them to two centimetres.

   Slightly smaller than the settings one: it sits in a table row, not on a form
   line, and forty of them down a page would otherwise shout. */
.mention-col { width: 3.4rem; }
.row-switch { all: unset; cursor: pointer; display: inline-flex; }
.row-switch .row-switch-track {
  position: relative; width: 2.2rem; height: 1.25rem; border-radius: 1rem;
  background: var(--border); flex: 0 0 auto;
  /* Sunk while off, so the knob reads as sitting IN the track rather than on
     top of a grey pill. */
  box-shadow: inset 0 1px 2px rgba(15, 35, 63, .18);
  transition: background .18s ease, box-shadow .18s ease;
}
.row-switch .row-switch-knob {
  position: absolute; top: .15rem; left: .15rem;
  width: .95rem; height: .95rem; border-radius: 50%;
  background: var(--card-bg); box-shadow: 0 1px 3px rgba(15, 35, 63, .35);
  /* Overshoots a hair and settles: the point of a slider over a tick is that
     the state visibly MOVES. */
  transition: transform .2s cubic-bezier(.34, 1.56, .64, 1), width .12s ease;
}
.row-switch.is-on .row-switch-track {
  background: var(--secondary);
  box-shadow: inset 0 1px 2px rgba(15, 35, 63, .1), 0 0 0 3px rgba(47, 111, 237, .12);
}
.row-switch.is-on .row-switch-knob { transform: translateX(.95rem); }
.row-switch:hover .row-switch-track {
  box-shadow: inset 0 1px 2px rgba(15, 35, 63, .18), 0 0 0 4px rgba(47, 111, 237, .1);
}
.row-switch:focus-visible .row-switch-track { box-shadow: 0 0 0 3px rgba(47, 111, 237, .35); }
.row-switch:active .row-switch-knob { width: 1.1rem; border-radius: .7rem; }
@media (prefers-reduced-motion: reduce) {
  .row-switch-track, .row-switch-knob { transition: none; }
}

/* A row nobody wants mentioned. Still readable — it is a decision, and it has
   to be findable to be undone — but plainly out of the way. */
table.tbl tr.is-silent > td:not(.mention-col) { opacity: .45; }

/* A row that opens the rows under it. The caret turns; nothing moves. */
.expander-col { width: 1.6rem; padding-right: 0 !important; text-align: center; }
.expander {
  all: unset; cursor: pointer; display: inline-block; width: 1.2rem; line-height: 1.2rem;
  color: var(--muted); font-size: .9rem; border-radius: var(--radius);
  transition: transform .12s ease, color .12s ease;
}
.expander:hover { color: var(--secondary); }
.expander:focus-visible { outline: 2px solid var(--secondary); outline-offset: 1px; }
.expander.is-open { transform: rotate(90deg); color: var(--text); }

/* The absences themselves, set back and quieter than the person above them.

   Read as one block hanging off the person's row: their row is shaded with the
   group while it is open, and a spine runs down the first column so where the
   group starts and stops is not something to work out from shading alone. */
table.tbl tr.absence-row > td { background: var(--surface); font-size: .8rem; }
table.tbl tr.absence-row > td:nth-child(2) { color: var(--muted); }
table.tbl tr.absence-row:hover > td { background: var(--surface-hover); }

/* Public holidays: one switch and, when it is on, what a holiday becomes.
   Set apart from the table below it, because it is not one of its rows. */
.holiday-block.is-on { border-color: var(--secondary); }
.holiday-head { display: flex; align-items: flex-start; gap: .75rem; }
.holiday-head strong { color: var(--heading); }
/* Four equal columns, not four things sharing whatever they can take.

   As a flex row they were sized by their contents and aligned on their bottom
   edge, so the one field carrying a note under it pushed its three neighbours
   up and left the labels on different lines. A grid gives each the same width;
   aligning at the TOP puts every label on one line and lets the note hang
   below its own column, where it belongs. */
.holiday-fields {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  align-items: start; gap: .85rem;
  margin-top: .9rem; padding-top: .9rem; border-top: 1px solid var(--border);
}
.holiday-fields .field { min-width: 0; }
/* The help under a field is the field's, not the row's: kept narrow enough to
   read and out of the way of the boxes beside it. */
.holiday-fields .help { margin-top: .3rem; line-height: 1.45; }
.holiday-note { margin: .7rem 0 0; max-width: 62rem; }

/* Two columns on a laptop, one on a phone: four boxes of nine characters is
   not four fields, it is a puzzle. */
@media (max-width: 70rem) {
  .holiday-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 40rem) {
  .holiday-fields { grid-template-columns: 1fr; }
}

/* A cell that opens its row. Only the cursor and the name's colour on hover:
   it is the same action as the chevron beside it, not a second control, and
   dressing it as a button would say otherwise. */
.is-openable { cursor: pointer; }
.is-openable:hover strong { color: var(--secondary); }

/* The head of the group belongs to it. */
table.tbl tr.is-expanded > td { background: var(--surface); }
table.tbl tr.is-expanded > td:first-child { box-shadow: inset 2px 0 0 var(--secondary); }

/* The indent, and the line that makes it a group rather than four loose rows.
   Drawn on the cell rather than as a border on the row: a border on a table row
   is a suggestion browsers are free to ignore. */
table.tbl tr.absence-row > td:first-child { position: relative; }
table.tbl tr.absence-row > td:first-child::before {
  content: ""; position: absolute; top: 0; bottom: 0; left: .78rem;
  border-left: 2px solid var(--border-strong);
}
/* Stopping halfway down the last one, so the group closes instead of running
   into whoever is underneath. */
table.tbl tr.absence-row.is-last > td:first-child::before { bottom: 50%; }
/* And a stub across to the row's own content, which is what makes it read as
   hanging off the spine rather than sitting beside it. */
table.tbl tr.absence-row > td:first-child::after {
  content: ""; position: absolute; left: .78rem; top: 50%; width: .45rem;
  border-top: 2px solid var(--border-strong);
}
table.tbl tr.absence-row > td:nth-child(2) { padding-left: .35rem; }

/* One absence, drawn the way Clockify holds it: the description first, because
   that is the line people read there, then where it is booked and its tag. */
.as-entry { margin-top: .35rem; padding-left: .55rem; border-left: 2px solid var(--border); }
.as-entry-said { font-size: .8rem; color: var(--text); }
.as-entry-where { font-size: .75rem; color: var(--muted); margin-top: .1rem; }
.as-entry-where .sep { opacity: .5; padding: 0 .15rem; }
.as-entry-tag {
  display: inline-block; margin-left: .35rem; padding: 0 .3rem;
  border: 1px solid var(--field-border); border-radius: 2rem;
  font-size: .7rem; color: var(--muted);
}

/* A number that opens something. It is a button, because it does something
   rather than going somewhere, and it reads as the number it is. */
.linkish {
  all: unset; cursor: pointer; font: inherit; color: inherit;
  text-decoration: underline; text-decoration-style: dotted; text-underline-offset: .2em;
}
.linkish:hover { color: var(--secondary); }
.linkish:focus-visible { outline: 2px solid var(--secondary); outline-offset: 2px; }

/* Fields that are one question ------------------------------------------------
   A bordered group rather than a heading and a gap: the boxes inside it are
   only asked in one mode, and a rule that appears and disappears with them
   makes that obvious without a sentence saying so. */
.field-group {
  grid-column: 1 / -1; margin: .3rem 0 .2rem; padding: .9rem 1rem 1rem;
  border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface);
}
.field-group > legend {
  padding: 0 .45rem; margin-left: -.45rem;
  font-size: .78rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
  color: var(--muted);
}
.field-group .group-help { margin: 0 0 .8rem; max-width: 46rem; }
.field-group .form-grid { margin: 0; }

/* The specimen answer, beside the boxes read off it. Two columns where there is
   room, stacked where there is not — the example is useless if it is not on
   screen at the same time as the fields. */
.shape-example {
  display: grid; grid-template-columns: minmax(18rem, 1fr) minmax(16rem, 1fr);
  gap: 1rem; margin: 0 0 1rem;
}
.shape-json {
  margin: 0; padding: .7rem .8rem; overflow-x: auto;
  background: var(--card-bg); border: 1px solid var(--field-border); border-radius: var(--radius);
  font-size: .74rem; line-height: 1.55; white-space: pre;
}
.shape-json .k { color: var(--muted); }
.shape-json .s { color: var(--secondary); }
.shape-json .v { color: var(--success, var(--text)); }
.shape-key { margin: 0; font-size: .78rem; }
.shape-key dt { font-weight: 600; color: var(--heading); }
.shape-key dd { margin: 0 0 .5rem; color: var(--muted); }
@media (max-width: 900px) {
  .shape-example { grid-template-columns: 1fr; }
}

/* A tenant that is stood in for. Said plainly, because a form that has just
   hidden half of itself and contacts nothing looks like one that broke. */
.stood-in {
  padding: .9rem 1rem; border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface);
}
.stood-in p { margin: 0 0 .6rem; font-size: .84rem; max-width: 52rem; }
.stood-in p:last-of-type { margin-bottom: .8rem; }

/* A number and the unit it is in. Beside the box, not under it: "8" on its own
   is a question, and a unit three lines below has already been read past. */
.with-suffix { display: flex; align-items: center; gap: .45rem; }
.with-suffix input { width: 7rem; }
.with-suffix .suffix { font-size: .8rem; color: var(--muted); white-space: nowrap; }

.form-actions { display: flex; gap: .6rem; align-items: center; padding-top: .4rem; }

/* The tail of a log file. Monospaced, scrolls on its own, and never widens the
   page: a stack trace is one very long line, and the rest of the console should
   not have to make room for it. */
.log-tail {
  margin: 0; padding: .8rem .9rem; max-height: 22rem; overflow: auto;
  background: var(--surface); border-radius: var(--radius);
  font-size: .76rem; line-height: 1.5; white-space: pre; tab-size: 2;
  color: var(--text);
}
/* A row that has just arrived, so the eye can find it in a table that is
   changing under it. */
@keyframes live-in { from { background: rgba(47, 111, 237, .14); } to { background: transparent; } }
tr.is-new { animation: live-in 1.6s ease-out; }
@media (prefers-reduced-motion: reduce) { tr.is-new { animation: none; } }

/* ── Login ───────────────────────────────────────────────────────────── */
/* The band is sized by its content and the card is pulled up over it, so the
   seam between the two grounds always falls INSIDE the card — no line of text
   can end up sitting on the join, whatever the name or tagline does. */
.login-page {
  min-height: 100vh; display: flex; flex-direction: column;
  background: var(--body-bg);
}
.login-hero {
  background: linear-gradient(180deg, var(--sidebar-top) 0%, var(--hero-bottom) 100%);
  padding: clamp(2.5rem, 8vh, 4rem) 1rem calc(clamp(2.5rem, 8vh, 4rem) + 2.75rem);
  display: flex; justify-content: center;
}
.login-brand {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  color: var(--on-dark);
}
.login-brand .brand-mark { margin-bottom: .85rem; }
.login-brand h1 { color: var(--on-dark); font-size: 1.24rem; font-weight: 600; letter-spacing: .005em; }
.login-tagline {
  font-size: .62rem; font-weight: 500; letter-spacing: .17em; text-transform: uppercase;
  color: rgba(255, 255, 255, .55); margin-top: .4rem;
}

.login-card {
  width: 100%; max-width: 26rem;
  margin: -2.75rem auto 0;            /* the overlap */
  padding: 0 1rem clamp(2rem, 6vh, 3.5rem);
  flex: 1;
}

/* The task belongs with the form it starts, not with the identity above it. */
.login-heading { margin-bottom: 1.25rem; }
.login-heading h2 { font-size: 1.05rem; font-weight: 600; }
.login-heading p { color: var(--muted); font-size: .82rem; margin: .2rem 0 0; }

.login-foot {
  display: flex; flex-direction: column; align-items: center; gap: .7rem;
  text-align: center; color: var(--muted); font-size: .76rem; margin-top: 1.15rem;
}
.login-foot p { margin: 0; }
.login-docs {
  display: inline-flex; align-items: center; gap: .35rem;
  color: var(--primary); font-size: .78rem; font-weight: 500;
  padding: .3rem .1rem; border-radius: 3px;
}
.login-docs:hover { text-decoration: underline; }
.login-docs:focus-visible { outline: 2px solid var(--secondary); outline-offset: 2px; }

/* ── Denied page ─────────────────────────────────────────────────────── */
.denied {
  min-height: 100vh; display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: .9rem; text-align: center; padding: 2rem;
}
.denied .face { font-size: 4.5rem; animation: blink 1.6s ease-in-out infinite; }
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: .15; } }

/* ── Misc ────────────────────────────────────────────────────────────── */
.muted { color: var(--muted); }
.small { font-size: .8rem; }
.pre {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: .8rem; font-size: .78rem; white-space: pre-wrap; word-break: break-word;
  max-height: 26rem; overflow: auto; margin: 0;
}
.chips { display: flex; flex-wrap: wrap; gap: .35rem; }
.chip { background: var(--surface); border: 1px solid var(--border); border-radius: 1rem;
        padding: .15rem .55rem; font-size: .76rem; }
.pagination { display: flex; gap: .3rem; flex-wrap: wrap; padding: .9rem 1.1rem; }
.pagination a, .pagination span {
  padding: .3rem .6rem; border: 1px solid var(--border); border-radius: var(--radius);
  font-size: .8rem; color: var(--text); background: var(--card-bg);
}
.pagination .active span { background: var(--primary); color: var(--on-dark); border-color: var(--primary); }
.inline-form { display: inline; }

@media (max-width: 1200px) {
  .tiles { grid-template-columns: repeat(2, 1fr); }
  .grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 900px) {
  :root { --sidebar-w: 0px; }
  .sidebar { display: none; }
  .grid, .grid-2, .tiles, .form-grid { grid-template-columns: 1fr; }
  .span2, .span3 { grid-column: auto; }
}

/* Password requirements --------------------------------------------------
   The marker is drawn, not typed: a tick as a CSS `content` escape is one more
   encoding layer between this file and the screen, and it only has to survive
   one bad round-trip to render as mojibake. A ring, a fill and two borders
   rotated 45deg cannot.

   Neutral until typing starts, so an untouched form is not a wall of red, and
   shape carries the state as well as colour. */
.reqs {
  list-style: none; margin: .55rem 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(13.5rem, 1fr));
  gap: .3rem .9rem;
}
.reqs li {
  display: flex; align-items: center; gap: .45rem;
  font-size: .78rem; color: var(--muted); transition: color .12s ease;
}
.reqs .mark {
  flex: 0 0 auto; width: .82rem; height: .82rem; border-radius: 50%;
  border: 1.5px solid var(--border); position: relative;
  transition: background .12s ease, border-color .12s ease;
}
/* met: filled disc with a tick drawn from two borders */
.reqs li.met { color: var(--success); }
.reqs li.met .mark { background: var(--success); border-color: var(--success); }
.reqs li.met .mark::after {
  content: ""; position: absolute; left: .26rem; top: .11rem;
  width: .2rem; height: .38rem; transform: rotate(45deg);
  border: solid var(--on-dark); border-width: 0 1.5px 1.5px 0;
}
/* unmet: an empty ring. Not red — nothing has gone wrong, it is just not done */
.reqs li.unmet .mark { border-color: var(--border); }

/* Password field row: the input, then Show and Generate beside it. */
.pw-row { display: flex; align-items: stretch; gap: .4rem; }
.pw-row input { flex: 1 1 auto; min-width: 0; }
.pw-row .btn { flex: 0 0 auto; white-space: nowrap; }

/* The generated value, offered once, in a form you can select in one click. */
.pw-made {
  margin-top: .5rem; padding: .5rem .65rem;
  background: var(--info-wash); border: 1px solid var(--border);
  border-radius: .35rem; font-size: .78rem; color: var(--text);
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
}
.pw-made .val {
  font-family: var(--font-mono); font-size: .82rem;
  -webkit-user-select: all; user-select: all; word-break: break-all;
}

/* Footbar -----------------------------------------------------------------
   The console is three in-flow siblings — topbar, content, footbar — laid
   beside a fixed sidebar. The footer takes the SAME --sidebar-w offset as the
   other two, so the narrow-screen rule that zeroes that variable already
   handles the mobile case; there is no separate mobile footer to write.

   body.app makes those three a column so .content absorbs the slack: on a short
   page the bar rests at the bottom of the viewport, on a long one at the end of
   the document, and in neither case does it float over the content. The fixed
   .sidebar is out of flow and is not a flex item, so no wrapper is needed. The
   class scopes all of this to the signed-in layout — the sign-in and denied
   pages build their own columns and would fight a flex parent. */
body.app { min-height: 100vh; display: flex; flex-direction: column; }
/* Mobile browsers count the collapsing URL bar inside 100vh, which parks the
   bar just below the fold on an otherwise empty page. */
@supports (min-height: 100dvh) { body.app { min-height: 100dvh; } }
body.app > .content { flex: 1 0 auto; }
body.app > .topbar,
body.app > .footbar { flex: 0 0 auto; }

/* White with a hairline, bookending the topbar: on the page canvas a grey
   footer would simply not be there, and a short page would appear to stop for
   no reason. Owner left under the brand, build right under the account — the
   topbar's own division, kept. */
.footbar {
  margin-left: var(--sidebar-w);
  padding: .85rem 1.5rem;
  background: var(--card-bg);
  border-top: 1px solid var(--border);
  display: flex; align-items: center; gap: .75rem; flex-wrap: wrap;
  font-size: .74rem; color: var(--muted);
}
/* Three or four short facts, laid out as a row that may wrap. Each is
   unbreakable inside itself, so a phone drops one to the next line rather than
   splitting "release" from its number. */
.colophon-ver {
  margin-left: auto;
  display: inline-flex; flex-wrap: wrap; align-items: center;
  justify-content: flex-end; gap: .2rem .6rem;
}
.colophon-env, .colophon-build, .colophon-deployed { white-space: nowrap; }
.colophon-build { opacity: .8; }
/* One click takes the whole build id — and only the build id — into a ticket. */
.colophon-ver .mono { -webkit-user-select: all; user-select: all; }

/* When it went out. Set as a chip rather than as more footer text: it answers a
   different kind of question from the two numbers beside it — not "which one",
   but "how long has this been the one". */
.colophon-deployed {
  padding: .05rem .4rem;
  border: 1px solid var(--border); border-radius: 999px;
  display: inline-flex; align-items: center; gap: .3rem;
  color: var(--muted);
}
.colophon-clock {
  width: .72rem; height: .72rem; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.2;
  stroke-linecap: round; stroke-linejoin: round;
}

/* The sign-in page is its own centred column; the same colophon sits under the
   card rather than in a bar. */
.login-colophon {
  margin-top: .9rem; padding-top: .8rem; border-top: 1px solid var(--border);
  display: flex; flex-wrap: wrap; gap: .5rem .75rem;
  justify-content: center; font-size: .72rem; color: var(--muted);
}
.login-colophon .colophon-ver { margin-left: 0; }

/* Tiles that open something ------------------------------------------------
   The tile is a <button>, so it must be talked out of looking like one; the
   affordance is added by console.js, so a tile whose dialog cannot open never
   invites the click. */
button.tile {
  font: inherit; color: inherit; text-align: left; width: 100%;
  border: 1px solid var(--border); cursor: default;
}
button.tile.is-clickable { cursor: pointer; }
button.tile.is-clickable:hover { border-color: var(--border-strong); transform: translateY(-2px); }
button.tile.is-clickable:focus-visible { outline: 2px solid var(--info); outline-offset: 2px; }

/* A tile that is a link needs no console.js to work, so it is clickable from
   the moment it renders — hence the affordance here rather than on a class. */
a.tile { text-decoration: none; color: inherit; cursor: pointer; }

/* A link that must not be followed yet. `disabled` is not an attribute a link
   has, so the refusal is the class — with the pointer events off, not just the
   colour, or it would look inert and still navigate. */
.btn.is-disabled { opacity: .5; pointer-events: none; }
a.tile:hover { border-color: var(--border-strong); transform: translateY(-2px); }
a.tile:focus-visible { outline: 2px solid var(--info); outline-offset: 2px; }

/* Modal -------------------------------------------------------------------
   A native <dialog>. Esc, the backdrop and focus handling are the browser's;
   this only sets the shape. */
.modal {
  border: 0; border-radius: var(--radius); padding: 0;
  width: min(52rem, calc(100vw - 2rem)); max-height: calc(100vh - 4rem);
  /* The dialog itself scrolls, so its head has something to stick to. It used
     to be the body that scrolled, which left the head pinned to a box that
     never moved and the title sliding away with the content. */
  overflow: auto;
  background: var(--card-bg); color: var(--text); box-shadow: 0 12px 40px rgba(56, 65, 74, .22);
}
.modal::backdrop { background: rgba(33, 37, 41, .45); }
/* The head does not scroll away. A dialog is scrolled with the pointer already
   inside it, and once the title and the close control have gone past the top
   edge the only way out is the Escape key — which is not a thing everybody
   knows. Sticky rather than fixed: it stays inside the dialog's own box. */
.modal-head {
  position: sticky; top: 0; z-index: 1;
  display: flex; align-items: center; gap: 1rem;
  padding: .9rem 1.2rem; border-bottom: 1px solid var(--border);
  background: var(--card-bg);
  border-radius: calc(var(--radius) - 1px) calc(var(--radius) - 1px) 0 0;
}
.modal-head h2 { margin: 0; font-size: .95rem; color: var(--heading); }
.modal-head form { margin-left: auto; line-height: 0; }

/* The close control is a drawn cross, not a word: at this size the shape reads
   faster than the label, and it leaves the head to the title. It turns a
   quarter on hover — the one flourish in this console, and it is the control
   that most wants to look reachable. */
.modal-close {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.9rem; height: 1.9rem; padding: 0;
  border: 1px solid transparent; border-radius: 50%;
  background: transparent; color: var(--muted); cursor: pointer;
  transition: color .15s ease, background .15s ease, border-color .15s ease, transform .25s ease;
}
.modal-close svg { width: .95rem; height: .95rem; display: block; }
.modal-close:hover {
  color: var(--danger); background: rgba(240, 101, 72, .10);
  border-color: rgba(240, 101, 72, .25); transform: rotate(90deg);
}
.modal-close:focus-visible { outline: 2px solid var(--info); outline-offset: 2px; }
/* Rotation is decoration; colour still carries the state. */
@media (prefers-reduced-motion: reduce) {
  .modal-close { transition: color .15s ease, background .15s ease; }
  .modal-close:hover { transform: none; }
}
.modal-body { padding: 1.2rem; }

/* Chart -------------------------------------------------------------------
   Inline SVG drawn on the server. Wide content scrolls inside its own box so
   the modal never scrolls sideways. */
.chart-wrap { overflow-x: auto; position: relative; }

/* ── A chart under the pointer ────────────────────────────────────────
   The hairline finds the moment, the dots say which line is where, and the
   readout names every helper at once — nobody should have to land on a 2px
   line to read it. All of it is built by console.js: with the script off the
   chart is exactly the drawing it always was, titles and all. */
.chart-cross { stroke: var(--border-strong); stroke-width: 1; pointer-events: none; }
.chart:focus-visible { outline: 2px solid var(--info); outline-offset: 2px; }
.chart-catch { fill: transparent; }
.slow-dot { stroke: var(--card-bg); stroke-width: 2; pointer-events: none; }
.chart-tip {
  position: absolute; z-index: 20; pointer-events: none;
  min-width: 12rem; max-width: 22rem; padding: .5rem .6rem;
  background: var(--card-bg); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-lift);
}
.chart-tip-when { font-size: .72rem; color: var(--muted); margin-bottom: .35rem; }
.chart-tip-row { display: flex; align-items: baseline; gap: .45rem; font-size: .78rem; }
.chart-tip-row + .chart-tip-row { margin-top: .18rem; }
.tip-key { flex: 0 0 auto; width: .8rem; height: 2px; border-radius: 1px; }
.chart-tip-secs {
  flex: 0 0 auto; font-weight: 600; color: var(--heading);
  font-variant-numeric: tabular-nums; min-width: 3.4rem; text-align: right;
}
.chart-tip-name {
  flex: 1 1 auto; color: var(--muted); overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap;
}
.chart-tip-bad { color: var(--danger); font-weight: 600; }
.chart { width: 100%; min-width: 30rem; height: auto; display: block; }
.chart-grid { stroke: var(--border); stroke-width: 1; }
.chart-axis { stroke: var(--border-strong); stroke-width: 1; }
.chart-tick { font-size: 10px; fill: var(--muted); }
.chart-bar-ok { fill: var(--info); }
.chart-bar-bad { fill: var(--danger); }
/* How long a run takes, as an area. The line is what is read; the fill only
   says "this much", so it fades out downwards (the gradient is in the SVG). */
.took-line { fill: none; stroke: var(--secondary); stroke-width: 2;
  stroke-linejoin: round; stroke-linecap: round; }
.took-dot { fill: var(--surface); stroke: var(--secondary); stroke-width: 1.5; }
.took-dot.is-bad { fill: var(--danger); stroke: var(--danger); }
/* The answer to "try it", beside the buttons. Long sentences from a far system
   land here, so it wraps rather than pushing the buttons about. */
.check-said { flex: 1 1 18rem; min-width: 0; font-size: .87rem; color: var(--muted); }
.check-said.is-good { color: var(--success-ink, #1a7f5a); font-weight: 600; }
.check-said.is-bad { color: var(--danger-ink, #c0432c); font-weight: 600; }

/* Whose workspace the id in the box belongs to. Four states, and the one that
   matters is "this id matches nothing" — it has to be impossible to read as a
   confirmation. */
.workspace-is { margin-top: .4rem; font-size: .85rem; }
.workspace-is.is-known { color: var(--success-ink, #1a7f5a); font-weight: 600; }
.workspace-is.is-wrong { color: var(--danger-ink, #c0432c); font-weight: 600; }
.workspace-is.is-unknown,
.workspace-is.is-empty { color: var(--muted); }

/* The addresses a preset builds. Long, and meant to be compared against a
   tenant's own — so they wrap rather than being cut off, and the label sits
   above rather than beside on a narrow screen. */
.preset-urls { margin: 0; }
.preset-urls dt { font-weight: 600; font-size: .78rem; }
.preset-url { word-break: break-all; margin-bottom: .45rem; }

/* The guide's references: the reasoning the checklist above deliberately does
   not carry. Closed by default, so the page reads as six things to do — the
   detail is one click away at the moment somebody wants it. */
.guide-ref { border-bottom: 1px solid var(--rule, var(--border)); }
.guide-ref:last-of-type { border-bottom: 0; }
.guide-ref > summary { cursor: pointer; padding: .7rem .2rem; font-weight: 600;
  font-size: .93rem; list-style: none; }
.guide-ref > summary::-webkit-details-marker { display: none; }
.guide-ref > summary::before { content: '▸'; display: inline-block; width: 1.1rem;
  color: var(--muted); }
.guide-ref[open] > summary::before { content: '▾'; }
.guide-ref > summary:hover { color: var(--primary); }
.guide-ref-body { padding: 0 .2rem .9rem 1.3rem; display: flex; flex-direction: column;
  gap: .6rem; max-width: 46rem; }
.guide-ref-body p, .guide-ref-body ul { margin: 0; }

/* The guide's opening line and its map. The line is the whole platform in one
   sentence, so it is set larger than anything around it and given room; the map
   is the picture that sentence needs before the five moves below mean anything. */
.guide-oneliner { font-size: 1.12rem; line-height: 1.55; color: var(--muted);
  max-width: 46rem; margin: 0 0 1rem; }
.guide-oneliner strong { color: var(--text); }

.guide-map .map-art { display: block; min-width: 700px; width: 100%; height: auto; }
.map-box rect { fill: var(--surface); stroke: var(--border); stroke-width: 1.5; }
.map-who { font-size: 13px; font-weight: 700; letter-spacing: .06em; fill: var(--text); }
.map-what { font-size: 12.5px; fill: var(--muted); }
.map-mid { text-anchor: middle; }
.map-say { font-size: 11px; fill: var(--muted); }
.map-line { stroke: var(--muted); stroke-width: 1.6; }

/* The three that hold the story are quiet; the platform is the subject of the
   picture, and the one thing it WRITES is the only line that is coloured. */
.map-jira rect { stroke: var(--jira-ink, #4a5bc4); }
.map-clockify rect { stroke: var(--clockify-ink, #17a398); }
.map-bamboo rect { stroke: var(--bamboo-ink, #7a9e3a); }
.map-avanti rect { stroke: var(--avanti-ink, #c07b28); }
.map-here rect { fill: var(--primary); stroke: var(--primary); }
.map-here .map-who, .map-here .map-what { fill: var(--on-dark, #fff); }
.map-here .map-what { opacity: .85; }
.map-write { stroke: var(--secondary); stroke-width: 2.2; }
.map-write-say { fill: var(--secondary); font-weight: 600; }

/* What one list looked like when it was asked for. The URL is shown because on
   a preset nobody can see it anywhere else, and it is the first thing a 404
   raises a question about. */
.preview-kind { margin-top: .8rem; padding: .7rem .8rem; border: 1px solid var(--border);
  border-radius: 8px; background: var(--surface-2, var(--surface)); }
.preview-head { display: flex; align-items: center; gap: .5rem; margin-bottom: .3rem; }
.preview-url { word-break: break-all; margin-bottom: .5rem; }

/* One line per helper on the overview. Six hues, handed out slowest-first, so
   the line at the top of the chart is the first row of the legend. Chosen to
   stay apart from each other on both grounds rather than to be pretty. */
.slow-line { fill: none; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; opacity: .9;
  transition: opacity .15s, stroke-width .15s; }
.slow-chart { cursor: pointer; }
.has-tip { text-decoration: underline dotted; text-underline-offset: 3px; cursor: help; }
.slow-chart.is-dim .slow-line { opacity: .12; }
.slow-chart.is-dim .slow-line.is-lit { opacity: 1; stroke-width: 3; }
.slow-chart.is-dim .took-dot { opacity: .25; }
tr[data-series].is-lit td { background: var(--surface-2, rgba(0, 0, 0, .04)); }
.slow-key { display: inline-block; width: .7rem; height: .7rem; border-radius: 2px;
  margin-right: .5rem; vertical-align: -1px; }
/* One place per colour. Everything that carries a series — the line, the
   legend key, the dot under the crosshair, the key in the readout — reads
   it from here, so a new one of those needs no new colour rule. */
[data-hue="0"] { --hue: #2f6fed; }
[data-hue="1"] { --hue: #e07b39; }
[data-hue="2"] { --hue: #17a398; }
[data-hue="3"] { --hue: #9b5de5; }
[data-hue="4"] { --hue: #c9184a; }
[data-hue="5"] { --hue: #6b8f2e; }
[data-hue="6"] { --hue: #b5179e; }
[data-hue="7"] { --hue: #b45309; }
[data-hue="8"] { --hue: #0891b2; }
[data-hue="9"] { --hue: #d90429; }
[data-hue="10"] { --hue: #6a4c93; }
[data-hue="11"] { --hue: #2b9348; }
.slow-line { stroke: var(--hue); }
.slow-key, .tip-key { background: var(--hue); }
.slow-dot { fill: var(--hue); }

.took-dot.is-partial { fill: var(--warning, #b8860b); stroke: var(--warning, #b8860b); }
.took-chart { min-width: 24rem; }

/* How the absence sync stands: one stacked column per day. Three colours and
   nothing else, because the whole point is to be read at a glance — green is
   done, amber is owed, grey is a decision.

   The steps are the darker ones of each ramp rather than the badge colours: the
   pale amber and the border grey sat outside the readable lightness band and
   washed out against the card. Grey is deliberately unsaturated — "will not be
   written" is the absence of a state, not a fourth category competing for
   attention. Both themes were checked for colour-blind separation. */
.sync-chart { min-width: 26rem; }
.sync-written { fill: var(--success); }
.sync-waiting { fill: var(--warning-ink); }
.sync-refused { fill: var(--muted); }

/* The trend, over the columns. Drawn twice — the halo underneath is the card's
   own colour, so the line survives crossing a bar of its own hue. */
.sync-trend {
  fill: none; stroke: var(--warning-ink-strong); stroke-width: 2;
  stroke-dasharray: 5 3; stroke-linecap: round; stroke-linejoin: round;
}
.sync-trend-halo { fill: none; stroke: var(--card-bg); stroke-width: 5; stroke-linejoin: round; }

/* Today, so "still to go" can be read as early or late. Faint: it is a
   reference line, not a reading. */
.sync-today { stroke: var(--border-strong); stroke-width: 1; stroke-dasharray: 2 3; opacity: .8; }

/* One row of keys, each an unbreakable piece. The swatch is a box of its own so
   it cannot be squeezed, and the number and its words share one box so they
   stay together when the row wraps. */
.sync-legend {
  list-style: none; margin: 0; display: flex; flex-wrap: wrap;
  gap: .3rem 1.2rem; font-size: .8rem; color: var(--muted);
  /* Indented to where the plot starts, not to the edge of the card. The chart
     reserves 32 of its 720 units for the scale down the left; the legend keeps
     out of that column so the first swatch sits under the first bar rather than
     out on its own under the numbers. */
  padding: .7rem 1.1rem .8rem calc(32 / 720 * 100%);
}
.sync-key { display: inline-flex; align-items: center; gap: .4rem; white-space: nowrap; }
.sync-key b { color: var(--text); font-variant-numeric: tabular-nums; }
.sync-key.is-bad b { color: var(--danger-ink); }
.sync-swatch { width: .68rem; height: .68rem; border-radius: 2px; flex: none; }
.sync-swatch.is-written { background: var(--success); }
.sync-swatch.is-waiting { background: var(--warning-ink); }
.sync-swatch.is-refused { background: var(--muted); }
.sync-swatch.is-failed { background: var(--danger); }
/* One line a far system sent back, on the run's own page. Wraps anywhere,
   because an API refusal is one long unbroken string more often than it is a
   sentence. */
.run-said {
  margin: .1rem 0; padding-left: .55rem; border-left: 2px solid var(--danger);
  overflow-wrap: anywhere; color: var(--danger-ink);
}

/* The trend's key is a line, because the trend is a line. */
.sync-swatch.is-trend {
  height: 0; border-radius: 0; align-self: center;
  border-top: 2px dashed var(--warning-ink-strong);
}

/* Full-height hit area so the tooltip works over an empty hour too. */
.chart-hit { fill: transparent; }
.chart-hit:hover { fill: rgba(47, 111, 237, .07); }
/* Range switch: a segmented control, quiet until one is chosen. */
.chart-ranges {
  display: inline-flex; margin: 0 0 .9rem; padding: .15rem;
  background: var(--body-bg); border: 1px solid var(--border); border-radius: .35rem;
}
.chart-range {
  border: 0; background: transparent; cursor: pointer;
  padding: .3rem .7rem; border-radius: .25rem;
  font: inherit; font-size: .78rem; color: var(--muted);
  transition: background .12s ease, color .12s ease;
}
.chart-range:hover { color: var(--text); }
.chart-range.is-on { background: var(--card-bg); color: var(--heading); box-shadow: var(--shadow); }
.chart-range:focus-visible { outline: 2px solid var(--info); outline-offset: 1px; }

.chart-caption { margin: .7rem 0 0; font-size: .78rem; color: var(--text); }

/* Who the bars were. Rows rather than a wrapped sentence: the numbers line up
   under each other, which is how you read "7 of these, 3 of those" without
   counting. The count is on the right of each row and tabular, so a two-figure
   number does not shift the name beside it. */
.chart-who {
  list-style: none; margin: .5rem 0 0; padding: 0;
  display: flex; flex-direction: column; gap: .15rem;
  font-size: .78rem; max-width: 34rem;
}
.chart-who li { display: flex; align-items: baseline; gap: .5rem; }
.chart-who-name {
  flex: 1 1 auto; min-width: 0; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.chart-who-n { flex: 0 0 auto; font-weight: 600; color: var(--heading); font-variant-numeric: tabular-nums; }

/* Tabs --------------------------------------------------------------------- */
.tabs { display: flex; gap: .25rem; border-bottom: 1px solid var(--border); margin-bottom: .9rem; }
.tab {
  padding: .55rem .9rem; font-size: .85rem; color: var(--muted); text-decoration: none;
  border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.tab:hover { color: var(--text); }
.tab.is-on { color: var(--primary); border-bottom-color: var(--secondary); font-weight: 600; }



/* Secret NAMES, never values — the point of the list is which ones arrived. */
.modal-body .kv dd.is-bad { color: var(--danger); font-weight: 600; }

.secret-list { display: flex; flex-wrap: wrap; gap: .3rem .5rem; margin-top: .6rem; }
.secret {
  display: inline-flex; align-items: baseline; gap: .35rem;
  padding: .15rem .45rem; border-radius: 999px; font-size: .72rem;
  background: var(--surface); color: var(--muted); border: 1px solid var(--border);
}
.secret.is-missing { color: var(--danger); border-color: var(--danger); }
.secret .mono { font-size: .72rem; }

/* A long dropdown you can type into ------------------------------------------
   The native <select> is still the control that holds and posts the value; it
   is only hidden while a filter box stands in front of it. Nothing here runs
   without JavaScript, and without JavaScript the select is simply visible. */
.pick { position: relative; }
.pick-native { display: none; }
.pick-input { width: 100%; }
.pick-list {
  position: absolute; z-index: 30;
  max-height: 16rem; overflow-y: auto;
  background: var(--card-bg); border: 1px solid var(--field-border);
  border-radius: var(--radius); box-shadow: var(--shadow-lift);
}
.pick-option { padding: .38rem .6rem; font-size: .85rem; cursor: pointer; }
.pick-option:hover, .pick-option.is-active { background: var(--surface-hover); }
.pick-option.is-chosen { font-weight: 600; color: var(--primary); }
.pick-none { padding: .45rem .6rem; font-size: .82rem; color: var(--muted); }
.chip-pick { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; }
.chip-pick-chosen { flex: 0 1 auto; }
.chip-pick-add { flex: 0 0 auto; width: auto; max-width: 16rem; }
.chip-all { color: var(--muted); border-style: dashed; }
.who-lbl { display: block; margin-top: .9rem; }
.who-scope { display: flex; flex-wrap: wrap; gap: .3rem 1.2rem; margin: .2rem 0 .5rem; }
.who-choice { display: inline-flex; align-items: center; gap: .4rem; font-size: .88rem; cursor: pointer; }
[data-who-people] { margin-top: .2rem; }
.chip-x { border: 0; background: none; padding: 0 0 0 .35rem; margin: 0; cursor: pointer;
  color: var(--muted); font-size: .9em; line-height: 1; }
.chip-x:hover { color: var(--danger); }

/* The chain from a time entry to a client. A break has ONE cause; the steps
   after it did not fail, they never ran, and they are drawn that way. */
.chain { list-style: none; margin: .6rem 0 .4rem; padding: 0; display: grid; gap: .5rem; }
.chain-link { display: flex; gap: .6rem; align-items: flex-start; }
.chain-mark { flex: 0 0 1.1rem; text-align: center; font-weight: 700; }
.chain-link.is-ok .chain-mark { color: var(--success); }
.chain-link.is-bad .chain-mark { color: var(--danger); }
.chain-link.is-skipped { opacity: .55; }
.chain-link.is-skipped .chain-mark { color: var(--muted); }
.chain-more > summary { color: inherit; }

/* Roles and permissions -------------------------------------------------------
   Two questions with two shapes. A role is one of three things an account can
   BE — cards, because you choose between them. A permission is one of ten
   things it may DO — a grouped list, because you read down it while ticking.
   The vault account is marked apart: it is the only one this page cannot touch. */
.role-grid {
  display: grid; gap: .9rem;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
}
.role {
  padding: .85rem .95rem; border: 1px solid var(--border); border-radius: var(--radius);
  border-top: 3px solid var(--border); background: var(--card-bg);
}
.role-vault { border-top-color: var(--info); }
.role-admin { border-top-color: var(--primary); }
.role-user  { border-top-color: var(--success); }
.role-head { display: flex; align-items: center; gap: .5rem; margin-bottom: .45rem; }
.role-head strong { color: var(--heading); font-size: .88rem; }
.role p { margin: 0; font-size: .82rem; }

.perm-grid {
  display: grid; gap: 1.1rem 1.6rem;
  grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
}
.perm-group h3 {
  margin: 0 0 .5rem; font-size: .72rem; text-transform: uppercase; letter-spacing: .07em;
  color: var(--muted); border-bottom: 1px solid var(--border); padding-bottom: .3rem;
}
.perm { padding: .4rem 0; border-bottom: 1px solid var(--border); }
.perm:last-child { border-bottom: 0; }
.perm-head { display: flex; align-items: baseline; gap: .5rem; flex-wrap: wrap; }
.perm-head strong { color: var(--heading); font-size: .84rem; }
.perm p { margin: .15rem 0 0; }

/* Guide --------------------------------------------------------------------
   A checklist, not a manual: each step carries whether it is already done, so
   the page reads differently on a fresh install and on a working one. */
.guide-steps { list-style: none; counter-reset: step; margin: 0; padding: 0; }
.guide-steps > li {
  counter-increment: step; position: relative;
  padding: 0 0 1.1rem 2.6rem; margin-bottom: 1.1rem;
  border-bottom: 1px solid var(--border);
}
.guide-steps > li:last-child { border-bottom: 0; margin-bottom: 0; padding-bottom: 0; }
.guide-steps > li::before {
  content: counter(step);
  position: absolute; left: 0; top: .1rem;
  width: 1.7rem; height: 1.7rem; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: .8rem; font-weight: 600;
  background: var(--surface); color: var(--muted); border: 1px solid var(--border);
}
.guide-steps > li.is-done::before {
  content: "\2713"; background: var(--success); border-color: var(--success); color: var(--on-dark);
}
.guide-steps h3 { margin: 0 0 .4rem; font-size: .92rem; color: var(--heading); }
.guide-steps p { margin: 0 0 .5rem; font-size: .88rem; }
.guide-state { color: var(--muted); font-size: .8rem !important; }
.guide-warn { color: var(--warning-ink-strong); font-size: .82rem !important; }

.guide-warn-inline { color: var(--warning-ink-strong); }
.guide-lede { font-size: .92rem; max-width: 68ch; }

/* The reel ------------------------------------------------------------------
   A walkthrough that plays itself: five frames of one time entry becoming a
   line in a report. Built from elements and keyframes rather than an embedded
   video, because nothing on this platform may fetch anything — a file that
   cannot load leaves a hole where the explanation was.

   `grid-area: 1 / 1` stacks every frame in the same cell, so the screen is as
   tall as its tallest frame with no measured height to keep in step. */
.reel {
  margin-top: 1.1rem; padding: 1rem 1.1rem .85rem;
  border: 1px solid var(--border); border-radius: calc(var(--radius) * 1.6);
  background:
    radial-gradient(110% 130% at 0% 0%, rgba(47, 111, 237, .07), transparent 58%),
    var(--surface);
}
.reel-frames { display: grid; list-style: none; margin: 0 0 .8rem; padding: 0; }
.reel-frame {
  grid-area: 1 / 1; opacity: 0;
  animation: reel-frame 21s cubic-bezier(.4, 0, .2, 1) infinite;
}
.reel-cap {
  display: flex; align-items: center; gap: .5rem;
  margin: 0 0 .65rem; font-size: .9rem; font-weight: 600; color: var(--heading);
}
.reel-no {
  flex: 0 0 auto; width: 1.45rem; height: 1.45rem; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--primary); color: var(--on-dark); font-size: .72rem;
}
.reel-say { margin: 0; font-size: .83rem; color: var(--text); max-width: 64ch; }

/* What the frame shows: a card or two, and what moves between them. */
.reel-art { display: flex; align-items: center; gap: .9rem; flex-wrap: wrap; margin: 0 0 .75rem; }
.art-card {
  display: flex; flex-direction: column; gap: .18rem;
  min-width: 14rem; max-width: 25rem; padding: .6rem .75rem;
  background: var(--card-bg); border: 1px solid var(--border);
  border-left: 3px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow);
}
/* Which system a card belongs to, by colour rather than by a logo nobody
   licensed us to draw. */
.art-clockify { border-left-color: var(--success); }
.art-jira { border-left-color: var(--secondary); }
.art-quiet { opacity: .72; box-shadow: none; }
.art-filled { border-color: var(--success); }
.art-src { font-size: .66rem; text-transform: uppercase; letter-spacing: .07em; color: var(--muted); }
.art-line { font-size: .85rem; color: var(--heading); }
.art-meta { font-size: .74rem; color: var(--muted); }
.art-field { font-size: .78rem; color: var(--muted); }
.art-field strong { color: var(--heading); font-weight: 600; }
.art-new {
  margin-top: .15rem; padding: .12rem .35rem; border-radius: 3px;
  background: rgba(10, 179, 156, .12); align-self: flex-start;
}
.art-key { background: rgba(247, 184, 75, .4); color: inherit; border-radius: 3px; padding: 0 .15rem; }
.art-chip {
  font-family: var(--font-mono);
  font-size: .8rem; padding: .3rem .55rem; border-radius: 999px;
  background: var(--primary); color: var(--on-dark);
}
.art-json {
  margin: 0; padding: .6rem .75rem; font-size: .76rem; line-height: 1.5;
  background: var(--card-bg); border: 1px solid var(--border);
  border-left: 3px solid var(--warning); border-radius: var(--radius);
  overflow-x: auto; max-width: 100%;
}
.art-arrow::before {
  content: "\2192"; color: var(--muted); font-size: 1.15rem;
  display: inline-block; animation: art-nudge 1.5s ease-in-out infinite;
}

/* Where you are in the loop, and how much is left. */
.reel-rail { height: 3px; border-radius: 2px; background: var(--border); overflow: hidden; }
.reel-progress {
  display: block; height: 100%; width: 100%; transform-origin: left center;
  background: linear-gradient(90deg, var(--secondary), var(--success));
  animation: reel-progress 21s linear infinite;
}
.reel-foot { display: flex; align-items: center; gap: 1rem; margin-top: .6rem; }
.reel-legend {
  display: flex; flex-wrap: wrap; gap: .2rem 1.1rem; list-style: none; margin: 0; padding: 0;
  font-size: .74rem; color: var(--muted);
}
.reel-legend li { display: flex; align-items: center; gap: .35rem; }
.reel-legend li::before {
  content: ""; width: .42rem; height: .42rem; border-radius: 50%;
  background: var(--border); animation: reel-dot 21s infinite;
}
.reel-toggle { margin-left: auto; }

.reel-frame:nth-child(1), .reel-legend li:nth-child(1)::before { animation-delay: 0s; }
.reel-frame:nth-child(2), .reel-legend li:nth-child(2)::before { animation-delay: 4.2s; }
.reel-frame:nth-child(3), .reel-legend li:nth-child(3)::before { animation-delay: 8.4s; }
.reel-frame:nth-child(4), .reel-legend li:nth-child(4)::before { animation-delay: 12.6s; }
.reel-frame:nth-child(5), .reel-legend li:nth-child(5)::before { animation-delay: 16.8s; }

.reel.is-paused .reel-frame,
.reel.is-paused .reel-progress,
.reel.is-paused .reel-legend li::before,
.reel.is-paused .art-arrow::before { animation-play-state: paused; }

@keyframes reel-frame {
  0%   { opacity: 0; transform: translate3d(0, 10px, 0); }
  3%   { opacity: 1; transform: none; }
  17%  { opacity: 1; transform: none; }
  20%  { opacity: 0; transform: translate3d(0, -8px, 0); }
  100% { opacity: 0; transform: translate3d(0, 10px, 0); }
}
@keyframes reel-progress { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes reel-dot {
  0%, 20%, 100% { background: var(--border); transform: scale(1); }
  3%, 17%       { background: var(--secondary); transform: scale(1.35); }
}
@keyframes art-nudge {
  0%, 100% { transform: translateX(0); }
  50%      { transform: translateX(4px); }
}

/* With motion refused the reel is not a reel: the frames stack and become the
   numbered list they always were underneath. */
@media (prefers-reduced-motion: reduce) {
  .reel-frames { gap: 1.2rem; }
  .reel-frame { grid-area: auto; opacity: 1; animation: none; transform: none; }
  .reel-rail, .reel-foot { display: none; }
  .art-arrow::before { animation: none; }
}


/* Mappings ------------------------------------------------------------------
   One row per source object, its target in a dropdown. No second panel and
   nothing to drag: the question is "where does this one go?", and the answer
   sits on the same line as the question. */
/* Which two instances this page is configuring. ONE LINE — it is context, and
   context should not outweigh the thing it frames. The label sits beside its
   dropdown rather than above it, and the dropdown is sized to its content:
   the global `width: 100%` on form controls is what made this fill the screen. */
/* A page you work down, not a screen you have to already know. Each card is a
   step, numbered where the number is the point — "choose the systems" before
   "link them" before "say where the client comes from". */
.map-step .card-head h2 { display: flex; align-items: center; gap: .5rem; }
.step-no {
  flex: 0 0 auto; width: 1.5rem; height: 1.5rem; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--primary); color: var(--on-dark); font-size: .75rem; font-weight: 600;
}
/* Room to the width of the card, and a measure that does not break a sentence
   into three ragged lines halfway across an empty row. */
.map-step-lede {
  margin: .8rem .9rem .4rem; max-width: 100ch; line-height: 1.6; text-wrap: pretty;
}
.map-step-next { margin: .7rem 0 0; }

/* The one decision that is repeated on every row, explained once. Quiet ground
   so it reads as a note beside the table rather than another control. */
.map-step-aside {
  margin: .8rem .9rem; padding: .7rem .85rem;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
}
.map-step-aside p { margin: 0; }
.map-step-title { display: flex; align-items: center; gap: .5rem; margin: 0 0 .35rem !important; color: var(--heading); }

/* What the instance beside it actually holds. */
.map-instance-state { font-size: .78rem; color: var(--muted); }
.map-instance-state.is-empty { color: var(--danger); }

.map-instances {
  display: flex; align-items: center; gap: .45rem; flex-wrap: wrap;
  margin-bottom: .9rem; font-size: .82rem;
}
.map-instance { display: flex; align-items: center; gap: .35rem; }
.map-instance label { margin: 0; font-size: .82rem; color: var(--muted); font-weight: 500; }
.map-instance select {
  width: auto; min-width: 9rem; max-width: 15rem;
  padding: .25rem 1.6rem .25rem .5rem; font-size: .82rem;
}
.map-instance-none { font-size: .82rem; color: var(--danger); }
.map-instances-arrow { color: var(--muted); }
.map-tools {
  display: flex; align-items: center; gap: 1rem;
  padding: .7rem .9rem; border-bottom: 1px solid var(--border);
}
/* The search box: an icon inside the field rather than a label beside it, so
   the toolbar reads as one control instead of three. */
.map-search { position: relative; display: flex; align-items: center; min-width: 18rem; }
.map-search svg { position: absolute; left: .6rem; width: 14px; height: 14px; color: var(--muted); pointer-events: none; }
.map-search input { width: 100%; padding-left: 2rem; }
.map-search input::-webkit-search-cancel-button { cursor: pointer; }

/* The one filter people reach for, shaped like something you press. */
.map-only {
  display: inline-flex; align-items: center; gap: .45rem; cursor: pointer;
  padding: .3rem .7rem; border-radius: 999px; font-size: .8rem;
  color: var(--muted); background: var(--surface); border: 1px solid var(--border);
  transition: background .14s ease, color .14s ease, border-color .14s ease;
}
.map-only:hover { color: var(--text); background: var(--surface-hover); }
.map-only input { margin: 0; }
.map-only.is-on { color: var(--primary); border-color: var(--secondary); background: rgba(47, 111, 237, .07); font-weight: 600; }
.map-clear { margin-left: .2rem; }
/* How many the typing matched, beside the box that did the matching. */
.map-live { white-space: nowrap; }

/* A link whose far side is no longer collected. Loud enough to be read before
   the row is touched, because touching it is what used to destroy the link. */
.map-gone { color: var(--warning-ink-strong); margin-top: .25rem; max-width: 26rem; }
.map-table td { vertical-align: middle; }
.map-table .map-id { margin-top: .1rem; }
.map-table select { width: 100%; max-width: 26rem; }
.map-set { margin: 0; display: flex; gap: .5rem; align-items: center; }
/* The arrow is a reading aid, not data: narrow, quiet, and gone on a phone
   where the two cells stack anyway. */
.map-arrow-col { width: 2rem; text-align: center; color: var(--muted); }
/* Unlinked rows are the work still to do, so they are the ones that stand out
   — the opposite of marking what is already done. */
.map-table tr.is-unlinked td:first-child { box-shadow: inset 3px 0 0 var(--field-border); }
@media (max-width: 720px) {
  .map-arrow-col { display: none; }
  .map-table select { max-width: none; }
}

/* Saved where the change was made, then gone. The alternative — a banner at the
   top of a scrolled page — reports success somewhere you are not looking. */
.map-saved {
  font-size: .78rem; font-weight: 600; color: var(--success);
  opacity: 0; transition: opacity .25s ease;
}
.map-saved.is-on { opacity: 1; }
.alert.is-going { opacity: 0; transition: opacity .35s ease; }

/* The rule: one answer, asked once. */
.rule-explain { max-width: 46rem; margin: 0 0 1rem; }
.rule-form .field { margin-bottom: .5rem; }
.rule-arrow { font-size: 1.2rem; color: var(--muted); margin: .1rem 0 .5rem .2rem; }

@media (prefers-reduced-motion: reduce) {
  .map-saved, .alert.is-going { transition: none; }
}

.warn-text { color: var(--warning-ink); font-weight: 600; }
.rule-target-state {
  margin-top: 1.1rem; padding-top: .9rem; border-top: 1px solid var(--border);
  grid-template-columns: auto 1fr; gap: .25rem .9rem; max-width: 34rem;
}
.rule-target-state dt, .rule-target-state dd { font-size: .82rem; }

.rule-new { margin-top: 1.1rem; padding-top: .9rem; border-top: 1px solid var(--border); }
.rule-new > summary { cursor: pointer; font-size: .85rem; font-weight: 600; color: var(--primary); }
.rule-new-form { margin-top: .7rem; }
.rule-new-form p { margin: 0 0 .6rem; max-width: 40rem; }


/* Danger zone -----------------------------------------------------------------
   Deliberately unlike every other control on the page. Closed it is a quiet
   hairline; opened it is unmistakably a different kind of place — hatched
   ground, a red rule down the side and a button that stays dead until the name
   has been typed. Nobody arrives here by accident, and nobody stays by accident
   either. */
.danger-zone {
  --danger-ink: #b02a37;
  margin-top: 1.4rem;
  border: 1px solid rgba(176, 42, 55, .35);
  border-left: 3px solid var(--danger-ink);
  border-radius: var(--radius);
  background: var(--danger-wash);
  overflow: hidden;
}
.danger-zone > summary {
  display: flex; align-items: center; gap: .6rem;
  padding: .7rem .9rem; cursor: pointer;
  color: var(--danger-ink); list-style: none;
}
.danger-zone > summary::-webkit-details-marker { display: none; }
.danger-zone > summary:hover { background: rgba(176, 42, 55, .05); }
.danger-zone > summary strong { font-size: .88rem; letter-spacing: .01em; }
.danger-zone-sub { display: block; font-size: .76rem; color: var(--danger-ink-soft); font-weight: 400; }

/* A drawn mark rather than an emoji: it inherits the ink and cannot arrive as a
   tofu box on a machine without the font. */
.danger-zone-mark {
  flex: 0 0 auto; width: 1.35rem; height: 1.35rem; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--danger-ink); color: var(--on-dark);
  font-size: .82rem; font-weight: 700; line-height: 1;
}

.danger-zone-body {
  padding: .2rem .9rem 1rem;
  border-top: 1px solid rgba(176, 42, 55, .2);
  /* Hatching says "works area" before a single word is read. */
  background-image: repeating-linear-gradient(
    45deg, rgba(176, 42, 55, .045) 0 6px, transparent 6px 12px);
}
.danger-zone-warning { max-width: 44rem; margin: .9rem 0 1rem; font-size: .85rem; }
.danger-zone-form { max-width: 30rem; }
.danger-zone-form .field { margin-bottom: .7rem; }
.danger-zone-echo {
  font-family: var(--font-mono);
  background: rgba(176, 42, 55, .1); color: var(--danger-ink);
  padding: 0 .25rem; border-radius: 3px;
}
.btn-danger[disabled] { opacity: .45; cursor: not-allowed; }
/* Armed: the button stops looking like a suggestion. */
.btn-danger:not([disabled]) { box-shadow: 0 0 0 3px rgba(176, 42, 55, .18); }

@media (prefers-reduced-motion: reduce) {
  .danger-zone > summary { transition: none; }
}

/* Statistics ------------------------------------------------------------------
   Totals first, then the breakdown: the tiles say whether the table below is
   the whole story or only the part that could be matched. */
.stats-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: .7rem; }
.stats-tiles .tile {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: .8rem .9rem;
}
.stats-tiles .tile-value { display: block; font-size: 1.5rem; font-weight: 600; color: var(--heading); font-variant-numeric: tabular-nums; }
.stats-tiles .tile-label { display: block; font-size: .76rem; color: var(--muted); margin-top: .15rem; }
/* A count that is not zero is work to do, not decoration. */
.stats-tiles .tile.is-warn { border-color: rgba(184, 134, 11, .45); background: rgba(184, 134, 11, .05); }
.stats-tiles .tile.is-warn .tile-value { color: var(--warning-ink); }

/* Master switch ---------------------------------------------------------------
   Quiet when the platform is running, unmissable when it is not: a stopped
   integration that looks like a normal card is how a suspension survives a
   weekend. */
.master-switch { margin-bottom: 1rem; }
.master-switch .card-body { display: flex; align-items: center; gap: 1rem; }
.master-switch-state { display: flex; align-items: center; gap: .7rem; }
.master-switch-dot {
  width: .7rem; height: .7rem; border-radius: 50%;
  background: var(--success); box-shadow: 0 0 0 3px rgba(10, 179, 156, .18);
}
.master-switch.is-suspended {
  border-color: rgba(220, 53, 69, .45);
  background: rgba(220, 53, 69, .04);
}
.master-switch.is-suspended .master-switch-dot {
  background: var(--danger); box-shadow: 0 0 0 3px rgba(220, 53, 69, .18);
}

/* Statistics charts ------------------------------------------------------------
   Drawn inline: no chart library, no external request, no JavaScript, and it
   prints. */
.stats-top { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr); gap: .8rem; }
.stats-chart { margin: 0; }
.donut-wrap { display: flex; align-items: center; gap: 1.2rem; flex-wrap: wrap; }
.donut { width: 140px; height: 140px; flex: 0 0 auto; }
.donut-rest { fill: none; stroke: var(--field-border); stroke-width: 14; }
.donut-done { fill: none; stroke: var(--secondary); stroke-width: 14; stroke-linecap: butt; }
.donut-value { text-anchor: middle; font-size: 26px; font-weight: 600; fill: var(--heading); }
.donut-caption { text-anchor: middle; font-size: 11px; fill: var(--muted); }
.donut-legend { display: grid; grid-template-columns: auto 1fr; gap: .25rem .8rem; margin: 0; font-size: .8rem; }
.donut-legend dt { color: var(--muted); display: flex; align-items: center; gap: .4rem; }
.donut-legend dd { margin: 0; color: var(--heading); font-variant-numeric: tabular-nums; }
.donut-legend .key { width: .65rem; height: .65rem; border-radius: 2px; display: inline-block; }
.donut-legend .key-done { background: var(--secondary); }
.donut-legend .key-rest { background: var(--field-border); }

.bar-row { display: grid; grid-template-columns: minmax(6rem, 12rem) 1fr auto; gap: .6rem; align-items: center; margin-bottom: .45rem; font-size: .82rem; }
.bar-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--heading); }
.bar-track { background: var(--field-border); border-radius: 3px; height: .55rem; overflow: hidden; }
.bar-fill { display: block; height: 100%; background: var(--secondary); border-radius: 3px; }
/* Time with no client resolved: present, counted, and visibly not a client. */
.bar-fill.is-none { background: var(--border); }
.bar-value { font-variant-numeric: tabular-nums; color: var(--muted); }

th.sortable a { color: inherit; text-decoration: none; }
th.sortable a:hover { color: var(--secondary); }
th.sortable.is-sorted { color: var(--secondary); }
/* A row that opens something says so before it is clicked: the ground shifts,
   and a rail runs down the leading edge so the eye can tell at a glance which
   rows in a mixed table are live. */
tr.is-clickable { cursor: pointer; transition: background .14s ease, box-shadow .14s ease; }
tr.is-clickable:hover { background: var(--surface-hover); box-shadow: inset 3px 0 0 var(--secondary); }
tr.is-clickable:hover td:first-child strong { color: var(--secondary); }
@media (prefers-reduced-motion: reduce) {
  tr.is-clickable { transition: none; }
}

@media (max-width: 900px) { .stats-top { grid-template-columns: 1fr; } }

.person-modal { width: min(46rem, calc(100vw - 2rem)); }
.person-window { margin: 0 0 .8rem; }
.person-clients { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: .9rem; }
.person-client {
  font-size: .78rem; padding: .2rem .5rem; border-radius: var(--radius);
  background: var(--surface); border: 1px solid var(--border); color: var(--muted);
}
.person-client strong { color: var(--heading); margin-left: .3rem; font-variant-numeric: tabular-nums; }
.person-table { max-height: 55vh; overflow: auto; }

/* A switch, not a button ------------------------------------------------------
   A button says what will happen; a slider says what IS. For a state somebody
   glances at from across a room, the second is the useful one. */
.switch-form { display: flex; align-items: center; gap: .6rem; }
.switch { display: inline-flex; align-items: center; gap: .55rem; cursor: pointer; user-select: none; }
.switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.switch-track {
  position: relative; width: 2.6rem; height: 1.45rem; border-radius: 1rem;
  background: var(--danger); transition: background .18s ease;
  flex: 0 0 auto;
}
.switch-knob {
  position: absolute; top: .175rem; left: .175rem;
  width: 1.1rem; height: 1.1rem; border-radius: 50%;
  background: var(--card-bg); box-shadow: 0 1px 2px rgba(0,0,0,.25);
  transition: transform .18s ease;
}
.switch input:checked + .switch-track { background: var(--success); }
.switch input:checked + .switch-track .switch-knob { transform: translateX(1.15rem); }
/* Keyboard focus has to be visible: the input itself is invisible, so the ring
   goes on the thing you can actually see. */
.switch input:focus-visible + .switch-track { box-shadow: 0 0 0 3px rgba(47, 111, 237, .35); }
.switch-text { font-size: .82rem; font-weight: 600; color: var(--heading); min-width: 4rem; }

/* The settings variant. The platform switch on Overview is red when off because
   off there means the integration is stopped; a setting that is off is not a
   fault, so this one rests on the border colour and goes to the accent. */
.switch-setting { align-items: flex-start; gap: .6rem; }
.switch-setting .switch-track {
  background: var(--border); margin-top: .1rem;
  /* Sunk while off, so the knob reads as sitting IN the track rather than on
     top of a grey pill. */
  box-shadow: inset 0 1px 2px rgba(15, 35, 63, .18);
}
.switch-setting input:checked + .switch-track {
  background: var(--secondary);
  box-shadow: inset 0 1px 2px rgba(15, 35, 63, .1), 0 0 0 3px rgba(47, 111, 237, .12);
}
.switch-setting:hover .switch-track { box-shadow: inset 0 1px 2px rgba(15, 35, 63, .18), 0 0 0 4px rgba(47, 111, 237, .1); }
.switch-setting .switch-knob {
  box-shadow: 0 1px 3px rgba(15, 35, 63, .35);
  /* Overshoots a hair and settles: the whole point of a slider over a tick is
     that the state visibly MOVES. */
  transition: transform .2s cubic-bezier(.34, 1.56, .64, 1);
}
.switch-setting:active .switch-knob { width: 1.25rem; border-radius: .7rem; }
.switch-setting .switch-label { font-weight: 500; line-height: 1.45; }
@media (prefers-reduced-motion: reduce) {
  .switch-track, .switch-knob { transition: none; }
}
/* Disabled has to look unavailable, or a slider that cannot move reads as one
   that is simply stuck. */
.switch-setting input:disabled + .switch-track { opacity: .5; }
.switch-setting:has(input:disabled) { cursor: not-allowed; color: var(--muted); }

/* A floating confirmation ------------------------------------------------------
   Fixed, so the page does not jump under the pointer at the moment you look at
   what you just clicked. Success only — errors stay in the flow, where they can
   be read at leisure and cannot vanish. */
.toast {
  position: fixed; right: 1.1rem; bottom: 1.1rem; z-index: 60;
  max-width: 26rem; padding: .7rem .95rem;
  border-radius: var(--radius); font-size: .85rem;
  background: var(--success-ink-strong); color: var(--on-dark);
  box-shadow: 0 6px 20px rgba(15, 35, 63, .22);
  animation: toast-in .22s ease-out;
}
.toast.is-going { opacity: 0; transition: opacity .35s ease; }
@keyframes toast-in { from { opacity: 0; transform: translateY(.5rem); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
  .toast { animation: none; }
  .switch-track, .switch-knob { transition: none; }
}

/* Form controls, dressed ------------------------------------------------------
   A native select draws a different arrow on every platform and none of them
   match the rest of this console. One chevron, one focus ring, and the control
   stops looking like a browser default dropped into a page. */
select {
  appearance: none; -webkit-appearance: none;
  padding-right: 1.9rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' fill='none' stroke='%236b7a90' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right .6rem center;
  background-size: .7rem;
  cursor: pointer;
}
select:hover { border-color: var(--border-strong); }
input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: none; border-color: var(--secondary);
  box-shadow: 0 0 0 3px rgba(47, 111, 237, .16);
}

/* The filter bar reads as one row of choices, not a pile of boxes. */
/* Filter bar ------------------------------------------------------------------
   The period control, wherever it appears — the report and the export ask the
   same question and must not answer it in two different shapes.

   `flex-end` is the whole point: a label sits above its input, so centring
   would float the buttons halfway up the taller controls and nothing would
   share a baseline. */
.filter-bar { margin-bottom: 1rem; }
/* A COLUMN of rows, not a row of everything.

   This was a wrapping flex row, which worked while the card held one line of
   controls: every field was an item and they flowed. Then the search was given
   a line of its own and became just another item in the same flow — sitting
   beside the dates, squeezed into whatever was left, and cut off mid-word in
   its own placeholder. The rows are rows; the card stacks them. */
.filter-bar .card-body {
  display: flex; flex-direction: column; align-items: stretch;
  gap: .85rem; padding: .85rem 1rem;
}
.filter-bar .field { margin: 0; }
/* One line, everything sitting on the same baseline, the search taking whatever
   room is left and the buttons pinned to the end. The controls used to be a
   plain flex row, so a field with help text under it pushed the rest out of
   line and the buttons drifted to the far edge. */
.filter-line {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: .85rem;
}
.filter-line .field.grow { flex: 1 1 12rem; min-width: 9rem; }
.filter-line .form-actions { margin-left: auto; padding-top: 0; }
.filter-line .bound-value { padding: .38rem 0; font-size: .86rem; }
/* The window presets stand in a field of their own, beside the two dates they
   are shorthand for; nudged so the buttons land on the inputs' line rather than
   on the line of their own labels. */
.filter-line .segmented { margin-top: .1rem; }

/* The search, on a line of its own under the rest.

   Beside four other controls it was whatever space was left, which was not
   enough for its own placeholder — and a box that cannot show an e-mail address
   is the wrong shape for a box you type e-mail addresses into. Wide enough for
   one comfortably, and stopped before it becomes a stripe across a large
   screen. A little taller than the fields above it, because it is the control
   on this page that gets used most. */
/* The column's own gap spaces it; a margin here would double that. */
.filter-who .field.grow { flex: 1 1 28rem; max-width: 38rem; }
.filter-who input[type="search"] {
  padding-top: .55rem; padding-bottom: .55rem; font-size: .92rem;
}
.filter-who .search-icon { width: .9rem; height: .9rem; }
/* The notes under the controls, side by side under one hairline. Each used to
   carry its own rule; stacked in a column that was two lines across the card. */
.filter-notes {
  display: flex; flex-wrap: wrap; gap: .25rem 1.6rem;
  padding-top: .7rem; border-top: 1px solid var(--border);
}
.filter-note {
  margin: 0; font-size: .78rem; color: var(--muted); max-width: 62rem;
  flex: 1 1 20rem;
}
.filter-note strong { color: var(--text); font-weight: 600; }
.filter-bar label { font-size: .7rem; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
.filter-bar input[type="date"], .filter-bar select { padding-top: .4rem; padding-bottom: .4rem; }
/* The buttons are shorter than a labelled field; nudged so their text lines up
   with the input text rather than with the box edge. */
.filter-bar .btn, .filter-bar .segmented { margin-bottom: 1px; }

/* Segmented control: one choice, and the chosen one looks it. */
.segmented { display: inline-flex; border: 1px solid var(--field-border); border-radius: var(--radius); overflow: hidden; }
.segmented a {
  padding: .38rem .7rem; font-size: .78rem; color: var(--muted);
  text-decoration: none; background: var(--surface);
  border-left: 1px solid var(--field-border);
}
.segmented a:first-child { border-left: 0; }
.segmented a:hover { background: var(--surface-hover); color: var(--text); }
.segmented a.is-on { background: var(--secondary); color: var(--on-dark); font-weight: 600; }

/* What is inside a field ------------------------------------------------------
   Beside the picker, not below it: the question "is this the right field" is
   answered by looking at the two side by side. */
.rule-form { max-width: none; }
.rule-form .field { max-width: 34rem; }
.field-values {
  margin: -.2rem 0 .9rem; padding: .6rem .75rem;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface); max-width: 34rem;
}
.field-values[data-state="loading"] { opacity: .6; }
.field-values-list {
  margin: .4rem 0 0; padding: 0; list-style: none;
  max-height: 9rem; overflow: auto;
  display: flex; flex-wrap: wrap; gap: .25rem;
}
.field-values-list li {
  font-size: .76rem; padding: .1rem .4rem;
  background: var(--card-bg); border: 1px solid var(--field-border);
  border-radius: 3px; color: var(--text);
}

/* The same values, inside the rules table. Shut by default: the table answers
   "which rules exist", and the contents of a field are the follow-up question,
   not the row. */
.values-peek > summary { cursor: pointer; color: var(--muted); font-size: .78rem; }
.values-peek > summary:hover { color: var(--primary); }
.values-peek[open] > summary { color: var(--primary); margin-bottom: .35rem; }
.values-peek-list {
  margin: 0; padding: 0; list-style: none;
  max-height: 11rem; overflow: auto;
  display: flex; flex-wrap: wrap; gap: .25rem;
}
.values-peek-list li {
  font-size: .76rem; padding: .1rem .4rem;
  background: var(--card-bg); border: 1px solid var(--field-border);
  border-radius: 3px; color: var(--text);
}
.values-peek p { margin: .35rem 0 0; font-size: .76rem; }
.values-peek-age { border-top: 1px dashed var(--border); padding-top: .3rem; }
/* A button that reads as a link: it does not submit anything and it sits inside
   a sentence, so a button's box would be the wrong promise. */
.btn-link {
  border: 0; background: none; padding: 0; margin-left: .4rem;
  font: inherit; color: var(--primary); cursor: pointer; text-decoration: underline;
}
.btn-link:hover { text-decoration: none; }
.btn-link[disabled] { color: var(--muted); cursor: default; text-decoration: none; }

/* Two ways in, and a line that says they are alternatives rather than steps. */
.login-or {
  display: flex; align-items: center; gap: .7rem;
  margin: 1rem 0 .8rem; color: var(--muted); font-size: .78rem;
}
.login-or::before, .login-or::after {
  content: ''; flex: 1; height: 1px; background: var(--border);
}

@media (min-width: 1100px) {
  /* Side by side once there is room for it. */
  .rule-form { display: grid; grid-template-columns: 34rem minmax(0, 1fr); gap: 0 1rem; align-items: start; }
  .rule-form > .field, .rule-form > .rule-arrow, .rule-form > .row { grid-column: 1; }
  .rule-form > .field-values { grid-column: 2; margin-top: 1.35rem; }
}

/* Changing a field's state ----------------------------------------------------
   Radios, not a dropdown: three states with different consequences should be
   readable at once, with the consequence next to the label rather than in a
   help text nobody opens. */
.rule-manage { margin-top: 1.1rem; padding-top: .9rem; border-top: 1px solid var(--border); }
.rule-manage > summary { cursor: pointer; font-size: .85rem; font-weight: 600; color: var(--primary); }
.state-radios { border: 0; margin: .8rem 0; padding: 0; display: grid; gap: .4rem; max-width: 34rem; }
.state-radios legend {
  padding: 0; font-size: .7rem; text-transform: uppercase;
  letter-spacing: .04em; color: var(--muted);
}
.state-radio {
  display: flex; gap: .6rem; align-items: flex-start;
  padding: .55rem .7rem; border: 1px solid var(--field-border);
  border-radius: var(--radius); background: var(--surface); cursor: pointer;
}
.state-radio:hover { background: var(--surface-hover); }
.state-radio input { margin: .15rem 0 0; flex: 0 0 auto; }
.state-radio strong { display: block; font-size: .84rem; color: var(--heading); }
.state-radio .small { display: block; line-height: 1.35; }
.state-radio:has(input:checked) { border-color: var(--secondary); box-shadow: 0 0 0 2px rgba(47,111,237,.12); }
/* The one that breaks the integration says so before it is chosen. */
.state-radio.is-risky:has(input:checked) { border-color: var(--danger); box-shadow: 0 0 0 2px rgba(220,53,69,.14); }

.rules-table td { vertical-align: middle; }
.rule-add-title {
  margin: 1.2rem 0 .7rem; font-size: .8rem; text-transform: uppercase;
  letter-spacing: .04em; color: var(--muted);
}

/* The few fields that must be filled. Marked instead of marking the many that
   need not be: a word on every label is not information. */
.req { margin-left: .4rem; font-size: .68rem; letter-spacing: .04em; text-transform: uppercase;
  color: var(--danger); font-weight: 600; }

/* The thing a list is chosen inside — a project, above its tasks. */
.map-parent { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  padding: .7rem 1.1rem; border-bottom: 1px solid var(--border); }
.map-parent label { font-size: .78rem; letter-spacing: .04em; text-transform: uppercase;
  color: var(--muted); }
.map-parent select { max-width: 26rem; }
.map-parent .small { flex: 1 1 18rem; }

/* A list kept by hand ---------------------------------------------------------
   Avanti has no API here, so its projects and work types are typed in. Plain
   rows rather than a table: there is one column of information. */
.hand-list { list-style: none; margin: .6rem 0 .9rem; padding: 0;
  border: 1px solid var(--border); border-radius: var(--radius); }
.hand-list li { display: flex; align-items: center; gap: .6rem;
  padding: .45rem .7rem; border-bottom: 1px solid var(--border); font-size: .86rem; }
.hand-list li:last-child { border-bottom: 0; }
.hand-list li > span { flex: 1 1 auto; color: var(--heading); }

/* One list at a time, full width — the id column needs the room. */
.hand-lists > .card { margin-bottom: 0; }
.tabs .tab .badge { margin-left: .4rem; }

/* A row is one form: id, then name, then what points at it, then the button.
   Laid out on the same grid as the Add row underneath so the id boxes line up
   in a column instead of stepping across the card. */
.hand-row { display: flex; align-items: center; gap: .6rem; flex: 1 1 auto; margin: 0; }
.hand-row .hand-id { flex: 0 1 20rem; min-width: 10rem; font-size: .82rem; }
.hand-row .hand-name { flex: 1 1 auto; color: var(--heading); }
.hand-row .badge { flex: 0 0 auto; }
.hand-row .btn { flex: 0 0 auto; }

.hand-add { gap: .6rem; }
.hand-add input[type="text"] { flex: 1 1 14rem; max-width: 34rem; }
/* The id box matches the one on the rows above, so the two columns read as one. */
.hand-add input[name="external_id"] { flex: 0 1 20rem; font-family: var(--font-mono); font-size: .82rem; }

/* Narrow: let a row wrap rather than squeeze the id into six characters. */
@media (max-width: 60rem) {
  .hand-list li { flex-wrap: wrap; }
  .hand-row { flex-wrap: wrap; }
  .hand-row .hand-id { flex: 1 1 100%; }
}

/* Present for a screen reader, absent for everyone else. */
/* The absence screen ----------------------------------------------------------
   Between the filter bar and the table sits one line: what this was matched
   against, and how fresh that is. The window presets moved up into the bar,
   beside the dates they stand for, and the row of totals went when the chart
   below started counting the same days. */
/* Something happening, while the page is being fetched.

   This screen asks BambooHR for a year of absences, which is seconds rather
   than milliseconds, and a click that produces nothing visible for that long is
   read as a click that did not register — so it gets clicked again. A hairline
   across the top costs nothing and answers it.

   Fixed to the viewport rather than to the page: the answer must be where the
   eye is, and on a long table the eye is not at the top. */
.page-busy {
  position: fixed; inset: 0 0 auto; height: 2px; z-index: 60;
  background: var(--border); overflow: hidden;
}
.page-busy::before {
  content: ""; position: absolute; inset: 0; width: 40%;
  background: var(--secondary); animation: page-busy-slide 1.1s ease-in-out infinite;
}
@keyframes page-busy-slide {
  0% { transform: translateX(-100%); }
  100% { transform: translateX(350%); }
}
/* And beside the control that was touched, because that is where the doubt is.
   Not disabled: a form whose fields go dead reads as broken rather than busy. */
.is-busy { opacity: .65; transition: opacity .12s ease; pointer-events: none; }

@media (prefers-reduced-motion: reduce) {
  .page-busy::before { animation: none; width: 100%; opacity: .5; }
}

/* What the answer was worked out from. Quiet — it is context, not a finding —
   but always present, because "not in this workspace" means nothing without
   which workspace and how many people were in it. */
.matched-against {
  display: grid; gap: .5rem; padding: .8rem 1rem;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  font-size: .82rem; color: var(--text);
}
.matched-against .lbl {
  display: inline-block; min-width: 8.5rem;
  font-size: .72rem; font-weight: 600; text-transform: uppercase; letter-spacing: .04em;
  color: var(--muted);
}

/* A value that is set elsewhere and shown here. It reads as a field so the row
   lines up, and as text so nobody tries to change it on this page. */
.bound-value {
  display: flex; align-items: center; gap: .45rem;
  padding: .5rem 0; font-size: .86rem; font-weight: 600;
}

/* A kind of absence nothing is recorded for. Said on the tab, quietly, rather
   than left off the page: leave that is deliberately passed over still has to
   be visible, or the decision looks like an oversight. */
.tabs .tab .tab-off {
  margin-left: .35rem; font-size: .68rem; text-transform: uppercase; letter-spacing: .04em;
  color: var(--muted);
}
table.tbl tbody tr.is-ignored td:first-child { box-shadow: inset .18rem 0 0 var(--border); }
.table-note { padding: .6rem .9rem 0; }
.row-actions-col { width: 1%; white-space: nowrap; text-align: right; }
/* The row's own button is always there. It used to fade in on hover, which
   made it tidy and undiscoverable: somebody who does not already know a row
   can be re-synced has no reason to move the pointer over one to find out. */
table.tbl tbody tr .row-actions-col .btn { transition: border-color .12s ease; }

/* A button waiting on something far away. The ring turns, so a row that says
   "Writing..." is visibly still doing it rather than stuck on a word. */
.btn[data-busy]::before {
  content: ""; display: inline-block; width: .62em; height: .62em;
  margin-right: .4em; vertical-align: -.06em; border-radius: 50%;
  border: 2px solid currentColor; border-right-color: transparent;
  animation: btn-busy-spin .7s linear infinite;
}
@keyframes btn-busy-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .btn[data-busy]::before { animation: none; border-right-color: currentColor; opacity: .5; }
}
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

/* The export payload, shown as it is sent. Scrolls inside itself: the page must
   never scroll sideways because one task name is long. */
.payload {
  margin: 0; padding: 1.1rem; max-height: 32rem; overflow: auto;
  font-family: var(--font-mono);
  font-size: .78rem; line-height: 1.5; color: var(--heading);
  background: var(--surface); white-space: pre;
}


/* Paging under a mapping table. The count sits beside the links so "50 of 229"
   answers the question the links raise. */
.map-pages { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;
  padding: .7rem 1.1rem; border-top: 1px solid var(--border); }
.map-pages .pagination { margin: 0 0 0 auto; }

/* Adding another tenant to keep a hand-written list under. Shut by default: it
   is the answer to a question most visits do not ask. */
.hand-new-instance { margin: 0 0 1rem; }
.hand-new-instance > summary { cursor: pointer; font-size: .85rem; font-weight: 600; color: var(--primary); }
.hand-new-instance p { margin: .5rem 0 .6rem; max-width: 52rem; }

/* The export payload, one line at a time -------------------------------------
   A month is thousands of entries. Shut by default and summarised, because the
   question the page answers is "does this look right", not "what are all the
   bytes" — those are one disclosure and one download away. */
.payload-line { border-bottom: 1px solid var(--border); }
.payload-line:last-of-type { border-bottom: 0; }
.payload-line > summary {
  display: grid; grid-template-columns: minmax(12rem, 1.4fr) minmax(10rem, 1fr) auto auto;
  align-items: center; gap: .8rem; padding: .7rem 1.1rem; cursor: pointer;
}
.payload-line > summary:hover { background: var(--surface-hover); }
.payload-line[open] > summary { background: var(--surface); }
.payload-who { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
.payload-who strong { color: var(--heading); }
.payload-who .mono { overflow: hidden; text-overflow: ellipsis; }
.payload-what { display: flex; flex-wrap: wrap; gap: .3rem; }
.payload-sum { text-align: right; font-size: .8rem; color: var(--muted); white-space: nowrap; }
.payload-sum strong { display: block; font-size: .95rem; color: var(--heading); font-variant-numeric: tabular-nums; }
.payload-line .table-wrap { border-top: 1px solid var(--border); max-height: 22rem; overflow: auto; }
.payload-more { margin: 0; padding: .5rem 1.1rem; border-top: 1px solid var(--border); }

/* The raw document, as a card that opens. */
.payload-raw > summary { cursor: pointer; list-style: none; }
.payload-raw > summary::-webkit-details-marker { display: none; }
.payload-raw > summary h2::before { content: '▸ '; color: var(--muted); }
.payload-raw[open] > summary h2::before { content: '▾ '; }

@media (max-width: 900px) {
  .payload-line > summary { grid-template-columns: 1fr; }
  .payload-sum { text-align: left; }
}

/* The feed's parameters. A list, because each one needs a sentence and a run-on
   paragraph is where "where do I get the id" goes unanswered. */
.feed-params { margin: .35rem 0 0; padding-left: 1.1rem; }
.feed-params li + li { margin-top: .3rem; }

/* Feed URL builder -----------------------------------------------------------
   Two questions and a button, on the baseline like every other filter here. */
.feed-builder {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: .85rem;
  padding: .8rem .9rem; margin: 0 0 1rem;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
}
.feed-builder .field { margin: 0; }
.feed-builder > form.field { flex: 0 1 16rem; }
.feed-builder label { font-size: .7rem; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
.feed-builder select { padding-top: .4rem; padding-bottom: .4rem; }
.feed-builder .btn { margin-bottom: 1px; }
/* Its own line, full width: as a child of a field it stretched that field and
   pushed everything else off the baseline. */
.feed-builder-help { flex-basis: 100%; margin: 0; }

/* The feed token, beside the thing it authenticates. Its state and its two
   actions on one line: issuing and revoking are the same decision seen from
   either side. */
.feed-token { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin-top: .5rem; }

/* Copy to the clipboard ------------------------------------------------------
   The value and its button on one line, the button hugging the top so a
   multi-line command does not push it to the middle of nowhere. The value stays
   selectable, which is the path when there is no clipboard to write to. */
.copy-row { display: flex; align-items: flex-start; gap: .5rem; }
/* min-width:0 so a long command scrolls inside itself instead of stretching the
   row — without it a flex item refuses to shrink below its content. */
.copy-row > :first-child { flex: 1 1 auto; min-width: 0; user-select: all; }
.copy-row > pre { margin: 0; overflow-x: auto; }

/* The button is an icon, sized to the line it sits on. */
.copy-btn {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 1.9rem; height: 1.9rem; padding: 0; cursor: pointer;
  color: var(--muted); background: var(--card-bg);
  border: 1px solid var(--field-border); border-radius: var(--radius);
  transition: color .12s ease, border-color .12s ease, background .12s ease;
}
.copy-btn:hover { color: var(--primary); border-color: var(--border-strong); background: var(--surface-hover); }
.copy-btn:focus-visible { outline: 2px solid var(--info); outline-offset: 2px; }
.copy-btn svg { width: 15px; height: 15px; }
/* Two icons, one shown. Swapping the class rather than the contents keeps the
   markup — and the accessible name — intact across a click. */
.copy-btn .copy-tick { display: none; }
.copy-btn.is-done { color: var(--on-dark); background: var(--success); border-color: var(--success); }
.copy-btn.is-done .copy-icon { display: none; }
.copy-btn.is-done .copy-tick { display: block; }
.copy-btn.is-refused { color: var(--danger); border-color: var(--danger); }

/* "Copied", beside the button for a moment. The tick alone is easy to miss on a
   page this dense, and a word says what happened without being read twice. */
.copy-btn { position: relative; }
.copy-btn::after {
  content: 'Copied'; position: absolute; right: calc(100% + .4rem); top: 50%;
  transform: translateY(-50%);
  padding: .1rem .35rem; border-radius: 3px;
  background: var(--success); color: var(--on-dark);
  font-size: .7rem; font-weight: 600; letter-spacing: .02em; white-space: nowrap;
  opacity: 0; pointer-events: none; transition: opacity .12s ease;
}
.copy-btn.is-done::after { opacity: 1; }
.copy-btn.is-refused::after { content: 'Press Ctrl+C'; background: var(--danger); opacity: 1; }

/* The label above the sample command. */
.feed-sample { font-size: .78rem; font-weight: 600; text-transform: uppercase;
  letter-spacing: .04em; color: var(--muted); margin: 1.1rem 0 .4rem; }

/* The metric catalogue. One row per metric, shut: the name and what it means
   answer "which one do I alert on", and the samples are the follow-up. */
.metric { border-bottom: 1px solid var(--border); }
.metric:last-of-type { border-bottom: 0; }
.metric > summary {
  display: grid; grid-template-columns: minmax(14rem, auto) auto minmax(0, 1fr) auto;
  align-items: baseline; gap: .6rem; padding: .6rem 1.1rem; cursor: pointer;
}
.metric > summary:hover { background: var(--surface-hover); }
.metric[open] > summary { background: var(--surface); }
.metric-name { font-weight: 600; color: var(--heading); }
.metric-help { color: var(--muted); font-size: .8rem; }
.metric > pre { margin: 0; border-top: 1px solid var(--border); max-height: 14rem; }
.metric-empty { margin: 0; padding: .5rem 1.1rem; border-top: 1px solid var(--border); }

@media (max-width: 900px) {
  .metric > summary { grid-template-columns: 1fr; }
}

/* The private ranges. Two columns of short facts, so the CIDRs line up and the
   eye can run down them looking for its own. */
/* Nine addresses read as one fact, so they are set as one line of chips rather
   than a two-column glossary that took a third of the card. */
.range-list {
  margin: .5rem 0; padding: 0; list-style: none;
  display: flex; flex-wrap: wrap; gap: .35rem;
}
.range-list li {
  display: inline-flex; align-items: baseline; gap: .35rem;
  padding: .12rem .45rem; border: 1px solid var(--field-border); border-radius: var(--radius);
  background: var(--surface); font-size: .74rem;
}
.range-list .mono { font-size: .74rem; }
.range-list .muted { font-size: .68rem; }

/* ── On paper ──────────────────────────────────────────────────────────────
   The export page is already a report; printing it is how it becomes a PDF, so
   the print rules are the report's design rather than an afterthought.

   What goes: the chrome (nothing to navigate on paper), every control (nothing
   to press), and the raw-JSON block (a machine's copy of what is printed above).
   What arrives: a title block with the workspace and the period, because a page
   with no header cannot be filed. */
.print-head { display: none; }

/* Paper is not themed: a printed page is white whatever the screen is doing,
   and these values are about ink on paper rather than about the palette. */
@media print {
  /* Ink, not screen: the dotted canvas and the card shadows cost toner and
     say nothing. */
  body { background: #fff !important; background-image: none !important; font-size: 11pt; }
  .sidebar, .topbar, .footbar, .tabs, .filter-bar, .toast, .alert,
  .btn, .segmented, .copy-btn, .payload-raw, .map-tools, .map-pages { display: none !important; }
  .content { margin: 0 !important; padding: 0 !important; }

  /* Paper cannot play. The frames stack and print as the numbered walkthrough
     they are underneath; left alone they would print on top of one another. */
  .reel { background: none !important; border: 0 !important; padding: 0 !important; }
  .reel-frames { gap: .8rem; }
  .reel-frame { grid-area: auto !important; opacity: 1 !important; animation: none !important; }
  .reel-rail, .reel-foot { display: none !important; }

  .print-head { display: block; margin: 0 0 1rem; }
  .print-head h1 { font-size: 16pt; margin: 0 0 .2rem; }
  .print-head p { margin: 0; font-size: 10pt; color: #333; }
  .print-head-when { color: #666 !important; font-size: 9pt !important; }

  .card, .tile {
    border: 1px solid #ccc !important; box-shadow: none !important;
    transform: none !important; break-inside: avoid; margin-bottom: .8rem;
  }
  .card-head { border-bottom: 1px solid #ccc; background: none !important; }

  /* Every line open: a printed disclosure that stayed shut is a row of
     information that silently did not make it onto the page. */
  details > *:not(summary) { display: block !important; }
  details > summary { list-style: none; }
  details > summary::-webkit-details-marker { display: none; }
  .payload-line, .metric { break-inside: avoid; border-bottom: 1px solid #eee; }
  .payload-line .table-wrap, .metric > pre { max-height: none !important; overflow: visible !important; }

  .tiles { display: flex !important; gap: .6rem; }
  .tile { border: 1px solid #ccc !important; flex: 1 1 auto; }

  table { break-inside: auto; }
  tr { break-inside: avoid; }
  thead { display: table-header-group; }

  /* A URL nobody can click is a URL nobody can read. */
  a { text-decoration: none; color: inherit; }

  /* Browsers drop background colours when printing, which would take the donut
     and the bars with them — they ARE the figure, not decoration behind it. */
  .donut-done, .donut-rest, .bar-fill, .bar-track, .badge {
    -webkit-print-color-adjust: exact; print-color-adjust: exact;
  }
  .stats-chart, .stats-tiles, .donut-wrap { break-inside: avoid; }
}

/* A row that opens ------------------------------------------------------------
   These are <details>, but styling the summary as a table row took the browser's
   own triangle with it, and a row that looks like a row does not invite a click.
   A chevron that turns says what will happen and then says it happened. */
.metric > summary,
.payload-line > summary { list-style: none; position: relative; padding-left: 2rem; }
.metric > summary::-webkit-details-marker,
.payload-line > summary::-webkit-details-marker { display: none; }

.metric > summary::before,
.payload-line > summary::before {
  content: ''; position: absolute; left: .9rem; top: 1.05em;
  width: .42rem; height: .42rem; margin-top: -.3rem;
  border-right: 1.6px solid var(--muted); border-bottom: 1.6px solid var(--muted);
  transform: rotate(-45deg); transform-origin: 60% 60%;
  transition: transform .14s ease, border-color .14s ease;
}
.metric > summary:hover::before,
.payload-line > summary:hover::before { border-color: var(--primary); }
.metric[open] > summary::before,
.payload-line[open] > summary::before { transform: rotate(45deg); }

/* And the words, once, on the right — a chevron is a hint, not an instruction,
   and the first time somebody meets this table they should not have to guess. */
.metric > summary::after,
.payload-line > summary::after {
  content: 'click to expand'; margin-left: auto; padding-left: .8rem;
  color: var(--muted); font-size: .72rem; white-space: nowrap; opacity: 0;
  transition: opacity .12s ease;
}
.metric > summary:hover::after,
.payload-line > summary:hover::after { opacity: 1; }
.metric[open] > summary::after,
.payload-line[open] > summary::after { content: 'click to collapse'; }

@media print {
  .metric > summary::before, .payload-line > summary::before,
  .metric > summary::after, .payload-line > summary::after { display: none; }
  .metric > summary, .payload-line > summary { padding-left: 1.1rem; }
}

/* ── Field visibility, project by project ─────────────────────────────
   A tick column narrow enough that the project name still starts near the
   left edge, and a footer row that keeps the choice, the dry run and the
   button on one line while there is width for it. */
.tick-col { width: 2.2rem; text-align: center; }
.tick-col input { margin: 0; }
.field-apply { display: flex; flex-wrap: wrap; gap: .7rem; align-items: center; margin-top: .8rem; }
.field-apply select { padding: .2rem .3rem; }
.field-picks { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; margin: .6rem 0; color: var(--muted); }

/* A number that opens the work behind it. Underlined on hover only: a column
   of permanently underlined figures reads as a list of links, not as numbers. */
.tbl td.num-cell > a[data-window-open] { color: inherit; text-decoration: none; border-bottom: 1px dashed var(--muted); }
.tbl td.num-cell > a[data-window-open]:hover { color: var(--info); border-bottom-style: solid; }

/* The project list is long enough to scroll, so its header stays and the
   filter sits above it. */
.field-filter { display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; margin: .2rem 0 .6rem; }
.field-filter input[type="search"] { flex: 1 1 16rem; }
.field-projects-table { max-height: 26rem; overflow: auto; }
.field-projects-table thead th {
  position: sticky; top: 0; z-index: 1;
  background: linear-gradient(180deg, var(--card-bg), var(--surface));
  box-shadow: 0 1px 0 var(--border);
}

/* Three states are one choice, so they sit side by side; below ~40rem they
   stack, because two words per column is not a label. */
.state-radios-row { grid-template-columns: repeat(3, 1fr); max-width: none; }
@media (max-width: 44rem) { .state-radios-row { grid-template-columns: 1fr; } }
/* The mandatory slider and its sentence, on one line under a hairline. */
.field-required {
  display: flex; flex-wrap: wrap; gap: .5rem 1rem; align-items: center;
  margin-top: .9rem; padding-top: .8rem; border-top: 1px solid var(--border);
}
.field-required .small { flex: 1 1 20rem; }



/* ── The account menu ─────────────────────────────────────────────────────
   The name is the button. Everything personal — language, palette, the way
   out — lives behind it, so the top bar carries the platform's state and one
   control rather than four things competing for the same corner. */
.account { position: relative; flex: 0 0 auto; }
.account > summary {
  display: flex; align-items: center; gap: .5rem; cursor: pointer;
  padding: .25rem .5rem; border-radius: var(--radius); list-style: none;
}
.account > summary::-webkit-details-marker { display: none; }
.account > summary:hover { background: var(--surface); }
.account[open] > summary { background: var(--surface); }
.account-caret { color: var(--muted); font-size: .7rem; transition: transform .14s ease; }
.account[open] .account-caret { transform: rotate(180deg); }

.account-menu {
  position: absolute; right: 0; top: calc(100% + .4rem); z-index: 40;
  min-width: 13rem; padding: .35rem;
  background: var(--card-bg); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-lift);
}
.account-menu form { margin: 0; }
.account-group { padding-bottom: .3rem; margin-bottom: .3rem; border-bottom: 1px solid var(--border); }
.account-label {
  display: block; padding: .25rem .5rem .15rem; font-size: .68rem;
  text-transform: uppercase; letter-spacing: .04em; color: var(--muted);
}
.account-item {
  display: flex; align-items: center; gap: .5rem; width: 100%;
  padding: .4rem .5rem; border: 0; border-radius: var(--radius);
  background: none; color: var(--text); font: inherit; font-size: .85rem;
  text-align: left; text-decoration: none; cursor: pointer;
}
.account-item:hover { background: var(--surface); color: var(--heading); text-decoration: none; }
.account-item.is-now { color: var(--heading); font-weight: 600; }
.account-item.is-leaving { color: var(--danger-ink-strong); }
.account-ico { width: 1.1rem; text-align: center; flex: 0 0 auto; }
.account-tick { margin-left: auto; color: var(--success); }
/* The drawn flags: aligned with the text they label, and never stretched. */
.flag { display: block; border-radius: 2px; }
.flag-code { font-size: .62rem; font-weight: 700; letter-spacing: .03em; color: var(--muted); }
