/* psh.now, design system
   One accent, one radius, dark only. Every page uses the same shell. */

:root {
--bg: #0a0a0a;
  --surface: #111319;
  --surface-2: #171a22;
  --line: #232838;
  --line-soft: #1b1f2b;
  --text: #eef1f7;
  --dim: #8b94a7;
  --green: #22c55e;
  --red: #f87171;
  --field-bg: #101218;
  --approve-bg: #16231a;
  --approve-bg-hover: #1c2f21;
  --approve-ink: #6ede8e;
  --approve-border: transparent;
  --deny-bg: #231a1a;
  --deny-bg-hover: #2e2121;
  --deny-ink: #f08a8a;
  --deny-border: transparent;
  --tab-accent: #ffffff;
  --badge-bg: #1c1f27;
  --badge-bg-hover: #232732;
  --badge-ink: #cfd4dd;
  --badge-border: #2a2f3a;
  --primary-bg: #ffffff;
  --primary-ink: #0b0d12;
  --hover-border: #3a4050;
  /* Empty-state track for progress bars: must stay readable against
     --surface, where --line is too close in value to be seen. */
  --track: #394054;
  --radius: 16px;
  --radius-sm: 11px;
  /* The page column: `.wrap` and the nav content both use these, so the logo
     and the nav actions sit on exactly the same edges as the page content. */
  --shell-max: 640px;
  --shell-pad: 20px;
  /* Notices sit narrower than the page column and centred, so the slack is
     split evenly instead of leaving one long empty run to the right. */
  --notice-max: 680px;
}
/* Light mode, toggled from the account menu, persisted in localStorage. */
:root[data-theme="light"] {
--bg: #ffffff;
  --surface: #f6f7f9;
  --surface-2: #eef0f3;
  --line: #dfe2e7;
  --line-soft: #eceef1;
  --text: #0f1115;
  --dim: #667085;
  --field-bg: #eef0f3;
  --badge-bg: #eef0f3;
  --badge-bg-hover: #e4e7ec;
  --badge-ink: #374151;
  --badge-border: #dfe2e7;
  --primary-bg: #111318;
  --primary-ink: #ffffff;
  --hover-border: #c3c8d0;
  --approve-bg: #e6f5e3;
  --approve-bg-hover: #d9efd5;
  --approve-ink: #14532d;
  --approve-border: transparent;
  --deny-bg: #fdeaea;
  --deny-bg-hover: #fadada;
  --deny-ink: #7f1d1d;
  --deny-border: transparent;
  --tab-accent: #111318;
}
/* The brand mark is a black outline; only invert it on dark. */
:root[data-theme="light"] .mark {
filter: none;
}
* {
box-sizing: border-box;
  -webkit-tap-highlight-color: transparent;
}
html,
body {
margin: 0;
  min-height: 100%;
}
body {
background: var(--bg);
  color: var(--text);
  font: 15px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  padding: env(safe-area-inset-top) env(safe-area-inset-right)
    env(safe-area-inset-bottom) env(safe-area-inset-left);
}
a {
color: var(--text);
  text-decoration: none;
}
.hide {
display: none !important;
}
.show {
display: block;
}
.muted {
color: var(--dim);
}
/* ---------- shell ---------- */

.wrap {
max-width: 1080px;
  margin: 0 auto;
  padding: 30px 26px 90px;
}
/* The minimal app: a single column of channel cards. */
.wrap.app {
max-width: var(--shell-max);
  padding: 24px var(--shell-pad) 48px;
}
/* A settings page with two lines of text and one row does not want 1080px: at that
   width the title and its button sit a screen apart. Same column as the app. */
.wrap.narrow {
  max-width: var(--shell-max);
  padding: 26px var(--shell-pad) 64px;
}
@media (max-width: 480px) {
  .wrap.narrow .page-head {
    flex-direction: column;
    align-items: flex-start;
    gap: 12px;
  }
}
/* ---------- nav links ---------- */
.nav-links {
display: flex;
  gap: 24px;
  margin-left: 10px;
}
.nav-link.active {
color: var(--text);
  font-weight: 600;
}
/* ---------- approvals page ---------- */
.approvals {
display: flex;
  flex-direction: column;
  gap: 12px;
  /* Match the space the channel list gets below the search bar. */
  margin-bottom: 30px;
}
/* Every waiting ask is its own card, oldest first. A backlog scrolls here
   instead of pushing the channel list off the screen. */
.approvals-list {
  display: flex;
  flex-direction: column;
  gap: 16px;
  max-height: min(64vh, 620px);
  overflow-y: auto;
  overscroll-behavior: contain;
  /* Room for the cards' borders / focus rings inside the scroll box. */
  padding: 2px;
  scrollbar-width: thin;
  scrollbar-color: var(--line) transparent;
}
.approvals-list::-webkit-scrollbar {
  width: 8px;
}
.approvals-list::-webkit-scrollbar-thumb {
  background: var(--line);
  border-radius: 999px;
}
.approvals-list::-webkit-scrollbar-track {
  background: transparent;
}
/* One ask (the usual case) is not a list: no scroll box, no extra padding. */
.approvals-list.is-single {
  max-height: none;
  overflow: visible;
  padding: 0;
}
/* One scroll surface on touch and narrow screens. The cap below keeps the
   channel list reachable under a long approvals list on a desktop, but on a
   phone it turns the middle of the screen into a nested scroller: the gesture
   scrolls the list itself, and `overscroll-behavior: contain` never hands it
   back to the page, so the page feels unscrollable. */
@media (hover: none), (max-width: 720px) {
  .approvals-list {
    max-height: none;
    overflow: visible;
    overscroll-behavior: auto;
  }
}
.approval-card {
  /* The verdict buttons are sized from here, so the message below can derive
     the offset that centres them on its last line. */
  --verdict-size: 40px;
  /* One curve for the whole card morph, so every part moves together. */
  --morph: cubic-bezier(0.22, 1, 0.36, 1);
border: 1px solid var(--line-soft);
  border-radius: 18px;
  /* The blue glow the limit notice used to wear. It sits over the card's own
     surface, so the card still reads as a card in both themes. */
  background:
    radial-gradient(120% 160% at 0% 0%, rgba(91, 156, 255, 0.14), transparent 58%),
    var(--surface);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
  padding: 20px;
}
/* Light mode: the same blue, much quieter. On white the full-strength wash reads as
   a lavender panel rather than a card with a glow, so the alpha drops and the inner
   highlight (which only exists to lift a dark surface) comes off. */
:root[data-theme="light"] .approval-card {
  background:
    radial-gradient(120% 160% at 0% 0%, rgba(91, 156, 255, 0.08), transparent 58%),
    var(--surface);
  box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05);
}
/* The card's colour now leads the request: the channel name had a row of its own,
   and the dot says the same thing in space the title already occupied. */
.approval-title {
display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 8px;
  font-size: 22px;
  font-weight: 600;
}
.approval-dot {
width: 9px;
  height: 9px;
  border-radius: 999px;
  background: var(--accent, var(--dim));
  flex: none;
}
.approval-title-text {
min-width: 0;
}
/* The agent's mark keeps the row's right edge, so the title can stay one line. */
.approval-title .agent-chip {
margin-left: auto;
  flex: none;
}
.approval-msg {
margin: 0 0 20px;
  color: var(--dim);
  font-size: 15px;
  line-height: 1.5;
  white-space: pre-wrap;
  /* Three lines, then it scrolls. A long message used to grow the card without limit and push
     the verdicts down the page. The padding that centres them on the last line still works,
     because max-height clamps the three text lines and the padding sits outside it. */
  max-height: 4.5em;
  overflow-y: auto;
  overscroll-behavior: contain;
}
/* The message and the verdicts share one row: the pill sits at the end of the
   last line instead of claiming a band of its own underneath. */
.approval-body {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 16px;
}
/* Basis 0, not auto: a long message must wrap inside its own column rather than
   claiming the full width and pushing the pill onto a line of its own. */
.approval-body .approval-msg {
  flex: 1 1 0;
  min-width: 0;
  margin: 0;
  /* With flex-end the bottoms line up, so a button taller than one line of text
     sits high. Half that difference below the text lines the centres up. */
  padding-bottom: calc((var(--verdict-size) - 1.5em) / 2);
  transition: padding-bottom 0.32s var(--morph);
}
/* Only the limited card puts the message on a row of its own, where there is
   nothing to centre against. The composer no longer affects this: opening it
   must not shift the row above. */
.approval-body.is-wide .approval-msg {
  padding-bottom: 0;
}
.approval-actions {
display: flex;
  flex: none;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
  /* The out-of-requests note shares this row instead of stacking above it. */
  flex-wrap: wrap;
}
/* Only the out-of-requests note forces the row wide. The composer lives outside
   this row, so the buttons never leave the corner they sit in. */
