/* --------------------------------------------------------------------------
   console.css — the tenant console and the platform panel.

   Loaded AFTER the template kit's compiled build, and it defines no palette of
   its own: every colour and size below comes from the kit's tokens, so the
   three surfaces — marketing site, console, platform panel — cannot drift.

   Two constraints shape this file.

   1. `script-src 'self'`, and never `'unsafe-inline'`. These pages render
      text a stranger wrote, and inline is the only way an escaping slip in a
      mail body could execute anything — so the protection that mattered when
      this said `'none'` is intact. The kit's JavaScript is still not loaded
      here: anything it would have done is either reachable in CSS
      (`<details>`, `:hover`, `:focus-within`) or dropped. The one file these
      panels do load, console.js, may only touch presentation and may never be
      the only way to do something.
   2. `style-src 'self'`. Everything, the font included, is served from this
      origin — see the note in inter-tight.css about what used to happen.
   -------------------------------------------------------------------------- */

:root {
  --ink:        var(--color-secondary);
  /* ⚠ USED IN THREE PLACES AND DEFINED IN NONE. .text-head, .bignum and the
     workspace name on the picker all say `color: var(--ink-head)`, and no
     sheet ever declared it — so the property was invalid and every one of them
     fell back to the INHERITED colour. On a light page that is the body ink
     and looks entirely correct, which is why it survived; the moment one of
     those elements sat inside something with a colour of its own it took that
     instead. The workspace picker on a dark ground rendered the workspace name
     white on a white card, and it had simply vanished. */
  --ink-head:   var(--color-secondary);
  --violet:     var(--color-primary-500);
  --ground:     #ffffff;
  --ground-2:   var(--color-background-3);
  --ground-3:   var(--color-background-4);
  /* ⚠ THE FALLBACK IS LOAD-BEARING, and its absence is why both panels looked
     washed out. --color-background-13 is defined in the kit's full main.css
     and NOT in kit-tokens.css, the trimmed build these panels actually load.
     A var() with no fallback that resolves to nothing makes the whole
     declaration invalid, so `body { background: var(--page) }` fell back to
     transparent — and every white card sat on a white canvas with nothing to
     lift it. The marketing sheet already carries this exact fallback for this
     exact token; the console was the copy that did not. */
  --page:       var(--color-background-13, #f1f4f9);
  --stroke:     var(--color-stroke-1);
  --stroke-2:   var(--color-stroke-3);
  /* ⚠ Raised from 60/45, then again from 74/56. At 74/56, --muted over the
     page ground measured ~3.4:1 at the 12px it is most often used — below the
     4.5:1 WCAG AA floor for text that small. 82/70 clears it on white and on
     every ground token here, and keeps three distinct steps in the same order. */
  --body:       color-mix(in srgb, var(--color-secondary) 82%, transparent);
  --muted:      color-mix(in srgb, var(--color-secondary) 70%, transparent);

  --ok:   #2f7d52;  --ok-wash:   rgba(47,125,82,.12);
  --warn: #8a5e10;  --warn-wash: rgba(138,94,16,.13);
  --bad:  #a3342f;  --bad-wash:  rgba(163,52,47,.12);

  --r-sm: 10px;  --r-md: 14px;  --r-lg: 20px;
  --shadow: 0 1px 2px rgba(18,22,31,.04), 0 12px 32px -18px rgba(18,22,31,.20);
  --sans: "Inter Tight", ui-sans-serif, system-ui, -apple-system, sans-serif;
  --ease: cubic-bezier(.22,.68,.28,1);
  --bounce: cubic-bezier(.34, 1.56, .64, 1);
  --shell: 1290px;   /* the kit's own container width */
  --side:  248px;    /* the sidebar; see the .side block below */
}

*, *::before, *::after { box-sizing: border-box; }
@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }

body {
  margin: 0; background: var(--page); color: var(--body);
  font-family: var(--sans); font-size: var(--text-tagline-1); line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}
/* The kit's type, but not its margins. Zeroing every heading margin left a
   section title sitting hard against the paragraph above it, so a page of
   several sections read as one undifferentiated column. screens.css already
   sets a rhythm for on-premise; the hosted panels keep it. */
h1, h2, h3 { color: var(--ink); font-weight: 500; letter-spacing: -.02em; line-height: 1.2; }
h1 { font-size: var(--text-heading-4); margin: 0 0 .25em; }
h2 { font-size: var(--text-heading-6); margin: 2.25rem 0 .5em; }
h3 { font-size: var(--text-tagline-1); margin: 1.5rem 0 .4em; }
h1:first-child, h2:first-child, h3:first-child { margin-top: 0; }
p { margin: 0; }
a { color: var(--ink); }

.shell { max-width: var(--shell); margin: 0 auto; padding: 0 1.5rem; }

/* ── the shell: a sidebar and a column ──────────────────────────────────────
   The navigation used to run along the top, which put a hard ceiling on it:
   at a common laptop width six links and a "More" menu was already the most
   that fit, so five screens lived behind a menu and the rest competed for one
   line of space. A column has as much room as it needs, so every screen is
   visible at once, grouped by when you'd want it, with the label beside the
   icon rather than instead of it.
   Below 1000px the same markup reflows into a top strip — see the media query
   at the end of this block — because a fixed column is most of a phone.       */
.app { display: grid; grid-template-columns: var(--side) 1fr; min-height: 100vh; }
.col { min-width: 0; display: flex; flex-direction: column; }
/* ⚠ screens.css sets `main { max-width: 1120px; margin: 0 auto }` for the
   on-premise admin, where <main> is a block child of <body>. Here it is a
   column flex item, and an auto margin on a flex item turns off stretch — so
   main sized to its own content and every screen sat at a different width,
   drifting left and right as you moved between them. The column owns the
   width now; the shell inside it does the centring. */
.col main { flex: 1; max-width: none; margin: 0; padding: 2rem 0 4rem; }
.col .shell { max-width: 1120px; padding: 0 2.5rem; }

/* ⚠ NO `overflow` ON THIS ELEMENT. It used to carry `overflow-y: auto`, to
   scroll a long navigation, and that one property was the whole of the
   horizontal scrollbar that appeared under the sidebar the moment the
   workspace switcher was opened.
   `overflow-y: auto` does not leave the other axis alone: CSS promotes an
   `overflow-x: visible` to `auto` whenever the other axis is not visible. So
   asking for a vertical scroller silently asks for a horizontal one too - and
   the switcher's menu is 18rem hanging out of a 15.5rem column, which is the
   54px it could then be scrolled sideways to reach.
   The scrollbar was the lesser half of it. A scroll container also clips, so
   the menu was being cut off at the sidebar's edge and scrolling was the only
   way to see the rest of a dropdown that is meant to sit over the page.
   The scroll belongs on .side-nav: it is the part that is actually long, and
   the only part with nothing absolutely positioned inside it. Which is what
   the narrow layout further down already does, for the reason it states. */
.side {
  position: sticky; top: 0; align-self: start; height: 100vh; overflow: visible;
  background: var(--ground); border-right: 1px solid var(--stroke);
  display: flex; flex-direction: column; gap: 1rem; padding: 1.15rem .8rem 1rem;
  z-index: 40;
}
.side-top { display: flex; flex-direction: column; gap: .7rem; }
.side-top-row { display: flex; align-items: center; gap: .4rem; }
.brand { display: flex; align-items: center; gap: .6rem; font-weight: 500; color: var(--ink);
         text-decoration: none; letter-spacing: -.02em; padding: 0 .35rem; }
/* ⚠ SCOPED TO THE SIDEBAR ROW, NOT ON .brand ITSELF. .brand is reused
   everywhere the wordmark appears, including .split-art's dark panel on
   sign-in — and .split-art is a flex COLUMN. flex: 1 1 auto on a plain
   .brand rule made the logo a growing flex item in that column too,
   stretching it to fill the column's leftover height and pushing it down
   off the top edge where it belongs. Only .side-top-row's own .brand needs
   to shrink-and-grow beside the pause toggle; nowhere else does. */
.side-top-row .brand { flex: 1 1 auto; min-width: 0; }

/* ── the compact pause toggle, beside the brand ──────────────────────────
   ⚠ WAS A FULL-WIDTH ROW AT THE FOOT OF THE NAV: a status word, a dot and a
   labelled button. "Subtle and compact" here means the colour and the icon
   ARE the report now, the same fact the dot always carried, with no text
   riding along - aria-label carries the word for anyone not reading
   colour, and the tooltip below carries it for anyone hovering, same as
   any other icon-only control on this page. */
.side-pause-mini { flex: none; position: relative; }
.side-pause-mini button {
  display: flex; align-items: center; justify-content: center;
  width: 1.9rem; height: 1.9rem; border-radius: 50%;
  border: 1.5px solid #16a34a; background: color-mix(in srgb, #16a34a 10%, transparent);
  color: #16a34a; cursor: pointer; padding: 0;
  transition: background .18s var(--ease), border-color .18s var(--ease), transform .18s var(--ease);
}
.side-pause-mini button svg { width: .85rem; height: .85rem; }
.side-pause-mini button:hover { transform: scale(1.08); }
.side-pause-mini.is-paused button {
  border-color: #f59e0b; background: color-mix(in srgb, #f59e0b 16%, transparent); color: #b45309;
}
/* ⚠ CSS-only tooltip, content: attr() reading straight off data-tip - no
   script, and no wait for the browser's own native title delay (a second
   or more, easy to miss on a button this small). Shown on hover AND
   keyboard focus, so it is not a mouse-only affordance.
   ⚠ OPENS LEFT, NOT DOWN. This button sits in .side-top-row, one flex row
   above .switcher (the workspace picker) with only .side-top's own .7rem
   gap between the two rows - nowhere near the ~2rem a dropped-down tooltip
   needs, so it used to land ON TOP of the switcher instead of in clear
   space below the button. .brand is flex:1 1 auto in this same row (see
   the note beside that rule) and its own content is left-aligned within
   the box that gives it, which leaves genuine empty sidebar between the
   wordmark and this button - real room, in the one direction this
   control's own row actually has any to spare. */
