/* shell.css — the page frame: sidebar, main bar, and the large dialog that
   Settings opens in.

   Anatomy:
     <div class="shell">
       <aside class="sidebar">            ← navigation + past sessions
         .sb-head / .sb-nav / .sb-sessions
       <div class="main">
         <header id="topbar">             ← the current session's title
         <div class="layout">             ← chat
     <div class="shell-dialog-overlay">   ← settings / notifications

   Sidebar state lives on <html> so theme_boot.js can set it before first
   paint: data-sidebar="open|collapsed" is the desktop preference, and
   data-sidebar-drawer="open" slides the sidebar in over the page on a phone.
   Colours come from the tokens in base.css; nothing here is theme-specific
   beyond the two sun/moon icon rules. */

:root {
  --sidebar-w: 268px;
  --topbar-height: 52px;
  --content-w: 780px;
}

.shell {
  display: flex;
  flex: 1 1 auto;
  min-height: 0;
  min-width: 0;
}

/* ── Icon buttons ───────────────────────────────────────────────────── */
.sb-ico {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  display: block;
}
.shell-icon-btn {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  transition: background .15s ease, color .15s ease;
}
.shell-icon-btn:hover {
  background: var(--shell-hover);
  border: 0;
  color: var(--text);
}
.shell-icon-btn:active { transform: none }
.shell-icon-btn:focus-visible {
  outline: 2px solid var(--focus-outline);
  outline-offset: 1px;
}

/* ── Sidebar ────────────────────────────────────────────────────────── */
.sidebar {
  flex: 0 0 var(--sidebar-w);
  width: var(--sidebar-w);
  min-height: 0;
  display: flex;
  flex-direction: column;
  background: var(--sidebar-bg);
  border-right: 1px solid var(--border);
  transition: margin-left .22s ease, visibility 0s linear 0s;
}
:root[data-sidebar="collapsed"] .sidebar {
  margin-left: calc(-1 * var(--sidebar-w));
  /* Hidden only once it has slid away, so the collapsed sidebar's controls
     leave the tab order without cutting the animation short. */
  visibility: hidden;
  transition: margin-left .22s ease, visibility 0s linear .22s;
}

.sb-head {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
  height: var(--topbar-height);
  padding: 0 .6rem 0 1rem;
}
.sb-head-actions {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 2px;
}
.sb-theme-toggle .ico-sun { display: none }
:root[data-theme="dark"] .sb-theme-toggle .ico-sun { display: inline }
:root[data-theme="dark"] .sb-theme-toggle .ico-moon { display: none }

/* Agent name — a click on it opens the about card (about.js); the pencil
   beside it renames it where it stands (agent_name.js). A click on the name
   used to open the field too, and readers who meant to go somewhere found themselves
   renaming the agent. The pencil shows when the pointer is over the name,
   and always on a touch screen, which has no pointer to hover with. */