.approval-actions.is-wide {
  flex: 1 0 100%;
}
/* Keep the verdict pill on the right, even when the note wraps. */
.approval-actions .approval-limit + .verdicts {
  margin-left: auto;
}
.approval-actions .btn {
display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 11px 22px;
  border-radius: 11px;
  font-weight: 600;
}
/* ---------- batched route (several asks, one tap) ---------- */
.route {
margin: 14px 0 18px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--surface-2);
  padding: 16px;
}
/* Out-of-requests card: visible, but nothing can be tapped. */
.approval-card.is-limited .approval-dot {
  background: #f59e0b;
}
.approval-limit {
  /* Stays left while the verdict pill is pushed right. */
  margin-right: auto;
  /* Inline in the action row: it takes the free space left of the buttons. */
  flex: 1 1 260px;
  min-width: 0;
  margin: 0;
  padding: 10px 12px;
  border-radius: 10px;
  background: color-mix(in srgb, #f59e0b 12%, transparent);
  border: 1px solid color-mix(in srgb, #f59e0b 40%, var(--line));
  color: #f59e0b;
  font-size: 13px;
}
.btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
.approvals-empty {
text-align: center;
  padding: 64px 0 40px;
}
.approvals-empty-mark {
display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  border: 1px solid var(--line);
  color: var(--green);
  margin-bottom: 14px;
}
.approvals-empty h2 {
font-size: 20px;
  font-weight: 600;
  margin: 0 0 6px;
}
.page-head {
display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
}
.page-title {
font-size: 30px;
  font-weight: 700;
  margin: 0;
  letter-spacing: -0.5px;
}
.page-sub {
color: var(--dim);
  margin: 6px 0 0;
  font-size: 16px;
}
.nav-actions {
display: flex;
  align-items: center;
  gap: 10px;
}
.icon {
display: inline-block;
  vertical-align: -2px;
  flex: none;
}
.icon-btn {
display: inline-grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: 9px;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--dim);
  cursor: pointer;
  padding: 0;
}
.icon-btn:hover {
color: var(--text);
  border-color: var(--hover-border);
}
.icon-btn.sm {
width: 30px;
  height: 30px;
}
/* ---------- buttons ---------- */

.btn {
display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  border-radius: 9px;
  font-weight: 600;
  cursor: pointer;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--text);
  padding: 10px 16px;
  font-size: 14px;
  font-family: inherit;
  transition: border-color 0.12s, background 0.12s;
}
.btn:hover {
border-color: var(--hover-border);
}
.btn.primary {
background: var(--primary-bg);
  color: var(--primary-ink);
  border-color: var(--primary-bg);
  border-radius: 12px;
  padding: 13px 18px;
  font-size: 15px;
}
.btn.primary:hover {
filter: brightness(0.92);
}
.btn.sm {
padding: 8px 13px;
  font-size: 13.5px;
}
.btn.block {
width: 100%;
}
.btn.ghost {
background: transparent;
}
.btn.danger-ghost {
background: transparent;
  color: var(--red);
  border-color: #2a1d20;
}
.btn.danger-ghost:hover {
border-color: var(--red);
}
/* The Answer button: a soft tint carries the meaning and the ink does the
   signalling, so the fill alone defines the button and no border is needed.
   Its hover is declared here too, `.btn:hover` only changes border-color,
   which would flash a grey edge onto a borderless button. */
.btn.answer {
  border-color: transparent;
  transition: transform 0.12s, background 0.12s, border-color 0.12s;
}
/* A small lift, so a decision feels pressable before you commit to it. */
.btn.answer:hover:not(:disabled) {
  transform: translateY(-1px);
}
/*
 * The agent's mark, sitting on its own colour. The mark is forced to white
 * because every chip is a saturated colour in both themes, so one rule covers
 * them all and no per-agent contrast check is needed.
 */
.approval-meta {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}
.agent-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  flex: 0 0 auto;
  border-radius: 8px;
}
.agent-chip img {
  display: block;
  width: 15px;
  height: 15px;
  filter: brightness(0) invert(1);
}
/* Neither verdict: the Answer button opens the free-text composer. */
.btn.answer {
  background: var(--badge-bg);
  color: var(--text);
}
.btn.answer:hover {
  background: var(--badge-bg-hover);
}
/* The three verdicts, as one compact pill of icons rather than three labelled
   buttons, so the action row stops setting the height of every request. Deny
   and Approve wear their verdict as a ring; Answer stays plain, because it
   opens a composer instead of deciding anything. */
.verdicts {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* Closed, this is just three icon buttons sitting in the card's bottom-right
     corner: no surface of their own, so the icons land on the card's own padding
     instead of 6px inside it. The pill only becomes a surface when it opens. */
  padding: 0;
  border-radius: 999px;
  background: none;
  /* The pill grows to full width as it opens rather than being swapped for the
     field. Gaps go to zero so the folded-away verdicts leave no phantom space
     between the chevron and the box. */
  transition: flex-grow 0.32s var(--morph), gap 0.32s var(--morph);
}
/* Pressed: the pill has opened, Deny and Approve have folded away, and the
   message icon is left on the left of a full-width typing field. */
/* Opening changes nothing about the row: the tiles keep their position and only
   the tile that owns the composer brightens, since the line opens underneath. */
.verdicts.is-open .verdict.answer {
  border-color: var(--dim);
}
/* The three verdicts read as tiles rather than bare icons: a bordered surface
   with a real tap area, the same shape at every width. */
.verdict {
  display: inline-grid;
  place-items: center;
  width: 68px;
  height: 40px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--badge-bg);
  color: var(--dim);
  cursor: pointer;
  overflow: hidden;
  transition: width 0.32s var(--morph), opacity 0.2s ease, background 0.12s,
    border-color 0.12s, border-width 0.32s var(--morph),
    transform 0.16s cubic-bezier(0.2, 0.8, 0.3, 1.6);
}
.verdict:hover:not(:disabled) {
  background: var(--badge-bg-hover);
  transform: translateY(-1px);
}
/* Every press answers back before the decision leaves. */
.verdict:active:not(:disabled) {
  transform: scale(0.86);
}
.verdict:disabled {
  opacity: 0.45;
  cursor: default;
}
.verdict.deny {
  color: var(--red);
  border-color: currentColor;
}
.verdict.approve {
  color: var(--green);
  border-color: currentColor;
  background: color-mix(in srgb, var(--green) 12%, var(--badge-bg));
}
.verdict .icon {
  width: 18px;
  height: 18px;
  /* Pressing a thumb turns it, so the press reads as a pointed choice: the
     down thumb swings clockwise, the up thumb back the other way. */
  transition: transform 0.14s cubic-bezier(0.2, 0.8, 0.3, 1.6);
}
.verdict.deny:active:not(:disabled) .icon {
  transform: rotate(20deg);
}
.verdict.approve:active:not(:disabled) .icon {
  transform: rotate(-20deg);
}
/* The answer button is the way in and the way back. */

/* The composer takes the whole row, under the buttons it replaced. */
/* The composer lives inside the pill, so the pill is what expands to hold it:
   it takes the room the folded verdicts gave up, and slides in from the icon. */
/* The composer carries the surface, which is what puts the chevron outside it.
   Padding is set so the box stands exactly as tall as the verdict buttons beside
   it: 34px circle + 4px each side = 42px, and 38 + 4 = 46px on a phone.

   Longhands rather than the shorthand so the parts fail independently - a
   browser that does not know `transition-behavior` drops that one line and still
   fades, where an unknown value inside the shorthand would kill all three. */
.answer-box {
  /* Its own flex line: the row above is never reflowed to make room. */
  flex: 1 0 100%;
  min-width: 0;
  display: flex;
  align-items: flex-start;
  gap: 8px;
  opacity: 1;
  transform: none;
  transition-property: display, opacity, transform;
  transition-duration: 0.32s;
  transition-timing-function: var(--morph);
  transition-behavior: allow-discrete;
}
@starting-style {
  .answer-box {
    opacity: 0;
    transform: translateX(-8px);
  }
}
/* Closing now eases out instead of vanishing: `allow-discrete` holds the box on
   screen until the fade has finished, then drops `display: none`. */
.answer-box.hide {
  opacity: 0;
  transform: translateX(-8px);
}
/*
 * One component: the field and its two buttons share a single bordered box, so
 * Cancel and Send answer read as part of the input rather than as a second row
 * of card actions.
 *
 * `resize: none` is deliberate. The native grip sits in the bottom-right corner
 * (exactly where the buttons now are) and would be both unreachable and
 * covered. The field grows with its text instead, which is all the grip was for.
 */
/* One rounded rectangle for the input, with the send as its own square beside
   it rather than nested inside. */
.answer-field {
  display: flex;
  align-items: center;
  flex: 1 1 0;
  min-width: 0;
  min-height: 42px;
  padding: 8px 12px;
  border-radius: 12px;
  background: var(--surface-2);
  border: 0;
}
.answer-field:focus-within {
  border-color: var(--hover-border);
}
.answer-input {
  display: block;
  flex: 1 1 0;
  min-width: 0;
  max-height: 180px;
  background: none;
  border: 0;
  outline: none;
  color: var(--text);
  padding: 0;
  font-size: 15px;
  font-family: inherit;
  line-height: 1.45;
  resize: none;
  overflow-y: auto;
}
.answer-input::placeholder {
  color: #5c6478;
}
/* Send sits at the end of the line as a grey circle. Not a .btn, so the global
   .btn:disabled fade never touches it - the empty state is a quieter circle
   rather than a faded one. */
/* Square, not round: the send is the field's height with its corners taken
   down, so the pair read as one control. */
