/*
 * Customer dashboard.
 *
 * The same system as the admin console (api/src/admin/styles/admin.css): one
 * palette, one shell, one set of components, one class vocabulary. The two
 * surfaces are the same product and should not look like two different eras of
 * it.
 *
 * They are duplicated rather than shared because they run in different runtimes
 * and repositories — the Worker has no filesystem and bundles its assets, this
 * service reads them from disk. Keep them in step by editing both.
 */

:root {
  --ink: #1b1a19;
  --body: #5f5c58;
  --muted: #908c87;
  --line: #e9e7e4;
  --line-soft: #f1efec;
  --panel: #ffffff;
  --rail: #fbfaf9;
  /* The dashboard's brand, not a separate admin blue. */
  --brand: #17150f;
  --accent: #004bff;
  --ok: #4ac583;
  --ok-bg: #eef8f1;
  --warn: #96690b;
  --warn-bg: #fdf6e7;
  --danger: #a72020;

  --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, Helvetica, Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --rail-w: 246px;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--panel);
  color: var(--ink);
  font: 15px/1.6 var(--sans);
  -webkit-font-smoothing: antialiased;
}

a { color: inherit; }

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

/* The shell owns the scrolling, not the page.
 *
 * `min-height` let the document scroll, which meant the sidebar's `sticky` only
 * held within its own grid area — as tall as the content — so it scrolled away,
 * and the page header stuck to the viewport top where the toast lives. Fixing
 * the shell to the viewport and scrolling INSIDE the main column fixes both:
 * the rail cannot move, and the header sticks within its own scroller.
 */
.app {
  display: grid;
  grid-template-columns: var(--rail-w) 1fr;
  height: 100vh;
  overflow: hidden;
}

.rail {
  background: var(--rail);
  border-right: 1px solid var(--line);
  padding: 16px 12px 12px;
  display: flex; flex-direction: column;
  /* Full height of the shell, scrolling on its own. No sticky needed: the
     shell no longer scrolls, so the rail cannot move. */
  height: 100%;
  overflow-y: auto;
}

.rail-head { display: flex; align-items: center; gap: 9px; padding: 2px 6px 20px; }
.logo { width: 30px; height: 30px; display: block; }
.rail-head .name { font-weight: 600; font-size: 15px; }

.rail-label {
  font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--muted); padding: 0 8px; margin: 14px 0 7px;
}

.rail a.item {
  display: flex; align-items: center; gap: 11px;
  padding: 8px 10px; border-radius: 9px; margin-bottom: 1px;
  color: var(--body); text-decoration: none; font-size: 14.5px;
  border: 1px solid transparent;
}
.rail a.item:hover { background: #f3f1ee; color: var(--ink); }
.rail a.item.on {
  background: #fff; color: var(--ink); font-weight: 500;
  border-color: var(--line); box-shadow: 0 1px 2px rgba(27,26,25,.05);
}

.item .ico {
  flex: 0 0 auto; width: 18px; height: 18px;
  fill: none; stroke: currentColor; stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round; opacity: .65;
}
.item.on .ico { opacity: 1; }

.rail .bottom { margin-top: auto; }

.whoami { display: flex; align-items: center; gap: 10px; margin-top: 12px; padding: 9px 8px; border-top: 1px solid var(--line); }
.avatar {
  width: 30px; height: 30px; border-radius: 50%;
  background: #ece9e5; color: #6d6862;
  display: grid; place-items: center; font-size: 12px; font-weight: 600;
}
.whoami .who { min-width: 0; }
.whoami .who b { display: block; font-size: 13.5px; font-weight: 600; }
.whoami .who span { display: block; font-size: 12px; color: var(--muted); }

/* The scroll container. Everything long scrolls here, under a header that
   sticks to the top of THIS box rather than to the viewport. */
.main { min-width: 0; height: 100%; overflow-y: auto; display: flex; flex-direction: column; }
.main-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
  padding: 20px 30px; border-bottom: 1px solid var(--line-soft);
  position: sticky; top: 0; background: var(--panel);
  /* Above the content it scrolls over, far below the toast. */
  z-index: 5;
}
.main-head h1 { margin: 0 0 3px; font-size: 20px; font-weight: 600; letter-spacing: -0.01em; }
.main-head p { margin: 0; color: var(--muted); font-size: 14px; }
.main-body { padding: 24px 30px 56px; }
.main-actions{ display: flex; align-items: center; gap: 8px; }