.topbar-left {
  min-width: 0;
  display: flex;
  align-items: center;
  gap: .1rem;
  font-size: 1.02rem;
  font-weight: 650;
  letter-spacing: -.01em;
  color: var(--text);
}
.topbar-left .brand {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The name is a button that reads as the name: no chrome until the pointer
   is on it, then the sidebar's own hover fill. Its padding sits outside the
   text, so the name stays where the field it turns into starts. */
.topbar-left .agent-about {
  min-width: 0;
  display: flex;
  align-items: center;
  height: 30px;
  margin-inline-start: -.45rem;
  padding: 0 .45rem;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  transition: background .15s ease;
}
.topbar-left .agent-about:hover {
  background: var(--shell-hover);
  border: 0;
  color: inherit;
}
.topbar-left .agent-about:active { transform: none }
.topbar-left .agent-about:focus-visible {
  outline: 2px solid var(--focus-outline);
  outline-offset: 1px;
}
.topbar-left .agent-name-edit {
  width: 26px;
  height: 26px;
  opacity: 0;
  transition: opacity .18s ease, background .15s ease, color .15s ease;
}
.topbar-left .edit-icon {
  width: 14px;
  height: 14px;
}
.topbar-left:hover .agent-name-edit,
.topbar-left .agent-name-edit:focus-visible { opacity: 1 }
@media (hover: none) {
  .topbar-left .agent-name-edit { opacity: .85 }
}
.topbar-left[hidden] { display: none }
/* The field the name turns into: the name's own type, in the room the name
   had. */
.agent-name-input {
  flex: 1 1 auto;
  min-width: 0;
  width: auto;
  height: 30px;
  margin-left: -.45rem;
  padding: 0 .45rem;
  border: 0;
  border-radius: 8px;
  background: var(--shell-hover);
  box-shadow: inset 0 0 0 1px var(--border2);
  color: var(--text);
  font: inherit;
  font-size: 1.02rem;
  font-weight: 650;
  letter-spacing: -.01em;
}
.agent-name-input:focus {
  outline: none;
  border: 0;
  box-shadow: inset 0 0 0 1px var(--outline-hover);
}
.agent-name-input:disabled {
  opacity: .6;
  cursor: progress;
}
.agent-name-input[hidden] { display: none }

.sb-nav {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: .25rem .5rem .5rem;
}

.sb-item,
.sb-session {
  display: flex;
  align-items: center;
  gap: .65rem;
  width: 100%;
  min-width: 0;
  padding: .48rem .6rem;
  border: 0;
  border-radius: 9px;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: .88rem;
  font-weight: 500;
  line-height: 1.25;
  letter-spacing: 0;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: background .15s ease, color .15s ease;
}
.sb-item[hidden] { display: none }
/* The navigation is one list the user arranges: the panel's own entries and
   the pinned settings categories are rows of the same column
   (sidebar_pins.js). With a pointer, a row's actions (move, unpin) wait
   behind its "more" button, which itself waits for the pointer or the
   keyboard to reach the row, and the rows can be dragged into order; a touch
   screen has a finger for all of it (below). The long press that lifts a
   row picks no text and raises no callout. */
.sb-nav-row {
  position: relative;
  display: flex;
  align-items: center;
  min-width: 0;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
.sb-nav-row .sb-item { flex: 1 1 auto }
/* A row whose entry is away (Notifications, until a finished run exists) is
   away with it. */
.sb-nav-row:has(> .sb-item[hidden]) { display: none }
.sb-nav-more {
  position: absolute;
  top: 0;
  bottom: 0;
  right: 3px;
  width: 26px;
  height: 26px;
  margin: auto 0;
  opacity: 0;
}
/* :focus-visible, not :focus-within: closing the Settings dialog hands focus
   back to the row that opened it, and a mouse-set focus that outlives the
   pointer would leave the button hanging on a row nobody is pointing at. */
.sb-nav-row:hover .sb-nav-more,
.sb-nav-row:has(:focus-visible) .sb-nav-more,
.sb-nav-more[aria-expanded="true"] { opacity: 1 }
.sb-nav-more[aria-expanded="true"] {
  background: var(--shell-hover);
  color: var(--text);
}
.sb-nav-row:hover .sb-item,
.sb-nav-row:has(:focus-visible) .sb-item,
.sb-nav-row:has(.sb-nav-more[aria-expanded="true"]) .sb-item { padding-right: 2.1rem }
.sb-nav-row.is-dragging { opacity: .45 }
/* While a row is dragged, a line with a dot at its head says where it lands:
   in the seam above or below the row under the pointer (sidebar_pins.js sets
   data-drop on that row). */
.sb-nav-row[data-drop]::before,
.sb-nav-row[data-drop]::after {
  content: '';
  position: absolute;
  background: var(--accent);
  pointer-events: none;
  z-index: 1;
}
.sb-nav-row[data-drop]::before {
  left: 5px;
  right: 3px;
  height: 2px;
  border-radius: 1px;
}
.sb-nav-row[data-drop]::after {
  left: 0;
  width: 7px;
  height: 7px;
  border-radius: 50%;
}
.sb-nav-row[data-drop="before"]::before { top: -1.5px }
.sb-nav-row[data-drop="before"]::after { top: -4px }
.sb-nav-row[data-drop="after"]::before { bottom: -1.5px }
.sb-nav-row[data-drop="after"]::after { bottom: -4px }
/* A touch screen shows no "more" button: a finger rests on a row to lift
   it, moves it up or down, and pulls a shortcut aside to take it off
   (sidebar_pins.js wireHold). The button stays for a keyboard or a screen
   reader, and a tap on the row's right end opens the row. */
@media (hover: none) {
  .sb-nav-more { pointer-events: none }
  .sb-nav-row:hover .sb-nav-more { opacity: 0 }
  .sb-nav-row:hover .sb-item { padding-right: .6rem }
  .sb-nav-row:has(:focus-visible) .sb-nav-more,
  .sb-nav-row .sb-nav-more[aria-expanded="true"] { opacity: 1; pointer-events: auto }
  .sb-nav-row:has(:focus-visible) .sb-item,
  .sb-nav-row:has(.sb-nav-more[aria-expanded="true"]) .sb-item { padding-right: 2.1rem }
}
/* The row a finger holds stands out on its tile like a menu item held
   (.menu-item.is-dragging) and follows the finger sideways, fading as it
   goes; pulled far enough, it turns red -- let go there and it is off the
   list. */
.sb-nav-row.is-lifted {
  z-index: 2;
  transform: translateX(var(--pull, 0px));
  opacity: calc(1 - var(--pull-share, 0) * .5);
}
.sb-nav-row.is-lifted .sb-item {
  background: var(--shell-hover);
  box-shadow: 0 0 0 1px var(--border2);
}
.sb-nav-row[data-throw] .sb-item,
.sb-nav-row[data-throw] .sb-item .sb-ico { color: var(--danger-action) }
.sb-item .sb-ico { color: var(--muted) }
.sb-item:hover,
.sb-session:hover {
  background: var(--shell-hover);
  border: 0;
  color: var(--text);
  text-decoration: none;
}
.sb-item:active,
.sb-session:active { transform: none }
.sb-item.active,
.sb-session.active { background: var(--shell-active) }
.sb-item.active .sb-ico { color: var(--text) }
.sb-item:focus-visible,
.sb-session:focus-visible {
  outline: 2px solid var(--focus-outline);
  outline-offset: -2px;
}
.sb-label {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The unread bubble sits right after the label, not at the far edge where
   the row's "more" button appears on hover; the status dot rides at the edge. */
.sb-item .tab-count { flex: 0 0 auto; margin-left: .4rem }
.sb-item .tab-count[hidden] { display: none }
.sb-item .tab-dot { margin-left: auto }

/* ── Sidebar: past sessions ─────────────────────────────────────────── */
.sb-sessions {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  padding: .35rem .5rem 0;
}
/* The project switcher (projects.js): a row over the list naming whose
   conversations it holds, flat like the navigation rows above it — the
   drawing, the name — with the chevrons of a choice to be made. Its
   "Project" label is for the screen reader; the row reads as the list's. */
.sb-project {
  flex: 0 0 auto;
  padding: 0 0 .35rem;
}
.sb-project[hidden] { display: none }
.sb-project-btn {
  display: flex;
  align-items: center;
  gap: .65rem;
  width: 100%;
  min-width: 0;
  padding: .48rem .6rem;
  border: 0;
  border-radius: 9px;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: .88rem;
  font-weight: 500;
  line-height: 1.25;
  letter-spacing: 0;
  text-align: left;
  box-shadow: none;
  cursor: pointer;
  transition: background .15s ease, color .15s ease;
}
.sb-project-btn:hover,
.sb-project-btn[aria-expanded="true"] {
  background: var(--shell-hover);
  border: 0;
  color: var(--text);
}
.sb-project-btn:active { transform: none }
.sb-project-btn:focus-visible {
  outline: 2px solid var(--focus-outline);
  outline-offset: -2px;
}
.sb-project-icon {
  flex: 0 0 auto;
  display: inline-flex;
  color: var(--muted);
}
.sb-project-icon svg {
  display: block;
  width: 18px;
  height: 18px;
}
.sb-project-text {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
}
.sb-project-label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.sb-project-name {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sb-project-chevron {
  flex: 0 0 auto;
  width: 15px;
  height: 15px;
  color: var(--muted2);
  transition: color .15s ease;
}
.sb-project-btn:hover .sb-project-chevron,
.sb-project-btn[aria-expanded="true"] .sb-project-chevron { color: var(--text) }
.sb-section-head {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: .25rem .1rem .15rem .6rem;
  font-size: .74rem;
  font-weight: 500;
  color: var(--muted);
}
/* The heading is the switch that puts the list away and brings it back. It
   stands as tall as the icon buttons beside it, so the head keeps its height
   (and the heading its place) when those buttons go away with the list. */
.sb-section-toggle {
  display: inline-flex;
  align-items: center;
  gap: .2rem;
  min-width: 0;
  height: 26px;
  margin-left: -.45rem;
  padding: 0 .45rem 0 .25rem;
  border: 0;
  border-radius: 6px;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
}
.sb-section-toggle:hover {
  background: var(--shell-hover);
  color: var(--text);
}
.sb-section-toggle:active { transform: none }
.sb-section-toggle:focus-visible {
  outline: 2px solid var(--focus-outline);
  outline-offset: -1px;
}
.sb-section-toggle .sb-section-chevron {
  width: 14px;
  height: 14px;
  transition: transform .15s ease;
}
/* The list put away: the heading alone stays, its chevron turned aside, so
   past conversations and their titles are off the screen. */
:root[data-sessions="hidden"] .sb-section-chevron { transform: rotate(-90deg) }
:root[data-sessions="hidden"] .sb-section-actions,
:root[data-sessions="hidden"] .sb-session-search,
:root[data-sessions="hidden"] .sb-session-list { display: none }
.sb-section-actions {
  display: flex;
  align-items: center;
}
.sb-section-head .shell-icon-btn {
  width: 26px;
  height: 26px;
}
.sb-section-head .shell-icon-btn[hidden] { display: none }
.sb-section-head .sb-ico {
  width: 15px;
  height: 15px;
}
.sb-session-search {
  flex: 0 0 auto;
  padding: .1rem .1rem .3rem;
}
.sb-session-search[hidden] { display: none }
.sb-session-search input,
.sb-session-input {
  min-width: 0;
  height: 30px;
  padding: 0 .6rem;
  border: 1px solid var(--border2);
  border-radius: 8px;
  background: var(--composer-bg);
  color: var(--text);
  font: inherit;
  font-size: .84rem;
}
.sb-session-search input { width: 100% }
.sb-session-search input:focus,
.sb-session-input:focus {
  outline: none;
  border-color: var(--outline-hover);
  box-shadow: none;
}
.sb-session-list {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding-bottom: .5rem;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: transparent transparent;
}
.sb-session-list:hover { scrollbar-color: var(--scrollbar-thumb) transparent }
.sb-session-group {
  flex: 0 0 auto;
  padding: .6rem .6rem .15rem;
  font-size: .7rem;
  font-weight: 500;
  color: var(--muted2);
}
.sb-session-group:first-child { padding-top: .15rem }
.sb-session-row {
  position: relative;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  min-width: 0;
}
.sb-session {
  flex: 1 1 auto;
  gap: .4rem;
  padding: .4rem .6rem;
  font-size: .84rem;
  font-weight: 400;
  color: var(--muted);
}
/* The session on screen: filled like every active row, its title in full
   ink. Its leading edge stays bare -- a stripe there says the agent is
   writing a reply (below). */
.sb-session.active {
  color: var(--text);
  font-weight: 600;
}
/* The agent works in the conversation (sidebar_sessions.js): an accent
   stripe at the leading edge fades in and out at its full length, on every
   row with a turn under way -- full on the open one, paler on the others,
   in step with their muted titles. */
.sb-session.is-running { position: relative }
.sb-session-running {
  --run-hi: 1;
  --run-lo: .35;
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 3px;
  height: 1.05rem;
  margin: auto 0;
  border-radius: 0 3px 3px 0;
  background: var(--accent);
  opacity: var(--run-hi);
  pointer-events: none;
}
.sb-session:not(.active) .sb-session-running {
  --run-hi: .5;
  --run-lo: .15;
}
/* Every mark of a turn under way pulses on this one beat -- the stripes
   here fade, the projects' drawings below throb their shadow -- and joins
   it from the page's clock whenever it is drawn (running_marks.js). A mark
   whose turn is over plays the beat out and fades before it goes. */
.sb-session-running,
.menu-item-icon.is-running svg,
.prj-icon.is-running svg { animation: sb-running-pulse 1.4s ease-in-out infinite }
@keyframes sb-running-pulse {
  50% { opacity: var(--run-lo, 1); filter: var(--run-shadow-lo, none) }
}
/* A shadow held still says the same without the motion. */
@media (prefers-reduced-motion: reduce) {
  .menu-item-icon.is-running svg,
  .prj-icon.is-running svg { animation: none }
}
/* A row's actions wait behind this button, which itself waits for the
   pointer or the keyboard to reach the row. */
.sb-session-more {
  position: absolute;
  top: 0;
  bottom: 0;
  right: 3px;
  width: 26px;
  height: 26px;
  margin: auto 0;
  opacity: 0;
}
/* :focus-visible for the same reason as .sb-nav-more: a clicked row keeps
   focus after the pointer has left it. */
.sb-session-row:hover .sb-session-more,
.sb-session-row:has(:focus-visible) .sb-session-more,
.sb-session-more[aria-expanded="true"] { opacity: 1 }
.sb-session-more[aria-expanded="true"] {
  background: var(--shell-hover);
  color: var(--text);
}
.sb-session-row:hover .sb-session,
.sb-session-row:has(:focus-visible) .sb-session,
.sb-session-row:has(.sb-session-more[aria-expanded="true"]) .sb-session { padding-right: 2.1rem }
/* No pointer to hover with: the button is simply there. */
@media (hover: none) {
  .sb-session-more { opacity: 1 }
  .sb-session-row .sb-session { padding-right: 2.1rem }
}
.sb-session-mark {
  flex: 0 0 auto;
  font-size: .72rem;
  color: var(--muted2);
}
.sb-session-mark svg {
  display: block;
  width: 13px;
  height: 13px;
}
.sb-label + .sb-session-mark { margin-left: auto }
.sb-session-mark.is-warn { color: var(--warn) }
/* Who started a shared conversation, when that is somebody else. */
.sb-session-mark.sb-session-starter {
  max-width: 6rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The project a session works in: a quiet tag after the title. */
.sb-session-mark.sb-session-project {
  max-width: 7rem;
  padding: 0 .38rem;
  border-radius: 6px;
  background: var(--shell-hover);
  color: var(--muted);
  font-size: .68rem;
  font-weight: 500;
  line-height: 1.3rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sb-session-input {
  flex: 1 1 auto;
  margin: 1px 0;
}
.sb-session-note {
  margin: 0;
  padding: .4rem .6rem;
  font-size: .8rem;
  line-height: 1.45;
  color: var(--muted2);
}
/* The auto-titler is still naming the conversation (sessions.js
   titlePending): its label breathes quietly until the name lands. The chat
   header (sessions.css) breathes with it. */
.sb-label.is-titling { animation: sb-titling 1.8s ease-in-out infinite }
@keyframes sb-titling {
  0%, 100% { opacity: .45 }
  50%      { opacity: 1 }
}
/* Before the first list arrives: a few quiet bars where the rows will be,
   breathing in turn. */
.sb-session-skeleton {
  display: flex;
  align-items: center;
  height: 1.95rem;
  padding: 0 .6rem;
}
.sb-skeleton-bar {
  height: .5rem;
  border-radius: 4px;
  background: var(--shell-hover);
  animation: sb-titling 1.8s ease-in-out infinite;
}
.sb-session-skeleton:nth-child(2) .sb-skeleton-bar { animation-delay: .2s }
.sb-session-skeleton:nth-child(3) .sb-skeleton-bar { animation-delay: .4s }

/* ── Menus (menu.js) ────────────────────────────────────────────────── */
.menu-pop {
  position: fixed;
  /* Above the phone drawer and the dialog, below modals. */
  z-index: 960;
  min-width: 11rem;
  max-width: 17rem;
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: .3rem;
  overflow-y: auto;
  background: var(--menu-bg);
  border: 1px solid var(--border2);
  border-radius: 12px;
  box-shadow: var(--shadow-pop);
}
.menu-item {
  display: block;
  width: 100%;
  padding: .45rem .65rem;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: .85rem;
  font-weight: 400;
  line-height: 1.3;
  letter-spacing: 0;
  text-align: left;
  cursor: pointer;
}
.menu-item:hover,
.menu-item:focus-visible {
  background: var(--shell-hover);
  border: 0;
  color: var(--text);
  outline: none;
  box-shadow: none;
}
.menu-item:active { transform: none }
.menu-item.is-danger,
.menu-item.is-danger:hover,
.menu-item.is-danger:focus-visible { color: var(--danger-action) }
/* Asked once more before it runs. */
.menu-item.is-armed {
  background: color-mix(in srgb, var(--danger-action) 12%, transparent);
  font-weight: 600;
}
/* An item in parts (menu.js): the icon on its square ahead, the label over
   a quieter hint, a check at the end of the one picked. A hairline parts
   the groups. */
.menu-item.has-parts {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .38rem .55rem .38rem .4rem;
}
.menu-item-icon {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 8px;
  background: var(--shell-hover);
  color: var(--muted);
}
.menu-item-icon svg {
  display: block;
  width: 15px;
  height: 15px;
}
/* An action, not one of the set: the drawing alone. */
.menu-item-icon.is-plain { background: transparent }
.menu-item:hover .menu-item-icon.is-plain,
.menu-item:focus-visible .menu-item-icon.is-plain { color: var(--text) }
.menu-item.is-checked .menu-item-icon {
  background: var(--accent-soft);
  color: var(--accent);
}
.menu-item-text {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.menu-item-label,
.menu-item-hint {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.menu-item.is-checked .menu-item-label { font-weight: 600 }
.menu-item-hint {
  font-size: .72rem;
  color: var(--muted2);
}
.menu-item-check {
  flex: 0 0 auto;
  display: inline-flex;
  color: var(--accent);
}
.menu-item-check svg {
  display: block;
  width: 15px;
  height: 15px;
}
.menu-sep {
  flex: 0 0 auto;
  height: 1px;
  margin: .25rem .45rem;
  background: var(--border);
}
/* The project switcher's (projects.js): room for a longer name, and a long
   list scrolls inside it. */
.menu-pop--projects {
  max-width: 20rem;
  max-height: min(28rem, 70vh);
}
/* Flat like the row it hangs from: the drawings without their tiles, the
   one picked in full colour beside its check. */
.menu-pop--projects .menu-item.has-parts { gap: .65rem; padding: .42rem .55rem }
.menu-pop--projects .menu-item-icon,
.menu-pop--projects .menu-item.is-checked .menu-item-icon {
  width: 18px;
  height: 18px;
  background: transparent;
}
.menu-pop--projects .menu-item-icon svg { width: 18px; height: 18px }
.menu-pop--projects .menu-item.is-checked .menu-item-icon { color: var(--text) }
/* Dragged into an order with the mouse or a finger (menu.js): the item held
   stands out on its tile, and the pointer grabs while it is held. The long
   press a finger lifts one with picks no text and raises no callout. */
.menu-item[data-sort-key] { -webkit-touch-callout: none; -webkit-user-select: none; user-select: none }
.menu-pop.is-sorting,
.menu-pop.is-sorting .menu-item { cursor: grabbing; user-select: none }
.menu-item.is-dragging {
  background: var(--shell-hover);
  box-shadow: 0 0 0 1px var(--border2);
}
/* A project the agent works in right now (projects.js): its drawing in the
   switcher's menu and the Settings list (server.css .prj-icon) keeps its
   ink and casts a small accent shadow down and to the right, which throbs
   out and back on the stripes' beat. That ink is the drawing's own, over
   its row's: once the turn is over, the drawing fades back to the row's
   (running_marks.js). The switcher itself, always in view, stays still. */
.menu-pop--projects .menu-item .menu-item-icon.is-running,
.prj-icon.is-running {
  --run-shadow-hi: drop-shadow(1.75px 1.75px 1px color-mix(in srgb, var(--accent) 70%, transparent));
  --run-shadow-lo: drop-shadow(.5px .5px 1px color-mix(in srgb, var(--accent) 20%, transparent));
}
.menu-pop--projects .menu-item .menu-item-icon.is-running svg,
.prj-row:not(.is-selected) .prj-icon.is-running svg { color: var(--text) }
.menu-item-icon.is-running svg,
.prj-icon.is-running svg { filter: var(--run-shadow-hi, none) }
.sidebar-scrim { display: none }

/* ── Main column ────────────────────────────────────────────────────── */
.main {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  background: var(--bg);
}

#topbar {
  flex: 0 0 auto;
  position: relative;
  z-index: 6;
  display: flex;
  align-items: center;
  gap: .35rem;
  height: var(--topbar-height);
  padding: 0 .9rem 0 .6rem;
}
/* The opener is only needed while the sidebar itself is out of sight. */
#sidebarOpen {
  display: none;
  position: relative;
}
:root[data-sidebar="collapsed"] #sidebarOpen { display: inline-flex }
/* With the sidebar out of sight its badge is too, so the opener carries a
   dot while Notifications has unread runs. */
.shell:has(#jobsTabCount:not([hidden])) #sidebarOpen::after {
  content: "";
  position: absolute;
  top: 5px;
  right: 5px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 2px var(--bg);
}

/* Notification chips centre on the main column rather than on the whole
   window while the sidebar takes its share of it. */
@media (min-width: 821px) {
  :root[data-sidebar="open"] .notify-tray { left: var(--sidebar-w) }
}

/* Session controls belong to the chat; on any other page they step out of
   sight. */
.shell:not([data-main="chat"]) .chat-session-float { visibility: hidden }

/* ── Plain pages (empty states) ─────────────────────────── */
.page-wrap {
  width: 100%;
  max-width: var(--content-w);
  margin: 0 auto;
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
}
.empty-state {
  margin: auto;
  max-width: 26rem;
  padding: 2rem 1.25rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .5rem;
  text-align: center;
  color: var(--muted);
}
.empty-state .sb-ico {
  width: 34px;
  height: 34px;
  margin-bottom: .35rem;
  color: var(--muted2);
}
.empty-state h2 {
  font-size: 1.15rem;
  font-weight: 600;
  color: var(--text);
}
.empty-state p {
  font-size: .92rem;
  line-height: 1.5;
}

/* ── Dialog: Settings, Notifications ─────────────────────────────────── */
.shell-dialog-overlay {
  position: fixed;
  inset: 0;
  /* Above the page and the phone drawer, below modals (9999) and the
     notification tray (10001) — both may open on top of the dialog. */
  z-index: 950;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem;
  background: var(--dialog-scrim);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  animation: shell-dialog-fade .16s ease;
}
.shell-dialog-overlay[hidden] { display: none }
.shell-dialog {
  width: min(1080px, 100%);
  height: min(860px, 100%);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--bg);
  border: 1px solid var(--border2);
  border-radius: 18px;
  box-shadow: var(--shadow-pop);
  animation: shell-dialog-rise .2s ease;
}
/* Focused programmatically on open; the frame itself needs no ring. */
.shell-dialog:focus { outline: none }
@keyframes shell-dialog-fade {
  from { opacity: 0 }
  to   { opacity: 1 }
}
@keyframes shell-dialog-rise {
  from { opacity: 0; transform: translateY(6px) scale(.985) }
  to   { opacity: 1; transform: none }
}
.shell-dialog-head {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: .7rem .8rem .7rem 1.4rem;
  border-bottom: 1px solid var(--border);
}
.shell-dialog-head h2 {
  font-size: 1rem;
  font-weight: 600;
  color: var(--text);
}
.shell-dialog > .view {
  flex: 1 1 auto;
  min-height: 0;
}
/* The settings search in the dialog's head, where shell.js puts it on a
   phone: the title keeps its width, the field takes the rest. */
.shell-dialog-head.has-search { gap: .6rem }
.shell-dialog-head h2 {
  flex: 0 0 auto;
  white-space: nowrap;
}
.shell-dialog-head .settings-search {
  flex: 1 1 auto;
  min-width: 0;
  position: relative;
  padding: 0;
  background: transparent;
}
.shell-dialog-head .settings-search input[type="search"] {
  height: 34px;
  padding: 0 .75rem 0 2rem;
  border-radius: 10px;
  font-size: .86rem;
}
.shell-dialog-head .settings-search-icon { left: .6rem }

/* Settings inside the dialog: a calm list on the left, the chosen category
   on the right. The rows are compact and each label stays on one line, so
   the whole list fits a laptop-height dialog without a scrollbar; the
   scroll below is only the fallback for a genuinely short screen. The glow rail of the old full-page layout gives way to a
   plain filled row, matching the sidebar. */
.shell-dialog .settings-layout {
  gap: 1.5rem;
  padding: 0 1.4rem 1.5rem;
}
/* The sticky list is never taller than the view it pins to (the dialog's
   height less its head and frame), so on a short screen the categories
   scroll inside the list instead of running off the dialog's bottom; the
   search above them stays put. */
.shell-dialog .settings-sidebar {
  flex-basis: 228px;
  max-height: calc(min(860px, 100dvh - 4rem) - 4.25rem);
}
.shell-dialog .settings-search { flex-shrink: 0 }
.shell-dialog .settings-content { padding-bottom: 3rem }
.shell-dialog .settings-nav {
  gap: 1px;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--scrollbar-thumb) transparent;
}
.shell-dialog .settings-nav-link {
  padding: .35rem .65rem;
  border-radius: 8px;
  color: var(--text);
  font-size: .86rem;
  line-height: 1.35;
  white-space: nowrap;
  transition: background .15s ease;
}
/* A label too long for the list in some language ends in an ellipsis
   rather than wrapping its row to two lines. */
.shell-dialog .settings-nav-link > [data-i18n] {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.shell-dialog .settings-nav-link::before,
.shell-dialog .settings-nav-link::after { content: none }
.shell-dialog .settings-nav-link:hover { background: var(--shell-hover) }
.shell-dialog .settings-nav-link.active {
  background: var(--shell-active);
  color: var(--text);
}
.shell-dialog .settings-category-title {
  justify-content: flex-start;
  font-size: 1.05rem;
  font-weight: 600;
}
.shell-dialog .settings-category-title .sct-emoji { font-size: 1.1rem }

/* A category or card opened from a sidebar shortcut stands alone (settings.js
   sets data-focus, and data-focus-section for a card, on the view): the list
   and the search stay out of sight, so does the category's heading — the
   dialog's head carries the name — and, for a card, its neighbours. */
.settings-view[data-focus] .settings-sidebar,
.settings-view[data-focus] .settings-category-title,
.shell-dialog:has(.settings-view[data-focus]) .shell-dialog-head .settings-search { display: none }
/* Notifications in the same dialog: the head is the name and the close
   button alone — the settings search (shell.js moves it into the head on a
   phone) and the pin of a focused settings card belong to Settings. */
.shell-dialog-overlay:not([data-view="settings"]) .shell-dialog-head .settings-search,
.shell-dialog-overlay:not([data-view="settings"]) .shell-dialog-pin { display: none }
.panel-section[data-focus-hidden] { display: none }
/* A card on its own is the name and the content, nothing twice: the dialog's
   head carries the name, so the card's own head stays out of sight (the card
   is held open, there is nothing to fold), and its frame goes with it. A card
   that bears its category's own name (settings.js marks it data-same-name)
   never shows its head — the category's heading already says it — and, shown
   on its own, sheds its frame too. */
.settings-view[data-focus-section] details.panel-section > summary.panel-section-head,
details.panel-section[data-same-name] > summary.panel-section-head { display: none }
.settings-view[data-focus-section] details.panel-section,
.settings-view[data-focus] details.panel-section[data-same-name] {
  border-color: transparent;
  background: transparent;
  overflow: visible;
}
.settings-view[data-focus-section] details.panel-section > .panel-section-body,
.settings-view[data-focus] details.panel-section[data-same-name] > .panel-section-body { padding: .25rem 0 0 }
/* Same for a category that is one plain card and nothing else (File manager,
   Terminal, Server access) shown on its own: the dialog is the frame, so the
   card sheds its own instead of drawing a second one inside it. A category
   of several cards (Agent server) keeps them, since the frames tell them
   apart. */
.settings-view[data-focus] .settings-category > section.panel-section.server-card:only-of-type {
  padding: 0;
  border-color: transparent;
  background: transparent;
}
/* The feedback form on its own (from the card under the agent's name, or a
   sidebar shortcut) is a short form, not a page: a compact window the size of
   the form rather than the whole frame of Settings (Mart, 2026-10-15). On a
   phone the dialog fills the screen as ever. */
@media (min-width: 821px) {
  .shell-dialog-overlay[data-view="settings"] .shell-dialog:has(.settings-view.active[data-focus="feedback"]) {
    width: min(560px, 100%);
    height: auto;
    max-height: min(860px, 100%);
  }
  .shell-dialog .settings-view[data-focus="feedback"] .settings-content { padding-bottom: 1.4rem }
}
/* A tool shown on its own — the file manager, the terminal — is the whole
   dialog: its frame takes every row the dialog has to give instead of
   stopping at its own fixed height and leaving a bare strip under it. The
   file manager does the same when it is picked in the Settings list (the
   view's data-category, set for a focus too), so its listing is the one
   thing that scrolls, never the dialog behind it as well.
   Every ancestor down from the view has to pass the dialog's height on
   (flex + min-height: 0), which the generic Settings layout, built for a
   scrolling page of cards, does not do by itself. The card itself is a
   flex column already (server.css); what fills it is each tool's own rule
   (files.css, server.css). */
.settings-view[data-category="files"] .settings-layout,
.settings-view[data-focus="terminal"] .settings-layout {
  flex: 1 1 auto;
  min-height: 0;
}
.settings-view[data-category="files"] .settings-content,
.settings-view[data-focus="terminal"] .settings-content {
  flex: 1 1 auto;
  align-self: stretch;
  min-height: 0;
  padding-bottom: 0;
}
/* One line, not a wrapping row: a wrapped flex line never shrinks below its
   content, so the card would keep its full height however short the dialog.
   The terminal's heading is out of sight here, so its card is the category's
   only child. */
.settings-view[data-category="files"] .settings-category[data-category="files"],
.settings-view[data-focus="terminal"] .settings-category[data-category="terminal"] {
  flex: 1 1 auto;
  flex-wrap: nowrap;
  min-height: 0;
}
/* Picked in the list, the file manager keeps its heading: the category
   stacks the two, the heading at its own height and the card in the rest. */
.settings-view[data-category="files"] .settings-category[data-category="files"] { flex-direction: column }
.settings-view[data-category="files"] .settings-category[data-category="files"] > .settings-category-title { flex: 0 0 auto }
.settings-view[data-category="files"] .settings-category[data-category="files"] > .panel-section { flex: 1 1 auto }
.settings-view[data-category="files"] .settings-category[data-category="files"] > .panel-section,
.settings-view[data-focus="terminal"] .settings-category[data-category="terminal"] > .panel-section {
  min-height: 0;
}

/* Pin a category or a card to the sidebar (sidebar_pins.js). Quiet until the
   heading or the card's head is pointed at; a pinned one keeps its pin in
   view. */
.settings-pin {
  width: 28px;
  height: 28px;
  margin-left: .2rem;
  color: var(--muted2);
  opacity: 0;
}
.settings-category-title:hover .settings-pin,
.panel-section-head:hover .settings-pin,
.settings-pin:focus-visible,
.settings-pin[aria-pressed="true"] { opacity: 1 }
.settings-pin[aria-pressed="true"] { color: var(--accent) }
.settings-pin[aria-pressed="true"] .sb-ico { fill: currentColor }
/* In a card's head the pin hugs the title; the chevron keeps the far edge. */
.panel-section-head .settings-pin {
  margin: -.25rem auto -.25rem -.5rem;
}
/* Beside a pinnable button (a row of buttons has no head to point at): in
   view, quiet until the button or the pin itself is pointed at. */
.settings-action-pin { opacity: .55 }
[data-pin-action]:hover + .settings-action-pin,
[data-pin-id]:hover + .settings-action-pin,
.settings-action-pin:hover { opacity: 1 }
/* A pin beside a button out of sight goes with it. */
[data-pin-action][hidden] + .settings-action-pin { display: none }
/* A button that stands among others in a row keeps its pin close beside it. */
.action-pin-group {
  display: inline-flex;
  align-items: center;
}
/* On a phone a row stacks its buttons full width (sections.css); a button
   with its pin keeps the pin beside it and takes the rest of the row. */
@media (max-width: 520px) {
  .panel-section-body .row > .action-pin-group > [data-pin-action] { flex: 1 1 auto }
}
/* A button that is a card of its own (a way to invite, a new project) keeps
   its pin in its top corner, in the slot the two share, so the cards around
   it stand as they did. */
.action-pin-slot {
  position: relative;
  display: flex;
  min-width: 0;
}
.action-pin-slot > [data-pin-action] { flex: 1 1 auto }
.action-pin-slot > .users-way { padding-right: 2.4rem }
.action-pin-slot > .settings-action-pin {
  position: absolute;
  top: .3rem;
  right: .3rem;
  margin: 0;
}
/* Among a provider's quiet links the pin is as small as they are. */
.llm-provider-actions .settings-action-pin {
  width: 22px;
  height: 22px;
  margin: -.25rem 0 -.25rem -.6rem;
}
/* The pin of what the dialog shows on its own, beside its title in the head:
   always in view, there is nothing to hover for. */
.shell-dialog-pin {
  margin: 0 auto 0 -.6rem;
  opacity: 1;
}
.shell-dialog-pin[hidden] { display: none }
@media (hover: none) {
  .settings-pin { opacity: 1 }
}
/* The same pin at the end of a row of the category list: quiet until the
   row is pointed at; a pinned category keeps it in view. */
.settings-nav-pin {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  margin-left: auto;
  /* Overhangs the row's padding so the pin's box never makes its row
     taller than a row without one -- every row keeps the same height. */
  margin-block: -.25rem;
  border-radius: 6px;
  color: var(--muted2);
  opacity: 0;
  transition: opacity .15s ease, background .15s ease;
}
.settings-nav-pin .sb-ico {
  width: 15px;
  height: 15px;
}
.settings-nav-link:hover .settings-nav-pin,
.settings-nav-pin:focus-visible,
.settings-nav-pin[aria-pressed="true"] { opacity: 1 }
.settings-nav-pin:hover {
  background: var(--shell-active);
  color: var(--text);
}
.settings-nav-pin[aria-pressed="true"] { color: var(--accent) }
.settings-nav-pin[aria-pressed="true"] .sb-ico { fill: currentColor }
.settings-nav-pin:focus-visible {
  outline: 2px solid var(--focus-outline);
  outline-offset: -2px;
}
@media (hover: none) {
  .settings-nav-pin { opacity: 1 }
}

/* ── Motion ─────────────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  .sidebar,
  :root[data-sidebar="collapsed"] .sidebar { transition: none }
  .shell-dialog-overlay,
  .shell-dialog { animation: none }
  .sb-label.is-titling,
  .sb-skeleton-bar,
  .session-pill .ct-id.is-titling { animation: none }
}

/* ── Phone: the sidebar becomes a drawer, the dialog fills the screen ── */
@media (max-width: 820px) {
  .sidebar,
  :root[data-sidebar="collapsed"] .sidebar {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    z-index: 900;
    width: min(var(--sidebar-w), 86vw);
    margin-left: 0;
    visibility: hidden;
    transform: translateX(-100%);
    box-shadow: none;
    transition: transform .24s ease, visibility 0s linear .24s;
  }
  :root[data-sidebar-drawer="open"] .sidebar {
    visibility: visible;
    transform: none;
    box-shadow: var(--shadow-pop);
    transition: transform .24s ease, visibility 0s linear 0s;
  }
  .sidebar-scrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 899;
    background: var(--dialog-scrim);
    animation: shell-dialog-fade .2s ease;
  }
  .sidebar-scrim[hidden] { display: none }

  #sidebarOpen,
  :root[data-sidebar="collapsed"] #sidebarOpen { display: inline-flex }
  #topbar { padding: 0 .6rem 0 .4rem }

  /* Touch targets: a little taller than the desktop rows. */
  .sb-item { padding: .62rem .6rem }
  .sb-session { padding: .55rem .6rem }

  .shell-dialog-overlay { padding: 0 }
  .shell-dialog {
    width: 100%;
    height: 100%;
    border: 0;
    border-radius: 0;
  }
  .shell-dialog-head { padding: .55rem .5rem .55rem 1rem }
  .shell-dialog .settings-layout { padding: 0 1rem 1rem }
}

@media (max-width: 820px) and (prefers-reduced-motion: reduce) {
  .sidebar,
  :root[data-sidebar="collapsed"] .sidebar,
  :root[data-sidebar-drawer="open"] .sidebar { transition: none }
}

/* ── Boot: the first seconds (theme_boot.js, boot.js) ──────────────────
   Under <html data-boot="pending"> the shell is laid out but out of sight,
   and the veil - the page's background, the mark breathing - is all there
   is. "ready" fades the veil and brings the columns in, the sidebar first,
   the main column a beat later and rising; "done" (or no mark at all, when
   the boot script never ran) leaves nothing of it. */
.boot-veil {
  position: fixed;
  inset: 0;
  z-index: 20000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  pointer-events: none;
  transition: opacity .32s ease;
}
.boot-mark {
  width: 44px;
  height: 44px;
  animation: boot-breathe 1.7s ease-in-out infinite;
}
:root:not([data-boot]) .boot-veil,
:root[data-boot="done"] .boot-veil { display: none }
:root[data-boot="ready"] .boot-veil { opacity: 0 }
:root[data-boot="pending"] .shell,
:root[data-boot="pending"] .shell-dialog-overlay {
  opacity: 0;
  pointer-events: none;
}
/* `backwards` holds the start frame through the delay and hands the
   element back to its stylesheet once the animation is over: nothing of the
   entrance lingers on the sidebar's drawer transform or the column's
   layout. */
:root[data-boot="ready"] .sidebar { animation: boot-fade .4s ease backwards }
:root[data-boot="ready"] .main { animation: boot-rise .48s cubic-bezier(.2, .7, .2, 1) .06s backwards }
@keyframes boot-breathe {
  0%, 100% { opacity: .5; transform: scale(.94) }
  50%      { opacity: 1;  transform: scale(1) }
}
@keyframes boot-fade { from { opacity: 0 } }
@keyframes boot-rise { from { opacity: 0; transform: translateY(6px) } }
@media (prefers-reduced-motion: reduce) {
  .boot-veil { transition: none }
  .boot-mark,
  :root[data-boot="ready"] .sidebar,
  :root[data-boot="ready"] .main { animation: none }
}

/* ── The update veil (update_veil.js) ─────────────────────────────────
   A new panel build is picked up behind it: the page blurs under the brand
   mark and one line, the tab reloads, the new build boots under the same
   veil (theme_boot.js marks <html data-update="pending"> from the note the
   old tab left), and the veil lifts once the agent answers again. It sits
   above the boot veil, whose own mark stays out of its way meanwhile. The
   page stays visible through a translucent blur throughout the wait. Only
   the document handoff briefly covers it fully, under the mark and the line,
   which never leave the screen. The veil takes the pointer so a
   click into the reload goes nowhere. A server reboot (reboot_agent.js)
   raises the same veil, with its own line, until the box is back. */
/* The pace (Mart, 2026-10-09: "not a flicker"): the page goes out of focus
   over 2 s, the mark and the line rise in over the next second, and the
   page under them fades to a solid colour at 4 s (TIMINGS.updateVeilLead)
   while they stay (Mart, 2026-10-13: no bare white screen, no blink). Only
   then does the reload follow. The new build resumes that frame —
   <html data-update-resumed data-update-blank>, set by theme_boot.js — the
   mark and the line on it from the first frame, with no entrance of their
   own, until the shell is settled beneath it. */
.update-veil {
  position: fixed;
  inset: 0;
  z-index: 20001;
  display: flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--bg) 72%, transparent);
  -webkit-backdrop-filter: blur(14px) saturate(.9);
  backdrop-filter: blur(14px) saturate(.9);
  transition: opacity 2s ease;
}
/* The page goes solid under the card before crossing documents; the new
   document starts with this same frame. */
.update-veil::after {
  content: '';
  z-index: 1;
  position: absolute;
  inset: 0;
  background: var(--bg);
  opacity: 0;
  transition: opacity .6s ease;
}
:root[data-update-blank] .update-veil::after { opacity: 1 }
:root:not([data-update]) .update-veil,
:root[data-update="done"] .update-veil { display: none }
:root[data-update="pending"]:not([data-update-resumed]) .update-veil { animation: update-veil-in 2s ease backwards }
:root[data-update="pending"]:not([data-update-resumed]) .update-veil-card { animation: update-veil-rise 1s cubic-bezier(.2, .7, .2, 1) 2s backwards }
:root[data-update="ready"] .update-veil {
  opacity: 0;
  pointer-events: none;
}
:root[data-update="pending"] .boot-mark { display: none }

/* Above the solid frame (::after): the mark and the line stay on screen
   through the handoff. */
.update-veil-card {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .95rem;
  max-width: min(360px, calc(100vw - 3rem));
  padding: 0 1.5rem;
  text-align: center;
}
.update-veil-mark {
  width: 52px;
  height: 52px;
  animation: boot-breathe 1.7s ease-in-out infinite;
}
.update-veil-text {
  color: var(--text);
  font-size: .95rem;
  font-weight: 600;
  line-height: 1.35;
  letter-spacing: .005em;
}
/* One thin track with a short run of the accent sliding along it: the
   update is under way, with no end to promise. */
.update-veil-bar {
  position: relative;
  width: 128px;
  height: 3px;
  border-radius: 2px;
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  overflow: hidden;
}
.update-veil-bar::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 40%;
  border-radius: 2px;
  background: var(--accent);
  animation: update-veil-run 1.4s cubic-bezier(.4, 0, .6, 1) infinite;
}
@keyframes update-veil-in { from { opacity: 0 } }
@keyframes update-veil-rise { from { opacity: 0; transform: translateY(8px) } }
@keyframes update-veil-run {
  0%   { transform: translateX(-100%) }
  100% { transform: translateX(250%) }
}
@media (prefers-reduced-motion: reduce) {
  .update-veil, .update-veil::after { transition: none }
  :root[data-update="pending"] .update-veil,
  :root[data-update="pending"] .update-veil-card,
  .update-veil-mark,
  .update-veil-bar::after { animation: none }
  .update-veil-bar::after { width: 100%; opacity: .55 }
}