.answer-send {
  flex: none;
  display: inline-grid;
  place-items: center;
  width: 42px;
  height: 42px;
  padding: 0;
  border: 0;
  border-radius: 12px;
  background: var(--text);
  color: var(--surface);
  cursor: pointer;
  transition: background 0.12s, color 0.12s,
    transform 0.16s cubic-bezier(0.2, 0.8, 0.3, 1.6);
}
.answer-send .icon {
  width: 18px;
  height: 18px;
}
.answer-send:hover:not(:disabled) {
  background: color-mix(in srgb, var(--text) 90%, var(--surface));
}
.answer-send:active:not(:disabled) {
  transform: scale(0.9);
}
/* The arrow stays black on its white square even with nothing typed yet: the
   grey version read as a different, lesser button. */
.answer-send:disabled {
  background: var(--text);
  color: var(--surface);
  cursor: default;
}

.answer-actions .btn:active:not(:disabled) {
  transform: scale(0.95);
}
.danger {
color: var(--red);
}
/* ---------- cards ---------- */

/* ---------- guides under the channel list ---------- */
/**
 * Three across, in a 640px shell, so each card is about 190px: the icon goes on its own
 * line, the title is sized to survive that width, and the arrow is parked in the corner
 * instead of competing with the text for horizontal room.
 */
.guides {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
  width: 100%;
  margin: 18px 0 8px;
  text-align: left;
}

.guide-card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 9px;
  padding: 13px 13px 14px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--text);
  text-decoration: none;
  transition: border-color 0.15s ease, transform 0.15s ease;
}

.guide-card:hover {
  border-color: var(--hover-border);
}

.guide-card:active {
  transform: translateY(1px);
}

.guide-icon {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  border: 1px solid var(--line);
  border-radius: 9px;
  color: var(--text);
}

.guide-icon svg {
  width: 16px;
  height: 16px;
}

.guide-text {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  min-width: 0;
  text-align: left;
}

.guide-title {
  font-size: 13.5px;
  font-weight: 600;
  line-height: 1.3;
}

.guide-sub {
  font-size: 11.5px;
  line-height: 1.4;
  color: var(--dim);
}

.guide-ext {
  position: absolute;
  top: 12px;
  right: 12px;
  display: inline-flex;
  color: var(--dim);
}

.guide-ext svg {
  width: 13px;
  height: 13px;
}

.card {
background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 16px;
  margin-top: 14px;
}
.label {
font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--dim);
  margin-bottom: 7px;
}
/* ---------- forms ---------- */

.field {
width: 100%;
  background: var(--surface-2);
  border: 1px solid transparent;
  border-radius: 14px;
  color: var(--text);
  padding: 14px 16px;
  font-size: 15px;
  font-family: inherit;
  transition: border-color 0.12s, background 0.12s;
}
.field::placeholder {
color: #5c6478;
}
.field:focus {
outline: none;
  border-color: var(--hover-border);
}
textarea.field {
resize: vertical;
  min-height: 58px;
}
.field-group {
margin-bottom: 16px;
}
.field-label {
display: block;
  font-size: 13px;
  font-weight: 600;
  color: var(--dim);
  margin-bottom: 7px;
}
.input-row {
display: flex;
  gap: 10px;
  align-items: center;
}
.input-row .field {
flex: 1;
}
.inline-form {
margin: 24px 0 8px;
  padding: 16px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--surface);
}
.inline-form-row {
display: flex;
  gap: 10px;
  align-items: center;
}
.inline-form-row .field {
flex: 1;
}
.inline-form .field {
margin-bottom: 8px;
}
.inline-form .btn {
margin-bottom: 0;
}
/* Sign in with an account key: the field sits directly above its button, so it
   needs the breathing room a label would otherwise provide. */
#authKeyRow .field {
  margin-bottom: 14px;
}
.prompt {
background: var(--field-bg);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 12px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12.5px;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
  color: var(--text);
  margin: 8px 0 12px;
}
/* ---------- sections (open / closed / settings) ---------- */

.section-label {
font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--dim);
  text-transform: uppercase;
  margin: 32px 0 4px;
}
.channel-list {
margin-top: 10px;
}
.closed-section {
margin-top: 36px;
}
.section-toggle {
width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: none;
  border: 0;
  cursor: pointer;
  color: var(--text);
  font-size: 15px;
  font-weight: 600;
  padding: 8px 2px;
}
.section-toggle .chev {
color: var(--dim);
  display: inline-flex;
  transition: transform 0.15s;
}
.section-toggle.open .chev {
transform: rotate(180deg);
}
.closed-list {
margin-top: 2px;
}
.closed-list > .card {
margin-top: 10px;
}
/* ---------- channel ---------- */

.channel {
border-top: 1px solid var(--line-soft);
  padding-top: 24px;
  margin-top: 24px;
}
.channel:first-child {
border-top: 0;
  padding-top: 0;
  margin-top: 0;
}
/* ---------- requests table ---------- */

.req-table {
margin-top: 4px;
}
.req-row {
display: grid;
  grid-template-columns: 1.4fr 1.8fr auto auto;
  align-items: center;
  gap: 14px;
  padding: 10px 0;
  border-bottom: 1px solid var(--line-soft);
}
/* The history's agent cell is where you name the agent: the label edits in
   place, and a pencil fades in to say so. */