.pill { display: inline-block; font-size: 11.5px; font-weight: 600; padding: 2px 9px; border-radius: 999px; border: 1px solid var(--line); color: var(--muted); background: #fff; }
.pill.ready, .pill.active { color: var(--ok); background: var(--ok-bg); border-color: #cfe9da; }
.pill.pairing, .pill.provisioning, .pill.paused { color: var(--warn); background: var(--warn-bg); border-color: #f5e4bd; }
.pill.down, .pill.banned, .pill.failed { color: var(--danger); background: #fdf1f0; border-color: #f2d5d2; }

.meter { display: block; width: 120px; height: 6px; margin-top: 5px; border: 0; border-radius: 999px; background: var(--line-soft); -webkit-appearance: none; appearance: none; }
.meter::-webkit-progress-bar { background: var(--line-soft); border-radius: 999px; }
.meter::-webkit-progress-value { background: var(--brand); border-radius: 999px; }
.meter::-moz-progress-bar { background: var(--brand); border-radius: 999px; }
.meter.full::-webkit-progress-value { background: var(--danger); }
.meter.full::-moz-progress-bar { background: var(--danger); }

/* ---- cards, forms, buttons ---- */

.card { border: 1px solid var(--line); border-radius: 12px; padding: 18px; margin-bottom: 14px; background: #fff; }
.card h3 { margin: 0 0 10px; font-size: 15.5px; font-weight: 600; }
.kv { display: grid; grid-template-columns: 160px 1fr; gap: 8px 14px; font-size: 14px; margin: 0; }
.kv dt { color: var(--muted); }
.kv dd { margin: 0; }

.btn, button {
  display: inline-flex; align-items: center; gap: 7px;
  background: #fff; color: var(--ink);
  border: 1px solid var(--line); border-radius: 9px;
  padding: 9px 14px; font: 500 14px/1 var(--sans);
  text-decoration: none; cursor: pointer;
}
.btn:hover, button:hover { border-color: #d9d6d2; }
.btn.primary, button.primary { background: var(--brand); border-color: var(--brand); color: #fff; }
.btn.primary:hover, button.primary:hover { background: #000; }
/* Also `.btn.danger`, not only `button.danger`: a destructive control is an
   <a> now — it goes to a page that asks first — and an anchor styled by an
   element selector arrives with no colour at all. */
button.danger, .btn.danger { color: var(--danger); border-color: #f0d6d4; }
button.danger:hover, .btn.danger:hover { border-color: var(--danger); background: #fdf1f0; }

/* The one that actually removes, at the end of a dialog that has already asked.
   Filled rather than outlined: it is the primary action of that dialog, and an
   outlined destructive button beside an outlined Cancel makes the reader work
   out which is which. */
.btn.danger-solid, button.danger-solid {
  background: var(--danger); border-color: var(--danger); color: #fff;
}
.btn.danger-solid:hover, button.danger-solid:hover { background: #8c1a1a; border-color: #8c1a1a; }
button.small, .btn.small { padding: 7px 11px; font-size: 13px; }
form.inline { display: inline; }
.actions { display: flex; gap: 8px; flex-wrap: wrap; }

label { display: block; font-size: 13.5px; font-weight: 500; margin: 14px 0 5px; }
input[type=text], input[type=number], input[type=date], input[type=file], input[type=password], input[type=email], input[type=url], input[type=tel], input[type=search] {
  width: 100%; padding: 10px 12px;
  border: 1px solid var(--line); border-radius: 10px; font: 14.5px var(--sans);
}
input:focus { outline: 2px solid #dfe4f5; border-color: #b9c4e8; }
.hint { color: var(--muted); font-size: 12.5px; margin: 5px 0 0; }

.section-title { margin: 26px 0 10px; font-size: 15px; font-weight: 600; }

.empty {
  border: 1.5px dashed #dcd9d5; border-radius: 14px;
  padding: 48px 24px; text-align: center; color: var(--muted); background: #fdfcfb;
}
.empty .glyph {
  width: 48px; height: 48px; border-radius: 14px; margin: 0 auto 14px;
  background: #f4f2ef; border: 1px solid var(--line); display: grid; place-items: center;
}
.empty .glyph svg { width: 22px; height: 22px; fill: none; stroke: var(--muted); stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.empty b { display: block; color: var(--ink); font-size: 15.5px; margin-bottom: 4px; }
.empty p { margin: 0; font-size: 14px; }

/* The roundup draft, shown as what it is: a quotation of something not yet
   said. Set apart from the page so an owner reads it as the message rather than
   as more dashboard copy, and preserving the line breaks the model wrote — a
   roundup is usually a few short lines and collapsing them changes it. */
.draft {
  margin: 0 0 16px; padding: 12px 14px;
  border-left: 3px solid var(--line);
  background: var(--soft, #faf9f8);
  border-radius: 0 8px 8px 0;
  white-space: pre-wrap;
  font-size: 14.5px; line-height: 1.55;
}

/* A pair of controls that read as one setting — "between 22:00 and 07:00" is
   one sentence, and stacking the halves makes it two. */
.field .row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.field .row select { flex: 0 1 auto; width: auto; }
.field .between { color: var(--muted); font-size: 14px; }

/* A question the group asked and nobody answered. Set apart from the sources
   below it, because one is what the assistant knows and the other is what it
   was asked and could not say. */
.gap { padding: 14px 0; border-bottom: 1px solid var(--line-soft, #f0eeec); }
.gap:last-child { border-bottom: 0; padding-bottom: 0; }
.gap:first-of-type { padding-top: 4px; }
/* A `<summary>`, so it has to say it is openable and behave like a control. The
   marker is the browser's own triangle, kept: drawing one is a second thing to
   keep aligned, and the native one already turns when it opens. */
.gap-question { margin: 0; font-size: 15px; line-height: 1.5; cursor: pointer; }
.gap-question::marker { color: var(--muted); }
.gap[open] .gap-question { margin-bottom: 10px; }
.gap-question:focus-visible { outline: 2px solid #b9c4e8; outline-offset: 2px; border-radius: 4px; }
.gap textarea { width: 100%; margin-bottom: 10px; }

/* A button that has been pressed and is waiting.
 *
 * The label stays in place and keeps its width — swapping the text for a
 * spinner makes the button resize under the cursor, which reads as the page
 * breaking rather than working. The mark sits beside it instead.
 *
 * Purely additive: without the script no button ever gets this class. */
.btn.is-busy, button.is-busy {
  position: relative;
  cursor: progress;
  opacity: .75;
}
.btn.is-busy::after, button.is-busy::after {
  content: '';
  flex: 0 0 auto;
  width: 13px; height: 13px;
  margin-left: 2px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: spin .6s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* A disabled control still reads as pressed rather than as broken. */
button[disabled], .btn[disabled] { opacity: .6; cursor: not-allowed; }

@media (prefers-reduced-motion: reduce) {
  /* No spin, but the mark stays: it is the only sign the press registered. */
  .btn.is-busy::after, button.is-busy::after { animation: none; opacity: .6; }
}

/* ---- toasts ----
 *
 * Identical to the customer dashboard, including the rule that a confirmation
 * clears itself and a failure does not. No scripts are allowed, so the
 * animation is the entire mechanism.
 */

.toast-area {
  position: fixed; top: 16px; left: 0; right: 0;
  display: flex; justify-content: center; padding: 0 16px;
  pointer-events: none;
  /* Above every part of the shell, including the sticky page header it floats
     over. Deliberately far clear of it rather than one step above. */
  z-index: 100;
}
.toast {
  display: flex; align-items: flex-start; gap: 11px;
  max-width: 460px; padding: 12px 15px;
  /* The strip above is transparent to the mouse so it does not swallow clicks
     on the header it floats over — but that made the toast itself unselectable
     too: visible, and impossible to highlight or copy. The message is the one
     thing in that strip anybody wants to touch, so it takes its events back.
     A confirmation ends its animation on `visibility: hidden`, which stops a
     spent toast intercepting anything. */
  pointer-events: auto;
  background: #fff; border: 1px solid var(--line); border-radius: 12px;
  box-shadow: 0 2px 6px rgba(27,26,25,.06), 0 14px 34px rgba(27,26,25,.12);
  animation: toast-in .34s cubic-bezier(.16,1,.3,1) both;
}
@keyframes toast-in { from { opacity: 0; transform: translateY(-14px) scale(.97); } to { opacity: 1; transform: none; } }
@keyframes toast-out { to { opacity: 0; transform: translateY(-8px); visibility: hidden; } }
.toast.good { animation: toast-in .34s cubic-bezier(.16,1,.3,1) both, toast-out .45s ease 6s both; }
/* A failure stays roughly twice as long. It used to stay for ever, on the
   grounds that a missed error is worse than a lingering one — still true, which
   is why this is 14s and not 6, and why the close button exists for anyone who
   has read it and wants it gone sooner. */
.toast.bad { animation: toast-in .34s cubic-bezier(.16,1,.3,1) both, toast-out .45s ease 14s both; }
@media (prefers-reduced-motion: reduce) {
  .toast { animation: none; }
  /* No entrance, but the timed exit stays: it is the mechanism, not decoration. */
  .toast.good { animation: toast-out .3s linear 6s both; }
  .toast.bad { animation: toast-out .3s linear 14s both; }
}
.toast-icon { flex: 0 0 auto; width: 20px; height: 20px; margin-top: 1px; border-radius: 50%; display: grid; place-items: center; }
.toast-icon svg { width: 13px; height: 13px; fill: none; stroke: #fff; stroke-width: 2.8; stroke-linecap: round; stroke-linejoin: round; }
.toast-text { margin: 0; font-size: 14px; line-height: 1.5; }
.toast.good { background: #f3fbf6; border-color: #cfe9da; }
.toast.good .toast-icon { background: var(--ok); }
.toast.good .toast-text { color: #10502e; }
.toast.bad { background: #fef5f4; border-color: #f2d5d2; }
.toast.bad .toast-icon { background: var(--danger); }
.toast.bad .toast-text { color: #8f2018; }

/* ---- sign in ---- */

.auth { max-width: 380px; margin: 0 auto; padding: 84px 24px 64px; text-align: center; }
.auth-mark {
  width: 66px; height: 66px; margin: 0 auto 18px; border-radius: 19px;
  background: var(--brand); color: #fff;
  display: grid; place-items: center; font-size: 30px; font-weight: 600; letter-spacing: -0.02em;
}

.auth h1 { margin: 0 0 10px; font-size: 25px; font-weight: 600; letter-spacing: -0.02em; }
.auth-lede { margin: 0 auto 26px; color: var(--body); font-size: 14.5px; line-height: 1.55; max-width: 330px; }
.auth-form input { width: 100%; padding: 13px 15px; margin-bottom: 10px; border: 1px solid var(--line); border-radius: 11px; font: 15px var(--sans); text-align: left; }
.auth-form input::placeholder { color: #a8a49f; }
.auth-code { text-align: center; font-size: 22px; letter-spacing: .35em; text-indent: .35em; font-variant-numeric: tabular-nums; }
.auth-submit { width: 100%; justify-content: center; padding: 13px 16px; border-radius: 11px; font-size: 15px; font-weight: 600; display: block; }
.auth-fine { margin: 18px auto 0; color: var(--muted); font-size: 12.5px; line-height: 1.6; max-width: 330px; }
.auth-fine a { color: var(--muted); text-decoration: underline; }

@media (max-width: 860px) {
  /* One column: the rail becomes a bar at the top and the page scrolls
     normally, because a 100vh shell with two scrollers is unusable on a phone. */
  .app { grid-template-columns: 1fr; height: auto; overflow: visible; }
  .main { height: auto; overflow: visible; }
  .rail { height: auto; overflow-x: auto; flex-direction: row; align-items: center; gap: 6px;
          overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--line); padding: 10px 12px; }
  .rail-label, .rail .bottom { display: none; }
  .main-head { padding: 16px 18px; position: static; }
  .main-body { padding: 18px 18px 40px; }
}

/* ---- customer components ----
 *
 * Pieces the console has no use for. Built from the same tokens and the same
 * shapes as everything above, so they belong to the one system.
 */

.card-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.card h3 a { text-decoration: none; }
.card h3 a:hover { text-decoration: underline; }
.card .meta { margin: 5px 0 0; color: var(--muted); font-size: 13.5px; }
.card form { margin-top: 12px; }

.pill.active { color: var(--ok); background: var(--ok-bg); border-color: #cfe9da; }
.pill.paused { color: var(--warn); background: var(--warn-bg); border-color: #f5e4bd; }

.btn.ghost, button.ghost { background: #fff; color: var(--ink); border-color: var(--line); font-weight: 400; }
.btn.ghost:hover, button.ghost:hover { border-color: #d9d6d2; }

.field { margin-bottom: 16px; }
textarea, select {
  width: 100%; padding: 10px 12px;
  border: 1px solid var(--line); border-radius: 10px;
  font: 14.5px var(--sans); color: var(--ink); background: #fff;
}
textarea { min-height: 92px; resize: vertical; }
textarea:focus, select:focus { outline: 2px solid #dfe4f5; border-color: #b9c4e8; }

/* Usage. The figure is the number; the meter beneath it is the same component
   the console uses for connector capacity. */
.figure { display: flex; align-items: baseline; gap: 8px; }
.figure b { font-size: 28px; font-weight: 600; letter-spacing: -0.01em; }
.figure span { color: var(--muted); font-size: 14px; }
.meter { width: 100%; height: 7px; margin-top: 12px; }


/* The membership roster. A list of dates, read soonest-first, so the date is
   the column that carries the eye rather than the name. */
.roster { width: 100%; border-collapse: collapse; margin-top: 4px; }
.roster th {
  text-align: left; font-size: 12px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .04em; color: var(--muted); 
  padding: 5px;
  border-bottom: 1px solid var(--line);
}
.roster td { padding: 12px 12px 12px 0; border-bottom: 1px solid var(--line); vertical-align: top; }
.roster tr:last-child td { border-bottom: 0; }
.roster .sub { display: block; font-size: 12.5px; color: var(--muted); margin-top: 3px; }
.roster .mono { font-variant-numeric: tabular-nums; }
/* `display: table-cell` back, because `.actions` above makes anything with that
   class a flex row — and a table cell that is a flex container stops spanning
   its row: the box ends at its content, so the row's bottom border was drawn
   18px high under the buttons and broke the line across every row. The members
   roster carries the same class and had the same broken line. */
.roster td.actions { display: table-cell; text-align: right; width: 1%; white-space: nowrap; }
.roster td.actions form { margin: 0; }

/* Wide enough to need it on a phone, and the page itself must never scroll
   sideways. */
.roster-scroll { overflow-x: auto; }


/* A typed note, shown back so the owner can see what they taught it. Clamped
   rather than scrolled: this is a reminder of what is there, not the editor. */
.card .preview {
  white-space: pre-wrap; margin-top: 8px; padding: 10px 12px;
  background: var(--wash, #faf9f7); border: 1px solid var(--line); border-radius: 8px;
  font-size: 13px; line-height: 1.55; color: var(--body);
}
.card .meta.mono { font-variant-numeric: tabular-nums; word-break: break-all; }

/* The way out, for someone who does not want to wait. A link because there is
   no JavaScript here and because dismissing genuinely IS navigation: it goes to
   this page without the query parameter that produced the toast, so a refresh
   does not bring it back. Sized to the 44px touch target, with the visible mark
   smaller than the area that accepts the tap. */
.toast-close {
  flex: 0 0 auto; align-self: flex-start;
  display: grid; place-items: center;
  width: 26px; height: 26px; margin: -3px -5px -3px 4px;
  border-radius: 8px; border: 0;
  color: var(--muted); text-decoration: none;
  font-size: 17px; line-height: 1;
}
.toast-close:hover { background: rgba(27,26,25,.06); color: var(--ink); }
.toast-close:focus-visible { outline: 2px solid #b9c4e8; outline-offset: 1px; }

/* ---- modals ----
 *
 * A native <details>. Closed by default, so the page is short before any script
 * runs, and the summary is the black button that opens it. With JavaScript the
 * panel becomes a real dialog — focus trapped, Escape and backdrop to close —
 * but nothing here depends on that: with no script it is an inline disclosure
 * that still works.
 */
.modal { display: inline-block; }
/* `.modal` and `[hidden]` have the same specificity, and an author rule beats
   the browser's own — so without this the shared confirm dialog is `hidden` in
   the markup and laid out anyway. Nothing shows (its panel is display:none
   while closed), but an empty inline-block sitting in the page on the strength
   of that is not something to rely on. */
.modal[hidden] { display: none; }
.modal > summary {
  list-style: none; cursor: pointer;
  display: inline-flex; align-items: center; gap: 7px;
}
.modal > summary::-webkit-details-marker { display: none; }

/* Once open, the summary is the backdrop's trigger and must not sit under the
   panel. The panel itself is fixed, so the details box stays where it was. */
.modal[open] > .modal-panel {
  position: fixed; z-index: 90;
  top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: min(560px, calc(100vw - 32px));
  max-height: min(80vh, 720px);
  background: var(--panel); border: 1px solid var(--line); border-radius: 14px;
  box-shadow: 0 2px 8px rgba(27,26,25,.08), 0 24px 60px rgba(27,26,25,.20);
  text-align: left; cursor: auto;
  /* A column, so the head stays put and only the body scrolls. Scrolling the
     title and the close button off the top of a long document form leaves no
     visible way out of the dialog. */
  display: flex; flex-direction: column; overflow: hidden;
}
.modal[open]::before {
  content: ''; position: fixed; inset: 0; z-index: 89;
  background: rgba(27,26,25,.34);
}
/* Only `display: none` here. The open panel's own rule above already makes it
   a flex column, and a second rule with the same selector was re-setting it to
   `block` — which quietly undid the column: the head and body flex rules
   stopped applying, `.modal-body` had no constrained height so its `overflow-y`
   never engaged, and the panel's `overflow: hidden` then clipped everything
   past 80vh with no way to reach it. */
.modal-panel { display: none; }

.modal-head {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 18px 22px 12px; border-bottom: 1px solid var(--line-soft);
}
.modal-head h2 { margin: 0; font-size: 17px; font-weight: 600; letter-spacing: -0.01em; }
.modal-x {
  flex: 0 0 auto; width: 30px; height: 30px; padding: 0;
  display: grid; place-items: center;
  border: 0; background: transparent; color: var(--muted);
  font-size: 20px; line-height: 1; border-radius: 8px; cursor: pointer;
}
.modal-x:hover { background: rgba(27,26,25,.06); color: var(--ink); }

.modal-body { flex: 1 1 auto; overflow-y: auto; padding: 16px 22px 22px; }

/* Inside a panel the cards lose their frame — the panel is the frame. */
.modal-body .card { border: 0; padding: 0; background: none; box-shadow: none; }
.modal-body .card + .card { margin-top: 22px; border-top: 1px solid var(--line-soft); padding-top: 18px; }
.modal-body .card h3 { font-size: 14px; }

/* While a dialog is open the page behind it does not scroll. Set by the script,
   so it never strands a no-script page in an unscrollable state. */
body.modal-open { overflow: hidden; }

/* ---- confirming a removal ----
 *
 * One panel per page rather than one per row: a 300-member roster would
 * otherwise carry 300 hidden dialogs. It is `hidden` in the markup and unhidden
 * by the script, because with no script there is nothing to open it — that
 * reader gets the confirm PAGE instead, which is a real page and needs no
 * dialog at all.
 *
 * Its summary exists only because <details> requires one. It is never a
 * control here, so it is taken out of the layout and out of the tab order, and
 * the script restores focus to the link that opened the dialog instead.
 */
.modal.confirm > summary {
  position: absolute; width: 1px; height: 1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

/* And the dialog itself takes no space in the page.
 *
 * `.modal` is `inline-block`, so once the script unhides this one it sat as an
 * empty line box AFTER `.app` — which is exactly `100vh`. That put the document
 * 24px taller than the viewport on EVERY dashboard page, so the whole thing
 * scrolled and took the rail with it: the shell's own "the rail cannot move"
 * arrangement was correct and was being defeated from outside it.
 *
 * Safe to take out of flow because nothing here is ever in it: the summary
 * above is visually hidden, and `.modal[open] > .modal-panel` is `position:
 * fixed`, so the open dialog does not depend on where this box sits. */
.modal.confirm { position: fixed; top: 0; left: 0; width: 0; height: 0; }
.confirm-body { margin: 0 0 18px; font-size: 14px; line-height: 1.55; color: var(--muted); }
.confirm-actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.confirm-card { max-width: 520px; }
.confirm-card h2 { margin: 0 0 8px; font-size: 17px; font-weight: 600; letter-spacing: -0.01em; }
.confirm-card .hint { margin: 0 0 18px; }


/* ---- the Sources app ----
 *
 * The list, the chips and the search box are drawn by `vendor/knowledge.entry.js`
 * using the classes already defined here, so the appearance is the page's and
 * not the app's. Only the search box and the counts are its own.
 */
.kb-search {
  display: flex; align-items: center; gap: 8px; margin: 0 0 14px;
  padding: 8px 12px; border: 1px solid var(--line); border-radius: 10px; background: #fff;
}
.kb-search .ico { width: 16px; height: 16px; color: var(--muted); flex: none; }
.kb-search input {
  flex: 1; border: 0; padding: 0; background: none; font: inherit; color: var(--ink);
}
.kb-search input:focus { outline: none; }
.kb-search:focus-within { border-color: #b9c4e8; box-shadow: 0 0 0 3px rgba(185,196,232,.25); }

/* How many that chip has, counted after the search — so a chip never disagrees
   with the list under it. */
.chip-count { margin-left: 6px; opacity: .6; font-variant-numeric: tabular-nums; }

.kb-error {
  margin: 0 0 14px; padding: 10px 12px; border-radius: 10px;
  border: 1px solid #f2d5d2; background: #fdf1f0; color: var(--danger); font-size: 13.5px;
}

/* ---- the Sources table ----
 *
 * Built on `.roster`, the same table the membership list uses, so the two read
 * as one system rather than two tables that happen to be on one dashboard.
 */
.kb-table { border: 1px solid var(--line); border-radius: 12px; background: #fff; overflow: hidden; }

.kb-rows { margin: 0; }
.kb-rows th, .kb-rows td { padding-left: 14px; }
.kb-rows tbody td { padding-top: 10px; padding-bottom: 10px; }
.kb-rows thead th { background: var(--wash, #faf9f7); border-bottom: 1px solid var(--line); }
.kb-rows td { vertical-align: middle; }
.kb-rows tbody tr:hover { background: var(--wash, #faf9f7); }
.kb-rows td b { font-weight: 600; }

/* Narrow, and never the column that gives when the table is squeezed. */
.kb-rows th.pick, .kb-rows td.pick { width: 1%; padding-right: 0; }

/* `.roster` gives its actions column `width: 1%`, which the table's auto layout
   reads as a PREFERENCE and happily under-allocates: the cell came out 28px
   wide with a 72px button hanging out of it, so the row's border stopped short
   underneath. A floor the button cannot be squeezed below is what fixes it. */
.kb-rows td.actions, .kb-rows th:last-child { padding-right: 14px; min-width: 96px; }
.kb-rows input[type="checkbox"] { width: 15px; height: 15px; accent-color: var(--ink); cursor: pointer; }

/* A header that sorts is a button, so it is reachable by keyboard and says what
   it does. The caret shows the direction; `⇅` marks the columns you could sort
   by but have not. */
.th-sort {
  display: inline-flex; align-items: center; gap: 5px;
  border: 0; padding: 0; background: none; cursor: pointer;
  font: inherit; color: inherit; letter-spacing: inherit; text-transform: inherit;
}
.th-sort:hover { color: var(--ink); }
.th-caret { font-size: 9px; opacity: .35; }
.th-sort.on .th-caret { opacity: 1; }
.th-sort:focus-visible { outline: 2px solid #b9c4e8; outline-offset: 2px; border-radius: 3px; }

/* Only on screen when something is selected, so it never takes room it has no
   use for. */
.kb-selected {
  display: flex; align-items: center; gap: 10px; margin: 0 0 10px;
  padding: 8px 12px; border: 1px solid var(--line); border-radius: 10px;
  background: var(--wash, #faf9f7); font-size: 13.5px;
}
.kb-selected span { color: var(--muted); margin-right: auto; }

.kb-foot {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 10px 14px; border-top: 1px solid var(--line); background: #fff;
}
.kb-count { color: var(--muted); font-size: 13px; flex: 1; }
.kb-pages { display: flex; align-items: center; gap: 4px; flex: none; }
/* The count takes the left and the per-page control the right, so the pager sits
   in the middle rather than against an edge. `margin-left: auto` rather than
   `flex: 1` on the select itself — a select is not a flex container, and giving
   it the growing job stretched the control instead of the space beside it. */
.kb-per { margin-left: auto; }
.kb-page {
  min-width: 28px; height: 28px; padding: 0 7px; border-radius: 7px;
  border: 1px solid transparent; background: none; cursor: pointer;
  font: inherit; font-size: 13px; color: var(--muted);
}
.kb-page:hover:not(:disabled) { background: var(--wash, #faf9f7); color: var(--ink); }
.kb-page.on { background: var(--ink); border-color: var(--ink); color: #fff; }
.kb-page:disabled { opacity: .35; cursor: default; }
.kb-page:focus-visible { outline: 2px solid #b9c4e8; outline-offset: 1px; }
.kb-gap { color: var(--muted); padding: 0 2px; }
/* `width: auto` on purpose: the form styles give every `select` a full width,
   which is right in a field and wrong for a control sitting in a toolbar — it
   took a line of its own and stretched the footer to two rows. */
.kb-per {
  width: auto; flex: none;
  height: 30px; padding: 0 8px; border-radius: 8px; border: 1px solid var(--line);
  background: #fff; font: inherit; font-size: 13px; color: var(--ink);
}

@media (max-width: 720px) {
  /* The second lines carry the detail, so the columns that repeat it can go
     rather than the table scrolling sideways on a phone. */
  .kb-rows th:nth-child(4), .kb-rows td:nth-child(4) { display: none; }
}

/* ---- the Sources filter ----
 *
 * Which owner's sources are on screen. Links, like the tabs above them: each
 * view is its own address, so it is bookmarkable and works with no script.
 */
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 10px; }
.chip {
  padding: 5px 12px; border-radius: 999px; border: 1px solid var(--line);
  font-size: 13px; font-weight: 500; color: var(--muted);
  background: #fff; text-decoration: none; cursor: pointer; font-family: inherit;
}
.chip:hover { color: var(--ink); }
.chip.on { background: var(--ink); border-color: var(--ink); color: #fff; }
.chip:focus-visible { outline: 2px solid #b9c4e8; outline-offset: 1px; }

/* Two pills on a card head — whose it is, and whether it is ready. */
.pills { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }

/* Which group asked. Quieter than the question, and on the same line. */
.gap-where { color: var(--muted); font-weight: 500; }

/* ---- the knowledge base's own tabs ----
 *
 * The same shape as the category picker below, and deliberately so: one visual
 * language for "choose which of these you are looking at". These are links
 * rather than radios because each tab is a page of its own, so there is no
 * state to hold and nothing to hide.
 */
/* Sized to its two tabs, not to the page. Stretched across a wide content column, two tabs read as
   a header rather than as a control. `flex: none` on the strip keeps it off the workspace app's
   height as well. */
.subnav {
  display: flex; flex: none; gap: 4px; margin: 0 0 20px; padding: 3px; width: max-content;
  max-width: 100%;
  background: var(--wash, #faf9f7); border: 1px solid var(--line); border-radius: 11px;
}
.subnav-tab {
  min-width: 116px; padding: 7px 14px; border-radius: 8px;
  font-size: 13.5px; font-weight: 500; text-align: center; color: var(--muted);
  text-decoration: none;
}
.subnav-tab:hover { color: var(--ink); }
.subnav-tab.on {
  background: var(--panel); color: var(--ink);
  box-shadow: 0 1px 2px rgba(27,26,25,.06);
}
.subnav-tab:focus-visible { outline: 2px solid #b9c4e8; outline-offset: 1px; }

/* ---- the category picker ----
 *
 * Radios and sibling selectors, so choosing what to teach it from works with no
 * JavaScript — the same rule the modal itself follows. The inputs stay in the
 * document and focusable; only their default appearance is hidden.
 */
.picker-state { position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none; }

.picker-tabs {
  display: flex; gap: 4px; margin: 4px 0 18px; padding: 3px;
  background: var(--wash, #faf9f7); border: 1px solid var(--line); border-radius: 11px;
}
.picker-tabs label {
  flex: 1; margin: 0; padding: 7px 10px; border-radius: 8px;
  font-size: 13.5px; font-weight: 500; text-align: center; color: var(--muted);
  cursor: pointer; user-select: none;
}
.picker-tabs label:hover { color: var(--ink); }

.pane { display: none; }

/* ── Classes, not ids ──
 *
 * There is a teach panel per section of the Sources tab now — the account's own
 * and one per group — so an id-based selector would style every panel from
 * whichever one was last clicked, and a shared radio NAME would make all of
 * them one group: choosing Document under a group would silently switch the
 * account's own panel too. The ids stay unique per panel for the labels to
 * point at; everything that styles is a class.
 */
.pick-text:checked ~ .picker-tabs label.for-text,
.pick-web:checked  ~ .picker-tabs label.for-web,
.pick-doc:checked  ~ .picker-tabs label.for-doc {
  background: var(--panel); color: var(--ink);
  box-shadow: 0 1px 2px rgba(27,26,25,.06);
}

.pick-text:checked ~ .pane-text,
.pick-web:checked  ~ .pane-web,
.pick-doc:checked  ~ .pane-doc { display: block; }

/* The radio itself is invisible, so the ring has to be drawn on its label. Without
   this the picker is unusable by keyboard: focus moves and nothing shows it. */
.picker-state:focus-visible ~ .picker-tabs label { outline: none; }
.pick-text:focus-visible ~ .picker-tabs label.for-text,
.pick-web:focus-visible  ~ .picker-tabs label.for-web,
.pick-doc:focus-visible  ~ .picker-tabs label.for-doc {
  outline: 2px solid #b9c4e8; outline-offset: 1px;
}

/* --- the workspace app -----------------------------------------------------

   Ported from the panel in the other product: a file explorer on the left, the
   artifact panel on the right. Only the frame is styled here — everything
   inside the panel is artifactuse's own, from `workspace.css`.

   Prefixed `ws-` so it is obvious which rules belong to the Vue app and which
   to the page around it. The app owns its markup; the page must not reach in.
*/
/* Fills the main area rather than sitting in a box inside it. The workspace IS
   the page — a file tree and an editor want the height, and a card floating in
   the middle wastes the one dimension they need most.

   `.main` is already a flex column, so the body only has to grow. Scoped with
   `:has()` to the page that holds the app, because `.main-body` is shared by
   every other page and none of them wants to be stretched. A browser without
   `:has()` simply keeps today's layout — the page still works, it just does not
   fill, which is the right way for this to degrade.

   `min-height: 0` on both: a flex child will not shrink below its content
   without it, and the tree would push the panel off the bottom instead of
   scrolling inside itself. */
/* A COLUMN, because the Knowledge base's tab strip sits above the app now. As a row this made the
   strip a flex item beside it: a narrow full-height column with two words in it. The app still
   fills what is left, which is what `flex: 1` on it does in either direction. */
.main-body:has(.workspace-app) {
  flex: 1; min-height: 0; display: flex; flex-direction: column; padding-bottom: 24px;
}

.workspace-app { display: flex; flex: 1; min-height: 0; min-width: 0; }

.ws { display: grid; gap: 0; align-items: stretch; flex: 1; min-width: 0;
  min-height: 0; border: 1px solid var(--line); border-radius: 12px;
  overflow: hidden; background: #fff; }
@media (min-width: 900px) { .ws { grid-template-columns: minmax(230px, 19rem) 1fr; } }

/* The divider belongs to the tree only while the tree is alone. With a file
   open the panel draws its own left edge, and the two sat side by side as a
   double line. */
.ws-side { min-width: 0; min-height: 0; display: flex; flex-direction: column; }
.ws:not(.is-open) .ws-side { border-right: 1px solid var(--line); }

/* --- the head: search and the overflow menu --- */
.ws-head { position: relative; display: flex; align-items: center; gap: 6px;
  padding: 4px; border-bottom: 1px solid var(--line); }
.ws-search { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0;
  padding: 5px 9px; 
  /* border: 1px solid var(--line);  */
  border-radius: 8px; color: var(--muted); margin:0; }
.ws-search input { width: 100%; border: 0; padding: 0; font: 13px var(--sans);
  background: none; }
.ws-search input:focus { outline: none; }
.ws-search:focus-within { 
  /* border-color: #b9c4e8; 
  box-shadow: 0 0 0 2px #dfe4f5;  */
  border:none;
}

.ws-more { display: flex; align-items: center; flex: 0 0 auto; padding: 5px 7px; border: 1px solid transparent; border-radius: 8px;
  background: none; color: var(--muted); font-size: 15px; line-height: 1; cursor: pointer; }
.ws-more:hover, .ws-more.is-open { border-color: #b9c4e8; color: var(--ink); }

/* --- both menus share one look, as theirs do --- */
.ws-menu { position: absolute; z-index: 40; min-width: 190px; padding: 4px;
  border: 1px solid var(--line); border-radius: 10px; background: #fff;
  box-shadow: 0 8px 24px rgba(27,26,25,.12); }
.ws-menu-overflow { top: 42px; right: 8px; }
.ws-menu-context { position: fixed; }
.ws-menu-item { display: block; width: 100%; padding: 7px 10px; border: 0; border-radius: 7px;
  background: none; font: 13.5px var(--sans); color: var(--ink); text-align: left; cursor: pointer; }
.ws-menu-item:hover { background: #faf8f6; }
.ws-menu-item.is-danger { color: var(--danger); }
.ws-menu-rule { height: 1px; margin: 4px 2px; background: var(--line); }

.ws-menu-meters { padding: 6px 10px 4px; border-top: 1px solid var(--line); margin-top: 4px; }
.ws-menu-meter { display: flex; justify-content: space-between; gap: 12px;
  color: var(--muted); font-size: 12px; padding: 2px 0; }
.ws-menu-bar { height: 4px; margin: 4px 0 6px; border-radius: 999px; background: #eee9e4;
  overflow: hidden; }
.ws-menu-bar span { display: block; height: 100%; background: var(--brand); }

/* --- the tree --- */
.ws-tree { flex: 1; overflow: auto; padding: 4px 0; font-family: var(--mono); font-size: 12.5px; }
.ws-row { display: flex; align-items: center; gap: 6px; padding: 4px 8px; cursor: pointer;
  color: var(--ink); white-space: nowrap; }
.ws-row:hover { background: #faf8f6; }
.ws-row.is-open { background: #f4f6fd; }
.ws-name { overflow: hidden; text-overflow: ellipsis; }
.ws-icon { width: 14px; height: 14px; flex: 0 0 auto; color: var(--muted); }

/* Always present, even on files, so names line up whether or not a row can open. */
/* An svg now rather than a character, so it turns cleanly and matches the
   weight of the icon beside it. Files get an empty one of the same width, so
   names line up whether or not a row can open. */
.ws-caret { width: 12px; height: 12px; flex: 0 0 auto; color: var(--muted);
  transition: transform .12s ease; }
.ws-caret.is-open { transform: rotate(90deg); }
span.ws-caret { display: inline-block; }

.ws-row-edit { padding: 2px 8px; }
.ws-inline { width: 100%; padding: 4px 6px; border: 1px solid #b9c4e8; border-radius: 6px;
  font: 12.5px var(--mono); }
.ws-inline:focus { outline: 2px solid #dfe4f5; }

.ws-empty, .ws-error { padding: 14px; color: var(--muted); font-size: 13px;
  font-family: var(--sans); }
.ws-error { color: var(--danger); }

/* --- the right pane ---

   No tab styles here any more. `multiTab: true` gives the panel its own tab
   bar and view controls; this file used to draw a second one for the same
   artifacts, which the panel did not know about and could not close.
*/
.ws-main { min-width: 0; min-height: 0; display: flex; flex-direction: column; }

.ws-panel { flex: 1; min-width: 0; min-height: 0; overflow: hidden; display: flex; }

/* The panel sizes ITSELF, and to a percentage of what contains it: the SDK
   applies `panelWidth` as an inline width, clamped to 25–75 with a default of
   65. That is right where the panel shares a viewport with a chat and slides
   over it — and wrong here, where the panel IS the right pane and the tree is
   the other half. At 65% it left a third of the pane blank.
   
   The clamp means no config or prop can reach 100, so this is CSS, and it needs
   `!important` because what it overrides is an inline style. */
.ws-panel .artifactuse-panel {
  width: 100% !important;
  height: auto !important;
  max-width: none !important;
  flex: 1;
  min-width: 0;
}

/* The panel's toolbar icons, which rendered ZERO PIXELS WIDE.

   Each is an `<svg>` with no width/height attributes inside a `display: flex`
   button, so it is a flex item with the default `flex-shrink: 1` and
   `min-width: auto`. An SVG has no min-content width to protect it, so it
   collapses to 0 — its declared `width: 16px` loses to the shrink, and setting
   the width again does nothing about it.

   Measured rather than reasoned: forcing `flex-shrink: 0` takes the icon
   straight from 0 to 16px, while forcing `width: 16px` leaves it at 0.

   The HEIGHT was always correct, which is what made this look like a colour
   problem — the icons had the right stroke and no width to draw it in. The app
   the panel ships with has a CSS reset that protects them; this dashboard has
   none, so it says so here. */
.ws-panel .artifactuse-panel svg { flex-shrink: 0; }

/* The empty state, in the shape the other product uses: an icon, a line, and a
   sentence saying what to do rather than an empty rectangle. */
.ws-none { height: 100%; width: 100%; min-height: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 6px; text-align: center; padding: 24px; }
.ws-none-icon { width: 30px; height: 30px; color: #cfc9c2; margin-bottom: 6px; }
.ws-none-title { margin: 0; font-weight: 600; font-size: 14.5px; }
.ws-none-body { margin: 0; max-width: 20rem; color: var(--muted); font-size: 13.5px; }

.ws-noscript { margin: 16px 0; padding: 14px 16px; border: 1px solid var(--line);
  border-radius: 12px; background: #fdfcfb; color: var(--muted); font-size: 14px; }