.side-pause-mini button[data-tip]::after {
  content: attr(data-tip);
  position: absolute; top: 50%; right: calc(100% + .6rem); left: auto;
  transform: translateY(-50%) translateX(4px);
  padding: .35rem .65rem; border-radius: .4rem;
  background: var(--ink); color: #fff; font-size: .74rem; font-weight: 500;
  white-space: nowrap; box-shadow: var(--shadow); z-index: 90;
  opacity: 0; pointer-events: none;
  transition: opacity .15s var(--ease), transform .15s var(--ease);
}
.side-pause-mini button[data-tip]:hover::after,
.side-pause-mini button[data-tip]:focus-visible::after {
  opacity: 1; transform: translateY(-50%) translateX(0);
}
@media (prefers-reduced-motion: reduce) {
  .side-pause-mini button[data-tip]::after { transition: opacity .15s var(--ease); transform: translateY(-50%); }
}
.brand .mark { width: 2rem; height: 2rem; border-radius: 10px; display: grid; place-items: center;
               background: var(--ink); color: #fff; font-size: var(--text-tagline-3); flex: none; }
.brand span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Which panel this is. It used to borrow `.pill.person`, which is a solid
   dark capsule — full width in a column, directly above the nav, it read as
   the selected item rather than as a label. */
.side-tag { align-self: flex-start; margin-left: .35rem; padding: .16rem .5rem;
            border: 1px solid var(--stroke); border-radius: 999px; background: var(--ground-2);
            color: var(--body); font-size: var(--text-tagline-3); line-height: 1.4; }

/* Takes the height left over and scrolls inside it, so the brand, the
   switcher, the pause control and the account row keep their places and the
   column as a whole never needs a scrollbar. min-height:0 because a flex item
   refuses to shrink below its content without it, and the scroll would simply
   never engage. */
.side-nav { display: flex; flex-direction: column; gap: .1rem;
            flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.side-group { margin: 1rem .6rem .25rem; font-size: var(--text-tagline-3); letter-spacing: .04em;
              text-transform: uppercase; color: var(--muted); }
.side-group:first-child { margin-top: 0; }
/* ⚠ .side-settings ALONGSIDE .side-nav a THROUGHOUT THIS BLOCK, not nested
   under it. It used to be the last row inside <nav>, styled entirely by
   being a .side-nav descendant; pinning it under the AI connector box
   moved the markup outside <nav>, and every rule below that named
   ".side-nav a" stopped matching it in the same moment. Extending the
   selector list is what a plain rename of one class cannot do here - the
   icon sizing, hover ground, active state and focus ring are shared with
   nine other rows, and duplicating all of it under a second selector is
   the two of them drifting apart the first time only one gets edited. */
.side-nav a, .side-settings {
  display: flex; align-items: center; gap: .6rem; padding: .5rem .6rem;
  border-radius: var(--r-sm); text-decoration: none; color: var(--body);
  font-size: var(--text-tagline-2); position: relative; white-space: nowrap;
}
.side-nav a > *, .side-settings > * { position: relative; }
/* The kit's row hover: a ground that fades in, not a colour that snaps. */
.side-nav a::before, .side-settings::before { content: ""; position: absolute; inset: 0; border-radius: var(--r-sm);
    background: var(--ground-2); opacity: 0; transition: opacity .3s var(--ease); }
.side-nav a:hover::before, .side-settings:hover::before { opacity: 1; }
.side-nav a:hover, .side-settings:hover { color: var(--ink); }
.side-nav a svg, .side-settings svg { width: 1.05rem; height: 1.05rem; flex: none; opacity: .55; }
.side-nav a .lbl, .side-settings .lbl { overflow: hidden; text-overflow: ellipsis; }
.side-nav a.on, .side-settings.on { background: var(--ink); color: #fff; }
.side-nav a.on svg, .side-settings.on svg { opacity: .9; }
.side-nav a:focus-visible, .side-settings:focus-visible { outline: 2px solid var(--violet); outline-offset: 1px; }

/* A count beside "Needs you", so the one screen with work waiting on it says
   so from every other screen. It is deliberately absent, not zero, when there
   is nothing waiting — a badge reading 0 is noise that has to be read. */
.side-nav .tag { position: relative; margin-left: auto; font-size: var(--text-tagline-3); line-height: 1;
                 padding: .2rem .45rem; border-radius: 999px; background: var(--warn-wash);
                 color: var(--warn); font-variant-numeric: tabular-nums; }
.side-nav a.on .tag { background: rgba(255,255,255,.18); color: #fff; }
/* ⚠ A slow ring, not a fast blink. This badge only ever appears for a queue
   with somebody waiting on a person - the review queue, a refund with a
   published three-day answer window - so it is allowed to say "this is
   still open" without shouting it. The ring is decorative once (::before
   would be invisible against a.on's own background), which is why it is a
   real element rather than a pseudo-element here, unlike .mesh-status. */
.side-nav .tag::after {
  content: ""; position: absolute; inset: -3px; border-radius: inherit;
  border: 1px solid var(--warn); pointer-events: none;
  animation: tag-pulse 2.6s ease-out infinite;
}
.side-nav a.on .tag::after { border-color: rgba(255,255,255,.55); }
@keyframes tag-pulse {
  0%   { transform: scale(.85); opacity: .8; }
  75%  { opacity: 0; }
  100% { transform: scale(1.35); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .side-nav .tag::after { animation: none; opacity: 0; }
}

/* ⚠ THE OLD FULL-WIDTH .side-pause/.side-pause-now/.side-pause-dot/
   .side-pause-do BLOCK THAT USED TO LIVE HERE IS GONE, moved and shrunk to
   .side-pause-mini beside .side-top's brand (see up near line 130). Nothing
   needs margin-top:auto to reach the bottom of the column any more:
   .side-nav's own flex: 1 1 auto (a few lines up) already consumes whatever
   space is left, which pushes .side-ai, .side-settings and .side-foot after
   it to the bottom on its own - the same reason removing that one
   margin-top:auto from .side-settings, above, needed nothing added to
   .side-foot to compensate. */
.side-foot { padding-top: .75rem; border-top: 1px solid var(--stroke); }
.side-foot .switcher > summary { width: 100%; justify-content: flex-start; }
.side-foot .switcher .menu { top: auto; bottom: calc(100% + .5rem); }
.side .switcher > summary { width: 100%; }
/* ⚠ min-width:0 FIRST, or the three properties after it do nothing. .grow
   carries min-width:15rem for the wide rows it was written for, and a flex item
   will not shrink below its min-width - so the ellipsis had nothing to act on
   and a long address simply overflowed the chip instead of being trimmed. */
.side .switcher > summary .grow {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.side .switcher > summary::after { margin-left: auto; }

/* Tenant switcher — a <details>, because there is no script to open a menu. */
.switcher { position: relative; }
.switcher > summary {
  list-style: none; cursor: pointer; display: flex; align-items: center; gap: .5rem;
  padding: .45rem .85rem; border: 1px solid var(--stroke); border-radius: 999px;
  font-size: var(--text-tagline-2); color: var(--ink); background: var(--ground);
  transition: border-color .2s var(--ease);
}
.switcher > summary::-webkit-details-marker { display: none; }
.switcher > summary:hover { border-color: var(--ink); }
.switcher > summary .tdot { width: 1.4rem; height: 1.4rem; border-radius: 999px; flex: none;
    background: var(--ink); color: #fff; display: grid; place-items: center; font-size: .6rem; }
.switcher > summary::after { content: ""; width: .5rem; height: .5rem; margin-left: .15rem;
    border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
    transform: rotate(45deg) translate(-2px,-2px); opacity: .5; }
.switcher[open] > summary::after { transform: rotate(225deg) translate(-1px,-1px); }
.switcher .menu {
  position: absolute; top: calc(100% + .5rem); left: 0; min-width: 18rem; z-index: 50;
  background: var(--ground); border: 1px solid var(--stroke); border-radius: var(--r-lg);
  box-shadow: var(--shadow); padding: .4rem;
}
.switcher .menu a { display: flex; align-items: center; gap: .6rem; padding: .6rem;
    border-radius: var(--r-sm); text-decoration: none; color: var(--ink);
    font-size: var(--text-tagline-2); position: relative; }
/* The kit's row hover: a ground that fades in, not a colour that snaps.
   ⚠ ON THE ANCHOR, NOT AN ::before OVERLAY. It was an absolutely-positioned
   ::before at inset 0, with `.menu a > * { position: relative }` to lift the
   contents back above it. That lifts ELEMENT children. The workspace name in
   this menu is a bare text node beside the .tdot span, so it stayed under the
   overlay and the hovered row showed its initials and no name at all.
   A background transition on the anchor fades exactly the same way and cannot
   paint over its own text. */
.switcher .menu a { transition: background .4s var(--ease); }
.switcher .menu a:hover { background: var(--ground-2); }
.switcher .menu .sep { height: 1px; background: var(--stroke); margin: .4rem .2rem; }

/* ⚠ There used to be a `.tabs` strip here, for the narrow-screen version of
   the old top bar, and it ended `@media (min-width: 1101px) { .tabs { display:
   none } }`. But `.tabs` is also a shared component: Settings uses it for its
   seven tab labels and Costs for its date ranges, dressed by screens.css. Two
   different components with one class name, and this file loads second — so on
   every hosted desktop screen the Settings tabs and the Costs range picker
   were `display: none`. Not misaligned. Gone. And because the Settings tabs
   are <label>s driving radios, losing them lost the only way to reach six of
   the seven groups of settings.
   The same collision, with the same cause, as `.btn`. The sidebar has no need
   of a second nav component, so the name goes back to its one owner.          */

/* The sidebar becomes a top strip where a fixed column would be most of the
   screen. Same markup, regrouped: identity and account on one line, the
   navigation scrolling horizontally beneath it. The scroll lives on .side-nav
   and not on .side, because an overflow ancestor clips absolutely positioned
   children, and both menus here are absolutely positioned. */
@media (max-width: 1000px) {
  .app { display: block; }
  .side {
    height: auto; overflow: visible; border-right: 0; border-bottom: 1px solid var(--stroke);
    display: grid; grid-template-columns: minmax(0,1fr) auto;
    /* The drawer button gets its own row, so the nav row below it collapses to
       nothing while it is shut. No "pause" row any more: the control that
       used to need one now lives in "top" - see .side-pause-mini below for
       where it actually ends up on a phone. */
    grid-template-areas: "top foot" "btn btn" "nav nav";
    align-items: center; gap: .35rem .75rem; padding: .6rem 1rem .1rem;
  }
  /* ⚠ A 1px hairline (the border-bottom above) was the only edge this panel
     had against the page it sits on top of when open — both var(--ground),
     which on a phone tall enough for the drawer's full content to fill the
     whole screen read as the menu simply running out and the dashboard
     starting, not as a panel closing over it. A shadow only while open says
     "above", the way the desktop sidebar never has to because it never
     covers what is behind it in the first place. */
  .side:has(.navtoggle:checked) { box-shadow: 0 16px 28px -12px rgba(15, 23, 42, .22); }
  .side-top  { grid-area: top; flex-direction: row; align-items: center; gap: .6rem; min-width: 0; }
  /* ⚠ HIDDEN HERE, VISIBLE EVERYWHERE ELSE. It used to stay in "top" on
     purpose, reasoning that always-on-screen beats drawer-hidden - correct
     for a control reporting a state worth noticing from across the room,
     wrong for how little room a phone header actually has: brand mark,
     workspace switcher and account switcher already compete for it, and
     Pause was the fourth thing fighting for space nothing else could give
     up. Reachable from Settings instead on a phone; the desktop row is
     unaffected. */
  .side-pause-mini { display: none; }
  /* ⚠ screens.css sends `.brand { order: 1 }` on narrow screens, for the
     on-premise bar where the brand belongs after the nav. Here it put the
     workspace switcher ahead of the identity it belongs to. */
  .side-top .brand { order: 0; flex: none; }
  .side-top .switcher { order: 1; }
  /* ⚠ The column gives every control the full width; a row must take it back,
     or the workspace switcher lays itself over the brand beside it. */
  .side .switcher > summary { width: auto; max-width: 100%; }
  .side-top .switcher { min-width: 0; flex: 0 1 auto; }
  .side-tag  { display: none; }
  .side-foot { grid-area: foot; margin-top: 0; padding-top: 0; border-top: 0; }
  /* The address is on the account screen; here the initial is enough, and the
     room it frees is what keeps the workspace name readable. */
  .side-foot .switcher > summary .grow { display: none; }
  .side-foot .switcher .menu { top: calc(100% + .5rem); bottom: auto; left: auto; right: 0; }
  /* ⚠ The same right-anchoring for the workspace menu, and a cap, because
     `left: 0` put 18rem of menu on a switcher that starts two thirds of the
     way across a 320px screen - 37px of it past the edge, which the page then
     grew a horizontal scrollbar to reach. Anchoring right alone is not enough:
     18rem hung off the switcher's right edge runs off the LEFT of the screen
     instead, where nothing can scroll to it at all. So it also has to be
     allowed to be narrower than 18rem here, which is what min-width:0 is for.
     Measured at 320, 360, 375 and 414, with a workspace name long enough to
     push the switcher as far right as it goes. */
  .side-top .switcher .menu {
    left: auto; right: 0; min-width: 0; width: max-content; max-width: calc(100vw - 7rem);
  }
  /* ⚠ A DRAWER, NOT A SCROLLER. This was a horizontally scrolling row of
     pills: everything past the fourth entry was off the edge with nothing to
     say so, and the group headings were dropped because a single row has no
     room for them. It read as a menu that had been cut off.
     Now the button names the screen you are on, and opening it gives the same
     grouped column the desktop has. */
  /* Placement only. Everything about how this button LOOKS, `display`
     included, lives with the rest of the drawer at the foot of this sheet —
     splitting it across two blocks is what let one half override the other. */
  .navbtn   { grid-area: btn; }
  .side-nav { display: none; grid-area: nav; flex-direction: column; gap: .1rem;
              padding: .25rem 0 .6rem; }
  .navtoggle:checked ~ .side-nav { display: flex; }
  /* ⚠ NEITHER NAMES A GRID AREA. Both are auto-placed into the implicit
     grid that follows "nav nav" - correct as long as each spans the full
     row rather than the auto-placement algorithm handing it half of it
     next to whatever comes after. Same open/shut rule as .side-nav itself:
     secondary content, worth a look once the drawer is open, not a reason
     to keep it open like Pause used to be. */
  /* ⚠ .side-ai-wrap, NOT .side-ai: wrapping the box to carry a dismiss
     button (see dismiss_button() beside it) moved .side-ai one level
     deeper, out of direct sibling reach of .navtoggle - a selector naming
     .side-ai here would simply stop matching anything, silently, since a
     general sibling combinator only ever reaches an actual sibling. */
  .side-ai-wrap, .side-settings { display: none; grid-column: 1 / -1; }
  .navtoggle:checked ~ .side-ai-wrap, .navtoggle:checked ~ .side-settings { display: flex; }

  /* The headings come back with the room to show them, which is most of the
     point of opening it rather than scrolling it. */
  .side-group { display: block; margin: .7rem .6rem .2rem; }
  .side-group:first-child { margin-top: .2rem; }
  .col .shell { max-width: var(--shell); margin: 0 auto; padding: 0 1rem; }
}

/* On a phone the wordmark is the least useful thing on that line — the mark
   still says which product this is, and the room goes to the workspace name,
   which is the one thing there that changes. */
@media (max-width: 620px) {
  .side-top .brand span:last-child { display: none; }
}

/* ── buttons: the kit's pill shape, screens.css's hierarchy ───────────────
   ⚠ A plain `.btn` used to be the solid near-black pill, and `.btn.primary`
   comes from screens.css as the brand fill. So on every screen with two
   choices the *secondary* one was the darker, heavier control: an empty
   Needs-you offered "See all mail" in brand violet beside "Try a message" in
   near-black, and the eye went to the wrong one. Plain is the quiet one now,
   and `.primary` — still screens.css's brand fill — is the loud one.        */
.btn { display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
       height: 3rem; padding: .25rem 1.4rem; border-radius: 999px;
       border: 1px solid var(--stroke); background: transparent; color: var(--ink);
       font-size: var(--text-tagline-2); font-weight: 500;
       text-decoration: none; cursor: pointer;
       transition: border-color .2s var(--ease), background .2s var(--ease), transform .25s var(--ease); }
.btn:hover { border-color: var(--ink); }
.btn:focus-visible { outline: 2px solid var(--violet); outline-offset: 2px; }

/* The kit's loudest control: a near-black pill with a lit capsule. Reached
   for by name, so a screen that wants it says so.
   ⚠ THE CAPSULE IS NOW var(--brand), NOT THE KIT'S HARDCODED LIME. The kit
   shipped this fixed to one gradient regardless of which operator was
   looking at it, which is exactly the "lime blob in the wrong colour"
   problem the comment below used to warn about - it just warned about it
   instead of fixing it, because nothing here read the brand colour yet.
   A radial mesh off the one brand variable, same technique as the
   marketing site's own primary button (apps/saas/public/assets/ui.css,
   .btn-v3-primary [data-button-v3-icon]) - two colour-mixes of the SAME
   value, not a second colour to keep in step with a rebrand. */
.btn.solid { background: var(--ink); color: #fff; border-color: var(--ink);
             padding: .25rem 1.25rem .25rem .25rem; }
.btn.solid:hover { transform: translateY(-1px); }
.btn .cap {
  width: 2.75rem; height: 2.75rem; flex: none; border-radius: 999px;
  background:
    radial-gradient(120% 130% at 24% 22%,
      color-mix(in srgb, var(--brand, var(--violet)) 55%, #fff) 0%,
      var(--brand, var(--violet)) 45%,
      color-mix(in srgb, var(--brand, var(--violet)) 78%, var(--ink)) 100%);
  color: #fff; display: grid; place-items: center;
  box-shadow: inset 0 1px 1px rgba(255,255,255,.4),
              0 2px 8px -2px color-mix(in srgb, var(--brand, var(--violet)) 70%, transparent);
  transition: transform .3s var(--ease);
}
.btn .cap svg { width: 1rem; height: 1rem; }
.btn:hover .cap { transform: rotate(10deg) scale(1.08); }

/* ⚠ A mark on a button that does NOT carry .cap. .btn is already a flex row
   with a gap, so a plain inline icon needs a size and nothing else - it
   takes the button's own colour rather than the capsule's. */
.btn > .ico { width: 1.05rem; height: 1.05rem; flex: none; opacity: .85; }
.btn.primary > .ico, .btn.solid > .ico { opacity: 1; }

/* screens.css fills .primary with --brand (the accent colour). The brand's
   loud button is the near-black pill — "Buttons are near-black, not the
   violet" (docs/brand). So .primary IS .solid's colour here, and a page
   reaching for either name gets one consistent control. The violet stays an
   accent: focus rings, links, the needs-you card. */
/* ⚠ A sheen, not a colour change - the marketing site's .btn-go proved this
   out first (see apps/saas/public/assets/ui.css). Two background layers so
   only the POSITION of the first one has to animate: a static colour swap
   on top of `background: var(--ink)` would have fought this file's own
   .btn transition (line ~1453, which already animates `background`) and
   made every hover lag behind the pointer.
   ⚠ THE LIFT NOW CARRIES A BRAND-COLOURED GLOW, at rest as a faint one and
   deeper on hover - not a colour swap on the body (still "near-black, not
   the violet"), a shadow underneath it, same device .btn-go and .cap both
   use: at rest the button already looks lit from the brand rather than
   flat, and hovering only says it more. */
.btn.primary, button.primary {
  background-color: var(--ink); border-color: var(--ink); color: #fff;
  background-image: linear-gradient(105deg, transparent 38%, rgba(255,255,255,.22) 50%, transparent 62%);
  background-repeat: no-repeat; background-size: 260% 100%; background-position: 150% 0;
  border-radius: 999px; height: 3rem; padding: .25rem 1.5rem;
  box-shadow: 0 4px 14px -8px color-mix(in srgb, var(--brand, var(--violet)) 50%, transparent);
  transition: transform .32s var(--bounce, var(--ease)), background-position .55s cubic-bezier(.3,.7,.3,1),
              border-color .18s var(--ease), box-shadow .22s var(--ease);
}
.btn.primary:hover, button.primary:hover {
  background-color: var(--ink); border-color: var(--ink); transform: translateY(-3px) scale(1.02);
  background-position: -60% 0;
  box-shadow: 0 12px 26px -10px color-mix(in srgb, var(--brand, var(--violet)) 60%, transparent);
}
.btn.primary:active, button.primary:active {
  transform: translateY(-1px) scale(.98);
  box-shadow: 0 3px 10px -6px color-mix(in srgb, var(--brand, var(--violet)) 55%, transparent);
  transition-duration: .06s;
}
@media (prefers-reduced-motion: reduce) {
  .btn.primary, button.primary { transition: border-color .18s var(--ease), box-shadow .18s var(--ease); background-position: 0 0; }
  .btn.primary:hover, button.primary:hover, .btn.primary:active, button.primary:active { transform: none; }
}
.btn.ghost { background: transparent; color: var(--ink); border-color: var(--stroke); }
.btn.ghost:hover { border-color: var(--ink); }
.btn.small { height: 2.4rem; font-size: var(--text-tagline-3); padding: .15rem 1rem; }
.btn.danger { color: var(--bad); }
.btn.danger:hover { border-color: var(--bad); background: var(--bad-wash); }

/* ── layout ─────────────────────────────────────────────────────────────── */
main { padding: 2rem 0 4rem; }
.page-head { display: flex; align-items: flex-start; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.5rem; }
.page-head p { color: var(--muted); font-size: var(--text-tagline-2); margin-top: .3rem; }
.page-head .actions { margin-left: auto; display: flex; gap: .5rem; }

.card { background: var(--ground); border: 1px solid var(--stroke); border-radius: var(--r-lg); padding: 1.5rem; }
.grid { display: grid; gap: 1.25rem; }
.g2 { grid-template-columns: repeat(auto-fit, minmax(19rem,1fr)); }

.stat { background: var(--ground); border: 1px solid var(--stroke); border-radius: var(--r-lg); padding: 1.4rem 1.5rem; }
.stat b { display: block; font-size: var(--text-heading-4); font-weight: 500; letter-spacing: -.03em;
          color: var(--ink); font-variant-numeric: tabular-nums; line-height: 1.15; }
.stat span { display: block; font-size: var(--text-tagline-3); color: var(--muted); margin-top: .25rem; }

/* ── the queue ──────────────────────────────────────────────────────────── */
.queue { background: var(--ground); border: 1px solid var(--stroke); border-radius: var(--r-lg); overflow: hidden; }
.queue .q { display: grid; grid-template-columns: 9rem 1fr auto; gap: 1rem; align-items: start;
            padding: 1.15rem 1.4rem; border-bottom: 1px solid var(--stroke);
            text-decoration: none; color: inherit; position: relative; }
.queue .q:last-child { border-bottom: 0; }
.queue .q::before { content: ""; position: absolute; inset: 0; background: var(--ground-2);
                    opacity: 0; transition: opacity .4s var(--ease); }
.queue .q:hover::before { opacity: 1; }
.queue .q > * { position: relative; }
.queue .who { color: var(--ink); font-weight: 500; font-size: var(--text-tagline-2); }
.queue .subj { display: block; margin-top: .1rem; font-size: var(--text-tagline-2); }
.queue .why { display: block; color: var(--muted); font-size: var(--text-tagline-3); margin-top: .3rem; }
.queue .meta { font-size: var(--text-tagline-3); color: var(--muted); text-align: right; white-space: nowrap; }
@media (max-width: 720px) { .queue .q { grid-template-columns: 1fr; } .queue .meta { text-align: left; } }

/* ── plan cards ────────────────────────────────────────────────────────────
   ⚠ A GRID, NOT .queue. A handful of plans is a shelf to compare side by
   side - price against price, allowance against allowance - not a log to
   read top to bottom, which is what .queue's fixed 9rem/1fr/auto row is
   built for. auto-fill/minmax wraps itself at whatever width is available;
   nothing here needs a breakpoint to stop being cramped, on a phone or a
   half-width admin window either one. */
.plan-cards {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 1rem;
}
.plancard {
  position: relative; display: flex; flex-direction: column; gap: .55rem;
  background: var(--ground); border: 1px solid var(--stroke); border-radius: var(--r-lg);
  padding: 1.25rem 1.4rem 1.4rem; box-shadow: var(--shadow);
  transition: border-color .18s var(--ease), transform .18s var(--ease);
}
.plancard:hover { border-color: var(--violet); transform: translateY(-2px); }
/* The one plan an operator marked as the sale: the same brand-coloured top
   edge the reference design uses for its own "most popular" state, rather
   than a second badge fighting the active/inactive pill for the same corner. */
.plancard.is-best { border-color: var(--violet); box-shadow: 0 0 0 1px var(--violet), var(--shadow); }
.plancard-best {
  position: absolute; top: -.6rem; left: 1.25rem;
  background: var(--violet); color: #fff; font-size: var(--text-tagline-3); font-weight: 600;
  padding: .2rem .7rem; border-radius: 999px;
}
.plancard-head { display: flex; align-items: center; justify-content: space-between; }
.plancard-n {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.6rem; height: 1.6rem; border-radius: 50%; background: var(--ground-2);
  color: var(--muted); font-size: var(--text-tagline-3); font-variant-numeric: tabular-nums;
}
.plancard-title { color: var(--ink); font-weight: 650; font-size: 1.05rem; text-decoration: none; }
.plancard-title:hover { color: var(--violet); }
.plancard-key { font-size: var(--text-tagline-3); }
.plancard-price { color: var(--ink); font-weight: 650; font-size: 1.4rem; margin-top: .15rem; }
.plancard-price span { color: var(--muted); font-weight: 500; font-size: var(--text-tagline-3); }
.plancard-stats { list-style: none; margin: .2rem 0 0; padding: 0; display: flex; flex-direction: column; gap: .3rem; }
.plancard-stats li {
  font-size: var(--text-tagline-3); color: var(--muted);
  padding-left: 1.1rem; position: relative;
}
.plancard-stats li::before {
  content: ""; position: absolute; left: .1rem; top: .5em; width: 5px; height: 5px;
  border-radius: 50%; background: var(--stroke-2);
}
.plancard-del { margin-top: auto; padding-top: .6rem; }

.pill { display: inline-flex; align-items: center; justify-content: center;
        font-size: var(--text-tagline-3); padding: .3em .8em; border-radius: 999px; white-space: nowrap; }
.pill.sent   { background: var(--ok-wash);   color: var(--ok); }
.pill.held   { background: var(--warn-wash); color: var(--warn); }
.pill.err    { background: var(--bad-wash);  color: var(--bad); }
.pill.person { background: var(--ink);       color: #fff; }
.pill.q      { background: var(--ground-2);  color: var(--muted); }

/* ── forms ──────────────────────────────────────────────────────────────── */
label { display: block; font-size: var(--text-tagline-2); color: var(--ink); font-weight: 500; margin-bottom: .4rem; }
input[type=text], input[type=email], input[type=password], input[type=file], textarea, select {
  width: 100%; font: inherit; font-size: var(--text-tagline-2); color: var(--ink);
  background: var(--ground); border: 1px solid var(--stroke); border-radius: var(--r-sm);
  padding: .7rem .85rem; transition: border-color .2s var(--ease), box-shadow .2s var(--ease);
}
/* ⚠ Focus was drawn in ink, the same colour as every border on the page, so
   the only difference between the field you are typing in and the four you are
   not was three pixels of grey. The brand colour is the one thing on these
   screens that means "this one", and this is the place to spend it. */
input:focus, textarea:focus, select:focus {
  outline: none; border-color: var(--violet);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--violet) 16%, transparent);
}
/* The label goes with it, so the pair reads as one active thing. */
.field:focus-within > label { color: var(--violet); }
.field > label { transition: color .2s var(--ease); }
textarea { min-height: 12rem; resize: vertical; line-height: 1.6; font-family: var(--sans); }
.field { margin-bottom: 1.2rem; }
/* ⚠ The shorthand, not margin-top alone. This rule sat on top of the .hint in
   screens.css and reset only the space above it, so a description inside a field
   kept whatever the base rule happened to say below - which was nothing, and the
   sentence sat directly on the control it was describing. */
.field .hint { font-size: var(--text-tagline-3); color: var(--muted); margin: .5rem 0 .75rem; }

/* ⚠ FOUR VALUES, and the fourth is the point.
   screens.css draws the tone icon as a ::before positioned at left: 16px and
   reserves room for it with `padding-left: 44px`. This sheet loads after it,
   and a two-value `padding` shorthand resets that left padding — so the icon
   sat on top of the first word of every banner in both hosted panels. It read
   as a rendering glitch; it was two correct rules from two sheets with the
   later one winning, which is the third time that has happened in this
   codebase and the reason the gutter is now spelled out here. */
.flash { border-radius: var(--r-sm); padding: .9rem 1.05rem .9rem 2.75rem;
         margin-bottom: 1.25rem; position: relative;
         font-size: var(--text-tagline-2); border: 1px solid var(--stroke); background: var(--ground); }
.flash.good { border-color: var(--ok);   background: var(--ok-wash);   color: var(--ok); }
.flash.bad  { border-color: var(--bad);  background: var(--bad-wash);  color: var(--bad); }
.flash.warn { border-color: var(--warn); background: var(--warn-wash); color: var(--warn); }

/* ⚠ A TOAST NEEDS AN OPAQUE CARD, NOT THE INLINE BANNER'S TINT. The washes
   three lines up are ~12% colour on purpose — an inline .flash sits in the
   page's own flow, against a background already known, so a light tint is
   plenty. toast_stack() (screens.css) floats the same .flash fixed, over
   whatever the page underneath happens to be, and a 12% wash over real
   content is not a background - it is a smear on top of that content,
   reported live as an unreadable "Connector token created" toast with the
   page bleeding straight through it. Text and icon keep the tone colour
   (currentColor is what draws .flash::before, so losing it here would lose
   the icon's own reason for existing - see that rule's note in screens.css)
   - only the fill goes opaque, with a solid-tinted left edge standing in for
   the wash. var(--surface) is screens.css's own opaque white; this sheet
   never defines it, and #fff is what an install serving this sheet alone
   (no screens.css) falls back to. */
.toast-stack .flash { background: var(--surface, #fff); border-left-width: 4px; }
.toast-stack .flash.good { border-left-color: var(--ok); }
.toast-stack .flash.bad  { border-left-color: var(--bad); }
.toast-stack .flash.warn { border-left-color: var(--warn); }
.toast-stack .flash.info { border-left-color: var(--info, #2c5aa0); }

/* ── sign-in ────────────────────────────────────────────────────────────── */
/* ── signing in and signing up ───────────────────────────────────────────
   Two halves: what the product is on the left, the form on the right. The
   left half is decoration in the strict sense — it carries no control — so it
   is the half that goes away first when the screen is small.

   Signing in and signing up are one page. Somebody who guessed wrong used to
   have to leave and lose what they had typed; now a label switches panes with
   no navigation, which also means no JavaScript, which this console still
   runs none of. The server picks which pane opens by checking one radio. */
.split { min-height: 100vh; display: grid; grid-template-columns: 1fr 1fr; }
.split-art {
  position: relative; overflow: hidden; padding: 3rem;
  display: flex; flex-direction: column; gap: 2rem;
  background: var(--ink); color: #fff;
}
/* Soft lights over near-black, drifting. No image: the panel has to survive
   `img-src 'self'` and a slow connection, and a gradient does both for
   nothing — and unlike a photograph it cannot be the wrong photograph for
   somebody's brand once the colours are theirs to set.

   The drift is 40 seconds for a few percent of movement. It is meant to be
   noticed only in the way a room with a window is noticed: the screen is not
   dead while you are typing your password into it. The global reduced-motion
   rule at the top of this file stops it. */
.split-art::before, .split-art::after {
  content: ""; position: absolute; border-radius: 50%; filter: blur(90px); pointer-events: none;
}
.split-art::before { width: 34rem; height: 34rem; top: -12rem; right: -12rem;
                     background: var(--violet); opacity: .55;
                     animation: drift-a 34s ease-in-out infinite alternate; }
.split-art::after  { width: 26rem; height: 26rem; bottom: -10rem; left: -8rem;
                     background: var(--color-gradient-14, #bfee67); opacity: .18;
                     animation: drift-b 44s ease-in-out infinite alternate; }
@keyframes drift-a {
  from { transform: translate3d(0,0,0) scale(1); }
  to   { transform: translate3d(-3rem, 2.5rem, 0) scale(1.12); }
}
@keyframes drift-b {
  from { transform: translate3d(0,0,0) scale(1); }
  to   { transform: translate3d(2.5rem, -2rem, 0) scale(1.18); }
}

/* A fine dot grid over the lights, and a vignette under the text. The grid is
   what stops a big blurred gradient reading as a rendering artefact; the
   vignette is what guarantees the copy stays legible whatever the lights are
   doing underneath it at that moment. Both are gradients, so both cost one
   paint and no request. */
.split-art .split-grid {
  position: absolute; inset: 0; pointer-events: none;
  background-image: radial-gradient(rgba(255,255,255,.16) 1px, transparent 1px);
  background-size: 24px 24px;
  /* Faded out at the bottom, where the copy is, and left at full strength
     across the top where there is nothing but gradient to look at. */
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, rgba(0,0,0,.55) 45%, transparent 80%);
          mask-image: linear-gradient(to bottom, #000 0%, rgba(0,0,0,.55) 45%, transparent 80%);
}
.split-art .split-veil {
  position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(to top, rgba(9,10,14,.72) 0%, rgba(9,10,14,.22) 45%, transparent 75%);
}

/* ── the operator's photograph, if they have set one ──────────────────
   ⚠ The same treatment the marketing hero gives its own (.hero-photo in
   assets/ui.css) and for the same reasons: dimmed and blurred HERE rather
   than in the file, so the strength is changeable without re-exporting and
   one picture can sit behind a light band and a dark one — and because an
   inline style is the one thing `style-src 'self'` throws away silently.

   It sits UNDER the lights, the grid and the veil, all of which stay: the
   gradients are what keep the panel readable whatever picture arrives, and
   an operator who uploads something busy or pale must not end up with white
   text on it and nothing anywhere warning them. Empty setting, no element,
   and the panel is exactly what it was. */
.split-art .split-photo {
  position: absolute; inset: 0; z-index: 0; margin: 0;
  overflow: hidden; pointer-events: none;
}
.split-art .split-photo img {
  width: 100%; height: 100%; object-fit: cover;
  opacity: .30;
  filter: blur(2px) saturate(.7);
  /* The blur samples past the edge and leaves a soft border of nothing; a
     hair of scale pushes that outside the panel. */
  transform: scale(1.04);
}
/* Over the picture, under everything else: the guarantee that the copy has a
   ground whatever the photograph is doing at that point. */
.split-art .split-photo::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(to top, rgba(9,10,14,.80) 0%, rgba(9,10,14,.35) 50%, rgba(9,10,14,.20) 100%);
}
/* The lights read as light ON something now, so they lift a little. */
.split-art:has(.split-photo)::before { opacity: .42; }
.split-art:has(.split-photo)::after  { opacity: .14; }
.split-art > * { position: relative; }

/* The half arrives rather than appearing. Staggered, and short: this is the
   first screen anybody sees, and a slow one reads as a slow product. */
.split-art .brand   { animation: rise .5s var(--ease) both; }
.split-pitch h2     { animation: rise .5s var(--ease) .08s both; }
.split-points li    { animation: rise .5s var(--ease) both; }
.split-points li:nth-child(1) { animation-delay: .14s; }
.split-points li:nth-child(2) { animation-delay: .20s; }
.split-points li:nth-child(3) { animation-delay: .26s; }
.split-foot         { animation: rise .5s var(--ease) .32s both; }

/* Each point gets its icon in a chip, so the three promises read as three
   things rather than as a paragraph with bullets. */
.split-points li > svg {
  box-sizing: content-box; padding: .4rem; border-radius: 9px;
  background: rgba(255,255,255,.10); margin-top: 0;
  transition: background .25s var(--ease);
}
.split-points li:hover > svg { background: rgba(255,255,255,.2); }

/* The specimen: one message, one draft, one decision. Glass over the lights —
   a translucent panel rather than a solid one, so the gradient moving behind
   it stays visible and the card belongs to the screen instead of sitting on
   top of it. */
.specimen {
  margin-top: auto; align-self: flex-start; width: min(100%, 27rem);
  background: rgba(255,255,255,.07); border: 1px solid rgba(255,255,255,.13);
  border-radius: var(--r-lg); padding: 1.15rem 1.25rem; backdrop-filter: blur(14px);
  box-shadow: 0 30px 60px -30px rgba(0,0,0,.7);
  animation: rise .6s var(--ease) .12s both;
  transition: transform .4s var(--ease), border-color .4s var(--ease);
}
.specimen:hover { transform: translateY(-3px); border-color: rgba(255,255,255,.22); }
.specimen-head { display: flex; align-items: center; gap: .5rem;
                 font-size: var(--text-tagline-3); color: rgba(255,255,255,.62); }
.specimen-dot { width: .5rem; height: .5rem; border-radius: 50%; flex: none;
                background: var(--color-gradient-14, #bfee67);
                animation: specimen-pulse 2.6s var(--ease) infinite; }
@keyframes specimen-pulse {
  0%, 100% { opacity: 1;  box-shadow: 0 0 0 0 rgba(191,238,103,.45); }
  60%      { opacity: .7; box-shadow: 0 0 0 .45rem rgba(191,238,103,0); }
}
.specimen-from { color: rgba(255,255,255,.82); }
.specimen-when { margin-left: auto; }
.specimen-subj { color: #fff; font-weight: 500; margin: .6rem 0 .45rem;
                 font-size: var(--text-tagline-1); }
.specimen-draft { color: rgba(255,255,255,.66); font-size: var(--text-tagline-2);
                  border-left: 2px solid rgba(255,255,255,.2); padding-left: .8rem; }
.specimen-foot { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
                 margin-top: .9rem; }
.specimen-pill { font-size: var(--text-tagline-3); padding: .25em .7em; border-radius: 999px;
                 background: rgba(191,238,103,.16); color: var(--color-gradient-14, #bfee67); }
.specimen-meta { font-size: var(--text-tagline-3); color: rgba(255,255,255,.45); }

/* The pitch now follows the specimen rather than being pushed to the bottom
   on its own; one auto margin between them is enough. */
.split-art .specimen + .split-pitch { margin-top: 1.75rem; }
.split-art .brand { color: #fff; padding: 0; }
.split-art .brand .mark { background: #fff; color: var(--ink); }
.split-pitch { margin-top: auto; display: flex; flex-direction: column; gap: 1.1rem; }
.split-pitch h2 { color: #fff; font-size: var(--text-heading-5); margin: 0; max-width: 16ch; }
.split-points { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .85rem; }
.split-points li { display: flex; gap: .7rem; align-items: flex-start;
                   color: rgba(255,255,255,.78); font-size: var(--text-tagline-2); }
.split-points svg { width: 1.1rem; height: 1.1rem; flex: none; margin-top: .2rem; color: #fff; opacity: .9; }
.split-foot { color: rgba(255,255,255,.55); font-size: var(--text-tagline-3); }
.split-foot a { color: #fff; }

/* ⚠ This half is a <main>, and both sheets have opinions about <main>:
   screens.css caps it at 1120px and centres it, console.css gives it
   vertical padding. Neither is wanted for a half-screen panel. */
.split-form { display: grid; place-items: center; padding: 3rem 1.5rem;
              max-width: none; margin: 0; background: var(--ground); }
.split-inner { width: 100%; max-width: 25rem; animation: rise .5s var(--ease) .06s both; }
.split-inner h1 { font-size: var(--text-heading-5); }
.split-inner .sub { color: var(--muted); font-size: var(--text-tagline-2); margin: .35rem 0 1.6rem; }
.split-inner .btn { width: 100%; }
.split-inner .alt { font-size: var(--text-tagline-2); color: var(--muted); margin-top: 1.4rem; }

/* The switch. Radios rather than links, so changing your mind costs no
   request and loses nothing you have typed. */
.paneradio { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.switch { position: relative; display: grid; grid-template-columns: 1fr 1fr; gap: .25rem;
          padding: .25rem; background: var(--ground-2); border-radius: 999px; margin-bottom: 2rem; }
/* ⚠ The selected half used to be a background colour on whichever label was
   checked, which meant it teleported: press the other one and the white block
   is simply somewhere else. One thumb that slides is the same information and
   a different feeling, and it is the only moving part on the screen that a
   person actually caused. */
/* ⚠ EXACTLY ONE COLUMN WIDE, and this is the arithmetic rather than a
   guess. The track is `1fr 1fr` with .25rem of padding each side and .25rem
   of gap, so a column is (100% - .5rem - .25rem) / 2. The thumb was
   `50% - .25rem`, which is wider than that — so it overhung its label and
   every word in the switch read as off-centre inside the white pill. It
   looked like a text-alignment bug; the labels were centred all along and
   the thing behind them was the wrong size. */
.switch::before {
  content: ""; position: absolute; z-index: 0; top: .25rem; bottom: .25rem;
  left: .25rem; width: calc((100% - .75rem) / 2); border-radius: 999px;
  background: var(--ground); box-shadow: var(--shadow);
  transition: transform .28s var(--ease);
}
#pane-up:checked ~ .switch::before { transform: translateX(calc(100% + .25rem)); }

/* ⚠ The one field on either panel that is read off a lock screen and typed
   back in. Wide letter-spacing and a monospaced face so six digits are six
   distinct things rather than a number, and centred because that is where the
   eye goes when the label is above it. */
.otp-input {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 1.6rem; letter-spacing: .38em; text-align: center;
  text-indent: .38em;   /* the tracking is added AFTER the last digit too */
  padding-top: .55rem; padding-bottom: .55rem;
}
.otp-input::placeholder { letter-spacing: .38em; opacity: .35; }
.switch label { position: relative; z-index: 1; padding: .55rem .5rem;
                border-radius: 999px; cursor: pointer;
                display: flex; align-items: center; justify-content: center; gap: .4rem;
                font-size: var(--text-tagline-2); color: var(--muted); font-weight: 500;
                transition: color .2s var(--ease); }
.switch label:hover { color: var(--ink); }
#pane-in:checked ~ .switch label[for="pane-in"],
#pane-up:checked ~ .switch label[for="pane-up"] { color: var(--ink); }

/* ⚠ The mark, not a decoration on the mark. It is what makes the pair
   readable before the words are — and it is dimmer than the word, because a
   full-strength icon beside 13px text reads as the louder half of the pair
   and the word is the part that means something. */
.switch label .ico {
  width: 1rem; height: 1rem; flex: none; opacity: .5;
  transition: opacity .2s var(--ease), transform .28s var(--ease);
}
.switch label:hover .ico { opacity: .8; transform: translateY(-1px); }
#pane-in:checked ~ .switch label[for="pane-in"] .ico,
#pane-up:checked ~ .switch label[for="pane-up"] .ico { opacity: 1; }
/* The one that is not selected steps back a hair, so the pair reads as a
   choice already made rather than two equal buttons. */
#pane-up:checked ~ .switch label[for="pane-in"] .ico,
#pane-in:checked ~ .switch label[for="pane-up"] .ico { transform: scale(.92); }

/* ── the password row ─────────────────────────────────────────────────────
   A field with something beside it: "Suggest one" on the sign-up form, an eye
   on the sign-in one. Both are <button type="button">, so neither can submit
   the form by accident, and both are inert with the script blocked — which is
   why the field is a plain input that fills the row on its own. */
.pw-row { display: flex; align-items: stretch; gap: .4rem; }
.pw-row > input { flex: 1 1 auto; min-width: 0; }

.pw-make, .pw-eye {
  flex: none; display: inline-flex; align-items: center; gap: .35rem;
  border: 1px solid var(--stroke); border-radius: var(--r-sm);
  background: var(--ground); color: var(--muted); cursor: pointer;
  font-size: var(--text-tagline-3); font-family: inherit; padding: 0 .7rem;
  transition: color .2s var(--ease), border-color .2s var(--ease),
              background .2s var(--ease), transform .18s var(--ease);
}
.pw-make .ico, .pw-eye .ico { width: .95rem; height: .95rem; }
.pw-make:hover, .pw-eye:hover {
  color: var(--ink); border-color: var(--ink); background: var(--ground-2);
}
.pw-make:active, .pw-eye:active { transform: translateY(1px); }
.pw-eye { padding: 0 .6rem; }
.pw-eye[aria-pressed="true"] { color: var(--brand); border-color: var(--brand); }

@media (max-width: 420px) {
  /* Below this the button and the field together leave the field too narrow
     to read a 20-character password in, so they stack. */
  .pw-row { flex-direction: column; }
  .pw-make, .pw-eye { justify-content: center; padding: .55rem; }
}
@media (prefers-reduced-motion: reduce) {
  .switch label .ico, .pw-make, .pw-eye { transition: none; }
}

/* The panes cross-fade rather than swapping. `display` cannot be animated, so
   the one that is not shown is taken out of the layout AND out of the tab
   order — visibility, not opacity alone, or a keyboard would walk into the
   form nobody can see. */
.pane { display: none; }
#pane-in:checked ~ .pane-in, #pane-up:checked ~ .pane-up {
  display: block; animation: pane-in .32s var(--ease) both;
}
@keyframes pane-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
/* Keyboard users need to see where they are; the radio itself is invisible. */
.paneradio:focus-visible ~ .switch label[for="pane-in"],
.paneradio:focus-visible ~ .switch label[for="pane-up"] { outline: 2px solid var(--violet); outline-offset: 2px; }

@media (max-width: 900px) {
  /* ⚠ `grid-template-rows: auto 1fr`, and without it the art half took a
     third of the screen and showed nothing.
     .split is min-height: 100vh, and a grid with two auto rows and a height
     to fill stretches them EQUALLY — so the brand bar, whose content is one
     logo, was given half the viewport and spent most of it empty. Everything
     inside it below the brand is already hidden on a phone; the row just did
     not know that. Now the bar is as tall as its logo and the form gets the
     rest. */
  .split { grid-template-columns: 1fr; grid-template-rows: auto 1fr; }
  /* The pitch is not worth a screenful above the form on a phone. The bar of
     colour keeps the brand present without costing anything to scroll past. */
  .split-art { min-height: 0; padding: 1.15rem 1.5rem; gap: 0; }
  .split-art::before { width: 20rem; height: 20rem; top: -10rem; }
  .split-art .split-grid, .split-art .split-veil { display: none; }
  .split-pitch, .split-foot, .specimen { display: none; }
  .split-form { padding: 2.5rem 1.5rem 4rem; }
}

/* The older single-column card, still used by the workspace picker. */
.auth { min-height: 100vh; display: grid; place-items: center; padding: 2rem 1.25rem; }
.auth .box { width: 100%; max-width: 26rem; }
.auth .card { padding: 2.25rem; border-radius: var(--r-lg); }
.auth .brand { justify-content: center; margin-bottom: 1.75rem; }
.auth h1 { font-size: var(--text-heading-5); text-align: center; }
.auth .sub { text-align: center; color: var(--muted); font-size: var(--text-tagline-2); margin: .5rem 0 1.9rem; }
.auth .btn { width: 100%; justify-content: center; }
.auth .alt { text-align: center; font-size: var(--text-tagline-2); color: var(--muted); margin-top: 1.4rem; }

/* ── the one place a stranger's words are rendered ──────────────────────── */
.emailbody {
  background: var(--ground-2); border: 1px solid var(--stroke); border-radius: var(--r-sm);
  padding: 1.05rem 1.15rem; white-space: pre-wrap; overflow-wrap: anywhere;
  font-size: var(--text-tagline-2); line-height: 1.65; color: var(--ink);
  max-height: 34rem; overflow-y: auto;
}
.mono { font-size: var(--text-tagline-3); }
.muted { color: var(--muted); }
.empty { text-align: center; padding: 3.5rem 1.5rem; color: var(--muted); }
.empty b { display: block; color: var(--ink); font-size: var(--text-tagline-1); margin-bottom: .35rem; }


/* ⚠ ITS OWN SURFACE, NOT A RULE ACROSS THE PAGE'S BACKGROUND. A 1px border
   was the only thing separating "the page" from "the footer", and against a
   page that is itself flat that read as more content rather than as a
   different region. A tinted ground and an inward shadow along the top edge
   give it the same sense of sitting behind the page that a modal's overlay
   or a raised card already has elsewhere - it drops away rather than merely
   stopping. */
footer.foot {
  position: relative;
  background: var(--ground-2, var(--ground)); padding: 1.85rem 0 1.75rem; color: var(--muted);
  font-size: var(--text-tagline-3);
  box-shadow: inset 0 1px 0 var(--stroke), inset 0 12px 20px -18px rgba(15,23,42,.12);
}
/* A thin line of the brand along the top edge - the one place in either
   panel's chrome that had gone all the way to the bottom of a page with
   nothing of the brand's own colour left in it. */
.foot-accent {
  position: absolute; top: 0; left: 0; right: 0; height: 3px;
  background: linear-gradient(90deg, var(--violet, var(--ink)) 0%,
              color-mix(in srgb, var(--violet, var(--ink)) 40%, transparent) 60%, transparent 100%);
}

/* ── the footer ───────────────────────────────────────────────────────────
   ⚠ It was two sentences on one line with a great deal of nothing between
   them. A footer is where somebody looks once they have finished what they
   came to do, so it carries the handful of places they are most likely to want
   next — and the policy pages, which the payment processor requires to be
   reachable and which had no home in either panel.

   ⚠ TWO ROWS NOW, NOT THREE GROUPS IN ONE. Identity, the quick actions and
   the legal links used to share a single flex row, and at anything short of
   a wide desktop the identity block gave way first — it is the one group
   whose width is not fixed by its own content, a workspace slug and a mode
   sitting inside running prose, and it wrapped that prose across four short
   lines rather than reading as one sentence that ran out of room. Legal
   links are the least important thing here; a border and a row of their own
   settles which group loses a fight for space, instead of leaving it to
   whichever one happens to be narrowest. */
.foot-grid {
  display: flex; flex-wrap: wrap; align-items: center; gap: .85rem 2rem;
}
.foot-id { flex: 1 1 18rem; min-width: 0; display: flex; align-items: flex-start; gap: .75rem; }
.foot-id code { white-space: nowrap; }
.foot-id > span:last-child { display: flex; flex-direction: column; gap: .3rem; min-width: 0; padding-top: .15rem; }
.foot-note { color: var(--muted); opacity: .8; }
/* The same mark the sidebar's own brand shows, small and quiet down here -
   an anchor for the eye at the start of the row rather than four lines of
   plain text with nothing to land on first. */
.foot-mark {
  flex: none; width: 2.1rem; height: 2.1rem; border-radius: 9px;
  display: grid; place-items: center; overflow: hidden;
  background: var(--ink); color: #fff; font-size: var(--text-tagline-2); font-weight: 650;
}
.foot-mark-img { width: 100%; height: 100%; object-fit: cover; }

.foot-acts { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; flex: none; }
/* The quick actions are the reason the footer exists now, so they read as
   controls; the legal links stay quiet text, because nobody's next action is
   the refund policy. A solid surface with its own shadow, not an outline on
   the footer's own tint, so they still read as buttons now that the footer
   itself carries a background. */
.foot-acts a {
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .45rem .9rem; border-radius: 999px;
  border: 1px solid var(--stroke); background: var(--surface, #fff);
  color: var(--ink); text-decoration: none; white-space: nowrap; font-weight: 500;
  box-shadow: var(--shadow, 0 1px 2px rgba(16,24,32,.06));
  transition: border-color .15s ease, background .15s ease, color .15s ease, transform .15s ease, box-shadow .15s ease;
}
.foot-acts a svg { width: 1rem; height: 1rem; flex: none; opacity: .65; }
.foot-acts a:hover svg { opacity: .9; }
.foot-acts a:hover {
  border-color: var(--violet, var(--ink)); color: var(--violet, var(--ink));
  transform: translateY(-1px); box-shadow: var(--shadow-lift, 0 8px 20px -10px rgba(16,24,32,.18));
}
.foot-acts a:focus-visible { outline: 2px solid var(--ink); outline-offset: 1px; }

/* Its own row, under a rule, rather than fighting the identity block for the
   same one — see the note above .foot-grid. */
.foot-legal {
  display: flex; flex-wrap: wrap; align-items: center; gap: .2rem .9rem;
  margin-top: 1.1rem; padding-top: 1rem; border-top: 1px solid var(--stroke);
}
.foot-legal a { color: var(--muted); text-decoration: none; white-space: nowrap;
  transition: color .16s var(--ease, ease), transform .16s var(--ease, ease); }
.foot-legal a:hover { color: var(--ink); transform: translateY(-1px); }

@media (max-width: 760px) {
  /* Stacked, and the actions go full width so they are still a comfortable
     tap target rather than four pills crushed onto one line. */
  .foot-grid { flex-direction: column; align-items: flex-start; gap: 1rem; }
  .foot-id { flex: 1 1 auto; }
  .foot-acts { width: 100%; }
  .foot-acts a { flex: 1 1 auto; text-align: center; }
}

/* The kit's own utilities are loaded before this file, so a screen can reach
   for them directly. These few only exist because the console's markup
   predates the kit and still uses them. */
code { font-size: .9em; background: var(--ground-2); padding: .12em .4em; border-radius: 4px; }

/* ── the account menu ────────────────────────────────────────────────────── */
/* It reuses the workspace switcher's details/summary, so it opens without a
   line of JavaScript — which this console still runs none of. */
.acct > summary { list-style: none; cursor: pointer; display: flex; }
.acct > summary::-webkit-details-marker { display: none; }
.acct .tdot { transition: box-shadow .2s var(--ease); }
.acct[open] .tdot, .acct > summary:hover .tdot { box-shadow: 0 0 0 3px var(--stroke); }

.switcher .menu.wide { min-width: 17rem; padding: .4rem; }
.switcher .menu.wide a { display: flex; align-items: flex-start; gap: .6rem; padding: .55rem .6rem; }
.switcher .menu.wide a svg { width: 1.05rem; height: 1.05rem; flex: none; margin-top: .15rem; opacity: .6; }
.switcher .menu.wide .ml { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
.switcher .menu.wide .ml strong { font-weight: 500; }
.switcher .menu.wide .ml small { color: var(--body); font-size: var(--text-tagline-3); line-height: 1.35; }
.switcher .menu-head { padding: .55rem .6rem .35rem; display: flex; flex-direction: column; gap: .15rem; }
.switcher .menu-head strong { font-weight: 500; font-size: var(--text-tagline-3); color: var(--body); }
.switcher .menu-head .mono { font-size: var(--text-tagline-3); color: var(--ink); overflow-wrap: anywhere; }
/* The active row is dark; its description must not vanish into it. */
.switcher .menu.wide a.on small { color: rgba(255,255,255,.72); }
.switcher .menu.wide a.on svg   { opacity: .85; }

/* ── waiting ─────────────────────────────────────────────────────────────
   Every one of these is driven by console.js adding a class, and every one of
   them is decoration: with the script blocked the button still submits and the
   link still navigates, they simply do it without saying so.               */

/* The bar across the top. It does not know how long the request will take —
   nothing does — so it eases towards nine tenths and waits there. Arriving at
   100% would be a lie, and stalling at 30% reads as broken. */
/* ⚠ Richer, not more dishonest - see ui.css's own note on the same bar on
   the marketing site (this is the same device, --violet in place of
   --color-primary-500). It still never claims to know how long the request
   takes: eases toward nine tenths and waits, same as always. The gradient,
   the glow and the pulse are presence, not a fake percentage. */
.pageload {
  position: fixed; top: 0; left: 0; height: 3px; width: 100%; transform-origin: 0 50%;
  transform: scaleX(0);
  background: linear-gradient(90deg, color-mix(in srgb, var(--violet) 55%, #fff) 0%, var(--violet) 100%);
  box-shadow: 0 0 10px -1px color-mix(in srgb, var(--violet) 75%, transparent);
  z-index: 200; pointer-events: none;
}
.pageload-on {
  transform: scaleX(.9); transition: transform 8s cubic-bezier(.05,.7,.1,1);
  animation: pageload-pulse 1.4s ease-in-out infinite;
}
.pageload-done { transform: scaleX(1); opacity: 0; animation: none;
                 transition: transform .2s linear, opacity .35s .15s linear; }
@keyframes pageload-pulse {
  0%, 100% { box-shadow: 0 0 10px -1px color-mix(in srgb, var(--violet) 75%, transparent); }
  50%      { box-shadow: 0 0 18px 1px color-mix(in srgb, var(--violet) 95%, transparent); }
}

@keyframes spin { to { transform: rotate(360deg); } }

/* The button keeps its label and its width — a spinner that replaces the text
   makes the button jump, and a control that changes size under the cursor is
   worse than one that says nothing. The label dims, the ring turns beside it. */
.btn.is-busy, button.is-busy { pointer-events: none; position: relative; }
.btn.is-busy::after, button.is-busy::after {
  content: ""; width: 1rem; height: 1rem; margin-left: .55rem; flex: none;
  border-radius: 50%; border: 2px solid currentColor; border-top-color: transparent;
  opacity: .85; animation: spin .7s linear infinite;
}
.btn.is-busy { opacity: .82; }

/* A link is text, not a box, so it gets the ring on its own rather than a
   whole button's worth of state. */
a.is-busy { position: relative; opacity: .6; }
.side-nav a.is-busy::after, .switcher .menu a.is-busy::after {
  content: ""; width: .85rem; height: .85rem; margin-left: auto; flex: none;
  border-radius: 50%; border: 2px solid currentColor; border-top-color: transparent;
  animation: spin .7s linear infinite;
}
/* A busy nav row has its count replaced by the spinner; two circles fighting
   for the same corner reads as a glitch. */
.side-nav a.is-busy .tag { display: none; }

/* Somebody who has asked not to be moved gets a fade instead of a spin. */
@media (prefers-reduced-motion: reduce) {
  .btn.is-busy::after, button.is-busy::after,
  .side-nav a.is-busy::after, .switcher .menu a.is-busy::after { animation: none; opacity: .5; }
  .pageload-on { transition: none; transform: scaleX(.9); animation: none; }
}

/* ── layout helpers ──────────────────────────────────────────────────────────
   These exist because the panels ship `style-src 'self'`, which discards inline
   style attributes outright. The views had 91 of them and every one was dead:
   present in the DOM, parsed to nothing, no error anywhere. A logo preview
   capped at 3rem rendered 256px tall, a sticky save bar did not stick, and a
   two-column row was a stack. Nothing to see in the source; nothing in the
   console. So spacing and arrangement live here, where they can take effect.
   tests/site_tests.php fails if a style attribute reappears in a view.        */
.mt-xs { margin-top: .2rem; }
.mt-sm { margin-top: .5rem; }
.mt-md { margin-top: .75rem; }
.mt-lg { margin-top: 1.25rem; }
.mt-xl { margin-top: 1.5rem; }
.mb-xs { margin-bottom: .4rem; }
.mb-sm { margin-bottom: .6rem; }
.mb-md { margin-bottom: 1rem; }
.mb-lg { margin-bottom: 1.25rem; }
.flush { margin: 0; }

.row      { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; }
.row-top  { align-items: flex-start; }
.row-base { align-items: baseline; gap: .5rem; }
.row-end  { justify-content: flex-end; }
.row-tight{ gap: .5rem; }
.grow     { flex: 1; min-width: 15rem; }

.w-auto     { width: auto; }
.btn-block  { width: 100%; justify-content: center; }
.narrow     { max-width: 44rem; }
.narrower   { max-width: 31rem; }
.field-short{ max-width: 12rem; font-family: var(--mono); }

.text-warn   { color: var(--warn); }
.text-accent { color: var(--violet); }
.text-head   { color: var(--ink-head); }
.text-sm  { font-size: .9rem; }
.text-xs  { font-size: .78rem; }
.text-xxs { font-size: .72rem; }
.plain    { font-weight: 400; }

/* a picture preview on the branding screen */
.preview {
  max-height: 3rem; max-width: 11rem; object-fit: contain;
  border: 1px solid var(--stroke); border-radius: var(--r-sm);
  padding: .25rem; background: var(--ground);
}

/* ── the media library ─────────────────────────────────────────────────── */
.media-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 1rem; }
.media-item {
  display: flex; flex-direction: column; gap: .4rem;
  border: 1px solid var(--stroke); border-radius: var(--r-md); padding: .6rem;
  background: var(--ground-2);
}
.media-thumb {
  width: 100%; height: 6rem; object-fit: contain;
  border: 1px solid var(--stroke); border-radius: var(--r-sm); background: var(--ground);
}
.media-meta { word-break: break-all; }
/* The popup's picker grid: a <button> standing in for what everywhere else
   on this screen is a whole .media-item card, so a click anywhere on the
   thumbnail submits — no separate "Use" button needed once the popup is
   already scoped to one field. */
button.media-item {
  width: 100%; cursor: pointer; font: inherit; text-align: left;
  transition: border-color .16s var(--ease), transform .16s var(--ease);
}
button.media-item:hover { border-color: var(--brand, var(--ink)); transform: translateY(-1px); }
/* ⚠ display: contents on the <form> wrapping each button: .media-grid is a
   CSS grid, and without this the form itself — not the button inside it —
   would be the grid item, one column-wide box holding a button free to
   size however it liked. The form disappears from the box tree; its one
   child takes its place in the grid as if the form were never there. */
.media-pick { display: contents; }

/* ── "Change" popup on a Pictures field ──────────────────────────────────
   The CSS-only checkbox modal itself — .modal-toggle/.modal/.modal-scrim/
   .modal-body/.modal-close — is shared, hosted-and-on-premise-both
   infrastructure now; see screens.css, "the CSS-only modal, shared by
   every popup on either panel". What is left here is specific to this one
   field: its layout, and the focus ring on the "Change" button that opens
   it. */
.pic-field { display: flex; flex-direction: column; gap: .5rem; }
.pic-field-label { font-weight: 500; }
.modal-toggle:focus-visible ~ .row-tight label.btn:first-child {
  outline: 2px solid var(--violet); outline-offset: 2px;
}
.modal .media-grid { margin: 1rem 0; }
.pic-upload {
  display: flex; align-items: center; gap: .6rem;
  margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--stroke);
}
/* the save bar that follows you down a long settings form */
/* ⚠ A sticky Save floats OVER the form, so the last field on a long screen
   sits underneath it and cannot be read or clicked. The bar reserves its own
   height with a negative pull, which keeps it where it was without leaving a
   gap when the form is short enough not to scroll. */
.stickybar {
  position: sticky; bottom: 1rem; display: flex; justify-content: flex-end;
  margin: 1.5rem -1rem 0; padding: .5rem 1rem;
  /* While pinned mid-scroll the bar floats over the form. An opaque, slightly
     wider strip means the fields behind it are covered cleanly, not showing
     through beside the button. */
  background: var(--page);
  box-shadow: 0 -12px 16px -8px var(--page);
}
form > .stickybar { margin-top: 2rem; }
/* The scroll container needs somewhere for that last field to go, and a field
   focused near the foot must clear the bar when the browser scrolls to it. */
.col main { padding-bottom: 7rem; scroll-padding-bottom: 6rem; }
form:has(.stickybar) :is(input, select, textarea, .field, label) { scroll-margin-bottom: 6rem; }
/* A disclosure heading — the summary of a <details> used as a section title.
   ⚠ Every rule here used to sit on the <details> element, and none of them
   reached the <summary> that is actually the control: list-style and the
   webkit marker mean nothing on the wrapper, and cursor: pointer there leaves
   a text caret over the words. So "Add another workspace" rendered as a
   heading nobody would think to click — no pointer, no marker, nothing to say
   it opened anything. The class now names the element it styles, and the two
   screens that use it spell it the same way. See tests/structure_tests.php. */
summary.opener {
  cursor: pointer; list-style: none; font-weight: 600;
  color: var(--ink-head); font-size: 1.05rem;
  display: flex; align-items: center; gap: .5rem;
  padding: .35rem 0; user-select: none;
}
/* ⚠ Both, and for different browsers: ::-webkit-details-marker is the old
   triangle, ::marker is the current one, and list-style: none alone leaves
   Chrome drawing a second chevron on the opposite side of the row. */
summary.opener::-webkit-details-marker { display: none; }
summary.opener::marker { content: ""; }
/* ⚠ And screens.css's, which is the third one. That sheet draws a generic
   border-and-rotate caret on EVERY summary, and this sheet draws a masked
   chevron on the ones with .opener — so every disclosure in both hosted
   panels had two markers, one at each end of the row, pointing different
   ways. Nothing failed: two correct rules, from two sheets, both applying.
   .opener is the more specific of the two, so it turns the other off. */
summary.opener::after { content: none; }
summary.opener::before {
  content: ""; flex: none; width: 1rem; height: 1rem;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20fill=%22none%22%20stroke=%22%23000%22%20stroke-width=%222.2%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%3E%3Cpath%20d=%22M9%206l6%206-6%206%22/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20fill=%22none%22%20stroke=%22%23000%22%20stroke-width=%222.2%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%3E%3Cpath%20d=%22M9%206l6%206-6%206%22/%3E%3C/svg%3E") center / contain no-repeat;
  transition: transform .15s ease;
}
[open] > summary.opener::before { transform: rotate(90deg); }
summary.opener:hover { color: var(--violet); }
@media (prefers-reduced-motion: reduce) {
  summary.opener::before { transition: none; }
}

/* ── the second question, inside the first ────────────────────────────────
   The switch above is what you came to DO: sign in, or make an account. This
   one is how you prove who you are, and it only exists once you have said
   "sign in" — which is why it lives inside that pane and is visibly smaller.
   Three tabs in one row asked a returning customer to choose between two
   answers to a question nobody had asked them yet. */
.howradio { position: absolute; opacity: 0; pointer-events: none; }

/* ⚠ THE SECOND PILL STRIP IS GONE. "Email me a code / Use my password" was
   rendered as a switcher identical to the "Sign in / Create account" one above
   it, and two of them stacked read as a single four-way control broken in
   half: same shape, same weight, nothing saying which question came first, so
   both had to be read before either could be answered.

   They were never equal questions. The first decides which form you get; the
   second is a preference inside one of them, and the code is the recommended
   way. So the alternative is now a link at the foot of whichever form is open
   — the shape every sign-in screen worth copying uses — and there is exactly
   one control at the top of the card. The radios stay, because the switching
   is still a label and a sibling selector: there is no script here. */
.linky {
  cursor: pointer; color: var(--brand); font-weight: 500;
  display: inline-flex; align-items: center; gap: .3rem;
  border-radius: .4rem; padding: .1rem .3rem; margin: -.1rem -.3rem;
  transition: background .16s var(--ease), transform .16s var(--ease);
}
/* Already at the vivid end of the brand colour at rest, so hover has
   nowhere left to shift it TO - a soft fill behind it plus the same small
   lift every other link gets is the stand-out instead of a second colour. */
.linky:hover { background: var(--brand-soft, var(--ground-2)); transform: translateY(-1px); }
.linky .ico { width: .95rem; height: .95rem; flex: none; opacity: .75; }
/* Keyboard reachability: the label points at a radio that is off-screen, so
   the ring has to be drawn here when that radio takes focus. */
.howradio:focus-visible ~ .how .linky,
.howradio:focus-visible ~ .how .alt .linky { outline: 2px solid var(--brand); outline-offset: 3px; }
.alt-sep { margin: 0 .45rem; opacity: .45; }

.how { display: none; }
#how-code:checked ~ .how-code,
#how-pw:checked   ~ .how-pw { display: block; animation: pane-in .28s var(--ease) both; }

/* ⚠ The one field on either panel that is read off a lock screen and typed
   back in. Wide tracking and a monospaced face so six digits read as six
   distinct things rather than as a number, and centred because that is where
   the eye goes when the label is above it. */
.otp-input {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 1.6rem; letter-spacing: .38em; text-align: center;
  text-indent: .38em;   /* the tracking is added AFTER the last digit too */
  padding-top: .55rem; padding-bottom: .55rem;
}
.otp-input::placeholder { letter-spacing: .38em; opacity: .35; }

/* long-answer boxes: the wizard's questions need more room than a setting */
textarea.tall  { min-height: 8rem; }
textarea.taller{ min-height: 11rem; }

/* the allowance bar on the plan screen */
.meter { height: .5rem; border-radius: 999px; background: var(--ground-3); margin-top: .9rem; overflow: hidden; }
.meter > span { display: block; height: 100%; background: var(--violet); border-radius: 999px; }
.bignum { font-size: 2.4rem; font-weight: 600; color: var(--ink-head); }

.checklist { list-style: none; padding: 0; margin: 0 0 1rem; display: grid; gap: .4rem; }
.checklist .num { min-width: 1.9rem; }
/* remaining shapes lifted out of dead style attributes */
.text-sm-2 { font-size: .88rem; }
/* ⚠ Renamed from .swatch. screens.css uses that name for something else
   entirely — a modifier on a .check label in the branding screen — and
   this sheet loads second, so this rule was sizing that whole LABEL to
   0.85rem square. The radio, the colour dot and the words all spilled out
   of a box thirteen pixels wide and landed on top of one another. Two
   single-use rules sharing a name, and the collision was invisible in
   either file on its own. */
.swatch-chip { display: inline-block; width: .85rem; height: .85rem; border-radius: 3px; border: 1px solid var(--stroke); }
.card-ok { border-color: var(--ok); }
.card-accent { border-color: var(--violet); }
.step-head { display: flex; align-items: center; gap: .7rem; margin-bottom: .35rem; }
.block { display: block; }
/* 9rem was not enough for "Business €79 / month": the period wrapped onto a
   second line under the name, and only on some plans, so the ladder's rows
   were different heights. */
.plan-row { grid-template-columns: 12rem 1fr auto; cursor: default; padding-left: 0; padding-right: 0; }
.plan-row > span:first-child { display: flex; flex-wrap: wrap; align-items: baseline; gap: .35rem; }
.plan-row > span:first-child .text-xs { white-space: nowrap; }

/* ⚠ A CHIP, NOT text-xs.muted. The price used to be small grey text easy to
   miss beside the plan name, and the same single figure whichever period the
   Monthly/Yearly toggle was on - see the note on .plan-price in usage.php.
   Bold, tinted, its own small surface, the way a price that decides "am I
   paying more or less" earns to look. */
.plan-price {
  display: inline-flex; align-items: baseline; gap: .2rem; white-space: nowrap;
  padding: .15rem .55rem; border-radius: 999px;
  background: var(--ok-wash); color: var(--ok);
  font-weight: 700; font-size: var(--text-tagline-2); font-variant-numeric: tabular-nums;
}
.plan-price small {
  font-weight: 500; font-size: var(--text-tagline-3); color: color-mix(in srgb, var(--ok) 75%, var(--muted));
}
.no-border { border: 0; }
.bignum { font-size: 2.4rem; font-weight: 600; letter-spacing: -.03em;
          color: var(--ink-head); font-variant-numeric: tabular-nums; }
.meter-svg { display: block; width: 100%; height: .5rem; margin-top: .9rem; }
/* the workspace picker */
.pick-list { gap: .5rem; }
.pick {
  display: flex; gap: .8rem; align-items: center; padding: .9rem 1rem;
  border: 1px solid var(--stroke); border-radius: var(--r-md);
  text-decoration: none; color: inherit; background: var(--ground);
  transition: border-color .18s var(--ease), box-shadow .18s var(--ease),
              transform .18s var(--ease);
}
/* ⚠ A row on this screen is the only thing to do on it, and it was a flat
   rectangle that reacted with a border colour. Lifting it says "pressable" in
   the way every other target in this product does. */
.pick:hover, .pick:focus-visible {
  border-color: var(--violet); box-shadow: var(--shadow); transform: translateY(-1px);
}
.pick-who { min-width: 0; flex: 1; }
.pick-who b, .pick-who .mono { display: block; overflow: hidden;
                               text-overflow: ellipsis; white-space: nowrap; }
/* The affordance. Grey until the row is under the cursor, then it moves the
   way the sidebar's icons already do. */
.pick-go { flex: none; display: inline-flex; color: var(--muted);
           transition: transform .18s var(--ease), color .18s var(--ease); }
/* ⚠ The shared chevron points DOWN — it is a disclosure marker everywhere else
   in this product. Pointing at the next screen means turning it, and the
   rotation goes on the SVG while the nudge goes on the span: put both on one
   element and the translate follows the rotated axis, so the arrow slides
   upwards on hover instead of forwards. */
.pick-go svg { width: 1.1rem; height: 1.1rem; transform: rotate(-90deg); }
.pick:hover .pick-go { color: var(--violet); transform: translateX(3px); }
.pick .tdot {
  width: 2.25rem; height: 2.25rem; border-radius: 999px; flex: none;
  display: grid; place-items: center;
  /* ⚠ THE BRAND, not a purple written into the sheet. --violet is remapped to
     BRAND_PRIMARY by site_palette_css(), so an install that chose orange had
     every workspace initial in somebody else's colour on the first screen a
     customer sees after signing in. */
  background: linear-gradient(140deg,
              color-mix(in srgb, var(--violet) 80%, #000),
              var(--violet));
  color: #fff; font-family: var(--mono); font-size: .72rem; font-weight: 600;
}
.pick b { display: block; color: var(--ink-head); }
.pick-count { color: var(--ink); font-weight: 600; }

/* ⚠ Ruled off from what is above it. Sign out sat flush under the create form,
   which is a mis-tap that costs a session. */
.alt-ruled { border-top: 1px solid var(--stroke); padding-top: 1.1rem; }
/* ⚠ Sign out carries the logout glyph in both sidebars and on the account
   screen; on the workspace picker it was the one place it was bare text. An
   action that looks different in one of the four places it appears reads as a
   different action. Inline-flex because the anchor is inside a <p>, where a
   bare SVG sits on the text baseline and rides low. */
.signout-link { display: inline-flex; align-items: center; gap: .4rem; }
.signout-link svg { width: 1rem; height: 1rem; flex: none; }
textarea.short { min-height: 5rem; }
.summary-link { cursor: pointer; font-size: .9rem; color: var(--muted); }
.summary-link::-webkit-details-marker { display: none; }
.plan-row.plan-now { background: var(--ground-2, rgba(0,0,0,.02)); border-radius: var(--r-sm); }

/* The action cell: a pill, a button, or a pill and a button side by side when
   a plan has been asked for and can be taken back. Right-aligned and hugging,
   so the ladder reads as one column of actions rather than as boxes of
   different widths. */
.plan-row > span:last-child { display: flex; align-items: center; justify-content: flex-end; gap: .5rem; }
.plan-row > span:last-child .row { justify-content: flex-end; gap: .5rem; }
.plan-row > span:last-child .pill { flex: none; }

/* One period's worth of actions — one Subscribe button, now that "buy once"
   is gone (see the note on pay_checkout_start() in control/pay.php for why).
   Both .plan-period groups (month, year) still render — the page-level
   .period-switch below picks which one shows — so .plan-buy only ever has
   one visible child at a time; flex-wrap is just the fallback for a plan
   sold on one period only, where there is no switch and both would be the
   same single group anyway. */
.plan-buy { display: flex; flex-wrap: wrap; gap: 1rem; justify-content: flex-end; }
.plan-period { display: flex; flex-direction: column; align-items: flex-end; gap: .3rem; }

/* ── the billing-period switch (usage.php) ────────────────────────────────
   ⚠ ONE TOGGLE FOR THE WHOLE PLAN TABLE, not a Monthly group and a Yearly
   group side by side on every row — reported live as "too many buttons,
   confusing". Pure CSS, same shape as every other switch in this product
   (hidden radios + sibling selectors, no script): #pv_month/#pv_year sit as
   .queue's OWN preceding siblings in usage.php, not nested inside
   .period-switch, because the sibling combinator below needs them at that
   level to reach into .queue at all. */
/* ⚠ BIGGER AND LOUDER THAN THE FIRST VERSION, ON PURPOSE. Small, muted,
   text-tagline-3 on a barely-tinted background read as chrome rather than a
   control — reported live as "I barely even noticed it". A border AND a
   shadow so it reads as its own object on the page rather than a wash of
   grey, a bolder active state in the brand colour instead of a plain white
   chip, and the free-months badge in --ok (the same green a savings figure
   gets everywhere else in this product) so "yearly is cheaper" is legible
   at a glance, not just on hover. */
.period-switch-input { position: absolute; width: 0; height: 0; opacity: 0; pointer-events: none; }
.period-switch {
  display: inline-flex; gap: .25rem; padding: .3rem; margin-bottom: 1.25rem;
  background: var(--ground); border: 1px solid var(--stroke); border-radius: 999px;
  box-shadow: var(--shadow);
}
.period-switch label {
  display: flex; align-items: center; gap: .5rem;
  padding: .65rem 1.4rem; border-radius: 999px; cursor: pointer;
  font-size: var(--text-tagline-2); font-weight: 600; color: var(--muted);
  transition: background .15s var(--ease), color .15s var(--ease);
}
#pv_month:checked ~ .period-switch label[for="pv_month"],
#pv_year:checked  ~ .period-switch label[for="pv_year"] {
  background: var(--violet); color: #fff;
}
.period-switch-save {
  background: var(--ok-wash); color: var(--ok); border-radius: 999px;
  padding: .15rem .55rem; font-size: var(--text-tagline-3); font-weight: 700;
  white-space: nowrap;
}
/* On the brand-violet active chip the wash reads muddy against it - the
   badge switches to a solid white pill with the same green text instead, so
   "months free" stays legible whichever label is selected. */
#pv_year:checked ~ .period-switch label[for="pv_year"] .period-switch-save {
  background: #fff;
}
#pv_month:checked ~ .queue [data-period="year"]  { display: none; }
#pv_year:checked  ~ .queue [data-period="month"] { display: none; }

/* ⚠ .plan-row NEVER COLLAPSED ON MOBILE, unlike every other .queue .q (see
   the 720px rule above) — it overrides grid-template-columns without
   restoring it, so its 12rem/1fr/auto grid stayed fixed on a phone screen.
   The action cell was squeezed into a sliver of "auto" width, and every
   button inside it wrapped onto its own full-width line: four buttons per
   plan reading as an undifferentiated wall (reported live as "the subscribe
   buttons are bad on mobile"). One column, like every other row in this
   product already gets. */
@media (max-width: 720px) {
  .plan-row { grid-template-columns: 1fr; }
  .plan-row > span:last-child,
  .plan-row > span:last-child .row,
  .plan-buy, .plan-period { justify-content: flex-start; align-items: flex-start; }
}


/* ══ colour, and just enough motion ═══════════════════════════════════════════
   Both panels were built in one ink and one violet, and read as a spreadsheet:
   five numbers in a row, identical, and the eye had nowhere to land. What was
   missing is not decoration — it is a second channel. A tinted icon says what a
   figure is about before the label is read, and a tone says whether it is good
   news, in a way that survives a glance.

   Restraint is the point. Colour here is a signal, so it is spent only where
   there is something to signal: the icon chip on a stat, a state that has gone
   wrong, the one action on a screen that is the action. Everything else stays
   in ink, and the surfaces stay quiet.

   The motion is the same argument. Nothing here animates for its own sake:
   things that appear rise a few pixels so the eye follows them, things that
   respond move under the pointer, and the global reduced-motion switch at the
   top of this file turns all of it off in one rule for anyone who asks.
   ══════════════════════════════════════════════════════════════════════════ */

:root {
  --info:      #2c5aa0;  --info-wash:  rgba(44,90,160,.12);
  --violet-wash: color-mix(in srgb, var(--violet) 12%, transparent);
  --lift:      0 2px 4px rgba(18,22,31,.05), 0 18px 40px -22px rgba(18,22,31,.30);
}

/* A stat that is a link says so on hover, and a stat that is not stays still.
   Before this every card lifted, which promised a destination five of them
   did not have. */
a.card:hover { box-shadow: var(--lift); }

/* ── things arriving ─────────────────────────────────────────────────────── */
@keyframes rise {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
.cards > *, .queue .q { animation: rise .38s var(--ease) both; }
/* A short stagger, capped: past the sixth item nobody is watching the order,
   and a long cascade on a fifty-row queue is a page that takes a second to
   settle for no reason. */
.cards > *:nth-child(2), .queue .q:nth-child(2) { animation-delay: .04s; }
.cards > *:nth-child(3), .queue .q:nth-child(3) { animation-delay: .08s; }
.cards > *:nth-child(4), .queue .q:nth-child(4) { animation-delay: .12s; }
.cards > *:nth-child(5), .queue .q:nth-child(5) { animation-delay: .16s; }
.cards > *:nth-child(n+6), .queue .q:nth-child(n+6) { animation-delay: .2s; }

main > h1, main > .page-head { animation: rise .34s var(--ease) both; }
.flash { animation: rise .3s var(--ease) both; }

/* ── things responding ───────────────────────────────────────────────────── */
.btn { transition: transform .12s var(--ease), background .18s var(--ease),
                   border-color .18s var(--ease), box-shadow .18s var(--ease); }
.btn:active { transform: translateY(1px); }
.btn.primary:hover, .btn.solid:hover { box-shadow: var(--lift); }

/* The capsule moves.

   The lime pill is the one piece of these buttons that is not text, and it sat
   still while everything around it responded. On hover it travels a few pixels
   the way the button is taking you, and the label steps aside to let it - the
   same gesture the marketing site's arrow makes, which is deliberate: that
   button and this one mean the same thing and should move the same way.

   ⚠ Small. The capsule is 3.5rem of saturated lime against near-black or
   white; the same travel that reads as a nudge on a 20px arrow reads as the
   button coming apart on this. Four pixels, and the label moves two so the gap
   between them barely changes.

   ⚠ Transform, not margin or width. Either of those relays out the flex row
   on every frame; a transform is composited and moves nothing else. */
.btn .cap { transition: transform .22s var(--ease); }
.btn:hover .cap, .btn:focus-visible .cap { transform: translateX(4px); }
.btn .cap ~ * { transition: transform .22s var(--ease); }
.btn:hover .cap ~ *, .btn:focus-visible .cap ~ * { transform: translateX(2px); }
.btn:active .cap { transform: translateX(1px); transition-duration: .06s; }

@media (prefers-reduced-motion: reduce) {
  .btn:hover .cap, .btn:focus-visible .cap, .btn:active .cap,
  .btn:hover .cap ~ *, .btn:focus-visible .cap ~ * { transform: none; }
}

/* The sheen, and only on the loud button.

   ⚠ The press above is NOT the marketing site's. There a button rises on
   hover and returns to 0 on click; here it sits flat and pushes INTO the page.
   Both read as a press; only the first needs the lift to make sense of it, and
   changing this one to match would take the console's own feel away for the
   sake of a consistency nobody experiences - the two surfaces are never on
   screen together.

   What the console genuinely lacked is the thing that makes the marketing
   button feel alive: a highlight travelling across it. Kept for .primary and
   .solid, which is one button per screen. On a Save pressed forty times in an
   afternoon a sweeping sheen stops being delight and becomes noise.

   ⚠ background-POSITION is animated, not background. The fill underneath is
   the kit's colour and a theme change still moves it; animating `background`
   would drag every colour swap into the sheen's slower duration and read as
   the button lagging behind the pointer. */
.btn.primary, button.primary, .btn.solid {
  background-image: linear-gradient(105deg, transparent 38%,
                    rgba(255,255,255,.16) 50%, transparent 62%);
  background-repeat: no-repeat;
  background-size: 260% 100%;
  background-position: 150% 0;          /* parked off the right-hand edge */
  transition: transform .12s var(--ease), background-color .18s var(--ease),
              border-color .18s var(--ease), box-shadow .18s var(--ease),
              background-position .6s cubic-bezier(.3,.7,.3,1);
}
.btn.primary:hover, button.primary:hover, .btn.solid:hover,
.btn.primary:focus-visible, button.primary:focus-visible, .btn.solid:focus-visible {
  background-position: -60% 0;
}

/* ⚠ Keeps the MEANING and drops the movement. The blanket rule at the top of
   this sheet already stops every transition, but a hover transform still
   APPLIES - instantly, which is a jump rather than a rise - and the sheen
   would sit frozen mid-sweep. The shadow stays: somebody who asked for less
   motion still has to be able to tell the primary action from the one beside
   it. */
@media (prefers-reduced-motion: reduce) {
  .btn.primary, button.primary, .btn.solid { background-image: none; }
  .btn:active, .btn.primary:hover, button.primary:hover, .btn.solid:hover,
  .btn.primary:focus-visible, button.primary:focus-visible,
  .btn.solid:focus-visible { transform: none; }
}

.side-nav a, .side-settings { transition: background .18s var(--ease), color .18s var(--ease); }
.side-nav a svg, .side-settings svg { transition: transform .3s var(--bounce, var(--ease)); }
.side-nav a:hover svg, .side-settings:hover svg { transform: translateX(4px) scale(1.12); }

/* Focus is a state too, and it was the one state with no colour at all. */
a:focus-visible, button:focus-visible, summary:focus-visible,
input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: 2px solid var(--violet); outline-offset: 2px; border-radius: 6px;
}

/* ── one more tone ───────────────────────────────────────────────────────── */
/* "waiting" is neither good nor bad, and it was being drawn in the same grey
   as "nothing". Blue is the third thing: in progress, nothing wrong. */
.pill.info { background: var(--info-wash); color: var(--info); }
.flash.info { background: var(--info-wash); color: var(--info); }

/* ⚠ The uploaded logo, in the same box the initials mark uses, so a brand with
   a picture and one without sit at exactly the same height and the sidebar
   does not reflow when somebody uploads one. `contain`, because a logo is
   whatever shape its owner made it and cropping somebody's mark is worse than
   leaving air around it. */
.brand .mark-img {
  background: none; object-fit: contain; padding: 0;
}

/* ⚠ AND IT HAS TO WIN WHEREVER .mark IS GIVEN A GROUND. The rule above is two
   classes; `.split-art .brand .mark` is three, so on the dark half of the
   sign-in page the uploaded logo was drawn on a white rounded square that is
   not in the picture. That white ground is right for the INITIALS mark, which
   is dark text and needs something to sit on, and wrong for an image, which
   brings its own transparency and its own shape.

   Kept as its own rule rather than by making the base selector heavier: the
   next dark surface that wants a light mark will add a rule beside this one,
   and a specificity arms race in the other direction is how this happened. */
.split-art .brand .mark-img { background: none; }

/* ── a letter beside its preview ─────────────────────────────────────────── */
/* ⚠ The preview column is STICKY and the editor column is not. A letter body
   is a tall textarea and the preview is a tall frame; without this, scrolling
   down to the end of the text scrolls the picture of it off the top, which is
   the one moment somebody actually wants to see both. */
/* ⚠ WIDER THAN EVERY OTHER SCREEN, and it has to be. `.col .shell` caps at
   1120px, which is right for a column of form fields and leaves 485px a side
   here — so the 600px letter was clipped down its right edge, silently, with
   the footer and the end of every line cut off. This is the only screen that
   must fit a real email beside a usable editor.

   Selected with :has() rather than a class on the layout, because the layout
   is shared by nine screens and none of the others wants this. The same
   mechanism the connect form already uses. */
.col .shell:has(.letter-split) { max-width: 1460px; }

/* The preview column is FIXED at the width of the thing it shows. Sharing the
   space evenly is what clipped it; the editor is elastic because a textarea
   can be any width and an email cannot. */
.letter-split {
  display: grid; grid-template-columns: minmax(340px, 1fr) 612px;
  gap: var(--sp-lg, 20px); align-items: start;
}
.letter-edit { min-width: 0; }
.letter-preview { min-width: 0; position: sticky; top: 12px; }

.letter-tabs { display: flex; gap: 8px; margin-bottom: 10px; }

/* A mail client's own chrome is white-ish and so is the panel; without the
   border the frame bleeds into the card and the preview stops reading as a
   separate thing being previewed. */
.letter-frame {
  display: block; width: 100%; height: 620px; border: 1px solid var(--line);
  border-radius: 10px; background: #fff;
}
/* A last resort. If the column is ever narrower than the letter again, it
   scrolls rather than quietly cutting the right-hand side off. */
.letter-preview { overflow-x: auto; }

/* ⚠ Below this the two columns stack, and a sticky preview under a textarea
   would pin the picture over the words being typed. Sticky is switched off
   with the grid, not separately, so the two can never disagree. */
/* ⚠ 1240, not 1100. The preview column is fixed at 612px now, so the point at
   which this must stack is the point at which the EDITOR stops being usable
   beside it, not the point at which the page gets narrow. */
@media (max-width: 1240px) {
  .letter-split { grid-template-columns: 1fr; }
  .letter-preview { position: static; }
  .letter-frame { height: 560px; }
}

/* ── the selected sidebar row, on hover ──────────────────────────────────── */
/* ⚠ The row hover is painted by `.side-nav a::before`, which is `inset: 0` and
   therefore sits ON TOP of the element's own background. On an ordinary row
   that is the point. On the SELECTED row the background is --ink with white
   text, so fading a pale ground in over it left white-on-pale-grey: pointing at
   the page you are already on made its name almost impossible to read.
   The selected row opts out of the shared ground and lifts its own instead. */
.side-nav a.on::before, .side-settings.on::before { content: none; }
.side-nav a.on:hover, .side-settings.on:hover { background: #33343c; }             /* fallback first */
.side-nav a.on:hover, .side-settings.on:hover { background: color-mix(in srgb, var(--ink) 86%, #fff); }
.side-nav a.on:hover .lbl, .side-settings.on:hover .lbl { color: #fff; }

/* ⚠ `.grow` carries `min-width: 15rem` for the wide layouts it was written
   for. In the sidebar footer that is wider than the sidebar, so the account
   address could not shrink, the ellipsis on it never triggered, and the chip
   ran off the edge mid-address. Sidebar context only: the utility keeps its
   minimum everywhere else. */
.side .switcher > summary .grow { min-width: 0; }

/* ── the mobile navigation drawer ────────────────────────────────────────── */
/* ⚠ A checkbox, because there is no script on these pages to make it anything
   else: the panels serve `script-src 'self'` with no inline script, and the
   settings tab strips already work exactly this way. A <details> reads better
   in the markup and cannot be forced back open on desktop from CSS, which is
   the requirement that decides it.

   Kept focusable rather than `display: none`, so the drawer can be opened from
   the keyboard. The button that drives it is a <label>, which needs no script
   to toggle the input it points at. */
.navtoggle { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.navbtn { display: none; }

@media (max-width: 1000px) {
  .navbtn {
    /* ⚠ `display` IS DECLARED HERE, and leaving it to the rule higher up the
       file is what removed the navigation from every phone. A media query adds
       no specificity: the base `.navbtn { display: none }` below sits LATER in
       the sheet, so it beat the `display: flex` in the earlier mobile block and
       the drawer button never appeared — while the same block had already
       hidden .side-nav. The result was a panel with no navigation at all on a
       phone, which is worse than the scrolling row it replaced. */
    display: flex;
    align-items: center; gap: .55rem; width: 100%;
    padding: .5rem .8rem; margin: .15rem 0 .1rem;
    border: 1px solid var(--stroke); border-radius: 999px;
    background: var(--ground-2); color: var(--ink);
    font-size: var(--text-tagline-2); cursor: pointer;
    transition: background .18s var(--ease), border-color .18s var(--ease);
  }
  .navbtn:hover { background: var(--ground-3, var(--ground-2)); border-color: var(--ink); }
  .navbtn svg { width: 1.05rem; height: 1.05rem; flex: none; opacity: .6; }
  .navbtn .grow { flex: 1; min-width: 0; text-align: left; font-weight: 500;
                  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* ⚠ Both states rotate the SAME element. The first version turned the svg
     for the shut state and the span for the open one, which compose rather
     than replace: shut pointed sideways and open pointed somewhere else again.
     The shared chevron already points DOWN, which is what a drawer that opens
     downwards wants, so shut needs no rotation at all. */
  .navbtn-chev { display: inline-flex; }
  .navbtn-chev svg { transition: transform .2s var(--ease); }
  .navtoggle:checked ~ .navbtn .navbtn-chev svg { transform: rotate(180deg); }
  /* Focus has to land on the label, since the input it belongs to is 1px wide
     and invisible. */
  .navtoggle:focus-visible ~ .navbtn { outline: 2px solid var(--violet); outline-offset: 2px; }
}

@media (max-width: 1000px) and (prefers-reduced-motion: reduce) {
  .navbtn, .navbtn-chev { transition: none; }
}

/* ── depth, so the surfaces separate ─────────────────────────────────────── */
/* ⚠ Once --page had a colour again (see the token above), the panels still
   read flat, because every surface on them is the same white rectangle with
   the same hairline border and nothing between them. The kit already ships a
   --shadow and almost nothing used it.

   This is deliberately the smallest change that separates a card from the page
   rather than a restyle: same shapes, same spacing, same borders. A card now
   sits ON the page instead of being drawn on it. */
.card, .stat, .queue, .panel { box-shadow: var(--shadow); }

/* The sidebar is white against a page that is no longer white, so it reads as
   a surface on its own and the hairline can go back to being a hairline. */
.side { background: var(--ground); }

/* ⚠ Group headings were --muted at the smallest size in the sheet, which after
   the contrast change is still the faintest thing on the screen. They are the
   map of the whole panel; letting them fade to nothing is what made the
   sidebar read as one undifferentiated list. */
.side-group { color: var(--body); font-weight: 600; }

/* A little of the brand where the eye already goes. --violet is remapped to
   BRAND_PRIMARY by site_palette_css(), so this follows whatever an operator
   set rather than shipping purple to an install that chose orange. */
.page-head h1 { color: var(--ink); }

/* ── connectors.php's page-head badge: a row of marks, not one icon ────────
   screens.css's .page-head-ico is a 44px circle built for a single glyph
   (display:grid, place-items:center); three provider marks side by side
   need a wider pill instead, not a bigger circle. */
.page-head-ico-cluster {
  width: auto; display: flex; align-items: center; gap: 7px;
  padding: 0 12px; border-radius: 999px;
}
.page-head-ico-cluster img { display: block; border-radius: 4px; }
.card > h2 svg, .panel > h2 svg { color: var(--violet); opacity: .85; }

/* The big number on a stat is the thing the card exists to show, and it was
   the same weight as its own caption. */
.stat b { color: var(--ink); }

/* ── site.php's own tab set ───────────────────────────────────────────────
   Same radio + sibling mechanism as apps/console/views/settings.php's .tabs
   (screens.css defines .tabs/.tab/.tabradio/.tabpanel's base look; this panel
   loads that sheet too). Different ids — sitetab-*/sitepanel-*, not tab-*/
   panel-* — so the two tab strips can never collide if a page somehow nested
   one inside the other. See the ⚠ comment in site.php for why this exists:
   the "site" section alone has 19 groups, every other admin section has 1-5. */
.tabs { margin-bottom: 1.25rem; }
/* ⚠ Solid fill, not the old border-bottom-into-the-surface trick left over
   from when this strip was still boxy file-tabs. settings.php's pills moved
   to a solid --ink fill a session ago; this parallel strip never got the
   same edit, so the one settings-shaped screen an operator was most likely
   to compare against the redesigned one was still the unrestyled original. */
#sitetab-brand-search:checked ~ .tabs .tab[for="sitetab-brand-search"],
#sitetab-homepage:checked ~ .tabs .tab[for="sitetab-homepage"],
#sitetab-pages:checked ~ .tabs .tab[for="sitetab-pages"],
#sitetab-more:checked ~ .tabs .tab[for="sitetab-more"] {
  background: var(--ink); border-color: var(--ink); color: #fff; box-shadow: var(--shadow-lift, none);
}
#sitetab-brand-search:checked ~ #sitepanel-brand-search,
#sitetab-homepage:checked ~ #sitepanel-homepage,
#sitetab-pages:checked ~ #sitepanel-pages,
#sitetab-more:checked ~ #sitepanel-more { display: block; }
@media (min-width: 1151px) {
  #sitetab-brand-search:checked ~ .tabs .tab[for="sitetab-brand-search"],
  #sitetab-homepage:checked ~ .tabs .tab[for="sitetab-homepage"],
  #sitetab-pages:checked ~ .tabs .tab[for="sitetab-pages"],
  #sitetab-more:checked ~ .tabs .tab[for="sitetab-more"] { box-shadow: none; }
}

/* ── Help: the three doors ─────────────────────────────────────────────
   ⚠ CARDS, NOT A LIST OF LINKS. Three ways of asking a person for something
   used to be three entries in the sidebar, and choosing between them was a
   guess. Side by side, each with the situation as its heading and the
   boundary with its neighbours underneath, the choice is made by reading
   one card rather than all three. See apps/saas/app/views/support.php. */
.help-choices {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: .875rem;
  margin-bottom: 2rem;
}
.help-choice {
  display: flex;
  flex-direction: column;
  gap: .5rem;
  padding: 1.15rem 1.25rem;
  background: var(--ground);
  border: 1px solid var(--stroke);
  border-radius: var(--r-lg);
  color: inherit;
  text-decoration: none;
  transition: border-color .18s ease, box-shadow .18s ease, transform .18s ease;
}
/* Children only: a card that opens (.help-fold, below) has a <strong> and
   .muted of its own in what it reveals, and those are body text, not a title. */
.help-choice > strong,
.help-fold > summary > h3 { font-size: var(--text-tagline-1); line-height: 1.35; }
.help-choice > .muted,
.help-fold > summary > .muted { font-size: var(--text-tagline-2); line-height: 1.55; }
.help-choice-ico {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.25rem; height: 2.25rem; border-radius: .7rem;
  background: var(--ground-2); color: var(--ink);
}
.help-choice-ico svg { width: 1.15rem; height: 1.15rem; }
/* The line that says this one goes somewhere, pushed to the bottom so the
   three cards agree on where their action sits however long the text is. */
.help-choice-go {
  margin-top: auto; padding-top: .35rem;
  display: inline-flex; align-items: center; gap: .3rem;
  font-size: var(--text-tagline-2); font-weight: 500; color: var(--brand);
}
.help-choice-go svg { width: .95rem; height: .95rem; transition: transform .18s ease; }
a.help-choice:hover {
  border-color: var(--brand);
  box-shadow: 0 2px 14px color-mix(in srgb, var(--ink) 8%, transparent);
  transform: translateY(-1px);
}
a.help-choice:hover .help-choice-go svg { transform: translateX(3px); }
a.help-choice:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* The one already open is a statement, not an offer: no lift, no arrow. */
.help-choice.is-open { background: var(--ground-2); }
.help-choice.is-open .help-choice-ico { background: var(--ground); }

/* ── Help: connecting a mailbox, as the same cards ───────────────────────
   ⚠ A <details> WEARING .help-choice. The face is the <summary> and the
   toggle sits where a door has its link, so the reference section above the
   doors looks like them rather than like a list stapled on top.

   ⚠ A CARD OPENS WHERE IT STANDS. Spanning the whole row instead left a hole
   in the row it came from, or, packed densely, dropped it below the cards
   that follow it, so Tab went one way while the eye went the other. That is
   also why the grid aligns to the start rather than stretching: stretched,
   one open card would pull every card beside it to its own height. */
.help-folds { align-items: start; margin-bottom: .875rem; }
/* Nine providers read as three rows of three, never four and five: a track is
   a third of the row for as long as a third still holds 15rem. */
.help-folds-3 {
  grid-template-columns: repeat(auto-fill, minmax(max(15rem, calc((100% - 1.75rem) / 3)), 1fr));
  margin-bottom: 1rem;
}
.help-fold > summary { display: flex; flex-direction: column; gap: .5rem; }
/* The sitewide marker in screens.css; the toggle carries its own chevron. */
.help-fold > summary::after { content: none; }
.help-fold > summary > h3 { margin: 0; font-weight: 700; letter-spacing: 0; color: inherit; }
/* Two lines kept for the line under the name, whatever it says, so a row of
   closed cards comes out one height with its toggles level. Stretching would
   do that for free, and cannot be had here - see above. */
.help-fold > summary > .muted { min-height: calc(2 * 1.55em); }
/* The marks are drawn on a 48 grid with their own margin, so they need a
   little more room than the house icons to look the same size. */
.help-choice-ico.is-mark svg { width: 1.45rem; height: 1.45rem; }
.help-fold:not([open]):hover {
  border-color: var(--brand);
  box-shadow: 0 2px 14px color-mix(in srgb, var(--ink) 8%, transparent);
  transform: translateY(-1px);
}
.help-fold:not([open]):hover .help-choice-go svg { transform: translateY(2px); }
.help-fold[open] .help-choice-go svg { transform: rotate(180deg); }
.help-fold[open] .help-fold-shut,
.help-fold:not([open]) .help-fold-open { display: none; }
/* The ring goes round the card, not the summary inside it. */
.help-fold > summary:focus-visible { outline: none; }
.help-fold:has(> summary:focus-visible) { outline: 2px solid var(--brand); outline-offset: 2px; }
/* The wizard links to #provider-gmail and friends; in a grid of nine the one
   it meant has to be told apart from the two beside it. */
.help-fold:target { border-color: var(--brand); }
.help-fold-body { margin-top: .75rem; padding-top: .25rem; border-top: 1px solid var(--stroke); }
.help-fold-body .help { font-size: var(--text-tagline-2); }
.help-fold-body .actions { margin-top: .75rem; }
/* The plan screen's three columns do not fit a third of a row - the host
   came out three lines tall - so "Sending" and "Reading" go above their
   values, and the rows sit on a flat panel rather than a card in a card. */
.help-fold .queue { margin-top: .5rem; background: var(--ground-2); box-shadow: none; border-radius: var(--r-md); }
.help-fold .plan-row {
  grid-template-columns: 1fr auto; gap: .1rem 1rem;
  padding: .6rem .85rem; font-size: var(--text-tagline-2);
}
.help-fold .plan-row > span:first-child { grid-column: 1 / -1; font-size: var(--text-tagline-3); color: var(--muted); }
.help-fold .plan-row .mono { overflow-wrap: anywhere; }

/* A heading that introduces a section rather than opening the page. Its job
   is the gap ABOVE it — sections that run straight into each other read as
   one long region with bold lines in it. */
.sec-head { margin: 2rem 0 .875rem; font-size: var(--text-heading-6); font-weight: 500; }
.sec-head:first-of-type { margin-top: 0; }

/* The way back out of a form that is one level down. */
.crumb { margin-bottom: .75rem; font-size: var(--text-tagline-2); }
.crumb a { color: var(--muted); text-decoration: none; display: inline-flex;
           align-items: center; gap: .25rem; }
.crumb a:hover { color: var(--ink); }
.crumb svg { width: .9rem; height: .9rem; transform: rotate(180deg); }

@media (prefers-reduced-motion: reduce) {
  .help-choice, .help-choice-go svg { transition: none; }
  a.help-choice:hover, .help-fold:not([open]):hover { transform: none; }
}

/* ── Getting started, as progress rather than a link ──────────────────────
   ⚠ IT WAS AN ORDINARY NAV ITEM, fourth in a list of eight, styled exactly
   like Mailboxes and Branding — so the one thing that decides whether the
   product works at all read as another screen to visit some time. And it went
   on looking identical after it was finished, when it is no longer relevant.

   Three states, because three things are genuinely different: nothing done,
   part way, finished. The finished one STAYS, quietly, rather than
   disappearing — a control that vanishes when you complete it leaves somebody
   wondering whether they broke something. */
/* ⚠ The wrapper exists because the cross cannot live inside the panel: the
   panel is an <a> and a <form> nested in an anchor is unnested by the parser.
   The cross floats over the top-right corner instead, out of the way of the
   label and the count. */
.side-setup-wrap { position: relative; }
.side-setup-wrap .dismiss { position: absolute; top: 4px; right: 4px; }
.side-setup-wrap .dismiss button { width: 22px; height: 22px; }
.side-setup-wrap .dismiss svg { width: 12px; height: 12px; }
/* Room for it, so a long label never runs under the cross. */
.side-setup .side-setup-top { padding-right: 22px; }

.side-setup {
  display: flex !important; flex-direction: column; align-items: stretch !important;
  gap: .4rem; padding: .7rem .75rem !important; margin-bottom: .5rem;
  border: 1px solid var(--stroke); border-radius: var(--r-md, .7rem);
  white-space: normal !important;
}
.side-setup .side-setup-top {
  display: flex; align-items: center; gap: .5rem;
  font-weight: 500;
}
.side-setup svg { width: 1rem; height: 1rem; flex: none; }
.side-setup .side-setup-count {
  margin-left: auto; font-size: var(--text-tagline-3);
  font-variant-numeric: tabular-nums; opacity: .8;
}
.side-setup .side-setup-bar { display: block; width: 100%; height: .25rem; }
.side-setup .side-setup-say { font-size: var(--text-tagline-3); opacity: .75; }
/* ⚠ The hover ground the other rows use is suppressed: this one carries its
   own fill, and two grounds fighting made the bar hard to read. */
.side-setup::before { display: none; }

/* Nothing done: the loudest of the three, because it is the only one where
   the product is not working yet. */
.side-setup.is-none {
  background: var(--warn-wash); border-color: var(--warn);
  color: var(--warn);
}
/* Part way: the brand, because it is progress and progress is the product's
   own colour. */
.side-setup.is-part {
  background: var(--violet-wash); border-color: color-mix(in srgb, var(--violet) 40%, transparent);
  color: var(--violet);
}
/* Finished: present, and quiet. It has nothing left to ask for. */
.side-setup.is-done {
  background: var(--good-wash, color-mix(in srgb, var(--good, #0f7b4f) 10%, transparent));
  border-color: color-mix(in srgb, var(--good, #0f7b4f) 35%, transparent);
  color: var(--good, #0f7b4f);
}
.side-setup:hover { filter: brightness(.98); }
.side-setup.on { outline: 2px solid currentColor; outline-offset: 1px; }

/* ── the allowance meter ─────────────────────────────────────────────────
   The honest version of a countdown: a real number about the reader, in the
   one place they always see. Three states, because three things are actually
   different - fine, nearly out, and out. Lives in console.css beside
   .side-setup: this sidebar is the hosted console's alone, and .side-setup-bar
   is dressed here too. */
/* ⚠ <details> NOW, NOT <a>. Collapsed by default (see the PHP: no `open`
   attribute below 80% used), summary shows exactly what it always showed -
   "This month", the number, the bar - and the caption line ("answers used",
   or the urgent ones) is what opening it reveals. At 80% and over the PHP
   adds `open`: the same "quiet normally, impossible to miss when it
   matters" rule Pause already followed, applied to the one line that says
   how many answers are left before sending stops. */
.side-meter {
  display: block !important; padding: .65rem .75rem !important; margin: .5rem 0 .25rem;
  border: 1px solid var(--stroke); border-radius: var(--r-md, .7rem);
  color: inherit; white-space: normal !important;
}
.side-meter::before { display: none; }
/* ⚠ NOT WHILE HOVERING .side-meter-more. Reported live and reproduced live,
   through several rounds of elimination: hovering the "Plan and usage" link
   corrupts its own text paint (blank, or badly washed out - the icon beside
   it, an SVG rather than a text glyph, is unaffected throughout), and this
   filter on the ANCESTOR measurably contributes - removing it alone did not
   fully clear the symptom in testing, but it was the single largest
   improvement of everything tried (transform, transition, forced
   compositing layers, explicit colour). Scoped off rather than removed
   outright, since the rest of the card's subtle dim-on-hover is unaffected
   and has never been part of the problem. */
.side-meter:hover:not(:has(.side-meter-more:hover)) { filter: brightness(.98); }
.side-meter > summary {
  display: flex !important; flex-direction: column; align-items: stretch !important;
  gap: .35rem; cursor: pointer; list-style: none;
}
.side-meter > summary::-webkit-details-marker { display: none; }
/* A small chevron earns its keep here where .disclose's rotate-45 trick
   would not: the row it sits in is a number and a label, not a heading,
   and borrowing that trick would mean drawing it off a heading that is not
   there. */
.side-meter > summary::after {
  content: ""; width: 6px; height: 6px; flex: none; align-self: center; margin-left: -.2rem;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  opacity: .5; transform: rotate(45deg); transition: transform .18s var(--ease);
}
.side-meter[open] > summary::after { transform: rotate(-135deg); }
.side-meter .side-meter-top { display: flex; align-items: baseline; gap: .5rem; }
.side-meter .side-meter-top .lbl { font-weight: 500; }
.side-meter-n {
  margin-left: auto; font-size: var(--text-tagline-3);
  font-variant-numeric: tabular-nums; opacity: .75;
}
.side-meter-bar { display: block; width: 100%; height: .25rem; opacity: 1; }
.side-meter-say { display: block; margin-top: .35rem; font-size: var(--text-tagline-3); opacity: .7; }
/* ⚠ WAS color: inherit, WHICH INHERITED NOTHING VISIBLE. .side-meter sets
   color: inherit too, and by the time that chain reaches whatever this
   card's own ancestor resolves to, the text this link carries could land
   at a colour close enough to the card's own background to read as an
   empty box with no visible label at all - not a hover-only problem, the
   resting state was exactly as blank. An explicit colour here breaks that
   chain outright. It is also styled as the button it is now, rather than
   bare text sitting where a button-shaped gap in the card suggested one
   should be - "Plan and usage" was there in the markup the whole time,
   just never visibly a control anyone would think to press. */
/* ⚠ HOVER TOUCHES ONLY THE BORDER, NOT THE FILL BEHIND THE TEXT. It used to
   also swap `background` on :hover with a transition on both properties and
   the page's own generic `a:hover { transform: translateY(-1px) }` riding
   along for free - and on real devices (reported live, reproduced live: the
   label goes fully blank on hover, sometimes a washed-out grey instead,
   nothing fixed one property at a time) that combination corrupts the text
   paint. Every computed style checked correct throughout - full opacity,
   the right color, the right -webkit-text-fill-color - so this is a paint
   bug, not a cascade bug, and no single property reproduced it in
   isolation. Removing all three at once (no transform, no transition, no
   background repaint sitting directly behind the glyphs) is the fix that
   actually held. */
/* ⚠ ROUND FOUR. transform: none on :hover neutralised the VALUE
   (screens.css's global a:hover sets translateY(-1px)) but never touched
   the PROPERTY: screens.css's global `a { transition: transform .16s }`
   still applies to this element regardless, since nothing here ever said
   otherwise, and a live transition on transform - even one whose start and
   end values are identical - is still enough on some browser/GPU
   combinations to trigger the exact repaint that corrupts this text. This
   is the one thing every earlier round changed the VALUE of and never
   simply severed. */
.side-meter-more {
  display: flex; align-items: center; justify-content: center; gap: .4rem;
  margin-top: .5rem; padding: .4rem .6rem; border-radius: var(--r-sm, .5rem);
  border: 1px solid var(--stroke); background: var(--ground-2);
  color: var(--ink); font-size: var(--text-tagline-3); font-weight: 500;
  text-decoration: none; transition: none;
}
.side-meter-more svg { width: .9rem; height: .9rem; flex: none; }
.side-meter-more:hover { border-color: var(--ink); transform: none; transition: none; }
/* Colour follows the STATE, and it is currentColor the SVG fill reads, so one
   declaration moves the bar and the text together. */
.side-meter.is-near {
  color: var(--warn); border-color: var(--warn); background: var(--warn-wash);
}
.side-meter.is-full {
  color: var(--bad); border-color: var(--bad); background: var(--bad-wash);
}
.side-meter.is-near .side-meter-say,
.side-meter.is-full .side-meter-say { opacity: .9; font-weight: 500; }

/* ── the AI connector promo ───────────────────────────────────────────────
   Deliberately the quietest box in the sidebar: no coloured wash like
   .side-setup or .side-meter carry for their more urgent states, because
   this is a feature to discover, not a number that demands attention.

   .side-ai-wrap follows .side-setup-wrap exactly: the dismiss cross is a
   sibling of the link, not a child of it (a <form> cannot nest inside an
   <a>), so the wrapper exists to give the two of them a shared position:
   relative parent to overlay onto. */
.side-ai-wrap { position: relative; }
.side-ai-wrap .dismiss { position: absolute; top: 4px; right: 4px; }
.side-ai-wrap .dismiss button { width: 22px; height: 22px; }
.side-ai-wrap .dismiss svg { width: 12px; height: 12px; }
.side-ai {
  display: flex !important; align-items: center !important; gap: .5rem;
  padding: .55rem .75rem !important; margin: .25rem 0 .5rem;
  border: 1px solid var(--stroke); border-radius: var(--r-md, .7rem);
  text-decoration: none; color: var(--ink-2); white-space: normal !important;
}
.side-ai::before { display: none; }
.side-ai:hover { filter: brightness(.98); border-color: var(--brand); color: var(--ink); }
.side-ai-marks { display: flex; align-items: center; gap: .25rem; flex: none; }
.side-ai-marks img { display: block; border-radius: 3px; }
.side-ai-say { font-size: var(--text-tagline-3); line-height: 1.3; }

/* ── Settings, at the foot and on its own ─────────────────────────────────
   ⚠ It was third in "Set up", between Mailboxes and Branding, which filed the
   screen that governs how the assistant BEHAVES under the same heading as the
   two that are done once and forgotten. It is the screen somebody returns to
   for the life of the workspace, and the convention everywhere else — every
   operating system, every application — puts it at the bottom, apart. */
/* ⚠ margin-top:auto, the border-top and the forced border-radius:0 are
   gone: those existed to make this row sit flush against the bottom edge
   of the <nav> flex column it used to be the last child of. It is an
   ordinary sibling of .side-ai now, spaced the same way that box already
   spaces itself from what is above it, so it gets an ordinary margin
   instead of compensating for a position it no longer has. */
.side-settings { margin: .4rem 0 0; }
/* ⚠ NO SEPARATE .on RULE HERE ANY MORE. There used to be one, written back
   when this row still needed its own copy of the active state — and it set
   `color: var(--ink)` directly on the row while its ::before carried the
   same dark `background: var(--ink)`, dark text on a dark pill, the label
   readable only because a more specific `.lbl`/`svg` override happened to
   rescue it. .side-settings.on already shares the correct, plain version
   of this with every other row in the sidebar — solid background, white
   text, both set directly on the element with no pseudo involved — at line
   197 (and the hover/no-pseudo variants further down this file). A second,
   half-conflicting copy was never buying this row anything the shared rule
   did not already give it. */

@media (prefers-reduced-motion: reduce) {
  .side-setup:hover { filter: none; }
}

/* ── three classes that were written and never dressed ────────────────────
   ⚠ Found by the panel test that exists because the platform panel's sign-in
   went on emitting `.subswitch` after its rules were removed. Two of these
   predate that: `.panel-done` has been on the confirmation screen of every
   help request and bug report since those screens were written, and `.plans`
   on the platform overview. Both rendered as a plain panel, which is not
   broken enough to notice and not what either was written for. */

/* The panel somebody lands on after asking us for something. It is good news,
   and it should not look identical to the form they just left. */
.panel-done {
  border-color: color-mix(in srgb, var(--good, #0f7b4f) 35%, transparent);
  background: color-mix(in srgb, var(--good, #0f7b4f) 6%, var(--ground));
}
.panel-done h1, .panel-done h2 {
  display: flex; align-items: center; gap: .5rem;
  color: var(--good, #0f7b4f);
}
.panel-done h1 svg, .panel-done h2 svg { width: 1.15rem; height: 1.15rem; flex: none; }


/* The two buttons closing a help request: "this is sorted" and "still not
   right". They sit under the reply, and they need a rule above them or they
   read as part of the last message rather than as a question being asked. */
.req-say {
  margin-top: 1rem; padding-top: .9rem;
  border-top: 1px solid var(--stroke);
}
.req-say .actions { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .6rem; }

/* ═══════════════════════════════════════════════════════════════════════
   The workspace picker: the sign-in panel's ground, and a phone that fits.

   ⚠ WHY THE BACKGROUND. This is the screen immediately after signing in, and
   it landed on a flat pale ground belonging to no part of the product — so
   the one moment the brand had just established, on the screen before, was
   dropped between one page and the next. `.is-lit` gives it the same two
   drifting lights, the same dot grid, the same vignette and the same
   operator-set photograph that .split-art carries.

   ⚠ WHY IT IS A MODIFIER. .auth also dresses message_plain.php, which is the
   apology page — rendered when something has already gone wrong, and
   deliberately dependency-free. It must not gain a photograph, a route to
   fetch it from, or anything else that can fail. So the treatment is opt-in.
   ═══════════════════════════════════════════════════════════════════════ */

.auth.is-lit {
  position: relative; overflow: hidden;
  background: var(--ink);
}
/* ⚠ THE WHITE IS ON WHAT SITS OUTSIDE THE CARD, NEVER ON THE PAGE. Setting it
   on .auth.is-lit itself made every element inside the white card inherit it,
   and anything whose own colour failed to resolve then rendered white on
   white. The card keeps its own colours; only the two things standing on the
   dark ground are inverted. */
.auth.is-lit > .brand,
.auth.is-lit > .brand a,
.auth.is-lit .box > .brand,
.auth.is-lit .box > .brand a { color: #fff; }
/* The two lights, on the same long offset cycles as the sign-in panel so the
   two screens drift at the same rate rather than at two different ones. */
.auth.is-lit::before, .auth.is-lit::after {
  content: ""; position: absolute; border-radius: 50%; filter: blur(90px);
  pointer-events: none; z-index: 0;
}
.auth.is-lit::before { width: 34rem; height: 34rem; top: -14rem; right: -12rem;
                       background: var(--violet); opacity: .5;
                       animation: drift-a 34s ease-in-out infinite alternate; }
.auth.is-lit::after  { width: 26rem; height: 26rem; bottom: -12rem; left: -8rem;
                       background: var(--color-gradient-14, #bfee67); opacity: .16;
                       animation: drift-b 44s ease-in-out infinite alternate; }
.auth.is-lit .split-grid,
.auth.is-lit .split-veil,
.auth.is-lit .split-photo { position: absolute; inset: 0; z-index: 0; }
.auth.is-lit .split-grid {
  background-image: radial-gradient(rgba(255,255,255,.14) 1px, transparent 1px);
  background-size: 24px 24px; pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, rgba(0,0,0,.5) 55%, transparent 85%);
          mask-image: linear-gradient(to bottom, #000 0%, rgba(0,0,0,.5) 55%, transparent 85%);
}
.auth.is-lit .split-veil {
  pointer-events: none;
  background: radial-gradient(120% 80% at 50% 50%, transparent 0%, rgba(9,10,14,.55) 100%);
}
/* ⚠ THE PHOTOGRAPH HAD NO TREATMENT ON THIS SCREEN. The dim and the blur live
   on `.split-art .split-photo img`, scoped to the sign-in panel, so here the
   picture rendered at full brightness and its own size: a bright photo across
   the top of the page with the wordmark sitting in the middle of it. The
   treatment belongs to the photo, not to the panel it happens to be in. */
.auth.is-lit .split-photo { overflow: hidden; margin: 0; }
.auth.is-lit .split-photo img {
  width: 100%; height: 100%; object-fit: cover;
  opacity: .22;
  filter: blur(3px) saturate(.65);
  transform: scale(1.05);
}
.auth.is-lit .split-photo::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(to bottom, rgba(9,10,14,.55), rgba(9,10,14,.8));
}
/* The card and the wordmark sit above all of it. */
.auth.is-lit > .box { position: relative; z-index: 1; }
/* ⚠ The wordmark is OUTSIDE the card, straight on the dark ground, so it has
   to invert with it — it was near-black text on near-black and simply gone. */
.auth.is-lit .brand,
.auth.is-lit .brand a { color: #fff; }

/* ── the phone ────────────────────────────────────────────────────────
   ⚠ NOTHING HERE HAD A BREAKPOINT. .auth carried one card padding (2.25rem)
   and one heading size at every width, so on a 390px screen 72px of the 390
   was padding — the rows had about 245px to say a workspace name, a slug and
   a role in. */
@media (max-width: 480px) {
  .auth { padding: 1.25rem .75rem; }
  .auth .card { padding: 1.35rem 1.15rem; border-radius: var(--r-md); }
  .auth .brand { margin-bottom: 1.15rem; }
  .auth h1 { font-size: var(--text-heading-6); }
  .auth .sub { margin: .4rem 0 1.25rem; }
  .pick { padding: .8rem .85rem; gap: .65rem; }
  .pick .tdot { width: 2rem; height: 2rem; font-size: .68rem; }
  .auth .alt { margin-top: 1.1rem; }
}

/* ⚠ 100dvh, WITH 100vh UNDERNEATH IT. On a phone 100vh is the viewport with
   the address bar hidden, which it is not when the page loads — so a screen
   that fits exactly is scrollable by the height of the bar, and a centred
   card sits slightly low with a strip of ground under it. dvh is the height
   actually available; the vh line stays first for anything that has never
   heard of dvh. */
.auth { min-height: 100vh; min-height: 100dvh; }

/* ⚠ A TOUCH TARGET CANNOT HOVER. .pick lifts on :hover, which on this screen
   is the only affordance saying the row is pressable — and on the device most
   likely to be used for a quick check it never fires at all. :active is what
   a finger produces, and it presses rather than lifts, because that is what a
   thing being touched does. */
.pick:active { transform: translateY(0) scale(.985); border-color: var(--violet); }
@media (hover: none) {
  .pick { transition: transform .12s var(--ease), border-color .12s var(--ease); }
  .pick:hover { transform: none; box-shadow: none; border-color: var(--stroke); }
}

@media (prefers-reduced-motion: reduce) {
  .auth.is-lit::before, .auth.is-lit::after { animation: none; }
  .pick:active { transform: none; }
}

/* ── Continue with Google ─────────────────────────────────────────────────
   ⚠ Google's brand guidelines want their mark at full colour on either a
   white or a dark button, with the wordmark left alone. White it is, because
   both these cards are light and a dark button here would compete with the
   primary action rather than sit under it. */
.btn-google {
  width: 100%; justify-content: center; gap: .6rem;
  background: #fff; color: #1f1f1f;
  border: 1px solid var(--stroke-2, #cbd2da);
  font-weight: 500;
}
.btn-google:hover {
  background: #f8f9fa;
  border-color: color-mix(in srgb, var(--ink) 28%, transparent);
}
.btn-google:focus-visible { outline: 2px solid var(--violet); outline-offset: 2px; }
.btn-google svg { flex: none; }

/* The rule between the third-party way in and our own. A word on the line
   rather than a bare rule, because a bare rule between two buttons reads as a
   section break and these are two answers to one question. */
.or {
  display: flex; align-items: center; gap: .75rem;
  margin: 1rem 0; color: var(--muted); font-size: var(--text-tagline-3);
}
.or::before, .or::after {
  content: ""; flex: 1 1 auto; height: 1px; background: var(--stroke);
}

/* ── the plan editor's model picker ───────────────────────────────────────
   ⚠ REPLACES A TEXTAREA OF IDS. plan_edit.php used to ask the operator to
   type model ids one per line, sourced from nowhere on the screen itself -
   a typo was silently dropped by usage_clean_models() with no error
   anywhere. Every model this build actually offers is a checkbox now,
   grouped by provider with its own mark, so there is nothing to type and
   nothing to get wrong. */
.model-pick-groups {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 1.25rem; margin-top: .25rem;
}
.model-pick-head {
  display: flex; align-items: center; gap: .5rem;
  margin: 0 0 .6rem; font-size: 13px; font-weight: 650; color: var(--ink-2);
  text-transform: uppercase; letter-spacing: .02em;
}
.model-pick-head img { width: 16px; height: 16px; flex: none; border-radius: 4px; }
.model-pick-row {
  display: grid; grid-template-columns: auto 1fr; align-items: baseline;
  column-gap: .6rem; row-gap: 1px; padding: .45rem 0;
  border-top: 1px solid var(--line);
}
.model-pick-group .model-pick-row:first-of-type { border-top: none; }
.model-pick-row input { margin-top: .2rem; }
.model-pick-name { font-size: 13px; font-weight: 600; color: var(--ink); }
.model-pick-note {
  grid-column: 2; font-size: 12px; color: var(--ink-3); line-height: 1.4;
}