.req-agent-name {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  max-width: 100%;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.req-agent-name:hover {
  color: var(--text);
}
.req-agent-pencil {
  display: inline-flex;
  flex: none;
  color: var(--dim);
  opacity: 0;
  transition: opacity 0.15s ease;
}
.req-agent-name:hover .req-agent-pencil,
.req-agent-name:focus-visible .req-agent-pencil {
  opacity: 1;
}
.req-agent .share-name-input {
  width: 100%;
}
.req-head {
color: var(--dim);
  font-weight: 500;
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 7px 0;
}
.req-head .right {
text-align: right;
}
.req-title {
font-weight: 600;
font-size: 12.5px;
line-height: 1.32;
}
.req-desc {
color: var(--dim);
font-size: 11.5px;
line-height: 1.38;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.req-status {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
min-width: 0;
}
.req-actions {
display: flex;
  gap: 8px;
  justify-content: flex-end;
}
.req-actions .btn {
display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 7px 14px;
  border-radius: 9px;
  font-weight: 600;
  font-size: 13px;
}
.table-empty {
color: var(--dim);
  padding: 16px 0;
}
/* ---------- history ---------- */

.history-row {
display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  background: none;
  border: 0;
  cursor: pointer;
  color: var(--dim);
  font-size: 17px;
  padding: 22px 0 0;
}
.history-label {
display: inline-flex;
  align-items: center;
  gap: 8px;
}
.history-row .chev {
display: inline-flex;
}
.history-body {
padding: 10px 0;
}
/* ---------- channel settings ---------- */

.settings {
display: flex;
  flex-direction: column;
  max-width: 600px;
}
.settings-block {
padding: 22px 0;
  border-top: 1px solid var(--line-soft);
}
.settings-block:first-child {
border-top: 0;
  padding-top: 6px;
}
.settings-title {
margin: 0;
  font-size: 15px;
  font-weight: 600;
}
.settings-desc {
margin: 4px 0 14px;
  color: var(--dim);
  font-size: 13.5px;
}
.settings-row {
display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.settings-row + .settings-desc {
margin-top: 4px;
}
.settings-row .btn {
flex: none;
}
.settings-block .field {
width: 100%;
}
.settings-block .btn.primary,
.settings-block .btn.danger-ghost {
margin-top: 14px;
}
/* Invite row: the address field and its send button read as one control.
   (Declared after the rule above so the shared 14px top margin is cancelled.) */
.settings-invite {
display: flex;
  align-items: center;
  gap: 8px;
}
.settings-invite .field {
flex: 1 1 auto;
  min-width: 0;
}
.settings .settings-invite .btn {
flex: none;
  margin: 0;
  white-space: nowrap;
}
/* The agent's key id, next to its "agent" label: what a decision card shows. */
.settings-meta .settings-id {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11.5px;
  color: var(--dim);
  background: var(--surface-2);
  border-radius: 6px;
  padding: 2px 6px;
}
.settings-head {
display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 600;
  font-size: 15px;
  margin-bottom: 10px;
}
.settings-head .btn,
.settings-head .icon-btn {
margin-left: auto;
}
/* channel URL (devices page) */
.settings-url {
display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 12px;
}
.settings-link {
flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 13px;
  color: var(--dim);
  text-decoration: none;
  background: var(--surface-2);
  border-radius: 12px;
  padding: 11px 14px;
}
.settings-link:hover {
color: var(--text);
}
/* ---------- paired devices (Devices page) ---------- */
.device {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 14px 0;
  border-bottom: 1px solid var(--line-soft);
}
.device:last-child {
  border-bottom: 0;
}
/* On a phone the revoke button ate the space the facts needed. A grid keeps the icon
   beside the name and drops only the button onto its own line. */
@media (max-width: 520px) {
  .device {
    display: grid;
    grid-template-columns: 38px 1fr;
    align-items: center;
    gap: 10px 14px;
  }
  .device .device-body {
    grid-column: 2;
  }
  .device .device-icon {
    align-self: start;
  }
  .device .device-revoke {
    grid-column: 2;
    justify-self: start;
  }
}
.device-icon {
  flex: none;
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border-radius: 11px;
  border: 1px solid var(--line);
  background: var(--surface-2);
  color: var(--dim);
}
.device-body {
  flex: 1;
  min-width: 0;
}
.device-top {
  display: flex;
  align-items: center;
  gap: 9px;
}
.device-name {
  margin: 0;
  font-size: 15px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.device-name:hover {
  color: var(--text);
}
.device-rename {
  flex: 1;
  min-width: 0;
  max-width: 340px;
  padding: 4px 8px;
  border-radius: 8px;
  border: 1px solid var(--hover-border);
  background: var(--field-bg);
  color: var(--text);
  font: inherit;
  font-size: 15px;
  font-weight: 600;
}
.device-rename:focus {
  outline: none;
  border-color: var(--primary-bg);
}
.device-state {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 1px 8px;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: 12px;
  color: var(--dim);
}
.device-state.on {
  color: var(--green);
  border-color: color-mix(in srgb, var(--green) 40%, var(--line));
}
.device-state.on::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: var(--green);
}
.device-facts,
.device-times {
  margin: 5px 0 0;
  font-size: 13px;
  color: var(--dim);
}
.device-times {
  font-size: 12px;
  opacity: 0.85;
}
.device-unknown {
  opacity: 0.7;
}
.device-revoke {
  flex: none;
}
.pair-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
}
.pair-hint {
  margin: 0;
  color: var(--dim);
  font-size: 13px;
}
.pair-expiry {
  color: var(--text);
}
/* Small print under the /invite/<token> card. */
.invite-foot {
  max-width: 520px;
  /* Aligned with the card above it, which sits at the left edge of .wrap. */
  margin: 14px 0 0;
  color: var(--dim);
  font-size: 12.5px;
  line-height: 1.5;
}
/* The "add requests" link inside a locked request's note, plus the flash that
   points at the Buy item when /app#topup is followed. */
.link-btn {
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent);
  font: inherit;
  text-decoration: underline;
  cursor: pointer;
}
.menu-item.flash {
  animation: menu-flash 1.3s ease-in-out 2;
  border-radius: 9px;
}
@keyframes menu-flash {
  0%,
  100% {
    background: transparent;
  }
  50% {
    background: color-mix(in srgb, var(--accent) 22%, transparent);
  }
}

/* ---------- lists of people / agents / devices ---------- */
.settings-list {
display: flex;
  flex-direction: column;
}
.settings-item {
display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 0;
  border-bottom: 1px solid var(--line-soft);
  font-size: 14px;
}
.settings-item:last-child {
border-bottom: 0;
}
.settings-name {
flex: 1;
  min-width: 0;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 8px;
  color: var(--text);
  font: inherit;
  font-weight: 600;
  font-size: 14px;
  padding: 5px 7px;
}
.settings-name:hover {
border-color: var(--line);
}
.settings-name:focus {
outline: none;
  border-color: var(--hover-border);
  background: var(--surface-2);
}
.settings-name.static,
.settings-name.static:hover {
padding-left: 0;
  border-color: transparent;
}
.settings-meta {
color: var(--dim);
  font-size: 12.5px;
  white-space: nowrap;
}
.settings-actions {
display: flex;
  gap: 4px;
}
.settings-empty {
color: var(--dim);
  font-size: 13.5px;
  padding: 8px 0;
}
/* An empty list should look finished, not like a box that failed to load. */
.empty-state {
  padding: 30px 18px 28px;
  text-align: center;
}
.empty-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: 1px solid var(--line);
  border-radius: 50%;
  color: var(--dim);
  margin-bottom: 10px;
}
.empty-title {
  color: var(--text);
  font-size: 14px;
  font-weight: 600;
  margin: 0 0 4px;
}
.empty-copy {
  margin: 0 auto;
  max-width: 36ch;
  font-size: 13px;
  line-height: 1.45;
}
.settings-danger .settings-title {
color: var(--red);
}
.icon-btn.danger {
color: var(--red);
}
.icon-btn.danger:hover {
border-color: var(--red);
}
/* requests table bits still used by channel history */
.hist-table .req-row {
grid-template-columns: auto 1.2fr 1.6fr 0.5fr 1fr auto;
  gap: 10px;
}
.req-agent {
display: flex;
flex-direction: column;
gap: 1px;
min-width: 0;
font-size: 12px;
line-height: 1.28;
}
.req-sub {
color: var(--dim);
  font-size: 10.5px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.req-by {
-webkit-line-clamp: 1;
}
.company {
margin: 10px 0 14px;
}
.company > summary {
cursor: pointer;
  color: var(--dim);
  font-size: 13.5px;
  padding: 6px 0;
}
.company .field {
margin-bottom: 8px;
}
.topup-summary {
margin-bottom: 12px;
}
.topup-summary .kv {
display: flex;
  justify-content: space-between;
  font-size: 14px;
  padding: 4px 0;
}
/* ---------- account card ---------- */

.account-card {
margin-top: 26px;
  padding: 24px 22px;
  border: 1px solid var(--line);
  border-radius: 16px;
  background: var(--surface);
}
.account-id {
display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 12px;
}
.account-handle {
font-size: 20px;
  font-weight: 700;
  margin: 0;
  letter-spacing: -0.3px;
}
.account-handle.is-key {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0;
  word-break: break-all;
}
.account-edit {
margin: 14px 0 0;
  display: flex;
}
.pill {
font-size: 12px;
  padding: 3px 10px;
  border-radius: 999px;
  background: transparent;
  border: 1px solid var(--line);
  color: var(--dim);
}
.pill.free {
border-color: rgba(34, 197, 94, 0.4);
  color: var(--green);
}
.claim-form {
margin: 20px auto 4px;
  padding-top: 20px;
  border-top: 1px solid var(--line-soft);
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-width: 520px;
}
.key-line,
.connect-line {
margin-top: 20px;
}
.key-label,
.connect-label {
display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--dim);
  margin-bottom: 7px;
}
.key-field {
display: flex;
  align-items: center;
  gap: 8px;
}
.key-value {
flex: 1;
  min-width: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 14px;
  background: var(--field-bg);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 9px 12px;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.key-blur {
filter: blur(5px);
  user-select: none;
  transition: filter 0.15s;
}
.key-value.revealed .key-blur {
filter: none;
  user-select: auto;
}
.handle-status {
font-size: 13px;
  color: var(--dim);
  min-width: 76px;
}
.handle-status.ok {
color: var(--green);
}
.handle-status.bad {
color: var(--red);
}
.signout {
margin-top: 34px;
}
.limit-cta {
margin-top: 16px;
  padding: 14px;
  border: 1px solid rgba(248, 113, 113, 0.35);
  border-radius: 12px;
  background: rgba(248, 113, 113, 0.06);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  text-align: center;
  font-size: 14px;
}
/* ---------- sign in ---------- */

#paired {
opacity: 0;
  transition: opacity 0.22s ease;
}
#paired.ready {
opacity: 1;
}
.signin {
max-width: 420px;
  margin: 9vh auto 0;
  padding: 0 4px 48px;
  display: flex;
  flex-direction: column;
  gap: 18px;
  text-align: left;
}
.signin-title {
margin: 0;
  font-size: 28px;
  font-weight: 700;
  letter-spacing: -0.02em;
}
.signin-sub {
margin: -10px 0 4px;
  color: var(--dim);
  font-size: 15px;
}
.signin-form {
display: flex;
  flex-direction: column;
  gap: 16px;
  text-align: left;
}
.field-label {
display: block;
  color: var(--text);
  font-size: 14px;
  font-weight: 600;
  margin-bottom: 8px;
}
.signin-form .field {
width: 100%;
}
.signin-form .field-label:not(:first-child) {
margin-top: 16px;
}
/* Small helper copy under a field or button. */
.form-hint {
margin: -6px 0 0;
  color: var(--dim);
  font-size: 13.5px;
}
/* "Didn't get it? Resend code" - the honest control on a host that emails codes. */
.code-resend {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 10px;
  font-size: 13px;
}
.code-resend .linklike {
  padding: 0;
  color: var(--text);
  font-weight: 500;
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.code-resend .linklike[disabled] {
  color: var(--dim);
  text-decoration: none;
  cursor: default;
}
/* Dev-only code helper, shown only to a request from this machine. */
.code-tools {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-top: 14px;
  flex-wrap: wrap;
}
.code-tools code {
color: var(--text);
  font-size: 13px;
}
.code-tools .btn {
display: inline-flex;
  align-items: center;
  gap: 6px;
}
.linklike {
background: none;
  border: 0;
  color: var(--dim);
  font-size: 13px;
  font-family: inherit;
  cursor: pointer;
  padding: 4px;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.linklike:hover {
color: var(--text);
}
.signin-foot {
margin-top: 12px;
  padding-top: 24px;
  border-top: 1px solid var(--line-soft);
  text-align: center;
  color: var(--dim);
  font-size: 14px;
}
.signin-foot a {
color: var(--dim);
}
.signin-foot a:hover {
color: var(--text);
}
/* Back out of the anonymous (account-key) login to the email one. */
.signin-back {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: none;
  color: var(--dim);
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s;
}
.signin-back:hover {
  color: var(--text);
  border-color: var(--dim);
}
/* ---------- misc ---------- */

.grid-2 {
display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}
.kv-list {
display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 10px;
}
.kv {
display: flex;
  justify-content: space-between;
  font-size: 14px;
}
.kv span:first-child {
color: var(--dim);
}
.code {
background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 12px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 13px;
  word-break: break-all;
}
.qr {
display: block;
  width: 200px;
  height: 200px;
  margin: 12px auto 0;
  background: #fff;
  border-radius: 12px;
}
.conn-list {
display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 12px;
}
.conn {
display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 0;
  border-bottom: 1px solid var(--line-soft);
}
/* ---------- channel card ---------- */
/* A channel is a line item, not a card: no fill, no border box, just the
   content and a hairline, so a long list reads as one clean column. */
.channel-card {
  /* No hairline between rows: spacing alone separates the channels, so the list
     reads as two channels rather than as a table of them. */
  border: 0;
  border-radius: 0;
  background: none;
  padding: 18px 2px;
  margin-top: 0;
  /* Land below the sticky nav when scrolled to. */
  scroll-margin-top: 76px;
}
/* The settings view is still a panel, not a line item: it replaces a row in
   place. The new-channel form is deliberately not in here - it is an ordinary
   row of the same list, so it inherits the plain .channel-card styling below
   with no surface, no box and no inset. */
.channel-card.settings-card {
  background: var(--surface);
  border: 1px solid var(--line-soft);
  border-radius: 20px;
  padding: 24px;
  margin-top: 20px;
}
/* Clicking a row opens it (its history expands); controls keep their own job. */
.channel-card[data-id]:not(.settings-card) {
  cursor: pointer;
  position: relative;
}
/* The hover tint is its own rounded panel, bleeding past the row box. The card is
   a list row (2px side padding, square corners, hairline underneath), so tinting
   the box itself reads as a cut-off band that stops flush against the text. */
.channel-card[data-id]:not(.settings-card)::after {
  content: "";
  position: absolute;
  inset: -6px -14px;
  border-radius: 12px;
  background: transparent;
  pointer-events: none;
  transition: background 0.15s ease;
}
.channel-card[data-id]:not(.settings-card):hover::after {
  background: color-mix(in srgb, var(--text) 4%, transparent);
}
/* The name stays a double-click-to-rename target, so it doesn't read as the
   row's single-click affordance. */
.channel-card .ch-name {
  cursor: default;
}
.ch-head {
display: flex;
  align-items: center;
  gap: 10px;
}
.ch-head-text {
flex: 1;
  min-width: 0;
}
/* One-line header: dot · name · copy pill · … · gear · chevron */
.ch-copy {
flex: none;
  white-space: nowrap;
}
.ch-spacer {
flex: 1;
  min-width: 8px;
}
.ch-fold {
flex: none;
  margin-left: 0;
}
.ch-history {
margin-top: 16px;
}
@media (max-width: 620px) {
/* Keep the row readable on phones: the pill drops its label to an icon. */
  .ch-copy .ch-copy-label {
display: none;
}
.ch-copy {
padding: 9px 11px;
}
}
.ch-kicker {
display: block;
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--dim);
  margin-bottom: 4px;
}
.ch-name {
margin: 0;
  font-size: 22px;
  font-weight: 600;
  display: flex;
  align-items: center;
  gap: 10px;
}
/* Accent swatch. Pickr replaces the element with its own wrapper, so style
   the generated button rather than the placeholder. */
.ch-head .pickr,
.new-channel .pickr {
  flex: none;
  line-height: 0;
}
/* The theme paints the colour on ::after and the checkerboard on ::before, so
   keep both and just round them into the dot. */
.ch-head .pcr-button,
.new-channel .pcr-button {
  width: 22px;
  height: 22px;
  padding: 0;
  border-radius: 999px;
  background: none;
  box-shadow: 0 0 0 2px var(--line);
}
.ch-head .pcr-button::before,
.ch-head .pcr-button::after,
.new-channel .pcr-button::before,
.new-channel .pcr-button::after {
  border-radius: 999px;
}
/* Pickr ships `transition: background .3s` on the swatch, so it animates in
   from transparent on every render, visible as a colour fade-in. The colour
   is already known when the swatch is built, so paint it immediately. */
.ch-head .pcr-button,
.ch-head .pcr-button::before,
.ch-head .pcr-button::after,
.new-channel .pcr-button,
.new-channel .pcr-button::before,
.new-channel .pcr-button::after {
  transition: none;
}
/* The colour anchor is invisible: Pickr draws its own swatch button. */
.ch-color {
  display: none;
}
/* Card title + URL, both double-click editable. */
.ch-head .ch-name {
  cursor: text;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ch-inline-input {
  background: var(--surface-2);
  border: 1px solid var(--hover-border);
  border-radius: 10px;
  color: var(--text);
  font: inherit;
  font-weight: 600;
  padding: 4px 8px;
  min-width: 140px;
  max-width: 320px;
}
.ch-inline-input:focus {
  outline: none;
}
/* The prompt caret sits INSIDE the copy pill, so a stray hover never
   triggers it, only a deliberate click on the caret does. */
.ch-copy {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}
.ch-copy-caret {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-left: 1px;
  cursor: pointer;
}
.ch-copy-caret svg {
  transition: transform 0.2s ease;
}
.ch-copy-caret.open svg {
  transform: rotate(180deg);
}

/* `…` menu on a channel card. */
.ch-more-wrap {
  position: relative;
  flex: none;
}
.ch-more-menu {
  top: 34px;
  right: 0;
  left: auto;
  min-width: 190px;
}

/* Share: who is in the channel, plus the invite link. */
.ch-share-wrap {
  position: relative;
  flex: none;
}
.ch-share-menu {
  top: 34px;
  right: 0;
  left: auto;
  min-width: 320px;
  padding: 0;
}
.share-body {
  padding: 4px 0;
  max-height: 320px;
  overflow-y: auto;
}
.share-section + .share-section {
  border-top: 1px solid var(--line-soft);
}
.share-label {
  padding: 9px 14px 3px;
  color: var(--dim);
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.share-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 6px 14px;
  font-size: 13.5px;
}
/* The name and its pencil are one cluster: the pencil sits immediately to the
   right of the name, and only the key id + remove stay out at the far right. */
.share-name {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}
.share-name-text {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* While renaming, the label (pencil included) steps aside for the input. */
.share-name.is-editing,
.req-agent-name.is-editing {
  display: none;
}
.share-row .share-name-input {
  flex: 1 1 auto;
  width: auto;
}
.share-meta {
  flex: none;
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--dim);
  font-size: 12px;
}
.share-meta .icon-btn {
  margin-left: 2px;
}
.share-name-input {
  width: 100%;
  min-width: 0;
  padding: 3px 7px;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: var(--surface-2);
  color: var(--text);
  font: inherit;
  font-size: 13.5px;
}
.share-name-input:focus {
  outline: 2px solid var(--accent);
  outline-offset: -1px;
}
.share-empty {
  padding: 8px 14px 10px;
  color: var(--dim);
  font-size: 12.5px;
}
/* The invite link is the first thing in the panel, where the old
   "Who's in …" title used to be, and it borrows the account menu's field
   shape (`menu-field` + `menu-key`): URL on the left, icons on the right. */
.share-link code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.share-note {
  min-width: 0;
  color: var(--dim);
  font-size: 12.5px;
}
.share-url {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11.5px;
  color: var(--text);
}

/* New-channel row: the name is typed straight onto the row. No box, no fill -
   just an underline, so it reads like the channel names below until it has
   text of its own. */
/* No box and no underline: the row already reads as a field, and the caret is
   the only affordance it needs. The base .ch-name-input rules are shared with
   the rename fields, so they are overridden here rather than changed. */
.new-channel .ch-name-input {
  flex: 1 1 auto;
  max-width: 420px;
  background: none;
  border: 0;
  padding-left: 0;
  padding-right: 0;
}
.new-channel .ch-name-input:hover,
.new-channel .ch-name-input:focus {
  background: none;
  border-color: transparent;
}
.ch-name-input {
  flex: 0 1 340px;
  min-width: 0;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 10px;
  color: var(--text);
  font: inherit;
  font-size: 20px;
  font-weight: 600;
  padding: 5px 8px;
}
.ch-name-input::placeholder {
  color: var(--dim);
  font-weight: 500;
}
.ch-name-input:hover {
  border-color: var(--line);
}
.ch-name-input:focus {
  outline: none;
  border-color: var(--hover-border);
  background: var(--surface-2);
}
.ch-qr {
width: 84px;
  height: 84px;
  flex-shrink: 0;
  border-radius: 10px;
  background: #fff;
  padding: 6px;
}
/* Shown in place of the pairing QR when the account secret key is missing. */
.ch-qr-note {
display: flex;
  align-items: flex-start;
  gap: 6px;
  max-width: 132px;
  font-size: 12px;
  line-height: 1.25;
  color: var(--dim);
  border: 1px dashed var(--line);
  border-radius: 10px;
  padding: 10px;
}
.ch-qr-note .icon {
flex-shrink: 0;
  margin-top: 1px;
}
.ch-connect {
display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 18px;
}
.ch-connect .btn.pill {
white-space: normal;
  text-align: left;
}
/* The settings view replaces a row's contents in place and closes back to it. */
.ch-close {
margin-left: auto;
  flex: none;
}
/* "Show the full prompt", same control as the marketing site. */
.prompt-toggle {
margin-top: 14px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 0;
  background: none;
  padding: 0;
  font-family: inherit;
  font-size: 13px;
  font-weight: 500;
  color: var(--dim);
  cursor: pointer;
}
.prompt-toggle:hover {
color: var(--text);
}
.prompt-toggle .chev {
display: inline-flex;
  transition: transform 0.2s ease;
}
.prompt-toggle[aria-expanded="true"] .chev {
transform: rotate(180deg);
}
.prompt-toggle .pt-hide {
display: none;
}
.prompt-toggle[aria-expanded="true"] .pt-show {
display: none;
}
.prompt-toggle[aria-expanded="true"] .pt-hide {
display: inline;
}
.ch-prompt {
margin: 12px 0 4px;
  padding: 14px;
  user-select: text;
  cursor: text;
  border: 1px solid var(--line-soft);
  border-radius: 12px;
  background: var(--surface-2);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12.5px;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
  color: var(--text);
}
.ch-prompt .key-blur {
filter: blur(4px);
}
/* Sits under the full prompt, revealed by the same chevron: a way through to the
   other templates instead of dead-ending on one snippet. */
.ch-prompt-more {
display: inline-flex;
  align-items: center;
  gap: 5px;
  margin: 0 0 6px 2px;
  font-size: 12px;
  color: var(--dim);
  text-decoration: none;
}
.ch-prompt-more:hover {
color: var(--text);
  text-decoration: underline;
}
.ch-prompt.revealed .key-blur {
filter: none;
}
.ch-sec {
width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  background: none;
  border: 0;
  border-top: 1px solid var(--line-soft);
  padding: 16px 0 6px;
  margin-top: 14px;
  color: var(--text);
  font: inherit;
  font-weight: 600;
  font-size: 15px;
  cursor: pointer;
}
.ch-sec .chev {
color: var(--dim);
}
.session-row {
display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 0;
}
.grow {
flex: 1;
}
.sub {
color: var(--dim);
  font-size: 13px;
}
.online {
color: var(--green);
}
.offline {
color: var(--dim);
}
.req-answer {
display: block;
max-width: 100%;
color: var(--dim);
font-size: 11px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.status-pill {
font-size: 11px;
padding: 2px 8px;
border-radius: 999px;
border: 1px solid var(--line);
color: var(--dim);
}
.status-pill.approved {
color: var(--green);
  border-color: rgba(34, 197, 94, 0.4);
}
.status-pill.answered {
  color: #7dd3fc;
  border-color: rgba(125, 211, 252, 0.4);
}
:root[data-theme="light"] .status-pill.answered {
  color: #0369a1;
  border-color: rgba(3, 105, 161, 0.35);
}
.status-pill.denied {
color: var(--red);
  border-color: rgba(248, 113, 113, 0.4);
}
.status-pill.pending {
color: var(--text);
}
.status-pill.expired {
color: var(--dim);
}
.revoke {
background: none;
  border: 0;
  color: var(--dim);
  cursor: pointer;
  font-size: 13px;
}
.revoke:hover {
color: var(--red);
}
.spinner {
display: inline-block;
  width: 14px;
  height: 14px;
  border: 2px solid var(--line);
  border-top-color: var(--text);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}
@keyframes spin {

  to {
    transform: rotate(360deg);
  }

}
/* A spinner inside a button sits beside its label, never under it. */
.btn .btn-spinner {
  margin-left: 8px;
  vertical-align: -2px;
}
.toast {
position: fixed;
  left: 50%;
  bottom: 26px;
  transform: translateX(-50%) translateY(20px);
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 11px;
  padding: 11px 18px;
  font-size: 14px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s, transform 0.2s;
  z-index: 60;
}
.toast.show {
opacity: 1;
  transform: translateX(-50%) translateY(0);
}
/* ---------- top nav ---------- */

.nav {
position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 28px;
  /* Full-bleed bar, content aligned to the page column (`--shell-*`). */
  padding: 14px
    max(var(--shell-pad), calc((100% - var(--shell-max)) / 2 + var(--shell-pad)));
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line-soft);
}
.nav-brand {
display: flex;
  align-items: center;
  gap: 9px;
  font-weight: 700;
  font-size: 16px;
  color: var(--text);
}
.nav-brand .mark {
width: 22px;
  height: 22px;
  display: block;
  /* The mark is a black outline; invert it for the dark surface. */
  filter: invert(1);
}
/* Docs link carries a small icon. */
.nav-docs {
display: inline-flex;
  align-items: center;
  gap: 6px;
}
.nav-docs svg {
color: var(--dim);
}
/* Inline notice card, claim prompt and usage warnings share one style.
   Kept deliberately neutral (no accent colour): it is a label, a line of copy
   and at most one action. */
.app-notices {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}
.app-notices:empty {
  display: none;
}
.app-notices:not(:empty) {
  margin-bottom: 10px;
}
.app-notice {
  display: flex;
  align-items: center;
  gap: 16px;
  width: 100%;
  max-width: var(--notice-max);
  margin: 0 auto 18px;
  padding: 18px 18px;
  border: 1px solid var(--line);
  border-radius: 20px;
  background: var(--surface-2);
}
.app-notices .app-notice {
  margin-bottom: 0;
}
/* Just the mark, no box around it: the notification itself is the container.
   The icon is drawn large and thin, so it reads as an outline rather than a blob. */
.app-notice-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  color: var(--text);
  padding-left: 4px;
}
.app-notice-icon .icon {
  width: 30px;
  height: 30px;
  stroke-width: 1.35;
}
.app-notice-text {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
  margin-right: auto;
}
/* The action keeps its own size and never breaks its label - the copy beside it
   is what shrinks and wraps. On phones the rule below gives it its own row. */
.app-notice .btn {
  flex: none;
  white-space: nowrap;
}
.app-notice-text strong {
  font-size: 15.5px;
  font-weight: 600;
  color: var(--text);
}
.app-notice-text span {
  font-size: 13.5px;
  color: var(--dim);
}
@media (max-width: 520px) {
  .app-notice {
    flex-wrap: wrap;
    row-gap: 10px;
  }
  /* Icon + copy share the first row; the action claims its own full-width row. */
  .app-notice-text {
    /* basis 0 so the copy shrinks beside the icon instead of wrapping below it */
    flex: 1 1 0;
    min-width: 0;
    margin-right: 0;
  }
  .app-notice .btn {
    flex: 0 0 100%;
    justify-content: center;
  }
}
.nav-actions {
display: flex;
  align-items: center;
  gap: 18px;
}
.nav-link {
color: var(--dim);
  font-size: 14px;
}
.nav-link:hover {
color: var(--text);
}
/* ---------- channel search (sits above the first channel) ---------- */
/*
 * The channel toolbar. The search field is deliberately narrow: a full-width
 * field for a handful of channels reads as the page's main input, which it is
 * not. "New channel" shares the row and sits at the far end of it.
 *
 * The button is sized here rather than inherited, because it used to live in
 * the nav and was sized by `.nav-actions .btn.pill`.
 */
.channel-tools {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 26px;
}
.channel-search {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 0 1 220px;
  /* Without this the field refuses to shrink past the input's intrinsic
     ~20-character width, and the row pushes the page sideways on a phone. */
  min-width: 0;
  padding: 0 14px;
  height: 40px;
  border-radius: 999px;
  background: var(--surface-2);
}
.channel-tools > .btn {
  flex: none;
  margin-left: auto;
  height: 40px;
  padding: 0 18px;
  border-radius: 999px;
  font-size: 13.5px;
  justify-content: center;
}
.channel-search-icon {
  color: var(--dim);
  display: inline-flex;
  flex: none;
}
.channel-search-input {
  flex: 1;
  min-width: 0;
  background: none;
  border: 0;
  outline: none;
  color: var(--text);
  font: inherit;
  font-size: 14px;
}
.channel-search-input::placeholder {
  color: #5c6478;
}
.channel-search-clear {
  display: inline-flex;
  align-items: center;
  flex: none;
  padding: 2px;
  border: 0;
  background: none;
  color: var(--dim);
  cursor: pointer;
}
.channel-search-clear:hover {
  color: var(--text);
}
.btn.pill {
border-radius: 999px;
  padding: 9px 16px;
}
/* Nav pills (New channel / Pair device) are deliberately compact. They need
   their own rule because `.pill` (the small plan badge further up) sets a
   font-size, and `.btn.pill`'s padding outranks `.btn.sm`'s, so `sm` alone did
   not actually shrink the New channel button. Scoped to the nav so the card's
   "Copy prompt for agent" pill keeps its size.
   `min-width` makes the pair *equal* boxes: the labels differ in width by ~12px
   ("New channel" 131 vs "Pair device" 119), which read as the solid one being
   bigger (owner, 2026-10-15: "New channel button can be equally big as Pair
   device button, cmon"). min-width, not width, so a longer label at a big font
   scale still grows instead of clipping. */
.nav-actions .btn.pill {
  padding: 6px 13px;
  font-size: 13px;
  min-width: 132px;
  justify-content: center;
}
.avatar-wrap {
position: relative;
}
.avatar {
display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 999px;
  /* Comes from the cached account (set by the inline head script) or from
     /api/v1/me. Never a placeholder colour, so there is nothing to flash. */
  background: var(--avatar-bg, var(--surface-2));
  color: #fff;
  border: 0;
  cursor: pointer;
  font-weight: 700;
  font-size: 15px;
}
/* The initial is drawn by CSS so the inline head script can paint it before
   app.js has fetched anything. */
.avatar::after {
  content: var(--avatar-initial, "");
}
/* Stay invisible until the account is known: cached before first paint, or
   live once /api/v1/me resolves. `visibility` keeps the slot so the nav
   does not shift when it appears. */
html:not([data-avatar]) .avatar {
  visibility: hidden;
}
.menu {
position: absolute;
  right: 0;
  top: 44px;
  z-index: 30;
  min-width: 290px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 6px;
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.5);
}
.menu-field {
padding: 10px 12px;
  border-bottom: 1px solid var(--line-soft);
}
.menu-label {
display: block;
  color: var(--dim);
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  margin-bottom: 5px;
}
.menu-key {
display: flex;
  align-items: center;
  gap: 6px;
}
.menu-key code {
flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12px;
  color: var(--text);
}
.menu-head {
padding: 10px 12px 12px;
  border-bottom: 1px solid var(--line-soft);
  margin-bottom: 6px;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
}
.menu-id {
display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.menu-id #menuName {
font-weight: 600;
}
.menu-id #menuEmail {
overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12.5px;
}
/* Status pill: Anonymous / Free / Token. */
.badge {
flex: none;
  margin-top: 1px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 4px 10px;
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  color: var(--dim);
  white-space: nowrap;
}
.badge.free {
color: #34c759;
  border-color: color-mix(in srgb, #34c759 45%, var(--line));
  background: color-mix(in srgb, #34c759 12%, transparent);
}
.badge.token {
color: #6c8cff;
  border-color: color-mix(in srgb, #6c8cff 45%, var(--line));
  background: color-mix(in srgb, #6c8cff 12%, transparent);
}
.menu-item {
display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  text-align: left;
  background: none;
  border: 0;
  color: var(--text);
  padding: 10px 12px;
  border-radius: 9px;
  cursor: pointer;
  font-size: 14px;
  font-family: inherit;
}
.menu-item:hover {
background: var(--surface-2);
}
.menu-item svg {
color: var(--dim);
}
.menu-item.danger {
color: var(--red);
}
.menu-item.danger svg {
color: var(--red);
}
.menu-progress {
display: flex;
  align-items: center;
  gap: 8px;
}
.menu-progress .bar {
flex: 1;
  height: 6px;
  border-radius: 999px;
  /* A visible track, so 0/20 still reads as a progress bar. --line is too
     close to --surface here and the empty bar disappeared. */
  background: var(--track);
  overflow: hidden;
}
.menu-progress .bar span {
display: block;
  height: 100%;
  width: 0;
  background: #6c8cff;
  border-radius: 999px;
  transition: width 0.3s ease;
}
.menu-pct {
color: var(--dim);
  font-size: 12px;
  white-space: nowrap;
}
/* ---------- pair-a-device modal ---------- */
.modal {
position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(4px);
}
.modal-card {
width: 100%;
  max-width: 340px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 20px;
  padding: 20px;
  text-align: center;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);
}
.modal-head {
display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 16px;
  font-weight: 600;
  font-size: 15px;
}
.modal-title {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  color: var(--text);
  text-decoration: none;
  font-weight: 700;
  font-size: 15px;
}
.modal-title strong {
  font-weight: 700;
}
.modal-title svg {
  color: var(--dim);
  transition: transform 0.15s ease, color 0.15s ease;
}
.modal-title:hover svg {
  color: var(--text);
}
.modal-title:hover svg:last-child {
  transform: translateX(2px);
}
.qr-lg {
display: block;
  width: 220px;
  height: 220px;
  margin: 0 auto;
  background: #fff;
  border-radius: 14px;
  padding: 10px;
}
.qr-lg[src=""] {
visibility: hidden;
}
.modal-note {
color: var(--dim);
  font-size: 13.5px;
  margin: 14px 0 12px;
}
/* ---------- empty state ---------- */

.empty-state {
text-align: center;
  padding: 14vh 0 0;
}
.empty-state h2 {
font-size: 20px;
  font-weight: 600;
  margin: 0 0 8px;
}
.empty-actions {
display: flex;
  gap: 10px;
  justify-content: center;
  flex-wrap: wrap;
}
/* ---------- docs ---------- */

.docs {
display: grid;
  grid-template-columns: 240px 1fr;
  gap: 48px;
  max-width: 1080px;
  margin: 0 auto;
  padding: 40px 26px 120px;
}
.docs-side {
position: sticky;
  top: 78px;
  align-self: start;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.docs-group {
font-size: 12px;
  font-weight: 600;
  color: var(--dim);
  margin: 18px 0 6px;
}
.docs-group:first-child {
margin-top: 0;
}
.docs-link {
color: var(--dim);
  padding: 7px 12px;
  border-radius: 9px;
  font-size: 14px;
}
.docs-link:hover {
color: var(--text);
}
.docs-link.active {
background: var(--surface);
  color: var(--text);
}
.docs-main {
min-width: 0;
}
.docs-main h1 {
font-size: 30px;
  font-weight: 700;
  margin: 0 0 18px;
}
.docs-main h2 {
font-size: 21px;
  font-weight: 700;
  margin: 40px 0 12px;
}
.docs-main p,
.docs-main li {
color: var(--text);
  line-height: 1.65;
}
.docs-main p {
margin: 12px 0;
}
.docs-main ul {
padding-left: 20px;
}
.docs-main code {
background: var(--surface-2);
  border-radius: 6px;
  padding: 2px 6px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 13px;
}
.docs-main pre {
position: relative;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 16px 44px 16px 16px;
  overflow-x: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 13px;
  line-height: 1.6;
  white-space: pre-wrap;
  word-break: break-word;
  margin: 14px 0;
}
.docs-main pre code {
background: none;
  padding: 0;
}
.pre-copy {
position: absolute;
  top: 10px;
  right: 10px;
  background: none;
  border: 0;
  color: var(--dim);
  cursor: pointer;
  padding: 4px;
}
.pre-copy:hover {
color: var(--text);
}
.docs-main a {
color: var(--text);
  text-decoration: underline;
  text-decoration-color: var(--line);
}
.docs-main .btn.pill {
margin: 8px 0 4px;
}
@media (max-width: 820px) {
.docs {
grid-template-columns: 1fr;
    gap: 20px;
}
.docs-side {
position: static;
    flex-direction: row;
    flex-wrap: wrap;
}
.docs-group {
width: 100%;
}
}
/* ---------- responsive ---------- */

@media (max-width: 720px) {
.wrap {
padding: 22px 18px 80px;
}
.page-title {
font-size: 24px;
}
.req-row {
grid-template-columns: 1fr;
    gap: 4px;
    padding: 16px 0;
}
.req-head {
display: none;
}
.req-actions {
justify-content: flex-start;
    margin-top: 12px;
}

}
/* Phones (iPhone 12 mini = 375px): keep the nav on one line. */
@media (max-width: 480px) {
.nav {
gap: 12px;
    /* Only the vertical padding shrinks: the horizontal side keeps the column
       alignment so the logo still lines up with the content on phones. */
    padding-top: 12px;
    padding-bottom: 12px;
}
.nav-brand {
font-size: 15px;
    gap: 7px;
}
/* The mark stays; the wordmark and the top Docs link live in the menu. */
  .nav-brand-text,
  .nav-docs {
display: none;
}
.nav-links {
gap: 14px;
    margin-left: 0;
}
.nav-link {
font-size: 13px;
}
.nav-actions {
gap: 10px;
}
.ch-qr {
width: 68px;
    height: 68px;
}
  /* "New channel" keeps its size and the search gives up the room. */
  .channel-tools > .btn {
    padding-left: 14px;
    padding-right: 14px;
  }
  /* Phones keep the verdicts on a row of their own, full width, so the offset
     that centres them beside the text is dead space here. */
  .approval-body {
    gap: 12px;
  }
  .approval-body .approval-msg {
    padding-bottom: 0;
  }
  .approval-actions {
    flex: 1 0 100%;
    gap: 6px;
  }
  /* On a phone the tiles share the row but stop growing past a comfortable
     width, so the extra space becomes gap rather than a wider tile. */
  .verdicts {
    width: 100%;
    gap: 10px;
    /* Centred, not spread: space-between pushes the leftover room into the gaps,
       which read as three disconnected tiles on a wider phone. */
    justify-content: center;
  }
  .verdict {
    flex: 1 1 0;
    width: auto;
    max-width: 88px;
    height: 42px;
  }
  .answer-field {
    min-height: 48px;
  }
  .answer-send {
    width: 48px;
    height: 48px;
  }
  /* Smaller text and tighter padding, so the title holds one line and the
     message fills the width instead of wrapping raggedly. */
  .approval-card {
    padding: 20px;
  }
  .approval-title {
    font-size: 17px;
  }
  .approval-msg {
    font-size: 14px;
  }
  /* The channel row: four 10px gaps, an 8px spacer and three 34px buttons left
     the name 68px for 73px of text at 375px. Tightening the chrome around it
     wins the name its full width back. */
  .ch-head {
    gap: 8px;
  }
  .ch-name-input {
    font-size: 18px;
  }
  .icon-btn {
    width: 30px;
    height: 30px;
  }
  .ch-spacer {
    min-width: 0;
  }
}

/*
 * Very narrow phones (~300-350px): the row cannot hold the colour swatch, a
 * readable channel name and four controls side by side. Left to shrink, the
 * name is squeezed to a few pixels and the tools row runs off the edge, so
 * both are allowed to wrap instead.
 */
@media (max-width: 360px) {
  .ch-head {
    flex-wrap: wrap;
    row-gap: 8px;
  }
  /* The swatch and the name share the first line, and the 30px is exactly the
     swatch plus its gap, so the controls are forced onto the next line instead
     of the name being the thing that gives. */
  .ch-name {
    flex: 1 1 calc(100% - 30px);
    min-width: 0;
  }
}
.pane-note {
display: flex;
  align-items: center;
  gap: 8px;
  color: var(--dim);
  font-size: 15px;
  margin: 16px 0;
}
.conn-table {
display: flex;
  flex-direction: column;
}
.conn-trow,
.conn-row {
display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 0;
  border-bottom: 1px solid var(--line-soft);
  font-size: 14px;
}
.conn-trow:last-child,
.conn-row:last-child {
border-bottom: 0;
}
.conn-tname {
font-weight: 600;
}
.conn-name {
flex: 1;
  min-width: 0;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 8px;
  color: var(--text);
  font: inherit;
  font-weight: 600;
  font-size: 14.5px;
  padding: 4px 6px;
}
.conn-name:hover {
border-color: var(--line);
}
.conn-name:focus {
outline: none;
  border-color: var(--line);
  background: var(--surface);
}
.conn-sub,
.conn-tstatus {
color: var(--dim);
  font-size: 13px;
  white-space: nowrap;
}

/* The limit line sits in the card's top-right corner and takes no space in the body row, so the
   verdicts stay centred on the last line of the message exactly as they are with no limit. */
.approval-card {
  position: relative;
}
/* One colour for the whole line, and underlined so it reads as the link it is. The link's own
   colour made the words look like a different thing from the sentence they belong to; its
   underline did not, so that is kept. */
.approval-limit .link-btn,
.approval-limit .link-btn:hover,
.approval-limit .link-btn:focus {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  cursor: pointer;
}
.approval-limit {
  position: absolute;
  top: 24px;
  right: 24px;
  margin: 0;
  padding: 0;
  background: none;
  border: 0;
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: #f59e0b;
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
}

/* ---------- App notices, in the modern register ----------
   One card that reads like a product surface rather than a system message: a dark
   gradient panel with a soft glow in the corner, a light pill for the action, and
   enough padding that the copy and the button stop crowding each other. */
.app-notice {
  position: relative;
  gap: 16px;
  padding: 18px 20px;
  border-radius: 18px;
  /* Written out per state rather than mixed from one custom property: the panel has
     to be unmistakably orange (or red), and a var() inside a colour function is one
     more thing that can fail silently and hand the panel back to a neutral surface.
     The base is warm near-black, so nothing cool shows through where the glow fades. */
  border: 1px solid rgba(249, 115, 22, 0.3);
  background-color: #17120e;
  background-image:
    radial-gradient(120% 180% at 0% 0%, rgba(249, 115, 22, 0.28), transparent 55%),
    radial-gradient(90% 140% at 100% 0%, rgba(249, 115, 22, 0.12), transparent 62%),
    linear-gradient(180deg, #221a13, #150f0b);
  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.34), inset 0 1px 0 rgba(255, 255, 255, 0.05);
  overflow: hidden;
}
/* Out of requests: the same panel, red rather than orange. */
.app-notice.is-out {
  border-color: rgba(239, 68, 68, 0.34);
  background-color: #190d0d;
  background-image:
    radial-gradient(120% 180% at 0% 0%, rgba(239, 68, 68, 0.3), transparent 55%),
    radial-gradient(90% 140% at 100% 0%, rgba(239, 68, 68, 0.12), transparent 62%),
    linear-gradient(180deg, #241312, #150b0a);
}
/* Light mode, amber state: the same treatment as the red one below, so the pair
   reads as one component in two severities rather than two different cards. */
:root[data-theme="light"] .app-notice {
  border-color: rgba(234, 88, 12, 0.26);
  background-color: #fff8f1;
  background-image:
    radial-gradient(120% 180% at 0% 0%, rgba(234, 88, 12, 0.12), transparent 55%),
    radial-gradient(90% 140% at 100% 0%, rgba(234, 88, 12, 0.06), transparent 62%),
    linear-gradient(180deg, #fff1e3, #fffaf5);
  box-shadow: 0 1px 2px rgba(16, 24, 40, 0.06);
}
:root[data-theme="light"] .app-notice::before {
  background: linear-gradient(90deg, transparent, rgba(234, 88, 12, 0.3), transparent);
}
:root[data-theme="light"] .app-notice .app-notice-text strong {
  color: #8a4519;
}
:root[data-theme="light"] .app-notice .app-notice-text span {
  color: rgba(138, 69, 25, 0.72);
}
:root[data-theme="light"] .app-notice .app-notice-icon {
  color: #ea580c;
}
:root[data-theme="light"] .app-notice .btn.pill.primary {
  background: #ea580c;
  color: #fff;
  box-shadow: 0 6px 16px rgba(234, 88, 12, 0.24);
}
:root[data-theme="light"] .app-notice .btn.pill.primary:hover {
  background: #c2410c;
}
/* Light mode, red state: same construction as the amber card above, with the alarm
   colour instead. Both states are light cards on a light page, so neither reads as a
   slab dropped onto it. */
:root[data-theme="light"] .app-notice.is-out {
  border-color: rgba(220, 38, 38, 0.26);
  background-color: #fdf3f3;
  background-image:
    radial-gradient(120% 180% at 0% 0%, rgba(220, 38, 38, 0.10), transparent 55%),
    radial-gradient(90% 140% at 100% 0%, rgba(220, 38, 38, 0.05), transparent 62%),
    linear-gradient(180deg, #fcedec, #fdf6f6);
  box-shadow: 0 1px 2px rgba(16, 24, 40, 0.06);
}
:root[data-theme="light"] .app-notice.is-out::before {
  background: linear-gradient(90deg, transparent, rgba(220, 38, 38, 0.3), transparent);
}
:root[data-theme="light"] .app-notice.is-out .app-notice-text strong {
  color: #8c1c1c;
}
:root[data-theme="light"] .app-notice.is-out .app-notice-text span {
  color: rgba(140, 28, 28, 0.72);
}
:root[data-theme="light"] .app-notice.is-out .app-notice-icon {
  color: #dc2626;
}
/* The white pill only works on the dark panel; on a pale red one it needs the
   weight of the colour itself. */
:root[data-theme="light"] .app-notice.is-out .btn.pill.primary {
  background: #dc2626;
  color: #fff;
  box-shadow: 0 6px 16px rgba(220, 38, 38, 0.24);
}
:root[data-theme="light"] .app-notice.is-out .btn.pill.primary:hover {
  background: #b91c1c;
}
/* A hairline of light along the top edge, which is what makes the panel look lifted
   off the page rather than drawn onto it. */
.app-notice::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(255, 198, 145, 0.3), transparent);
}
/* No tile behind the mark. It stands on its own, drawn large and thin at the
   panel's own left edge, so it reads as an outline rather than a filled badge. */
.app-notice-icon {
  width: auto;
  height: auto;
  border-radius: 0;
  color: #eef1f7;
  background: none;
  box-shadow: none;
}
/* Out of requests: the mark follows the panel into red. A warm white triangle on a
   red card reads as a different warning than the amber one; the colour carries the
   severity, and the copy only explains it. */
.app-notice.is-out .app-notice-icon {
  color: #f87171;
}
/* Lucide draws on a 24-unit grid with a 2-unit stroke. At 38px that stroke is
   scaled by ~1.58, so a nominal 1.25 still renders near 2px. 0.85 lands at about
   1.3px on screen, which is what reads as a thin outline rather than a drawn one. */
.app-notice-icon .icon { width: 38px; height: 38px; stroke-width: 0.85; }
/* The exclamation is set as a pair: the bar reads as the stroke of a letter, the
   dot as its punctuation. At 38px the triangle is ~1.3px, the bar ~2.1px and the
   dot ~2.7px across, which is the ratio that stops the dot looking like a blob. */
.app-notice-icon .icon .alert-bar { stroke-width: 1.3; }
.app-notice-icon .icon .alert-dot { stroke-width: 1.7; }
.app-notice-text { gap: 4px; }
/* The panel is dark in both themes, so its type is light in both as well. Reading
   these from --text meant the headline turned near-black on a dark panel the moment
   the app was in light mode. */
.app-notice-text strong {
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: #f8efe6;
}
.app-notice-text span {
  font-size: 13px;
  line-height: 1.45;
  color: rgba(248, 239, 230, 0.66);
}
.app-notice .btn.pill.primary {
  background: #fff;
  color: #0b0d12;
  border-color: transparent;
  font-weight: 600;
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.28);
  transition: transform 140ms ease, box-shadow 140ms ease;
}
.app-notice .btn.pill.primary:hover {
  transform: translateY(-1px);
  box-shadow: 0 10px 22px rgba(0, 0, 0, 0.34);
  background: #fff;
}
@media (prefers-reduced-motion: reduce) {
  .app-notice .btn.pill.primary:hover { transform: none; }
}

/* ---------- boot loader ----------
   The shell paints before it knows whether it is signed in, so this holds the space
   with a quiet spinner instead of a blank column. It is removed, not hidden, the
   moment connect() picks a branch. */
.boot-loading {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 120px 0;
  color: var(--dim);
  font-size: 13px;
}
.boot-spinner {
  width: 18px;
  height: 18px;
  border-radius: 999px;
  border: 2px solid color-mix(in srgb, var(--dim) 32%, transparent);
  border-top-color: var(--text);
  animation: boot-spin 700ms linear infinite;
}
@keyframes boot-spin {
  to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
  .boot-spinner { animation-duration: 2.4s; }
}
