  /* Roles, verbatim from internal/adapter/tui/styles.go — nervDark / nervLight. */
  :root {
    color-scheme: dark light;
    --magi-ref-primary:#FF7A1A; --magi-ref-accent:#5CD8E6; --magi-ref-muted:#C9C2B8; --magi-ref-outline:#72675C;
    /* Secondary takes primary's hue at a third of its chroma, which is the rule the guide
       states for it: "secondary, neutral variant, and neutral colors match primary in hue but
       are progressively less chromatic". It pointed at --magi-ref-accent, and so did tertiary, so the
       two roles were one colour and the scheme had no secondary at all. The cyan is right
       WHERE IT IS — tertiary is the complement, arrived at by changing the hue — and it is
       still the councillor Balthasar's. Only the role that was borrowing it changes. */
    --magi-ref-secondary:#E8B89F;
    --magi-ref-error:#F2B8B5; --magi-ref-success:#86EFAC; --magi-ref-surface:#211B14;
    --magi-ref-primaryContainer:#4A2E0B; --magi-ref-outlineVariant:#463E34; --magi-ref-warn:#FFD479;
    /* The three council members' colours — one palette, two surfaces. A test requires this page to
       carry every role of the terminal's, so retuning one can never leave the two disagreeing.
       These were declared and unused for a long time under a comment saying this console showed no
       council; it does, and now they paint it (see .row.m-*). */
    --magi-ref-melchior:#FFB454; --magi-ref-balthasar:#5CD8E6; --magi-ref-casper:#D8B4FE;
    --magi-ref-bg:#14110d; --magi-ref-fg:#E8E2D8;
    --magi-ref-shadow:#000000; --magi-ref-scrim:#000000;

    /* ── M3, dark ─────────────────────────────────────────────────────────── */
    /* The roles above are the terminal's, verbatim (a test pins them). These are the Material 3
       roles the terminal has no use for: a TUI paints on a background it does not own, so it
       cannot have tonal surfaces, and it never needed an on- pair because it draws text on one
       background. A browser has both, and without them "Material 3" would be a set of borrowed
       names — which is exactly what this page was until it was measured. */
    --magi-ref-on-primary:#2A1500;              /* on #FF7A1A */
    --magi-ref-on-primary-container:#FFD9B8;    /* on #4A2E0B */
    --magi-ref-on-error:#3A0A08;
    --magi-ref-on-surface:#E8E2D8;
    --magi-ref-on-surface-variant:#C9C2B8;
    /* Tonal layers, low → high. Dark themes get LIGHTER as they rise. */
    --magi-ref-surface-dim:#14110d;
    --magi-ref-surface-container-lowest:#0F0D0A;
    --magi-ref-surface-container-low:#1B1712;
    --magi-ref-surface-container:#211B14;
    --magi-ref-surface-container-high:#2B251C;
    --magi-ref-surface-container-highest:#352E24;

    /* ── the same roles under the names Material Web reads ────────────────── */
    /* The components are themed by these and nothing else. Setting a few of them per component —
       which is what this page did first — leaves every role it did not mention drawn in the
       library's baseline purple, which is what "the colours are the default ones" looks like.
       Declared once, at the root, so a component added later is magi-coloured by existing. */
    --md-sys-color-primary:var(--magi-ref-primary);
    --md-sys-color-on-primary:var(--magi-ref-on-primary);
    --md-sys-color-primary-container:var(--magi-ref-primaryContainer);
    --md-sys-color-on-primary-container:var(--magi-ref-on-primary-container);
    --md-sys-color-secondary:var(--magi-ref-secondary);
    --md-sys-color-on-secondary:var(--magi-ref-on-primary);
    --md-sys-color-secondary-container:var(--magi-ref-surface-container-high);
    --md-sys-color-on-secondary-container:var(--magi-ref-on-surface);
    --md-sys-color-tertiary:var(--magi-ref-accent);
    --md-sys-color-on-tertiary:var(--magi-ref-on-primary);
    --md-sys-color-error:var(--magi-ref-error);
    --md-sys-color-on-error:var(--magi-ref-on-error);
    --md-sys-color-error-container:var(--magi-ref-surface-container-high);
    --md-sys-color-on-error-container:var(--magi-ref-error);
    --md-sys-color-background:var(--magi-ref-bg);
    --md-sys-color-on-background:var(--magi-ref-fg);
    --md-sys-color-surface:var(--magi-ref-bg);
    --md-sys-color-on-surface:var(--magi-ref-on-surface);
    --md-sys-color-surface-variant:var(--magi-ref-surface);
    --md-sys-color-on-surface-variant:var(--magi-ref-on-surface-variant);
    --md-sys-color-surface-container-lowest:var(--magi-ref-surface-container-lowest);
    --md-sys-color-surface-container-low:var(--magi-ref-surface-container-low);
    --md-sys-color-surface-container:var(--magi-ref-surface-container);
    --md-sys-color-surface-container-high:var(--magi-ref-surface-container-high);
    --md-sys-color-surface-container-highest:var(--magi-ref-surface-container-highest);
    --md-sys-color-outline:var(--magi-ref-outline);
    --md-sys-color-outline-variant:var(--magi-ref-outlineVariant);
    --md-sys-color-inverse-surface:var(--magi-ref-fg);
    --md-sys-color-inverse-on-surface:var(--magi-ref-bg);
    /* Through the palette layer like every other colour role, rather than as two hex values
       sitting among twenty var()s. The guide's rule for a system token is that it point at a
       reference rather than hold a value, and these two were the only colours here not doing it —
       which also meant styles.go could retune them and this page would not follow. */
    --md-sys-color-shadow:var(--magi-ref-shadow);
    --md-sys-color-scrim:var(--magi-ref-scrim);

    /* ── and the type, under the names Material Web reads ─────────────────── */
    /* A component takes its font from --md-sys-typescale-<role>-font, not from the ref typeface
       alone, so setting only the latter leaves every label in the library's fallback. Declared
       across the roles at the root, the way the handbook project does it: one place, and a
       component added later is already in magi's face.
       Sizes are the M3 scale (see the type tokens above); the faces are ours, which M3 allows —
       the scale is what it asks you to keep. */
    --md-ref-typeface-plain:var(--magi-ref-mono);
    --md-ref-typeface-brand:var(--magi-ref-display);
    --md-sys-typescale-label-small-font:var(--magi-ref-mono);
    --md-sys-typescale-label-small-size:0.6875rem;
    --md-sys-typescale-label-small-line-height:1rem;
    --md-sys-typescale-label-medium-font:var(--magi-ref-mono);
    --md-sys-typescale-label-medium-size:0.75rem;
    --md-sys-typescale-label-medium-line-height:1rem;
    --md-sys-typescale-label-large-font:var(--magi-ref-mono);
    --md-sys-typescale-label-large-size:0.875rem;
    --md-sys-typescale-label-large-line-height:1.25rem;
    --md-sys-typescale-body-small-font:var(--magi-ref-mono);
    --md-sys-typescale-body-small-size:0.75rem;
    --md-sys-typescale-body-small-line-height:1rem;
    --md-sys-typescale-body-medium-font:var(--magi-ref-mono);
    --md-sys-typescale-body-medium-size:0.875rem;
    --md-sys-typescale-body-medium-line-height:1.25rem;
    --md-sys-typescale-body-large-font:var(--magi-ref-mono);
    --md-sys-typescale-body-large-size:1rem;
    --md-sys-typescale-body-large-line-height:1.5rem;
    --md-sys-typescale-title-small-font:var(--magi-ref-mono);
    --md-sys-typescale-title-small-size:0.875rem;
    --md-sys-typescale-title-small-line-height:1.25rem;
    --md-sys-typescale-title-medium-font:var(--magi-ref-mono);
    --md-sys-typescale-title-medium-size:1rem;
    --md-sys-typescale-title-medium-line-height:1.5rem;
    --md-sys-typescale-title-large-font:var(--magi-ref-display);
    --md-sys-typescale-title-large-size:1.375rem;
    --md-sys-typescale-title-large-line-height:1.75rem;
    --md-sys-typescale-headline-small-font:var(--magi-ref-display);
    --md-sys-typescale-headline-small-size:1.5rem;
    --md-sys-typescale-headline-small-line-height:2rem;
  }
  /* Light, said twice on purpose.

     The media query is the machine's answer and the attribute is the reader's, and the reader has
     to be able to override the machine in BOTH directions — a dark-at-night machine chosen light,
     and a light machine chosen dark. A page with only the query can be moved one way and not back.
     The :not([color-theme]) is what makes the query yield the moment somebody chooses.

     The two blocks carry the SAME declarations and TestBothLightThemesSayTheSameThing (in
     cmd/magi-web/palette_test.go) fails if they stop doing so — CSS has no way to give one ruleset
     two selectors across a media query, so the copy is unavoidable and the drift is not.

     The attribute is written by the inline script in the markup, before this stylesheet paints, so
     a chosen theme does not flash the other one first. */
  @media (prefers-color-scheme: light) {
    :root:not([color-theme]) {
      --magi-ref-primary:#B45309; --magi-ref-accent:#0E7490; --magi-ref-muted:#4A453C; --magi-ref-outline:#8A7E6E;
      --magi-ref-secondary:#82604F;
      --magi-ref-error:#B3261E; --magi-ref-success:#15803D; --magi-ref-surface:#F5EEE3;
      --magi-ref-primaryContainer:#F8D9A8; --magi-ref-outlineVariant:#D8CFC0; --magi-ref-warn:#92600A;
      --magi-ref-melchior:#B45309; --magi-ref-balthasar:#0E7490; --magi-ref-casper:#6D28D9;
      --magi-ref-bg:#FBF8F3; --magi-ref-fg:#221D16;

      /* ── M3, light ─────────────────────────────────────────────────────── */
      /* The layers invert: a light theme gets DARKER as it rises. Built as its own ramp rather
         than by dimming the dark one — a light theme has less headroom, and this page has been
         caught before with eight of thirteen dimmed pairs under AA, the worst at 2.47:1. */
      --magi-ref-on-primary:#FFFFFF;
      --magi-ref-on-primary-container:#3A1B00;
      --magi-ref-on-error:#FFFFFF;
      --magi-ref-on-surface:#221D16;
      --magi-ref-on-surface-variant:#4A453C;
      --magi-ref-surface-dim:#EFE9DF;
      --magi-ref-surface-container-lowest:#FFFFFF;
      --magi-ref-surface-container-low:#F7F3EC;
      --magi-ref-surface-container:#F2ECE2;
      --magi-ref-surface-container-high:#ECE5D9;
      --magi-ref-surface-container-highest:#E6DED1;
    }
  }
  :root[color-theme="light"] {
    --magi-ref-primary:#B45309; --magi-ref-accent:#0E7490; --magi-ref-muted:#4A453C; --magi-ref-outline:#8A7E6E;
    --magi-ref-secondary:#82604F;
    --magi-ref-error:#B3261E; --magi-ref-success:#15803D; --magi-ref-surface:#F5EEE3;
    --magi-ref-primaryContainer:#F8D9A8; --magi-ref-outlineVariant:#D8CFC0; --magi-ref-warn:#92600A;
    --magi-ref-melchior:#B45309; --magi-ref-balthasar:#0E7490; --magi-ref-casper:#6D28D9;
    --magi-ref-bg:#FBF8F3; --magi-ref-fg:#221D16;

    /* ── M3, light ─────────────────────────────────────────────────────── */
    /* The layers invert: a light theme gets DARKER as it rises. Built as its own ramp rather
       than by dimming the dark one — a light theme has less headroom, and this page has been
       caught before with eight of thirteen dimmed pairs under AA, the worst at 2.47:1. */
    --magi-ref-on-primary:#FFFFFF;
    --magi-ref-on-primary-container:#3A1B00;
    --magi-ref-on-error:#FFFFFF;
    --magi-ref-on-surface:#221D16;
    --magi-ref-on-surface-variant:#4A453C;
    --magi-ref-surface-dim:#EFE9DF;
    --magi-ref-surface-container-lowest:#FFFFFF;
    --magi-ref-surface-container-low:#F7F3EC;
    --magi-ref-surface-container:#F2ECE2;
    --magi-ref-surface-container-high:#ECE5D9;
    --magi-ref-surface-container-highest:#E6DED1;
  }

  /* Newsreader, an editorial serif drawn for reading on screens, served from this binary — see
     fonts/README.md. A font CDN would make the page's appearance depend on somebody else's machine
     and tell it when you look at your agents; nothing here leaves the host.
     swap, so the page is readable in the fallback before the face arrives. */
  @font-face {
    font-family:"Newsreader"; font-style:normal; font-weight:400; font-display:swap;
    src:url(../font/newsreader-400.woff2) format("woff2");
  }
  @font-face {
    font-family:"Newsreader"; font-style:normal; font-weight:600; font-display:swap;
    src:url(../font/newsreader-600.woff2) format("woff2");
  }
  @font-face {
    font-family:"Newsreader"; font-style:italic; font-weight:400; font-display:swap;
    src:url(../font/newsreader-italic.woff2) format("woff2");
  }
  /* Korean, in the same two weights.
     Chosen by looking: set beside Newsreader, Nanum Myeongjo's hangul sits a size smaller and
     Gowun Batang's sits larger and lighter — in both cases a heading reads as two typefaces. Noto
     Serif KR matches Newsreader's cap height and its colour closely enough that a line of mixed
     script reads as one face, which is the whole test.
     It carries hanja as well as hangul, so a name or a quotation with 漢字 in it does not drop out
     of the face halfway through a word.
     unicode-range, not a language setting. The browser fetches these only when Korean is actually
     on the page, and a Korean workspace name on an English console still renders in the right face
     — which a per-locale switch gets wrong in exactly the case that matters. */
  @font-face {
    font-family:"Noto Serif KR"; font-style:normal; font-weight:400; font-display:swap;
    src:url(../font/notoserifkr-400.woff2) format("woff2");
    unicode-range:U+1100-11FF, U+3000-303F, U+3130-318F, U+4E00-9FFF, U+A960-A97F,
      U+AC00-D7A3, U+D7B0-D7FF, U+F900-FAFF, U+FF01-FF60, U+FFE0-FFE6;
  }
  @font-face {
    font-family:"Noto Serif KR"; font-style:normal; font-weight:600; font-display:swap;
    src:url(../font/notoserifkr-600.woff2) format("woff2");
    unicode-range:U+1100-11FF, U+3000-303F, U+3130-318F, U+4E00-9FFF, U+A960-A97F,
      U+AC00-D7A3, U+D7B0-D7FF, U+F900-FAFF, U+FF01-FF60, U+FFE0-FFE6;
  }

  /* Two families. The serif for what a person READS — names, the lead line, the empty state — and
     monospace for everything that is a fact from the machine: paths, commands, transcript. The
     system stack behind them is not decoration: it carries every script neither subset does, so a
     Thai workspace name renders in the platform's face rather than in tofu.
     Korean sits in BOTH, ahead of the system fallback. In the display stack for the obvious reason,
     and in the monospace one because hangul is not monospaced by anybody: it was falling through to
     whatever the platform happened to install, which is a different page on every machine — the
     thing embedding a face exists to stop. A serif hangul in a line of monospace Latin is a choice,
     and it is the one that keeps the page a single voice. */
  :root {
    --magi-ref-display: "Newsreader", "Noto Serif KR", "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
    --magi-ref-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Noto Serif KR", monospace;
    /* ── state layers ───────────────────────────────────────────────────────── */
  /* M3 does not recolour text on hover; it lays the on- colour over the surface at a fixed
     opacity — 8% hover, 12% focus and press. One recipe, applied by adding the state class to
     anything that responds, so a new control gets the behaviour by being told what it is rather
     than by somebody remembering four rules. */

  /* ── the M3 shape scale, and nothing off it ───────────────────────────── */
    /* 4 · 8 · 12 · 16 · 28 · full. Every radius on this page is one of these; the page used to be
       2px everywhere, which is not a value the scale has. The scale has no 24 — it steps 20 then 28,
       and this page said 24 until it was measured against the token table. */
    --magi-sys-shape-xs:4px; --magi-sys-shape-s:8px; --magi-sys-shape-m:12px; --magi-sys-shape-l:16px; --magi-sys-shape-xl:28px;
    --magi-sys-shape-full:9999px;

    /* ── M3 motion ────────────────────────────────────────────────────────── */
    /* Verified against material-components-android's Motion.md. The .12s ease this page used was a
       number somebody typed; these are the system's, and using them is what makes two surfaces
       feel like one. */
    --magi-sys-ease-standard:cubic-bezier(0.2, 0, 0, 1);
    --magi-sys-ease-decelerate:cubic-bezier(0.05, 0.7, 0.1, 1);
    /* Read out of the shipped bundle rather than a document, and the two disagree. The guide says
       emphasized has NO css form ("N/A — use standard as a fallback"), because the real curve is a
       two-segment path a single cubic-bezier cannot draw. Material Web resolved that its own way:
       EMPHASIZED:"cubic-bezier(.3,0,0,1)" is a literal constant in the bundle. Following the
       bundle over the guide is deliberate — the components on this page move on the library's
       curve whatever we declare, and a container opening beside them has to match what they
       actually do, not what the document wishes they did. */
    --magi-sys-ease-emphasized:cubic-bezier(0.3, 0, 0, 1);

    /* How much room the rail takes. Declared HERE and not only on #rail, because the page's own
       left offset is computed from it — and a var() that resolves to nothing does not fall back to
       the shorthand underneath it. The declaration becomes invalid at computed-value time and the
       property takes its initial value, which for padding is 0: the offset silently vanished and
       the rail stood on top of the page at every width. */
    /* 80px, which is the narrow collapsed rail in the spec. It was 4.5rem — 72px — which is under
       both numbers the spec gives (96dp standard, 80dp narrow), and under the 88dp a vertical item
       needs for a 56dp indicator between two 16dp insets. */
    /* 96dp, the standard collapsed width — not the 80dp narrow variant it was.
       The narrow one leaves a 64px item, and the longest destination is 73px at Label Medium: the
       guide forbids truncating a rail label, forbids an ellipsis and forbids shrinking the type to
       make one fit, so the only lever left is the rail. At 96 the item is 80 and the word fits. */
    --magi-comp-rail-w:6rem;
    /* The side pane's width, named because two places have to agree about it: the grid track it
       sits in, and the pane itself — which is pinned to it so a narrowing track clips the pane
       instead of reflowing its text. */
    --magi-comp-side-w:22rem;
    /* The file pane's, for the same reason and narrower: a tree is names, not prose, and the
       column it sits in is taken from the conversation. 18rem holds about forty characters of a
       path before it has to ellipse, which is a filename and its parent. */
    --magi-comp-files-w:18rem;
    /* One step of a tree's depth: the indent per level, and where the guide line for that level is
       drawn. One number so the two cannot drift. */
    --magi-comp-tree-step:0.75rem;
    /* How much of the window the masthead takes. A fallback, replaced by the measured height on
       every layout — the bar wraps at some widths and in some languages, and a shell sized against
       a guess is a shell whose last line is under the fold or whose bottom is empty. */
    --magi-comp-masthead:5.5rem;
    --magi-sys-dur-short2:100ms; --magi-sys-dur-short4:200ms; --magi-sys-dur-medium2:300ms;

    /* ── the M3 type scale ────────────────────────────────────────────────── */
    /* size/line-height pairs, taken as pairs: matching a size and inventing a line height is how
       the rhythm goes. The page used to carry 9.5 · 10.5 · 11.5 · 12.5 · 13.5 · 15.5 · 17px, none
       of which is on the scale. The typeface is ours — M3 allows that; the scale is not.

       These are shorthands for the font property, not a second scale. They read the same
       --md-sys-typescale-* tokens the components read, so the scale is stated once and a change
       to it reaches the hand-written CSS and the library together. They were px, which is the
       one thing a size may not be: px does not answer the reader who sets a larger default. */
    --magi-sys-headline-s:var(--md-sys-typescale-headline-small-size)/var(--md-sys-typescale-headline-small-line-height);
    --magi-sys-title-l:var(--md-sys-typescale-title-large-size)/var(--md-sys-typescale-title-large-line-height);
    --magi-sys-title-m:var(--md-sys-typescale-title-medium-size)/var(--md-sys-typescale-title-medium-line-height);
    --magi-sys-title-s:var(--md-sys-typescale-title-small-size)/var(--md-sys-typescale-title-small-line-height);
    --magi-sys-body-l:var(--md-sys-typescale-body-large-size)/var(--md-sys-typescale-body-large-line-height);
    --magi-sys-body-m:var(--md-sys-typescale-body-medium-size)/var(--md-sys-typescale-body-medium-line-height);
    --magi-sys-body-s:var(--md-sys-typescale-body-small-size)/var(--md-sys-typescale-body-small-line-height);
    --magi-sys-label-l:var(--md-sys-typescale-label-large-size)/var(--md-sys-typescale-label-large-line-height);
    --magi-sys-label-m:var(--md-sys-typescale-label-medium-size)/var(--md-sys-typescale-label-medium-line-height);
    --magi-sys-label-s:var(--md-sys-typescale-label-small-size)/var(--md-sys-typescale-label-small-line-height);

    /* ── the spacing scale ────────────────────────────────────────────────
       8dp, where space100 = 8dp, with the 4dp half step the dense parts of a terminal need.
       The page carried twenty-six distinct paddings and gaps — 1.6 · 2.4 · 3.2 · 3.5 · 4.8 ·
       5.6 · 6.4 · 7.2 · 9.6 · 11.2 · 12.8 · 13.6 · 14.4 · 17.6 · 19.2 · 22.4 · 25.6 · 38.4dp —
       which is not a rhythm but the absence of one: each was chosen against its neighbour and
       none against a scale. Eight remain, and nothing moved more than 3.2dp getting here.
       Written as tokens so a value off the scale reads as a literal and a test can say so. */
    --magi-sys-space-50:0.25rem;  --magi-sys-space-100:0.5rem; --magi-sys-space-150:0.75rem; --magi-sys-space-200:1rem;
    --magi-sys-space-300:1.5rem;  --magi-sys-space-400:2rem;   --magi-sys-space-500:2.5rem;  --magi-sys-space-600:3rem;
    --magi-sys-space-700:3.5rem;  --magi-sys-space-800:4rem;   --magi-sys-space-1000:5rem;   --magi-sys-space-1200:6rem;
    --magi-sys-space-1600:8rem;

    /* ── the widths where the layout changes ──────────────────────────────
       Written in em, and the em in a media query is the reader's default font size — not this
       page's, which is why it is the one unit that answers them. M3's breakpoints are dp
       (600 · 840 · 1000), and these are those numbers at the 16px default: 37.5 · 52.5 · 62.5.

       A reader who sets their browser default to 32px is asking for text at twice the size, and
       the fleet table cannot honour that and stay a table: its seven columns have rem minima
       summing to 52.9rem, which at that setting wants 1693px of a 1265px window. In px the
       breakpoints sat still while the text grew and the row scrolled off the side. In em the
       window reads as half as wide, the narrow layout arrives, and the table becomes a list —
       which is what "reflow" asks for. Page zoom was never the broken case; it scales px too. */
    --magi-sys-measure: 74ch;   /* prose */
    --magi-sys-wide: 108ch;     /* transcript, where lines are code and wrapping costs more than width */
    /* What a whole screen of console may take. Wider than the transcript's measure on purpose: the
       fleet is a table and a table uses room, while prose inside it keeps --magi-sys-measure. Capped rather
       than unbounded so an ultrawide monitor does not stretch a row to a metre. */
    --magi-sys-page: 170ch;
    /* And what a companion's page may take, which is a different question since it grew two panes.
       170ch caps the middle at a comfortable measure on a list; with 18rem of files on one side and
       22rem of state on the other it caps the CONVERSATION at something narrow while the monitor
       has room to spare — the reader ends up with two panes squeezing a column in the middle and
       wide empty margins either side of the whole thing. A companion's page is allowed the wider
       bound, and the transcript inside it keeps its own measure regardless. */
    --magi-sys-page-wide: 230ch;
  }

  * { box-sizing:border-box; }

  /* Keyboard focus, said once and loudly.
     A dashboard is a page of links and buttons, and the fleet is navigated with tab as readily as
     with a mouse — the underline this layout uses for a pressed state is not a focus ring, and a
     border colour that shifts by one step is not one either. :focus-visible so a mouse click does
     not leave a ring behind it, and an offset so the ring is not mistaken for the element's own
     rule. The outline:none below applies to :focus, which this then overrides for the keyboard. */
  :focus-visible {
    outline:2px solid var(--magi-ref-primary); outline-offset:3px; border-radius:var(--magi-sys-shape-xs);
  }
  html { scrollbar-gutter:stable; -webkit-text-size-adjust:100%; }
  body {
    /* Block, again. It was a flex column so the rail could be moved to the foot of a narrow page
       with an order property — and the rail is not drawn at all on that width any more, so the
       reason is gone. It cost something while it lasted: an auto margin on a flex item's cross axis
       sizes it to its CONTENT, which is why centring the page silently pinned it to 720px. */
    min-height:100vh;
    margin:0; background:var(--magi-ref-bg); color:var(--magi-ref-fg);
    font:var(--md-sys-typescale-body-medium-size)/1.65 var(--magi-ref-mono);
    -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
    font-variant-numeric:tabular-nums;  /* ages and step counts line up down the column */
  }
  [hidden] { display:none !important; }
  /* No caps blocks anywhere. The section heads on this page were set in uppercase with wide
     tracking — an editorial look — and the guide forbids it without an exception worth the name:
     "Avoid using caps blocks altogether; they're not accessible" and "use sentence case for all
     product text". It names the replacement too, and this page was already using it: these heads
     carry font-weight 600, so the hierarchy survives the change. The tracking came down with the
     caps, because letter-spacing set for capitals is too loose for lowercase. */
  /* Headings carry the structure, not the styling. Each of these already had its own size, weight
     and colour as a div; becoming h1/h2/h3 must not also drag in the browser's default type scale
     and margins, or the page would resize itself for a change that is meant to be invisible. */
  h1, h2 { font:inherit; margin:0; }
  /* Read, not seen. A live region has to be in the accessibility tree, so it cannot be display:none
     or visibility:hidden — it is clipped to nothing instead. */
  /* Plain tooltip, at the spec's numbers: 24dp tall, 8dp of padding, a 4dp corner, and the
     inverse surface pair so it reads against whatever it covers. Body-small is the type. The one
     number the spec does not give is a max width, so this picks one. */
  #tip {
    position:fixed; z-index:9; pointer-events:none;
    min-height:24px; padding:4px 8px; box-sizing:border-box;
    border-radius:var(--magi-sys-shape-xs); max-width:20rem;
    background:var(--md-sys-color-inverse-surface, var(--magi-ref-fg));
    color:var(--md-sys-color-inverse-on-surface, var(--magi-ref-bg));
    font:var(--md-sys-typescale-body-small-size, .75rem)/var(--md-sys-typescale-body-small-line-height, 1rem) var(--magi-ref-mono);
    letter-spacing:.4px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  }
  .sr-only { position:absolute; width:1px; height:1px; margin:-1px; padding:0; overflow:hidden;
             clip-path:inset(50%); white-space:nowrap; border:0; }

  /* On opacity: every value below is set so the RESULT clears WCAG AA (4.5:1) against the page in
     BOTH themes, which is checked in cmd/magi-web/contrast_test.go. Editorial layouts get theirs from
     dimming secondary text, and the arithmetic is easy to get wrong twice over — the muted role is
     already lowered, and light mode has less headroom than dark. Measured before this note: eight
     of thirteen dimmed pairs were under, the worst at 2.47:1. */

  /* ── motion ─────────────────────────────────────────────────────────────
     M3's fade-through: what is leaving goes, then what arrives fades up from 96% rather than
     cutting in. Used where the page swaps one body of content for ANOTHER — a destination, a
     companion's two panels — which is exactly the transition the pattern is for. Not used on the
     three-second poll: the fleet redraws itself constantly and a page that flickered every tick
     would be unreadable, so this fires on navigation only.

     The scale is 96%, not 92%: a table of monospaced text at 92% is visibly the wrong size for a
     tenth of a second, and the point is to say "this is new", not to zoom. */
  @keyframes fadeThrough {
    from { opacity:0; transform:scale(.96); }
    to   { opacity:1; transform:none; }
  }
  /* Lateral: peers slide, they do not fade. The guide is explicit that a tab switch "does not use
     a fade or parallax effect" and says why — fading makes the peer relationship and the swipe
     gesture less obvious, and reads as forward-and-back instead of sideways. Two keyframes because
     the direction has to say which way you moved. */
  @keyframes slideFromRight { from { transform:translateX(12px); } to { transform:none; } }
  @keyframes slideFromLeft  { from { transform:translateX(-12px); } to { transform:none; } }
  @keyframes riseIn {
    from { opacity:0; transform:translateY(10px); }
    to   { opacity:1; transform:none; }
  }
  .enter { animation:fadeThrough 200ms var(--magi-sys-ease-emphasized) both; }
  /* No opacity in these: grouped elements moving in unison is the whole pattern, and a fade on top
     of the slide is the thing being avoided. */
  .slideL { animation:slideFromRight 200ms var(--magi-sys-ease-emphasized) both; }
  .slideR { animation:slideFromLeft 200ms var(--magi-sys-ease-emphasized) both; }
  .rise  { animation:riseIn 250ms var(--magi-sys-ease-emphasized) both; }

  /* Somebody who asked their machine to stop moving things gets a page that does not MOVE.
     Not a page that stops answering. The guide asks for "subtle fades instead of intense sliding
     or scaling", which is a swap and not a deletion, and the blanket 0.01ms was the deletion: a
     panel replaced its contents between two frames with nothing to say it had, which is the
     change a reader most needs told about and the one least likely to make anybody ill. What
     goes is displacement — translate, scale, and any transition that would carry a box across
     the screen. Opacity and colour stay, at a length short enough not to be a performance.
     0.01ms rather than 0 where a duration is still killed: it FIRES, so an animationend that
     something waits on still arrives. */
  @keyframes stillFade { from { opacity:0; } to { opacity:1; } }
  @media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
      animation-duration:0.01ms !important; animation-iteration-count:1 !important;
      transition-property:opacity, color, background-color, border-color, fill, stroke, box-shadow !important;
      transition-duration:120ms !important;
      scroll-behavior:auto !important;
    }
    /* The four the page animates itself. A class beats the universal selector, so these keep a
       duration where everything else loses one — the same fade for all four, because what made
       them different was the direction they moved and none of them moves now. */
    .enter, .rise, .slideL, .slideR { animation:stillFade 120ms var(--magi-sys-ease-standard) both !important; }
  }

  /* The page-wide "still working" bar. Above the masthead (z-index 3) because it is about the
     whole screen, and FIXED rather than in the flow so turning it on never moves the page a
     reader is in the middle of. Two pixels: enough to be seen moving out of the corner of an eye,
     not enough to be a second thing to look at. */
  #turnwrap { position:fixed; top:0; left:0; right:0; z-index:4; pointer-events:none; }
  #turnwrap[hidden] { display:none; }
  #turnbar {
    display:block;
    --md-linear-progress-track-height:2px;
    --md-linear-progress-active-indicator-height:2px;
    --md-linear-progress-track-color:transparent;
  }
  /* The figure rides under the bar at the trailing edge, out of the masthead's way. Its own
     surface, because it sits over whatever the page has scrolled to and unreadable-over-content is
     how a floating label fails. */
  #turnfor {
    position:absolute; top:var(--magi-sys-space-100); inset-inline-end:var(--magi-sys-space-200);
    font-size:var(--magi-sys-type-label-small-size, 0.6875rem); line-height:1.4;
    padding:0 var(--magi-sys-space-100);
    border-radius:var(--magi-sys-shape-corner-extra-small, 4px);
    color:var(--magi-ref-primary); background:var(--magi-ref-bg);
  }
  #turnfor:empty { display:none; }

  /* The session id inside a child's account, as the door it is. Inline in a <pre>, so it takes the
     monospace it sits in and is marked by colour and underline rather than by a shape that would
     break the line it is part of. */
  .childlink {
    font:inherit; color:var(--magi-ref-primary); background:none; border:0; padding:0;
    text-decoration:underline; text-underline-offset:2px; cursor:pointer;
  }
  .childlink:hover { text-decoration-thickness:2px; }
  .childlink:focus-visible { outline:2px solid var(--magi-ref-primary); outline-offset:2px; }

  /* Provider and model, side by side. They are one question in two parts — which backend, then
     which of its models — so they share a row and wrap together on a narrow screen rather than
     each taking a line of the facts grid. */
  .modelpair { display:flex; gap:var(--magi-sys-space-100); flex-wrap:wrap; align-items:center; }
  .modelpair > * { min-width:0; flex:1 1 auto; }
  .modelpair > [hidden] { display:none; }

  /* ── the settings destination ────────────────────────────────────────────
     It was a dialog and outgrew one: seven groups, its own scrollbar, and a scope axis on top.
     As a screen it takes the reading width the other destinations take — the rows are a label
     and a control, and a line of settings stretched to a wide window puts the control an inch
     from the thing it is named by. The dialog's own container styling is gone with it; what is
     left is the same prefgroup/prefrow classes doing the same work in a wider box. */
  #settings { max-width:var(--magi-sys-page); }
  #settings > #prefsK {
    font-size:var(--md-sys-typescale-headline-small-size);
    line-height:var(--md-sys-typescale-headline-small-line-height);
    margin-bottom:var(--magi-sys-space-300);
  }
  #settings[hidden] { display:none; }
  /* The scope line is the screen's most important sentence — it says which file every switch below
     writes — so it is set apart rather than left to read as the first row of settings. The
     companion's name inside it carries the emphasis: "which one am I configuring" is the question,
     and the name is the answer. */
  #settingsScope {
    padding:var(--magi-sys-space-200); margin-bottom:var(--magi-sys-space-300);
    border-left:3px solid var(--magi-ref-primary);
    background:var(--magi-ref-surface-container-low);
    border-radius:var(--magi-sys-shape-s);
  }
  #settingsScope .k { font-weight:600; color:var(--magi-ref-fg); }

  /* ── masthead ───────────────────────────────────────────────────────────── */
  header {
    /* Above the content, and by more than a tie. The panes beside a conversation are z-index 2 as
       well, and an equal z-index is settled by document order — which puts them, further down the
       markup, in front. Any moment the page can scroll at all beside a companion, a card slid up
       over the masthead: the report was "shrink the window and the cards come out over the
       header", and the tie is why. 3 leaves the rail and the drawer above it, where they were. */
    /* A bar takes a TONE, and the two bars on one screen were on two systems: the rail and the
       phone's navigation bar are painted with a surface container role, while this one was painted
       the page's own background and separated by a rule in the on-surface role — the text colour,
       at 14.6:1, drawn louder than the headings under it. One step off the body, and the hairline
       in the divider role, which is how the guide separates two surfaces. */
    /* Nothing at rest, a container tone once the page has moved under it. That is the guide's own
       description of this component — "처음에는 배경과 같은 색이었다가, 스크롤하면 대비되는 색으로
       채워져 분리를 만든다" — and neither of the two states this bar has had. It was the page
       colour with a rule in the ON-SURFACE role (14.6:1, a divider louder than the headings under
       it), and then a permanent tone one step off the page, which separated at 1.05:1 and never
       changed. body[scrolled] is set from the page's own scroll; the hairline comes with the fill. */
    position:sticky; top:0; z-index:3; background:var(--magi-ref-bg);
    transition:background-color var(--magi-sys-dur-short2) var(--magi-sys-ease-standard);
    /* 64dp, the small app bar's height, at every width — "기본 높이를 그대로 쓰고 … 기본보다 낮게
       만들지 말 것". It was set on the phone only, so from 600px up the bar was content-sized: 62. */
    box-sizing:border-box; min-height:64px;
    padding:var(--magi-sys-space-150) var(--magi-sys-space-300) var(--magi-sys-space-100);
    padding-top:calc(var(--magi-sys-space-150) + env(safe-area-inset-top));
    /* Centred, not baseline. The text elements baseline-align among themselves cleanly, but an
       icon button has no text baseline, so under align-items:baseline the trailing icon cluster
       centred in the bar and sat ~8px above the crumb and brand it shares the line with — measured
       at every width from 600 up, while the compact bar (which already centres) was consistent.
       One optical line for text and icons alike. */
    display:flex; gap:var(--magi-sys-space-200); align-items:center; flex-wrap:wrap;
    max-width:var(--magi-sys-page); margin-inline:auto; padding-right:var(--magi-sys-space-500);
  }
  /* Scrolled: the bar and whatever is stuck to it fill together, so they stay one block. */
  body[scrolled] header { border-bottom:1px solid var(--magi-ref-outlineVariant); }
  /* The strip fills with the bar but keeps its own divider — a second one here took a pixel off
     every tab, which is the whole margin a 48dp tab has. */
  /* Opaque, and the page's own ground — not a tone of its own.
     It must be opaque: the transcript scrolls UNDER this bar, and text showing through a masthead
     is the defect a fill exists to prevent. What it does not need is a DIFFERENT colour. The
     guide's scrolled state asks for "a contrasting fill that creates separation", and this took
     that literally with surface-container-low — which was one material more than the screen has
     now that the bay and the panels both paint the page's ground. Scrolled, the bar became the
     only tinted box on the page and read as laid over it rather than part of it.
     The separation is the hairline above, which is the other half of the same guidance and the
     half that survives a screen whose boxes are all one ground. Legibility is unchanged by
     construction: at REST this bar is already the page colour with the brand on it, so painting
     the same colour when scrolled changes nothing about what sits on what. */
  body[scrolled] header, body[scrolled] #ptabs { background:var(--magi-ref-bg); }
  .mark {
    font:600 var(--magi-sys-headline-s) var(--magi-ref-display); letter-spacing:.01em; color:var(--magi-ref-primary);
    font-feature-settings:"liga" 1;
  }
  /* The session's own id, in the muted role — a nameplate's standing line. Not a councillor hue:
     those belong to the three who vote, and this is a serial number. */
  .sid { color:var(--magi-ref-muted); font-size:var(--md-sys-typescale-label-small-size); letter-spacing:.04em; opacity:.8; overflow-wrap:anywhere; }
  /* The message sits after the count, in the same quiet role, and is allowed to be cut — it is the
     one thing on this line that has a tooltip carrying the rest. Empty it takes no room at all,
     which is why the masthead does not shift when something is said. */
  /* While a modal is open. Set from the page, released when the last one closes. */
  html.nomove { overflow:hidden; padding-right:var(--magi-comp-scrollbar-w, 0px); }
  #note {
    font:var(--md-sys-typescale-label-small-size)/1.4 var(--magi-ref-mono); letter-spacing:0.0533em;
    color:var(--magi-ref-muted); max-width:32ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  }
  #note:not(:empty) { padding-left:var(--magi-sys-space-100); border-left:1px solid var(--magi-ref-outlineVariant); }
  /* Gone when empty, not just invisible. An empty flex child still eats the row's gap on both
     sides — measured, the count readout's gap to the palette icon was 16px against the 8px between
     the two icons, which is the "misaligned" the header looked. The comment above always claimed
     this ("empty it takes no room") and the rule that makes it true was missing; .whereami already
     has it. #sid is the same, latent behind margin-left:auto — fixed for consistency. */
  #note:empty, #sid:empty { display:none; }
  #state {
    margin-left:auto; font:600 var(--md-sys-typescale-label-small-size)/1.4 var(--magi-ref-mono); letter-spacing:0.0533em;
    color:var(--magi-ref-muted); display:flex; align-items:center; gap:var(--magi-sys-space-100);
  }
  #state::before { content:""; width:6px; height:6px; border-radius:var(--magi-sys-shape-full); background:var(--magi-ref-outline); }
  /* The count is a readout; this part of it is a control, and it says so by being one. */
  .jump .jshort { display:none; }
  #state .jump {
    --md-text-button-label-text-color:var(--magi-ref-warn);
    --md-text-button-hover-label-text-color:var(--magi-ref-warn);
    margin-left:calc(-1 * var(--magi-sys-space-50));
  }
  /* Three states and three SHAPES, not three hues of one shape.
     The guide's through-line is that a state is never told by colour alone, and this dot was
     telling three different things — connected, somebody is waiting, the stream is gone — with
     nothing but its colour to tell them apart. Anyone who cannot separate green from amber from
     red was reading one dot that never changed.
     Connected is small and quiet with a soft halo. Waiting is bigger and solid, which is the one
     that wants to be noticed. Gone is a ring with nothing in it, which is the shape of an absence
     and reads as such in grey. */
  #state.live::before {
    background:var(--magi-ref-success);
    box-shadow:0 0 0 3px color-mix(in srgb, var(--magi-ref-success) 20%, transparent);
  }
  #state.asking::before {
    width:10px; height:10px; background:var(--magi-ref-warn);
  }
  #state.lost::before {
    background:transparent; border:2px solid var(--magi-ref-error); width:10px; height:10px;
  }
  /* The crumbs sit apart from each other and from their separators.
     The markup has no whitespace between them — it cannot, or the underline under a crumb would
     run through the space beside it — so the room comes from a gap. Two reasons for it: the line
     read as one word broken by slashes, and each crumb now carries a 48dp press target that is
     wider than the word it belongs to. Touching, those targets overlapped, and the crumb you
     pressed was decided by which came later in the markup rather than by where you pressed. */
  /* The instance, beside the brand. Monospace and quiet: it is a literal somebody compares with
     what they typed, not a title — and at the size and colour of the crumb below it, so the eye
     reads the pair as "this magi, at this place" rather than as two headings. */
  .whereami {
    font:var(--md-sys-typescale-label-medium-size)/1.6 var(--magi-ref-mono); color:var(--magi-ref-muted);
    min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  }
  .whereami:empty { display:none; }
  #crumbs { display:flex; align-items:baseline; gap:var(--magi-sys-space-100); }
  /* The SEPARATORS are drawn in the divider role. Written as "every span in here", it also caught
     the leaf — the crumb naming where you actually are — and painted the one word on the line that
     matters in the colour of a hairline: measured 1.79:1 dark and 1.46:1 light against the
     masthead, at 11px, where the floor for small text is 4.5:1. */
  #crumbs > span:not(#crumbLeaf) { color:var(--magi-ref-outlineVariant); }
  #back, #crumbHere, #crumbDeep {
    color:var(--magi-ref-muted); text-decoration:none; font-size:var(--md-sys-typescale-label-small-size); letter-spacing:0.04em; border-bottom:1px solid var(--magi-ref-outlineVariant); padding-bottom:2px;
  }
  /* Where you are is not a link and does not look like one, but it is the thing being read: the
     on-surface role, not the divider's. */
  #crumbLeaf, #crumbs .here { color:var(--magi-ref-fg); border-bottom-color:transparent; }
  /* Two indicators, and a pressed state: the colour swap alone is the hover the checklist calls
     not-M3, and one of the two readers it fails is anybody who cannot tell the two hues apart. */
  #back:hover, #crumbHere:hover, #crumbDeep:hover {
    color:var(--magi-ref-primary); border-bottom-color:var(--magi-ref-primary);
    background:color-mix(in srgb, var(--magi-ref-fg) 8%, transparent);
  }
  #back:active, #crumbHere:active, #crumbDeep:active { background:color-mix(in srgb, var(--magi-ref-fg) 12%, transparent); }

  /* A press must find 48dp even where the control is drawn smaller.
     The library does this inside its own shadow root with a .touch span; these are hand-built, so
     they say it here. The expander is centred on the visible box, taken out of flow so no row
     grows around it, and it belongs to the control — a press on it is a press on the control.
     ⚠ It is not applied to text links inside running prose: a card's title wraps across lines,
     and a 48dp box on an inline box is either fragmented or a lie. Those are the targets the rule
     exempts, because their size is set by the line height of the text they sit in. */
  /* Only on a phone, where the dialog is the whole screen. On a basic dialog the bottom row is
     both the way out and the way on, and a second one in the headline is a control saying nothing
     new. */
  .dlgclose { display:none; }
  .hit48 { position:relative; }
  /* The one place the expander grows DOWNWARD instead of around the middle.
     A transcript row carries content-visibility:auto, which contains paint — so an expander centred
     on a control at the very top of a row has its upper half clipped away by the row's own box, and
     the copy control measured 38px of the 48 it asks for. Anchored to the top, all 48 are inside. */
  /* Anchored to the top, still centred across. `transform:none` took the translateX with it while
     left:50% stayed, so the 48px box ran from +12 to +60 of a 24px glyph — 36px of press area
     lying over the first line of the message beside it. */
  .who .copy.hit48::after { top:0; transform:translateX(-50%); }
  .hit48::after {
    content:''; position:absolute; left:50%; top:50%; transform:translate(-50%, -50%);
    /* Wide as well as tall. left/right:0 gave the expander the element's own width, so a 24px
       glyph — the copy control in the transcript's gutter — kept a 24×48 target after being given
       the class that exists to make it 48×48. The crumb next door already said min-width:48px for
       the same reason; this says it once, for everything wearing the class. */
    width:100%; min-width:48px; height:48px; border-radius:inherit;
  }
  /* By id, not by the class: the page assigns back.className outright when the crumb changes,
     so a class put on it in the markup lasts until the first navigation and then is gone. */
  #back { display:inline-block; position:relative; }
  #back::after {
    content:''; position:absolute; left:50%; top:50%; transform:translate(-50%, -50%);
    width:100%; min-width:48px; height:48px;
  }
  /* The crumbs further along the line are the same kind of thing and were the same size as the
     text they are made of — 16px tall, measured. They are the way back out of a detail screen, so
     the one rule that made the first crumb pressable applies to them too; .here marks the level
     you are standing on, which is not a link and gets no target. */
  /* Every rung, including the one you are standing on. The exclusion below said ".here is not a
     link and gets no target" — but paintCrumbs gives crumbHere an href before it marks it .here,
     and back keeps one on every list screen, so both were live links with a 21px target. Either
     the href goes or the target comes; the href is what a middle-click and a copied address use. */
  #crumbHere, #crumbDeep { display:inline-block; position:relative; }
  #crumbHere::after, #crumbDeep::after {
    content:''; position:absolute; left:50%; top:50%; transform:translate(-50%, -50%);
    width:100%; min-width:48px; height:48px;
  }

  /* ── the rail: navigation on a wide screen, settings on a narrow one ────── */
  /* One element in two modes rather than two that have to agree. Wide: it stands beside the page
     as a rail and the hamburger widens it into a drawer. Narrow: it is off-screen and the same
     button slides it in over the page, with the tabs still doing the navigating — which is the
     handbook's arrangement and the one M3 describes for these two widths.

     The breakpoint is 768/769px, the handbook's, so the two products break in the same place. */
  /* The scrim is an ELEMENT, not a shadow on the rail. Drawn as a box-shadow it darkened the page
     without covering it: everything under it stayed clickable, so a page that looked disabled took
     a click and navigated away under an open drawer. It also gives the drawer the dismissal every
     modal surface has — a click on the page you can see is the way out of it. */
  /* Coloured on its background rather than on opacity. Not a style preference: the contrast check
     reads every opacity in this sheet as text being dimmed, because a container's opacity takes
     everything inside it down too — and it cannot tell from CSS that this box never holds text.

     It fades, and it fades FAST. The objection this comment used to record — that a scrim arriving
     over a quarter of a second leaves the page half covered and still live-looking — was about
     appearance, not about reach: pointer-events flip on the same frame the drawer opens, so nothing
     under it is clickable at any point in the fade. What is left is the choice between a dim that
     appears between two frames (which reads as a flash) and one that arrives in under a fifth of a
     second, which is how every other surface in this console arrives — short4, the same duration and
     easing this sheet already gives a surface that changes state.
     (It used to be a box-shadow on the rail, whose spread grew with the rail's width — so the
     dimming really did sweep across the page. That is the thing this must not go back to.) */
  #scrim {
    position:fixed; inset:0; z-index:3; background-color:transparent; pointer-events:none;
    transition:background-color var(--magi-sys-dur-short4) var(--magi-sys-ease-standard);
  }
  /* Somebody who asked for less motion gets the dim without the fade — the scrim's job is to say
     "not this", and that is said the moment it appears. */
  @media (prefers-reduced-motion:reduce) { #scrim { transition:none; } }
  body[nav="open"] #scrim {
    background-color:color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent);
    pointer-events:auto;
  }
  #rail {
    position:fixed; top:0; bottom:0; left:0; z-index:3;
    width:var(--magi-comp-rail-now, var(--magi-comp-rail-w)); box-sizing:border-box;
    /* Over the page, not beside it: a floating drawer is the one that can open without moving
       anything. It says that it overlaps with a TONAL difference and an edge, not a shadow — a
       container role one step off the body's, plus a hairline. That is the guide's default way to
       separate two surfaces, and it is the one that survives when the scrim below it does the rest
       of the work. The shadow this comment used to promise went out with the box-shadow scrim. */
    z-index:4;
    padding:calc(var(--magi-sys-space-150) + env(safe-area-inset-top)) var(--magi-sys-space-100) var(--magi-sys-space-200);
    background:var(--magi-ref-surface-container-low); border-right:1px solid var(--magi-ref-outlineVariant);
    display:flex; flex-direction:column; gap:var(--magi-sys-space-200); overflow:hidden auto;
    /* Same curve and duration as the components use for a container that changes size, so the rail
       and the page's own margin arrive together rather than one chasing the other. */
    transition:width 250ms var(--magi-sys-ease-emphasized), transform 250ms var(--magi-sys-ease-emphasized);
  }
  /* Two numbers, not one. --magi-comp-rail-w is the gutter the PAGE reserves and it never changes; --magi-comp-rail-now
     is how wide the rail is drawing itself right now. Widening the rail used to widen the gutter,
     so the table shifted 184px right and lost 184px of width every time the drawer opened. The
     drawer floats over the gutter instead, and nothing on the page moves. */
  /* 20rem open, not 16. The drawer floats over the gutter and pushes nothing, so its width costs
     the page nothing — and at 16 the line under each destination wrapped to two or three, which
     turned a rail into a paragraph. The guide's own drawer is 360dp; this stays under that and
     gives the sentence one line at the sizes this ships in. */
  body[nav="open"] { --magi-comp-rail-now:20rem; }
  /* Collapsed, the rail is 4.5rem and a word like "connections" is not — so collapsed shows the
     icon and nothing else. Clipping the label instead would put half a word on screen, which reads
     as a bug rather than as a choice. The label still exists for a screen reader: it is the
     item's aria-label, set beside the text in paint(). */
  /* Wrapping, not clipping. At 2x text the guide asks that the whole label stay on screen and
     lets navigation items grow taller to hold it — so the label may take two lines rather than
     lose its tail. Collapsed still shows no label at all, which is the line below and a different
     decision. */
  /* One line, always. The label is revealed the instant the drawer is asked to open and the rail
     is still 80px wide at that instant, so a label allowed to break anywhere broke into TEN lines
     and stood the item 264px tall, then collapsed to 56px over the next 120ms as the rail caught
     up. Measured, and it is the flinch a reader sees every time they open the drawer.
     Clipped rather than wrapped: the rail already hides its overflow, and a longer word in another
     language now runs out of the item instead of pushing the row apart. */
  #rail .lbl { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; min-width:0; }
  /* Only the phone's bar reads the short one — and the COLLAPSED rail, where the long one does not
     fit either: "People and permissions" wants 172px in a 96dp rail and was drawn cut to "People
     and", at every width from 600 up. The guide's word on a navigation label is that it is one or
     two words and is not cut, so where the long one cannot fit the short one is the label. */
  .raili .lblshort { display:none; }
  body:not([nav="open"]) #rail .raili:has(.lblshort:not(:empty)) .lbl { display:none; }
  body:not([nav="open"]) #rail .raili .lblshort { display:block; }
  /* The second line, and only where there is room for one.
     Collapsed and open showed the same four words, so opening the rail bought width and spent it
     on making the same thing bigger. What an open rail has is room for the sentence that says what
     a destination is FOR — which is the part "shared" and "meeting" cannot carry on their own.
     Hidden rather than absent when collapsed: the markup is the same in both states, and a screen
     reader gets the label either way.
     Two lines now, so the word and its sentence stack in a column beside the icon, and the item is
     as tall as what is in it. Wrapping is allowed here where the LABEL forbids it — a label that
     breaks makes the row jump while the rail is still widening; this appears only once the drawer
     is open and the width has stopped moving. */
  #rail .sub { display:none; }
  /* Shown while the rail IS open, and gone the instant it starts closing.
     Keyed on nav rather than on nav-wide, which is the shape and outlives the width by a quarter
     second on the way out: the sentence stayed while the lane it was written in was being taken
     away, so for the length of the animation it wrapped into three lines inside a 96px rail and
     the whole column jumped. nav goes at the moment of the press, which is when the width starts
     moving.
     And it clips rather than wraps, for the same reason the label above it does: the width is
     mid-flight, and text that re-wraps while a box is moving is the flinch the reader sees. */
  body[nav="open"] #rail .sub {
    display:block; min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
    font:400 var(--md-sys-typescale-label-small-size)/1.35 var(--magi-ref-mono);
    letter-spacing:0; color:var(--magi-ref-muted);
    /* In after the lane exists, not while it is being made. The 250ms is the rail's own width
       transition — the sentence arrives once there is room for it, which reads as the drawer
       opening and then filling rather than as text being dragged out of the edge of the screen.
       An animation and not a transition: the element goes from display:none, and a transition
       from a box that was not being rendered does not run. `both` keeps it at zero through the
       delay. Out is not animated at all — see above, it goes the moment the width starts back. */
    animation:railSubIn var(--magi-sys-dur-short4) var(--magi-sys-ease-standard) 250ms both;
  }
  @keyframes railSubIn { from { opacity:0; } to { opacity:1; } }
  /* With motion turned down the rail does not travel — the width transition is not in the list
     that block keeps — so there is no arrival to wait for, and a delay would only be a gap. */
  @media (prefers-reduced-motion:reduce) {
    body[nav="open"] #rail .sub { animation-delay:0s !important; }
  }
  /* The words box is never wider than the item it is in. Collapsed, the rail is 96px and "People
     and permissions" is 172 — without this the label ran out of the item on both sides and the
     rail clipped it mid-word, because the ellipsis on .lbl only works if something above it says
     how much room there is. Measured: x = -39 in a 96px rail. */
  .raili .words { min-width:0; max-width:100%; overflow:hidden; }
  body[nav-wide] .raili .words { display:grid; gap:2px; }
  /* The label waits for the room. It is revealed the instant the drawer is ASKED to open, and at
     that instant the rail is still 80px wide — the item has no lane for a word beside a 24dp icon,
     so it grew to 66px and came back to 56 as the rail caught up. Ten pixels of flinch on every
     open, and 264 before the label was told not to wrap.
     So the width goes first and the word follows it: the rail sets nav=open, its width transition
     runs, and only then does the body say the labels may be drawn. Closing is the reverse and
     immediate — a word cannot be left standing in a lane that is being taken away. */
  /* The label is drawn in both states now. It used to be hidden while the rail was narrow because
     the word had nowhere to go beside a 24dp icon in 64px — the item swelled to 66px and came back
     to 56 as the rail widened, which is the flinch that machinery existed to prevent. With the
     rail at its standard 96dp and the word UNDER the icon, it has a row of its own and there is
     nothing to flinch. The guide asks for the label; this is what it costs. */
  #rail .lbl { animation:noticedWord 150ms var(--magi-sys-ease-standard); }
  /* Collapsed, the icon belongs on the rail's centre line, and it was 4px to the left of it: the
     item is 63px inside an 80px rail and its leading space put the 24px icon at the item's leading
     edge, not at the middle of either. The menu button was 8px out for the same reason.

     Moved with a transform rather than by changing the leading space, because the space is a
     custom property and a custom property does not transition — the icon would arrive at its new
     column between two frames while the rail beside it took 250ms to get there. Same curve and
     length as the rail's own width, so the two are one movement. */
  /* The icon is centred by the 56px indicator it sits in. The menu button is centred by being
     told to be: it was nudged there with a translate measured against an 80px rail, and when the
     rail became 96 the nudge was 12px short — a number that has to be recomputed whenever anything
     around it moves is a number that will be wrong again. align-self does it at any width. */
  /* The menu button does not move. Not by 4px, not ever: it is the one control that is in the same
     place before and after the press, and a button that shifts under the finger that just pressed
     it is the thing the eye follows instead of the drawer.
     Fixed at the collapsed centre — (96 rail − 40 button) ÷ 2 — and the destinations are given a
     leading space that puts their icons on that same line (below), so the whole icon column stands
     still and only the words arrive. */
  /* The menu button's position is fixed and is written once, further down, beside the other rail
     controls it shares a colour with. */
  /* Expanded, on the same vertical line as the destinations' icons rather than 20px to the left of
     them. The item insets its content by space-200 and the icon sits centred in a 56px indicator;
     the button is 40px, so it needs that inset plus half the difference to land on the same line. */

  #rail .ic { flex:none; display:block; }
  #railNav { display:flex; flex-direction:column; gap:var(--magi-sys-space-150); }
  /* The foot of the rail, which is where the guide puts the items about the tool rather than about
     the work. margin-top:auto is what makes it the foot: the rail is a column with a fixed height,
     so the space the page does not use is given to this margin and the group falls to the bottom.
     A hairline above it, not just the gap: on a short window the gap can close to nothing and the
     two groups would read as one list whose last item happens to be about permissions. */
  #railFoot {
    margin-top:auto; display:flex; flex-direction:column; gap:var(--magi-sys-space-150);
    padding-top:var(--magi-sys-space-200); border-top:1px solid var(--magi-ref-outlineVariant);
  }
  /* A foot with nothing in it is a line across the bottom of the rail with nothing under it, which
     is what a reader who is not an admin would see. :empty cannot see it — the anchor is still
     there, hidden — so the hairline goes when everything inside is. */
  #railFoot:not(:has(.raili:not([hidden]))) { border-top:0; padding-top:0; }
  /* A destination. Expanded it is a row — icon, then the word — and collapsed it is the word UNDER
     the icon, which is the shape the spec draws and the shape md-list-item could not make.
     The active indicator is a pill behind the ICON at 56x32, not behind the whole item: that is
     what the collapsed anatomy shows, and the label sits below it rather than inside it. */
  .raili {
    display:flex; align-items:center; gap:var(--magi-sys-space-150);
    text-decoration:none; color:var(--magi-ref-muted);
    font:600 var(--md-sys-typescale-label-medium-size)/1rem var(--magi-ref-mono);
    letter-spacing:0.05em; border-radius:var(--magi-sys-shape-full);
    /* 12px, not the scale's 16: it is what puts the 56px indicator's centre on the line the
       collapsed rail stands its icons on — 8 of rail padding + 12 + 28 = 48, and collapsed
       8 + 80/2 = 48. Four pixels off the token buys an icon column that does not move when the
       drawer opens, which is the difference somebody actually sees. */
    padding-inline:12px;
  }
  /* 56dp expanded, 64dp with the word under the icon — the two the spec gives a rail item.
     Named through #railNav because .raili also carries .state, and .state sets the 48dp floor for
     every hand-built control on the page; equal specificity, and the later one was winning. */
  /* The one number that must not jump. Everything else about the item's shape is state and changes
     between two frames behind the rail's clip; the height is a length, so it travels — otherwise
     every destination below the first hops 8px at the instant of the press. Same curve and length
     as the width it happens under, so it reads as one movement. */
  #railNav .raili { min-height:56px; transition:min-height 250ms var(--magi-sys-ease-emphasized); }
  body:not([nav-wide]) #railNav .raili { min-height:64px; }
  .raili .icwrap {
    display:grid; place-items:center; position:relative;
    width:56px; height:32px; border-radius:var(--magi-sys-shape-full); flex:none;
  }
  body:not([nav-wide]) .raili {
    flex-direction:column; gap:var(--magi-sys-space-50); padding-inline:0;
    min-height:64px; justify-content:center;
  }
  /* Where you are, in three properties rather than one: the indicator, a heavier stroke on the
     icon, and a bold label. The guide names colour alone as the case that is not enough, and its
     answer for an icon with no filled twin is a thicker one. */
  /* The state layer is the INDICATOR's, not the item's.
     .state paints its overlay across the whole control, and collapsed the indicator is a 56x32
     pill behind the icon — so hovering a selected destination drew two shapes at once, the pill
     underneath and a full-width wash on top of it. The layer goes where the indicator is: on the
     icon's pill collapsed, on the whole row expanded, which is where the indicator is then. */
  .raili { position:relative; }
  .raili .icwrap::after, .raili::after {
    content:''; position:absolute; inset:0; border-radius:inherit; pointer-events:none;
    background:currentColor; opacity:0; transition:opacity var(--magi-sys-dur-short2) var(--magi-sys-ease-standard);
  }
  .raili:hover .icwrap::after { opacity:.08; }
  .raili:focus-visible .icwrap::after, .raili:active .icwrap::after { opacity:.12; }
  body[nav-wide] .raili .icwrap::after { opacity:0 !important; }
  body[nav-wide] .raili:hover::after { opacity:.08; }
  body[nav-wide] .raili:focus-visible::after, body[nav-wide] .raili:active::after { opacity:.12; }
  .raili:focus-visible { outline:3px solid var(--md-sys-color-secondary, var(--magi-ref-accent)); outline-offset:2px; }
  .raili[selected] { color:var(--magi-ref-primary); }
  .raili[selected] .icwrap { background:color-mix(in srgb, var(--magi-ref-primary) 14%, transparent); }
  body[nav-wide] .raili[selected] { background:color-mix(in srgb, var(--magi-ref-primary) 14%, transparent); }
  body[nav-wide] .raili[selected] .icwrap { background:transparent; }
  .raili .lbl, .raili .lblshort { font-weight:500; }
  /* Both labels, because the phone draws the short one. The rule named .lbl only, which is
     display:none at that width — so on the bar at the foot of a phone every destination was the
     same weight and the guide's second indicator ("a filled icon with a BOLD label for selected
     destinations") existed on one screen and not the other. */
  .raili[selected] .lbl, .raili[selected] .lblshort { font-weight:700; }
  /* And the selected label is on-surface, not primary.
     Primary on the bar's own container measures 4.27:1 in the light theme at 11px — under the
     4.5:1 floor for small text. The primary hue still says "here" twice, on the icon and on the
     indicator behind it, which is where it carries no text. */
  .raili[selected] .lbl, .raili[selected] .lblshort { color:var(--magi-ref-fg); }
  .raili[selected] .ic path { stroke-width:2.4; }
  /* The badge, corrected. Its inner box is position:absolute at top / 50% across, which is right
     when the badge is laid over an icon and wrong everywhere else — dropped into a flow it anchors
     to whatever ancestor happens to be positioned and lands somewhere unrelated. Giving the host a
     size and a position makes the host the thing it anchors to, which is what a caller has to do
     for a component still in the library's unstable half. */
  md-badge {
    position:relative; display:inline-block; vertical-align:middle;
    width:18px; height:18px; flex:none;
    --md-badge-color:var(--magi-ref-warn);
    --md-badge-large-color:var(--magi-ref-warn);
    --md-badge-large-label-text-color:var(--magi-ref-bg);
    --md-badge-large-label-text-font:var(--magi-ref-mono);
  }
  /* On a tab the badge sits beside the word rather than over it: these labels are words, not icons,
     and a count parked on top of "companions" lands on a letter. */
  /* On a tab the host is NOT positioned: a tab lays its label out itself, and giving the badge a
     relative box of its own dropped it onto a line below the word. Absolute inside the tab instead,
     riding the label's top-right the way it rides the icon in the rail. */
  /* The label inside a tab. `.badgewrap` went with the destination strip that was deleted — it was
     the wrapper that held that strip's waiting count, and nothing else ever used it. */
  .tablbl { display:inline-flex; align-items:center; }
  /* In the rail it rides the icon, which is what a badge is for — and when the rail is collapsed
     the icon is the only thing there. */
  .icwrap { position:relative; display:inline-flex; width:24px; height:24px; }
  .icwrap md-badge { position:absolute; top:-5px; right:-7px; width:16px; height:16px; }
  /* Widened, the row has a word in it and the count belongs after the word — riding the icon is
     what a badge does when the icon is all there is. Anchored to the ITEM rather than to the icon,
     so it lands at the end of the row. */
  body[nav-wide] .raili { position:relative; }
  body[nav-wide] #rail .icwrap { overflow:visible; }
  /* Expanded, the badge is a child of the list item and lays itself out beside the label; the
     move is placeRailBadge()'s, and this rule only says what it looks like once it is there. */
  /* The badge's own box, and a positioning context to go with it.
     Static, the count was laid out at the end of the row — and then drawn 87px to the left of
     there, on top of the word. The component positions its inner circle ABSOLUTELY, so with the
     host static that inner box resolved against the nearest positioned ancestor, which is the
     item itself: the badge stood where the item's top-right corner said, not where its own box
     was. Measured at x=130 with the host box at x=217, over the "ni" of "Companions".
     position:relative changes nothing about where the host sits and makes it the thing its own
     content is measured from. */
  body[nav-wide] .raili > md-badge { position:relative; align-self:center; }
  /* Gaps, not dividers. This is a contained list — four settings inside a dialog — and the guide
     keeps dividers for uncontained or complex ones, "only when a stronger visual separation is
     necessary". A line between every row of four is a fence where a space does the work. */
  #prefsForm {
    /* min-width in rem grows with the reader's text: at 200% 16rem is 512px, wider than the compact
       full-screen dialog's 360px surface — whose scroller is overflow-x:hidden, so the trailing edge
       of every settings row (the switches and selects) was CLIPPED and unreachable. The same bug the
       skills field and #mcpForm already guard against; guarded here the same way. */
    display:flex; flex-direction:column; gap:var(--magi-sys-space-300); min-width:min(16rem, 100%);
  }
  /* Both rows lay their controls out on one line and wrap on a narrow screen. .sktools had no
     display at all — four controls in a block, no gap, no shared baseline — which is what "the
     screen's controls" looked like until somebody measured it. */
  .skfind { display:flex; margin:0 0 var(--magi-sys-space-300); }
  .skfind md-outlined-text-field { flex:1 1 22rem; max-width:34rem; }
  /* Centred on the field, not sitting on its floor. A field is 56dp (80 with two rows) and a
     button is 40, so aligning their bottoms puts the button's middle 8-20px below the middle of
     the thing it acts on and the row reads as two rows that failed to line up. The guide has no
     sentence about this — it rules on horizontal alignment and leaves the cross axis alone — so
     it is a house rule: a field and its controls are one control, centred on the field.
     Safe here because no field on this page grows: measured with six lines typed, the composer's
     box stays 56px and scrolls inside itself. */
  .skwrite {
    display:flex; flex-wrap:wrap; gap:var(--magi-sys-space-200); align-items:center;
    margin:var(--magi-sys-space-400) 0 0; padding-top:var(--magi-sys-space-300); border-top:1px solid var(--magi-ref-outlineVariant);
  }
  /* Allowed to shrink. A rem grows with the reader's text size, so at twice it this 14rem field is
     448px and pushed the page 160px sideways on a 320px screen. */
  .skwrite md-outlined-select { flex:0 1 14rem; max-width:100%; }
  .skwrite md-outlined-text-field { flex:1 1 22rem; }
  .skwrite .skmodel {
    flex:1 1 100%; font:var(--magi-sys-body-s) var(--magi-ref-mono); color:var(--magi-ref-muted); overflow-wrap:anywhere;
  }
  /* The switch is TRAILING, which is where a switch goes: the guide arranges settings in stacked
     rows with the control at the end of the line, and this one was a column left over from when
     the control was a button with a sentence under it — so the switch sat below its own label,
     left-aligned, reading as a second thing rather than as the setting's control. The sentence
     that explains it still goes underneath, because it is a paragraph and not part of the row. */
  /* The middle of a row: the sentence that says what the setting does, or why it cannot be
     changed. Quieter than the name and allowed to wrap — min-width:0 so it wraps inside the row
     rather than pushing the control off the end of it. */
  /* The leading half of a settings row: the label, and the supporting line under it. */
  .prefsay { display:flex; flex-direction:column; gap:var(--magi-sys-space-50); min-width:0; }
  /* Supporting text: quieter than the label, wrapping rather than pushing the control off the end,
     and capped at the length the guide asks for — "limit supporting text to one to three lines". */
  .prefsay .say {
    font:var(--magi-sys-body-s) var(--magi-ref-mono); color:var(--magi-ref-muted);
    overflow-wrap:anywhere;
  }
  .prefsay .say:empty { display:none; }

  #prefsForm .k {
    font:600 var(--md-sys-typescale-label-small-size)/1.4 var(--magi-ref-mono); letter-spacing:0.06em; color:var(--magi-ref-muted);
  }
  #console { font:var(--md-sys-typescale-label-medium-size)/1.6 var(--magi-ref-mono); color:var(--magi-ref-muted); overflow-wrap:anywhere; }
  #console b { color:var(--magi-ref-fg); font-weight:600; }
  #railMenu, #themeToggle, #prefs {
    --md-icon-button-icon-color:var(--magi-ref-muted); color:var(--magi-ref-muted);
  }
  /* The menu button does not move. Not by four pixels, not ever: it is the one control that is in
     the same place before and after the press, and a button that shifts under the finger that just
     pressed it is what the eye follows instead of the drawer.
     20px puts its centre at 48 — 8 of rail padding, then 20, then half of a 40px button — which is
     the line the collapsed rail stands its destination icons on. The expanded item's leading space
     (see .raili) is chosen to land its icons on that same 48, so the whole column stands still and
     only the words arrive. */
  #railMenu { align-self:start; margin-left:20px; }
  /* The lines travel; they are not swapped for a cross.
     Each bar of the hamburger is two halves meeting in the middle. Opening, every half keeps the
     end it shares with its twin, walks that end to the centre of the box and turns 45° — so the
     four halves arrive as the four arms of the cross, and what you watched was the same five
     strokes moving rather than one icon replaced by another. The middle bar has nothing to become:
     both of its ends walk to its own centre until there is no line left.
     Closing runs the same transitions backwards, for free, because this is a state and not an
     animation: the attribute goes away and every stroke returns the way it came.

     transform-box:view-box so an origin can be written in the coordinates the markup uses — the
     default measures from the element's own bounding box, which for a horizontal line is a strip
     of zero height and puts every rotation somewhere unintended.

     The arithmetic, once: a half is 9 long and its shared end sits 6 above or below the centre. Walk
     that end 6 and turn 45°, and the far end lands at 12 ∓ 6.36 in both axes — a cross a shade
     larger than the bars were wide, centred, and symmetric in all four directions by construction
     rather than by four hand-tuned numbers. */
  .burger line {
    stroke:currentColor; stroke-width:2; stroke-linecap:round;
    transform-box:view-box;
    transition:transform var(--magi-sys-dur-short4) var(--magi-sys-ease-emphasized);
  }
  .burger .bt { transform-origin:12px 6px; }
  .burger .bb { transform-origin:12px 18px; }
  .burger .bm { transform-origin:12px 12px; }
  body[nav="open"] .burger .bt.bl { transform:translateY(6px)  rotate(45deg); }
  body[nav="open"] .burger .bt.br { transform:translateY(6px)  rotate(-45deg); }
  body[nav="open"] .burger .bb.bl { transform:translateY(-6px) rotate(-45deg); }
  body[nav="open"] .burger .bb.br { transform:translateY(-6px) rotate(45deg); }
  /* Both ends to the centre. scaleX rather than a fade: a line that dims is still a line crossing
     the other four halfway through every press. */
  body[nav="open"] .burger .bm { transform:scaleX(0); }
  /* One sky, two bodies, and the horizon is the icon's own edge.
     Which one is up follows the theme in force — including when that theme is the machine's, so
     the query appears here as well as the attribute, the same pairing the palette uses.
     Neither is hidden and neither fades: they TRAVEL, out through the bottom of the box and back
     in through the top, and the box clips them. That is what makes it a rising and a setting
     rather than a cross-fade — and it means there is no rule anywhere dimming a shape to nothing,
     which is the state a reader can do nothing with.
     transform-box:view-box so the turn happens about the middle of the icon rather than about
     whatever bounding box each shape happens to have. */
  .sky { overflow:hidden; }
  .sky g {
    transform-box:view-box; transform-origin:12px 12px;
    transition:transform var(--magi-sys-dur-medium2) var(--magi-sys-ease-emphasized);
  }
  /* All three wait BELOW the horizon and rise. The one that is leaving sinks the same way it came
     up, which is the only arrangement where the sun is seen to rise: parked above, it descended
     into the middle of the sky on its way in — a sunrise played backwards. One direction for every
     body, so the press reads as one sky turning rather than icons passing.
     
     What is up says which PREFERENCE is set, not which theme resolved. No attribute means "follow
     the machine", and the half-lit circle is that answer; the sun would be a claim about the time
     of day that nobody made. */
  .sky .moon, .sky .sun { transform:translateY(160%) rotate(-40deg); }
  :root[color-theme="light"] .sky .sun { transform:none; }
  :root[color-theme="light"] .sky .auto { transform:translateY(160%) rotate(-40deg); }
  :root[color-theme="dark"] .sky .moon { transform:none; }
  :root[color-theme="dark"] .sky .auto { transform:translateY(160%) rotate(-40deg); }
  /* One shape for every preference: what it is called, what it does, and the control — in that
     order, at the same three places on each row, so the eye finds the next control where it found
     the last one. The name column is fixed so the sentences start on one line down the dialog. */
  .prefrow { display:flex; align-items:center; gap:var(--magi-sys-space-200); }
  /* A settings subheader (M3 list group header): the small primary-tinted label that names a group
     of preferences, with a divider above it so the groups read as blocks rather than one long list.
     The first one in the dialog carries neither — nothing precedes it to divide from. */
  .prefgroup {
    margin:var(--magi-sys-space-300) 0 0;
    padding-top:var(--magi-sys-space-200);
    border-top:1px solid var(--magi-ref-outline);
    font-size:.72rem; font-weight:600; letter-spacing:.06em; text-transform:uppercase;
    color:var(--magi-ref-primary);
  }
  .prefgroup:first-child { margin-top:0; padding-top:0; border-top:none; }
  /* Before the label JS runs, an unpopulated header is an empty bordered div; hide it so there is no
     flash of a stray divider on load. (A group hidden by data-may is hidden by `hidden`, not by this.) */
  .prefgroup:empty { display:none; }
  /* The profile editor list + add form. Each row is name · meta · edit · remove; the add form is a
     column of fields so a base_url and a key are not squeezed onto one line. */
  .proflist { display:grid; gap:var(--magi-sys-space-100); width:100%; }
  .profrow { display:grid; grid-template-columns:minmax(0,auto) minmax(0,1fr) auto auto;
    gap:var(--magi-sys-space-100); align-items:center; }
  .profrow .profnm { font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  .profrow .profmeta { color:var(--magi-ref-muted); font-size:.82rem; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .profempty { color:var(--magi-ref-muted); font-size:.85rem; }
  .profadd { display:grid; gap:var(--magi-sys-space-100); margin-top:var(--magi-sys-space-100); }
  .profadd md-outlined-text-field { width:100%; }
  /* And the pickers in that block, for the same reason the fields have it: a select sizes to its
     widest option, and the provider list carries model names nobody chose the length of. Without
     this the two of them pushed a phone screen sideways (measured at 390px: 405px of page). */
  .profadd md-outlined-select { width:100%; min-width:0; }
  .profadd md-text-button { justify-self:end; }
  /* A row that belongs to the width where the rail is not drawn. The default is hidden and the
     narrow query puts it back, so the phone case is the exception that has to be stated — the
     other way round, a new width would inherit "show it" and quietly give the screen two doors. */
  .prefrow.narrowonly { display:none; }
  .prefrow > .prefsay { flex:1 1 auto; min-width:0; }
  /* Shrinkable and capped: 0 0 11rem cannot shrink and, in rem, grows with the reader's text size —
     at 200% it is 352px and clips off the edge of the full-width prefs dialog. Same guard the skills
     select carries. */
  .prefrow > md-outlined-select { flex:0 1 11rem; max-width:100%; }

  /* The dock is fixed and its height changes — a prompt bar appears above the composer, and the
     composer itself grows with what you type. A constant padding here either wastes a screen of
     space or hides the last thing the agent said behind the controls, and on a phone it is the
     second one. The page measures the dock and reserves exactly that. */
  /* Left-aligned beside the rail rather than centred in what is left of the window. Centred, the
     page moved by HALF the rail's growth and lost the other half off its own width, so opening the
     drawer re-wrapped every column instead of sliding the page across. A block that keeps its width
     and moves by exactly the distance the rail took is the one a reader can follow.
     
     No auto margin. Body is a flex column, and an auto margin on the cross axis makes a flex item
     size to its CONTENT instead of stretching — so this was 720px wide inside a 1264px cap on a
     2497px screen, which is what "the right margin disappeared" and "a wide screen is not used"
     both were. A stretched item with a max-width already sits at the start of a column. */
  /* Centred in what is left beside the rail. Left-aligned it left a 1233px gutter on a 2497px
     screen — the page in one corner of the window and nothing in the rest of it. Centring cannot
     re-wrap anything at that width, because the block is already narrower than the room: the
     re-wrap this was avoiding only happens when the drawer opening takes the available width BELOW
     the cap, and there it still fills and still does not move. */
  main {
    padding:var(--magi-sys-space-300) var(--magi-sys-space-500) calc(var(--dock, var(--magi-sys-space-1600)) + var(--magi-sys-space-400));
    max-width:var(--magi-sys-page); margin-inline:auto;
  }
  /* The panes get the room the margins were holding. Only on a companion's page and only where
     there is room for both — below that the panes are not beside the conversation anyway. */
  @media (min-width:100em) {
    body[at="agent"] main { max-width:var(--magi-sys-page-wide); }
    /* The masthead widens with what is under it. It kept the page's own cap while the conversation
       took the wider one, so on a 2488px monitor the header ran 568..2001 over a page running
       315..2254 — a narrow hat on a wide head, and the border under it stopped a quarter of the
       way in from each side. */
    body[at="agent"] header { max-width:var(--magi-sys-page-wide); }
    /* The dock's contents are measured with main, at the same breakpoint: the two are one column,
       and a column that widens at the top and not at the bottom is the thing this pair of rules
       exists to prevent. */
    body[at="agent"] #dock .bay { max-width:var(--magi-sys-page-wide); }
  }


  /* ── what I had to say ───────────────────────────────────────────────────── */
  /* Grouped by what was said, because the repetition IS the finding: one correction is a remark,
     the same one to three companions is a rule waiting to be written. */
  /* Every section is the same width. They were not: this one was 74ch while lessons and MCP were
     108ch and the fleet filled the page, so changing menus moved the left edge and re-set the line
     length — three different pages rather than four views of one. The prose inside keeps its own
     measure, which is where a reading width belongs. */
  /* Both halves of one story, on one page: what has been said often enough to become a rule, and
     the rules. They were two destinations, and a reader had to know that promoting on one made
     something appear on the other.

     The corrections destination and its promotion pipeline are gone; what was learned is one list
     now. Its rules went with it — #ivs, .sectionhead and the whole .iv family styled a section
     that no longer exists, and a stylesheet that keeps them is one where the next reader cannot
     tell which selectors mean anything. */

  /* ── what they have learned ─────────────────────────────────────────────── */
  /* Two tiers on one page, the crossing one first. The boundary between them is the whole of
     context hygiene, and it is only as good as somebody's ability to see it: a rule in the global
     tier reaches every prompt on every project, and after the day it was written nothing else in
     the system mentions it again. */
  /* Wider than the prose measure: a rule's description reads like prose but the line under it
     carries a name, a date range and sometimes a file path, and 74ch put those on three lines. */
  #skills { display:block; max-width:var(--magi-sys-page); }
  .sk { border-bottom:1px solid var(--magi-ref-outlineVariant); padding:var(--magi-sys-space-200) 0; }
  .sk .top { display:flex; gap:var(--magi-sys-space-150); align-items:baseline; flex-wrap:wrap; }
  .sk .tier {
    font:600 var(--md-sys-typescale-label-small-size)/1.4 var(--magi-ref-mono); letter-spacing:0.06em; color:var(--magi-ref-muted);
    flex-basis:100%; order:-1;
  }
  .sk.global .tier { color:var(--magi-ref-warn); }
  /* A team's reach sits between the other two, and it gets the third colour rather than sharing
     one: painted --magi-ref-accent it was indistinguishable from a project skill, which is the one thing the
     tier word is on the row to tell you. */
  .sk.team .tier { color:var(--magi-ref-primary); }
  .sk.project .tier { color:var(--magi-ref-accent); }
  /* Three lines in the list; the rest is behind Read, which is what Read is for.
     Unclamped it drew up to twelve — measured, a 421px row, 74% of a phone's window, twenty of
     them making the destination eleven screens deep. The guide's list rule is one to three lines,
     cut by screen size, with the detail on the page the row opens. */
  .sk .what {
    font:600 var(--md-sys-typescale-body-large-size)/1.35 var(--magi-ref-display); color:var(--magi-ref-fg); overflow-wrap:anywhere;
    display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden;
  }
  /* A fact is quoted, not instructed: it reads as something the companion believes rather than
     something it was told to do, which is the difference a person is judging on this page. */
  .sk.fact .what { font:italic 400 var(--md-sys-typescale-body-large-size)/1.4 var(--magi-ref-display); }
  /* A rule's name has no spaces in it — skill-hand_off_read_and_summarize — and at twice the text
     size it is wider than a phone: measured, 99px of sideways page scroll at 320, which took a
     navigation destination off the screen with it. */
  .sk .meta { margin-top:var(--magi-sys-space-50); font-size:var(--md-sys-typescale-label-small-size); letter-spacing:.05em; color:var(--magi-ref-muted); overflow-wrap:anywhere; }
  .sk .drop { margin-left:auto; }
  .sk .fold { margin-left:auto; }
  .sk .fold + .drop { margin-left:0; }
  /* The rule as written. A reading measure, because it is prose and the row is not. */
  .sk .body {
    margin:var(--magi-sys-space-100) 0 var(--magi-sys-space-50); padding:var(--magi-sys-space-100) 0 0; max-width:var(--magi-sys-measure);
    border-top:1px solid var(--magi-ref-outlineVariant);
    font:var(--md-sys-typescale-body-medium-size)/1.6 var(--magi-ref-mono); color:var(--magi-ref-fg); white-space:pre-wrap; overflow-wrap:anywhere;
  }

  /* ── what they can reach ────────────────────────────────────────────────── */
  /* An MCP server is where a companion's reach leaves this machine's file system. The list is
     read to answer one question — which of them can see that thing — so the transport line is
     monospace and complete rather than tidied. */
  /* Not prose at all: the transport line is a command with arguments and the line under it is an
     absolute path. Clipping either to a reading measure hides the part being read for. */
  #mcp { display:block; max-width:var(--magi-sys-page); }
  .srv { border-bottom:1px solid var(--magi-ref-outlineVariant); padding:var(--magi-sys-space-200) 0; }
  .srv .top { display:flex; gap:var(--magi-sys-space-150); align-items:baseline; flex-wrap:wrap; }
  .srv .tier {
    font:600 var(--md-sys-typescale-label-small-size)/1.4 var(--magi-ref-mono); letter-spacing:0.06em; color:var(--magi-ref-muted);
    flex-basis:100%; order:-1;
  }
  .srv.global .tier { color:var(--magi-ref-warn); }
  .srv.project .tier { color:var(--magi-ref-accent); }
  .srv .what { font:600 var(--magi-sys-body-l) var(--magi-ref-mono); color:var(--magi-ref-fg); overflow-wrap:anywhere; }
  .srv .how { margin-top:var(--magi-sys-space-50); font:var(--md-sys-typescale-label-medium-size)/1.5 var(--magi-ref-mono); color:var(--magi-ref-muted); overflow-wrap:anywhere; }
  .srv .where { margin-top:var(--magi-sys-space-50); font-size:var(--md-sys-typescale-label-small-size); color:var(--magi-ref-muted); opacity:.85; overflow-wrap:anywhere; }
  /* The pair sits at the trailing end together. Its own class, not the remove button's: a test
     finds the destructive control by that class, and two buttons wearing it made the second one
     the one being pressed. */
  .srv .srvedit { margin-left:auto; }
  .srv .srvedit + .drop { margin-left:0; }
  .srv .drop { margin-left:auto; }
  /* Nothing here draws a box or a border: the field and the select bring their own outline, their
     own shape and their own 48dp target, and a second set drawn over them was two descriptions of
     one control that could only ever agree by accident. The form says how the controls are
     arranged and stops. */
  #mcpForm { display:grid; gap:var(--magi-sys-space-200); min-width:min(22rem, 80vw); }
  #mcpForm .note { font-size:var(--md-sys-typescale-label-small-size); color:var(--magi-ref-muted); }
  /* Wraps rather than pushing the page sideways. The action beside a section's name is a component
     whose label does not break, and at twice the text size "Companions" is 223px — measured, the
     meeting's head resolved to 373px in a 326px column and the document grew with it. */
  /* A heading, in the reading face, above the rows it heads — it was inheriting the body's 14px
     mono and coming out smaller and lighter than the list items under it. Chosen by role, which is
     what the type scale is for: this is title-medium's job. */
  .sectionhead {
    display:flex; align-items:center; gap:var(--magi-sys-space-200); flex-wrap:wrap; min-width:0;
    font:600 var(--md-sys-typescale-title-medium-size)/1.4 var(--magi-ref-display);
    color:var(--magi-ref-fg);
  }
  /* No margin-left:auto on the head's action. The MCP screen's "add a server" had one, which
     pushed it to the far edge of a page-wide heading — on a wide window the section name and its
     one action sat a screen apart, and the access screen next door draws the same shape (a head
     and an add button) at the head's own 16px gap. One convention, the closer one. */

  /* The recipe. The layer is a pseudo-element so the label's own contrast is never touched, and it
     is inert to the pointer so it can cover the whole control without eating its clicks. */
  .state { position:relative; }
  .state::after {
    content:''; position:absolute; inset:0; border-radius:inherit; pointer-events:none;
    background:currentColor; opacity:0; transition:opacity var(--magi-sys-dur-short2) var(--magi-sys-ease-standard);
  }
  .state:hover::after { opacity:.08; }
  .state:focus-visible::after, .state:active::after { opacity:.12; }
  /* A ring, not only a wash. The state layer is the focus STATE; the guide asks separately for a
     "ring-like keyboard focus indicator" so a keyboard user can see where they are — the library's
     own components draw one with md-focus-ring, and the elements built here had nothing. */
  .state:focus-visible { outline:3px solid var(--md-sys-color-secondary, var(--magi-ref-accent)); outline-offset:2px; }
  /* Material's minimum touch target is 48dp, with 8dp between targets. */
  .state { min-height:48px; }

  /* Something changed here, and a person who was looking elsewhere gets to find out.
     A wash of the surface behind the row, once, over 900ms — long enough to catch the eye that was
     not on it and gentle enough not to read as an alarm. Colour and opacity only: the row does not
     move, because a table whose rows jump while being read is the anxious version of this.
     The reduced-motion block leaves this alone on purpose — it is a fade, which is what that block
     keeps. */
  @keyframes noticed {
    from { background-color:color-mix(in srgb, var(--magi-ref-primary) 18%, transparent); }
    to   { background-color:transparent; }
  }
  /* No fill mode. With a fill mode the last frame is held, and the last frame is a transparent
     background — which is not nothing: it would sit on top of whatever background the row has of
     its own for as long as the class is there. The wash should end and hand the row back. */
  .noticed { animation:noticed 900ms var(--magi-sys-ease-standard); }
  /* The state word itself, which is the thing that actually changed. */
  @keyframes noticedWord { from { opacity:0; } to { opacity:1; } }
  .noticed .badge { animation:noticedWord 300ms var(--magi-sys-ease-standard); }

  /* ── the fleet, as a resource table ─────────────────────────────────────── */
  /* The shape a Kubernetes console reaches for, and for the same reason: one row per thing, fixed
     columns, a status word you scan down rather than read across. "kubectl get pods" is the
     archetype of seeing twenty of something at once, and twenty agents is what this is for.
     The editorial part survives in the type — hairlines instead of borders, a serif name, tabular
     figures — because a stock listing is a table too. */

  /* The summary: how many of each, and a filter. A dashboard's first question is "does anything
     need me", and counting cards to answer it is the thing this row removes. */
  /* Filter chips, because that is what these are: four selectable filters over one list, and a
     chip already knows what selected looks like, how to be reached with arrow keys, and how to
     draw a state layer. Written as buttons here before, they knew none of it. The chip renders a
     slot when it has no label attribute, so the count and the word stay ours. */
  /* Clear of whatever is above it. The chips sat straight under the tab row with nothing between,
     so the row of filters read as part of the tabs — two different kinds of control touching. */
  #summary { display:flex; flex-wrap:wrap; gap:var(--magi-sys-space-100); padding-bottom:var(--magi-sys-space-200);
             margin:var(--magi-sys-space-300) 0 var(--magi-sys-space-50); border-bottom:1px solid var(--magi-ref-outlineVariant); }
  /* Height left to the component. It was 40px, which is neither the 32dp the token asks for nor
     the 48dp target the same page asks for — the target is the bundle's job and it draws a 48px
     .touch regardless of how tall the container is. */
  .tile { --md-filter-chip-label-text-font:var(--magi-ref-mono); }
  /* Label large, which is the chip's own type role — it was title-medium, a heading size inside a
     chip. At 24px of line box in a 32dp container it left 4px above and below and read as cramped;
     the count is still the loud thing here because it is 600 against an 11px word. */
  /* On one line with the word beside it. cell() builds a div, and a div is a BLOCK: the count took
     a line of its own and pushed the word onto a second one, so 20px + 15px of content sat in a
     32dp chip and spilled out of both ends of it. Measured in a browser — the count's box started
     1.7px above the chip and the word's ended 1.7px below. Label large, which is the chip's own
     type role; it was title-medium, a heading size, which made the same overflow worse. */
  .tile .n {
    display:inline-flex; align-items:center;
    font:600 var(--md-sys-typescale-label-large-size, .875rem)/var(--md-sys-typescale-label-large-line-height, 1.25rem) var(--magi-ref-display);
    color:var(--magi-ref-fg); margin-right:var(--magi-sys-space-100);
  }
  .tile .k {
    font:600 var(--md-sys-typescale-label-small-size)/1.4 var(--magi-ref-mono); letter-spacing:0.06em; color:var(--magi-ref-muted);
    display:inline-flex; align-items:center; gap:var(--magi-sys-space-50);
  }
  /* A status dot AND the word — the colour is never the only thing carrying the state. */
  .tile .k::before { content:""; width:7px; height:7px; border-radius:var(--magi-sys-shape-full); background:currentColor; }
  .tile.waiting .k { color:var(--magi-ref-warn); }
  .tile.working .k { color:var(--magi-ref-success); }
  .tile.idle    .k { color:var(--magi-ref-accent); }
  .tile.gone    .k { color:var(--magi-ref-error); }
  .tile.remote  .k { color:var(--magi-ref-muted); }
  /* The mark takes the state's colour, the same one the word beside it wears. A chip paints its
     leading icon from a token of its own, so left alone the mark came out in the label's default
     ink while the word next to it was amber or green — two halves of one statement, in two
     colours. Set per state rather than once, because the token is what the component reads and it
     cannot inherit from a sibling. */
  .tile.waiting { --md-filter-chip-leading-icon-color:var(--magi-ref-warn);
                  --md-filter-chip-selected-leading-icon-color:var(--magi-ref-warn); }
  .tile.working { --md-filter-chip-leading-icon-color:var(--magi-ref-success);
                  --md-filter-chip-selected-leading-icon-color:var(--magi-ref-success); }
  .tile.idle    { --md-filter-chip-leading-icon-color:var(--magi-ref-accent);
                  --md-filter-chip-selected-leading-icon-color:var(--magi-ref-accent); }
  .tile.gone    { --md-filter-chip-leading-icon-color:var(--magi-ref-error);
                  --md-filter-chip-selected-leading-icon-color:var(--magi-ref-error); }
  .tile.remote  { --md-filter-chip-leading-icon-color:var(--magi-ref-muted);
                  --md-filter-chip-selected-leading-icon-color:var(--magi-ref-muted); }
  .tile[disabled] { --md-filter-chip-leading-icon-color:var(--magi-ref-muted); }
  /* A count of zero reads as zero; it does not need to be faint as well, and dimming it put the
     label under AA in both themes (2.25:1 in light — measured by the contrast check). */
  .tile[disabled] .n, .tile[disabled] .k { color:var(--magi-ref-muted); }

  #fleet { display:block; }
  /* One grid for the header and every row, so the columns line up without a table element and
     collapse to two lines on a phone. */
  .thead, .card {
    display:grid; align-items:baseline;
    /* Sized to what actually arrives. The doing column is the widest because the server clips a
       task at 160 characters and that is what has to fit — at ~0.5ch per character in this face,
       160 wants about 40rem before it wraps to a third line, and it gets that whenever there is
       room. The fixed columns are sized to their content and not to a guess: a state word, a step
       count, an age, a host and an address.

       These are MINIMA and their sum is what has to fit. The previous set added up to 1086px with
       the gaps, which is wider than this page's own measure — so every width tested scrolled
       sideways, including a narrow desktop. Sizing each column to what it holds is only half the
       job; the other half is checking the total against the space there is. */
    grid-template-columns: 7rem minmax(8rem, 1fr) minmax(12rem, 2.6fr) 3.5rem 4rem 7rem 6rem;
    gap:var(--magi-sys-space-200);
  }
  /* The head's grid must span the same width as a row's, or the columns drift apart across the
     table: the two agreed on the first column and then separated by four pixels, then thirteen,
     because a row is drawn 12px wider than the head and pulls that back with its own padding.
     Measured: the row's content box ends at 1970 and the head's ended at 1982. So the head gives
     back 12 on the RIGHT — the left edges already agree, and padding both sides would have moved
     the labels off the values by the same 12 in the other direction. */
  /* The head stays while the list scrolls under it. With twenty-five companions the document is
     2777px and by row twelve the reader is looking at bare numbers — "8 · 5h ago" — with no label
     anywhere on screen. The masthead above it is sticky for the same reason. */
  .thead { position:sticky; top:var(--magi-comp-appbar-h, 64px); z-index:1; background:var(--magi-ref-bg); }
  .thead {
    font:600 var(--md-sys-typescale-label-small-size)/1.4 var(--magi-ref-mono); letter-spacing:0.06em; color:var(--magi-ref-muted);
    /* Out to the row's own edge, then back in by the same amount. A row's box is 12px wider on the
       left than its text — that is where its hover shading comes from — so a head that started at
       the text began 12px inside the band the row lights up, and the shading stuck out to the left
       of the rule above it. Now the rule, the shading and the team line all begin on one edge, and
       the labels still stand over their columns. */
    margin-inline-start:calc(-1 * var(--magi-sys-space-150));
    padding:var(--magi-sys-space-200) var(--magi-sys-space-150) var(--magi-sys-space-100) var(--magi-sys-space-150);
    border-bottom:1px solid var(--magi-ref-outlineVariant);
  }
  /* The way OUT of the list, at the far end of the row of filters that are about it. Beside them it
     read as a fifth chip; at the other edge it reads as what it is — the counts are on the left
     because they are about this table, and this is a door somewhere else. */
  /* The group is pushed to the trailing end together: the first of them takes the space and
     the rest follow it, so which control happens to be first does not change the layout. */
  .toview.lead { margin-left:auto; }
  .thead .r, .card .r { text-align:right; }

  /* A row, and it must not read as a card. It carried a coloured left edge and a rounded corner
     while sitting flush against the next one — the two devices belong to different things: a card
     is a bounded surface with space around it, a row is a line in a table separated by a rule.
     Having both, with no gap, asked the reader to see cards that had been stacked without margins.
     The state is already said in the badge, twice over, as a word and a coloured dot. */
  .card {
    text-decoration:none; color:var(--magi-ref-on-surface); border-bottom:1px solid var(--magi-ref-outlineVariant);
    padding:var(--magi-sys-space-150) var(--magi-sys-space-150) var(--magi-sys-space-150); margin-left:calc(-1 * var(--magi-sys-space-150)); position:relative;
  }
  /* The row's hover is its state layer and nothing else. A 5% primary wash under the layer is a
     second overlay saying the same thing in a colour that is not the surface's `on-` role, which
     is the one thing the guide says a state layer is not. */
  /* Stopped is a TONE, not a dimmer.
     `opacity:.8` on the whole card multiplied every role inside it, and in the light theme that
     put the role at 3.52:1 and the "stopped" badge at 4.39:1 — a card saying it is finished by
     making itself unreadable. A step down the tonal ladder says the same thing to the eye, leaves
     every word at the contrast it was designed for, and keeps the badge as the second indicator. */
  .card.stopped { background:var(--magi-ref-surface-container-lowest); }


  /* A team's heading. Set as a rule with a name on it rather than as a bar: this page separates
     with lines, and a filled band per team would turn a table into a stack of boxes. */
  .teamhead {
    display:flex; align-items:baseline; gap:var(--magi-sys-space-150); flex-wrap:wrap;
    /* The same edges as the head above it and the rows below it: its rule and its count line up
       with the band a row lights up when the cursor is on it. */
    margin:var(--magi-sys-space-300) 0 var(--magi-sys-space-50) calc(-1 * var(--magi-sys-space-150));
    padding:0 var(--magi-sys-space-150) var(--magi-sys-space-50) var(--magi-sys-space-150);
    border-bottom:1px solid var(--magi-ref-outlineVariant);
  }
  /* The first team sits closer to the table head above it. Written :first-of-type it matched
     nothing — the table head is a div too, and it is the first one — so all three headers carried
     the same 1.6rem and the list opened with a gap that looked like a missing row. */
  .thead + .teamhead { margin-top:var(--magi-sys-space-100); }
  .teamhead .tname {
    font:600 var(--md-sys-typescale-label-medium-size)/1.4 var(--magi-ref-mono); letter-spacing:0.06em; color:var(--magi-ref-fg);
  }
  .teamhead .thub { font:var(--md-sys-typescale-label-small-size)/1.5 var(--magi-ref-mono); color:var(--magi-ref-accent); }
  .teamhead .tn { margin-left:auto; font:var(--md-sys-typescale-label-small-size)/1.5 var(--magi-ref-mono); color:var(--magi-ref-muted); }
  /* NOT position:static. The general md-badge rule above makes the host the thing the component's
     absolutely-positioned inner box anchors to, and that is the whole reason it exists — "dropped
     into a flow it anchors to whatever ancestor happens to be positioned and lands somewhere
     unrelated". Static put it back: both team badges rendered at the same point 616px and 498px
     away from their rows, stacked on each other, as a stray number floating over the table head.

     The pill rides at `left:calc(50% + 2px)` of the host, which is right for a badge on the corner
     of an icon and wrong in a line of text: widening the host to 26px left the count sitting in the
     right-hand half of its own box with ten pixels of nothing before it, reading as a number
     shoved away from the word it counts. Measured: a 26px host with the pill drawn from +15 to +31,
     hanging out of the box the layout had reserved.

     The shadow root exposes no part, so the correction is on the host: the box is the pill's size,
     and a transform slides what is DRAWN back over it. A transform moves the painting and leaves
     the layout alone, which is exactly the half that was wrong. */
  .teamhead md-badge { position:relative; width:16px; transform:translateX(calc(-50% - 2px)); }

  /* status */
  /* The column's word, for the width where the column heads are not drawn. */
  .colk { display:none; }
  @media (max-width:74.9375em) {
    .colk {
      display:inline; margin-left:var(--magi-sys-space-50);
      font:600 var(--md-sys-typescale-label-small-size)/1.4 var(--magi-ref-mono); letter-spacing:0.0467em; color:var(--magi-ref-muted);
    }
  }
  .card .badge {
    font:600 var(--md-sys-typescale-label-small-size)/1.6 var(--magi-ref-mono); letter-spacing:0.0467em; color:var(--magi-ref-muted);
    display:flex; align-items:center; gap:var(--magi-sys-space-100); flex-wrap:wrap;
  }
  .card .badge::before { content:""; width:7px; height:7px; border-radius:var(--magi-sys-shape-full); background:currentColor; flex:none; }
  .card.working .badge { color:var(--magi-ref-success); }
  .card.waiting .badge { color:var(--magi-ref-warn); }
  .card.idle .badge { color:var(--magi-ref-accent); }
  .card.abandoned .badge, .card.stopped .badge { color:var(--magi-ref-error); }
  /* Elsewhere: muted, because there is nothing here to act on — no colour claiming a state
     nothing observed, and none claiming trouble either. */
  .card.remote .badge { color:var(--magi-ref-muted); }

  /* name + workspace, the way a console stacks a resource over its namespace */
  .card .name { font:600 var(--md-sys-typescale-body-large-size)/1.3 var(--magi-ref-display); color:var(--magi-ref-fg); overflow-wrap:anywhere; }
  /* …and a row with nowhere to go says nothing. A companion on somebody else's console is drawn as
     an anchor with no href, and it was still lighting its state layer and turning its name primary
     under the pointer — two indicators, both pointing at a press that does nothing. */
  a.card:not([href]) { cursor:default; }
  a.card:not([href]):hover::after, a.card:not([href]):focus-visible::after { opacity:0; }
  a.card:not([href]):hover .name { color:inherit; }
  .card .plan {
    font:600 var(--md-sys-typescale-label-small-size)/1.4 var(--magi-ref-mono); letter-spacing:0.05em; color:var(--magi-ref-muted); align-self:center;
  }
  /* What a neighbour asked this companion for. Muted rather than coloured: the state's colour
     belongs to the state, and this is a second fact about the same row, not a second state. */
  .card .badge .load {
    font:var(--md-sys-typescale-label-small-size)/1.4 var(--magi-ref-display);
    letter-spacing:normal; color:var(--magi-ref-muted); align-self:center;
  }
  .card .role {
    font:600 var(--md-sys-typescale-label-small-size)/1.4 var(--magi-ref-mono); letter-spacing:.04em; color:var(--magi-ref-accent);
    overflow-wrap:anywhere; margin-top:var(--magi-sys-space-50);
  }
  .card .path { font-size:var(--md-sys-typescale-label-small-size); color:var(--magi-ref-muted); opacity:.9; overflow-wrap:anywhere; }

  /* what it is doing: one line, clipped — the detail view is one click away for the rest */
  .card .last {
    font:italic var(--md-sys-typescale-body-medium-size)/1.45 var(--magi-ref-display); color:var(--magi-ref-fg);
    display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
  }
  /* The question, clipped like the line above it — the row is a summary and the whole of it is one
     press away. Unclipped, a companion asking about a 4000-character command drew 106 lines in the
     list and put its own Allow and Deny three screens below the question. */
  .card .asking {
    font:600 var(--md-sys-typescale-label-medium-size)/1.45 var(--magi-ref-mono); color:var(--magi-ref-warn); overflow-wrap:anywhere;
    display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden;
  }
  .card .note { margin-top:var(--magi-sys-space-50); font:var(--md-sys-typescale-label-small-size)/1.45 var(--magi-ref-mono); color:var(--magi-ref-muted); overflow-wrap:anywhere; }
  .card .num { font-size:var(--md-sys-typescale-label-medium-size); color:var(--magi-ref-muted); }
  .card .host { font-size:var(--md-sys-typescale-label-small-size); color:var(--magi-ref-muted); overflow-wrap:anywhere; }
  .card .host b { font-weight:400; color:var(--magi-ref-fg); opacity:.85; }
  .card .ver { display:inline; }
  /* Behind the fleet's newest build: the version itself turns the warn colour, and the hint names
     what is out. Full-opacity warn on the surface passes the 4.5:1 the audit holds text to. */
  .card .ver.behind { color:var(--magi-ref-warn); }
  .card .ver .vhint { display:inline; color:var(--magi-ref-warn); }
  .card .ver .vhint::before { content:' · '; }

  /* Row actions. Open is the row itself as well, but a named control is what makes it discoverable
     — and stopping must never require entering first, which is the whole point of a console. */
  /* One icon, inside the column. "open" was a word for something the whole row already does — the
     row is a link — and the pair of labelled buttons was wider than the 6rem column they sat in, so
     they hung off the right edge of the table. */
  .actions { display:flex; gap:var(--magi-sys-space-50); justify-content:flex-end; align-items:center; }
  .actions md-icon-button {
    --md-icon-button-icon-color:var(--magi-ref-muted);
    --md-icon-button-state-layer-width:40px; --md-icon-button-state-layer-height:40px;
    color:var(--magi-ref-muted);
  }
  .actions md-icon-button:hover { --md-icon-button-icon-color:var(--magi-ref-error); color:var(--magi-ref-error); }
  /* Except the one that stops a turn: it wears the error role at rest, because a destructive
     control that is grey until the cursor reaches it is a control whose meaning is hidden until
     after you have found it. */
  .actions md-icon-button.stop { --md-icon-button-icon-color:var(--magi-ref-error); color:var(--magi-ref-error); }

  /* The grounds a decision is put on: a key-and-value block, set like the rest of this page's
     labelled readings. Two columns on anything but a phone, because the keys are short words and
     giving each its own line would push the reasoning off the screen it exists to be read on. */
  /* No surface of its own. M3 expresses height with TONE, so a tinted box inside a row is a second
     layer, and a second layer inside a region that is already one asks the reader to work out a
     hierarchy that means nothing — the grounds are not deeper than the row, they are part of it.
     Separated by the device this page already uses everywhere else: a rule and a gutter of
     small-caps labels. */
  /* Sections side by side, each keeping a readable line.
     One column of pairs capped at the reading measure put the whole report in the left 45% of a
     1400px row with the rest of the card empty beside it — the block looked pushed against the
     left edge, which it was. Widening the column instead would have bought 130-character lines,
     which is the other way to make a report unreadable. auto-fit does neither: three sections
     become three columns on a wide row, two on a medium one, one on a phone, and the block ends
     where the card does. */
  .grounds {
    grid-column:1 / -1;
    display:grid; grid-template-columns:repeat(auto-fit, minmax(min(100%, 22rem), 1fr));
    gap:var(--magi-sys-space-200) var(--magi-sys-space-400);
    margin:var(--magi-sys-space-150) 0 var(--magi-sys-space-50); padding:var(--magi-sys-space-150) 0 0;
    border-top:1px solid var(--magi-ref-outlineVariant);
  }
  .grounds .gsec { display:grid; gap:var(--magi-sys-space-50); align-content:start; min-width:0; }
  .grounds .gk {
    font:600 var(--md-sys-typescale-label-small-size)/1.6 var(--magi-ref-mono); letter-spacing:0.0533em;
    color:var(--magi-ref-muted);
  }
  .grounds .gv {
    font:var(--md-sys-typescale-label-medium-size)/1.55 var(--magi-ref-mono); color:var(--magi-ref-fg);
    overflow-wrap:anywhere; max-width:var(--magi-sys-measure);
  }
  /* A report is written to be read on the decision screen; in a list it is a preview, and a long
     one must not push the companions under it off the page. Ten lines a section, and the row
     itself is the way to all of it — the whole card opens the companion, where the decision has a
     screen of its own. The clamp puts its own ellipsis on the last line it kept and only when it
     kept fewer than there were, which is the marker: no fade over text that was never cut. */
  .card .grounds .gv {
    display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:10; line-clamp:10;
    overflow:hidden;
  }

  /* answering, inline in the row that is asking */
  /* Answering is the one place on the fleet where a person types, so it is the library's field and
     the library's buttons: focus ring, state layers and a 48dp target all come with them. What is
     said here is that the choice is the warning colour, because the agent is stopped until it. */
  /* 16dp apart, because the targets are wider than the buttons: each 40dp button carries a 48dp
     press target that bleeds 4dp past each edge, so at the 8dp visual gap the guide's minimum the
     TARGETS were edge to edge — Allow and Deny sharing a boundary pixel. 16 visual = 8 between
     the things a finger actually hits. */
  .answer { display:flex; gap:var(--magi-sys-space-200); margin-top:var(--magi-sys-space-100); flex-wrap:wrap; align-items:center; }
  /* Outlined, because a tonal container in this palette cannot be told from the page: measured
     1.24:1 dark and 1.18:1 light where the guide wants 3:1 from the container of a filled or tonal
     button standing in a group. The outline role clears it. Same words, same weight, same order. */
  .answer md-outlined-button {
    --md-outlined-button-label-text-color:var(--magi-ref-fg);
    /* The layer goes with the label. The component's default is primary, so a page that moved the
       label to on-surface and left the layer alone washed these four in orange on press — a state
       layer is the surface's own `on-` colour at a fixed opacity, not a second hue. */
    --md-outlined-button-hover-state-layer-color:var(--magi-ref-fg);
    --md-outlined-button-pressed-state-layer-color:var(--magi-ref-fg);
    --md-outlined-button-hover-label-text-color:var(--magi-ref-fg);
    --md-outlined-button-pressed-label-text-color:var(--magi-ref-fg);
    --md-outlined-button-label-text-font:var(--magi-ref-mono);
    --md-outlined-button-outline-color:var(--magi-ref-outline);
    letter-spacing:0.05em;
  }
  .answer md-outlined-text-field { flex:1; min-width:11rem; }
  /* The options a question came with. Not the permission buttons beside them, which stay where the
     eye already looks for them, and not the free-text field, which fills its line. */
  .answer.choices { justify-content:center; }

  /* A tool call's arguments, read as the small named object they are. The name goes in a gutter to
     the left — the same shape the report's sections and the transcript's speakers use — and a value
     with newlines in it takes the full width underneath, because that one is a file body or a
     command block and a 12rem column would ruin it. */
  .args { display:grid; grid-template-columns:auto minmax(0, 1fr); gap:var(--magi-sys-space-50) var(--magi-sys-space-200);
    align-items:baseline; }
  .argk { font:var(--md-sys-typescale-label-medium-size)/1.6 var(--magi-ref-mono); color:var(--magi-ref-muted);
    text-align:right; white-space:nowrap; }
  .argv { margin:0; white-space:pre-wrap; overflow-wrap:anywhere; min-width:0; }
  .argv.tall { grid-column:1 / -1; }

  /* ── the workspace, beside the conversation ────────────────────────────────
     A tree of names and one file at a time. Nothing here is an editor: the console reads through
     the companion's own tools and has no route that writes, so there is no caret, no dirty state
     and no save. */
  /* A card each, like the pane on the other side, and a scrollbar each. One scroller for both put
     git's section below three hundred files, and scrolling to reach the branch took the tree away
     from wherever the reader had got to — two things looked at for two reasons should not move
     each other. The tree takes what is left after git has what it needs. */
  /* The edge of a pane, as a control.
     A window splitter in the ARIA sense — focusable, arrow keys move it — writing the custom
     property the grid track is made of. A hairline until the pointer is on it: most of the time the
     width is already right, and a permanent bar between every column is furniture. */
  .grip {
    position:absolute; top:0; bottom:0; right:-3px; width:6px; z-index:3;
    cursor:col-resize; touch-action:none;
  }
  .grip.lead { right:auto; left:-3px; }
  .grip::after {
    content:''; position:absolute; inset:0 2px; border-radius:var(--magi-sys-shape-full);
    /* outline, not outline-variant: this bar is the only hover affordance a drag target has, and
       the variant role measures 1.46:1 against the light page — the colour-roles page names
       defining a target's boundary in outline-variant as a Don't. */
    background:var(--magi-ref-outline); opacity:0;
    transition:opacity var(--magi-sys-dur-short2) var(--magi-sys-ease-standard);
  }
  .grip:hover::after, .grip:focus-visible::after, .grip.gripping::after { opacity:1; }
  .grip:active::after { background:var(--magi-ref-fg); opacity:1; }
  .grip:focus-visible { outline:3px solid var(--md-sys-color-secondary, var(--magi-ref-accent)); outline-offset:1px; }
  /* No handle where there is no column to resize: below the two-pane breakpoint the panes are
     above and below the conversation, and a vertical splitter would move nothing. */
  @media (max-width:52.4375em) { .grip { display:none; } }
  body[files="shut"] #filesGrip, body[side="shut"] #sideGrip { display:none; }

  /* The card's head and the controls that belong to the card rather than to its contents. The head
     is the fold; anything beside it is its own target, so a press never does one of two things
     depending on where in the row it lands. */
  .panerow { display:flex; align-items:center; gap:var(--magi-sys-space-100); }
  .panerow .panehead { flex:1 1 auto; min-width:0; }
  .panerow .paneagain { flex:none; --md-icon-button-icon-size:18px; }
  .filescard {
    display:flex; flex-direction:column; min-height:0;
    border:1px solid var(--magi-ref-outlineVariant); border-radius:var(--magi-sys-shape-m);
    background:var(--magi-ref-surface-container-low);
    overflow:hidden;
  }
  /* The heading is the control. A button, so a keyboard reaches it and its state is announced —
     which is the part that gets left out when a disclosure is built from a div. */
  .panehead {
    display:flex; align-items:center; gap:var(--magi-sys-space-100);
    width:100%; border:0; background:none; color:var(--magi-ref-fg); text-align:left; cursor:pointer;
    padding:var(--magi-sys-space-100) var(--magi-sys-space-150);
    font:600 var(--md-sys-typescale-label-medium-size)/1.5 var(--magi-ref-display);
    border-radius:var(--magi-sys-shape-m) var(--magi-sys-shape-m) 0 0;
  }
  .panehead .panecaret { flex:none; color:var(--magi-ref-muted);
    transition:transform var(--magi-sys-dur-short2) var(--magi-sys-ease-standard); }
  .filescard.shut .panehead .panecaret { transform:rotate(-90deg); }
  .panetitle { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0; }
  /* What is under it, and the thing that scrolls. Shut, it is gone rather than merely hidden: a
     collapsed section that still took its own scroll height would leave the card the same size. */
  .panebody {
    display:flex; flex-direction:column; gap:var(--magi-sys-space-100); min-height:0;
    overflow:hidden auto; overscroll-behavior:contain;
    /* Room at the top for a label that floats above its box.
       This scrolls, so it also clips — and with no top padding it clipped the first 8px of
       whatever sat at the very top. Measured on the git screen: the branch select's floating
       "Branch name" was cut in half, at every width, which is the one thing the guide says a text
       field's label may never be. 8dp is what the label overhangs by. */
    padding:var(--magi-sys-space-100) var(--magi-sys-space-150) var(--magi-sys-space-150);
  }
  .filescard.shut .panebody { display:none; }
  .filescard.shut { flex:0 0 auto; }
  /* The two cards share the column by what is IN them.
     A fixed split is wrong in both directions: on a tall screen the tree took everything and git
     sat in a strip at the bottom with four rows and empty space under them, and on a repository
     mid-rebase the reverse. flex-basis:auto with both cards allowed to grow means each asks for
     its content's height and the leftover is split between them — and neither can be squeezed to
     nothing, because a card holding only its heading is a card nobody can use.
     max-content on the basis so a SHORT card (a clean tree, a clean status) gives its slack to the
     other one rather than holding a share it does not need. */
  #files > .filescard { flex:1 1 auto; flex-basis:max-content; min-height:5rem; }
  /* Git is as tall as what is in it, up to half the column, then it scrolls inside itself.
     The old premise — "a branch and a short, countable list" — is a fixture's premise: a real
     checkout mid-feature had 29 changed files, the git card grew to 1307px in a 1229px column
     with flex:0 0 auto, the tree was pinned at its 80px floor showing nothing, and the Commit
     button at the bottom of git was 182px outside a column with overflow:hidden and no scrollbar.
     Both cards are shrinkable now and both have a floor, so a long status yields to the tree and
     scrolls its own overflow (.panebody already does), and neither is ever squeezed to just its
     heading. */
  #files > .pane-git { flex:1 1 auto; flex-basis:max-content; min-height:5rem; max-height:60%; }
  /* Shut, it is its heading and nothing else, and it must not take a share at all. */
  #files > .filescard.shut { flex:0 0 auto; min-height:0; }
  .filesnote {
    font:var(--md-sys-typescale-body-medium-size)/1.6 var(--magi-ref-display); color:var(--magi-ref-muted);
  }
  /* What git makes of the workspace, over the tree it is the state of. */
  .gitinner { display:flex; flex-direction:column; gap:var(--magi-sys-space-50); min-width:0; }
  .gittop { display:flex; align-items:center; gap:var(--magi-sys-space-100); flex-wrap:wrap; }
  .gitbranch {
    font:600 var(--md-sys-typescale-label-large-size)/1.5 var(--magi-ref-mono);
    color:var(--magi-ref-fg); overflow-wrap:anywhere; min-width:0;
  }
  /* How far from the upstream, in the direction it is: one number each way rather than a sentence,
     because this is glanced at and not read. */
  .gitab { font:600 var(--md-sys-typescale-label-small-size)/1.5 var(--magi-ref-mono); }
  .gitab.ahead { color:var(--magi-ref-primary); }
  .gitab.behind { color:var(--magi-ref-warn); }
  .gitclean, .gitmark { color:var(--magi-ref-muted); }
  .gitclean { font:var(--md-sys-typescale-label-medium-size)/1.5 var(--magi-ref-display); }
  /* A changed file is a row like any other in this pane, with what kind of change in front of it.
     The word, not git's two letters: XY is git's alphabet, and a console printing "RM" is asking
     its reader to know it. */
  /* A changed file and what can be done to it, on one line: the row takes the width and the
     buttons sit at the trailing end, which is where the lists guidance puts a secondary action. */
  /* The branch menu, the four branch-level buttons, and the two group headings — the shape every
     editor's git panel has, in a column 18rem wide. */
  .gitpick { flex:1 1 6rem; min-width:0; max-width:100%; --md-outlined-select-text-field-input-text-font:var(--magi-ref-mono); }
  /* Ranged right, under the branch they act on. Left, they sat in the middle of the column with
     the empty half of the row beside them — measured 352..524 in a card whose contents run to 614
     — and read as the start of a list rather than as the card's own controls. */
  /* 16dp between 40dp buttons, because each carries a 48dp press area: at 8 they abut and at the
     4 this had they OVERLAP, and two of these five stash work and open a pull request. */
  .gitbranchacts { display:flex; flex-wrap:wrap; justify-content:flex-end; gap:var(--magi-sys-space-200); }
  .gitgroup {
    margin-top:var(--magi-sys-space-100);
    font:600 var(--md-sys-typescale-label-small-size)/1.5 var(--magi-ref-display);
    letter-spacing:.04em; color:var(--magi-ref-muted);
  }
  /* Nothing in here is allowed to be wider than the column. A path, a branch name and a commit
     message are all unbreakable strings, and a flex or grid child's default minimum is its
     CONTENT — so one 60-character path made the whole card wider than the pane and the pane grew a
     sideways scrollbar. min-width:0 on every box that holds one, and the text itself clips. */
  .gitinner, .gitline, .gitacts, .gittop, .gitbranchacts, .gitcommit { min-width:0; max-width:100%; }
  .gitline { display:flex; align-items:center; gap:var(--magi-sys-space-50); min-width:0; }
  .gitline .treerow { flex:1 1 auto; min-width:0; }
  .gitrow .treename { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0; }
  /* The whole path in a tooltip's place: the row clips, and the title is what a reader falls back
     on when the middle of a path is the part that was cut. */
  .gitinner .gitbranch { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .gitcommit md-outlined-text-field { width:100%; min-width:0; }
  /* One control per row rather than four. The column is 18rem and the path is what a reader is
     there for; three or four icons beside every file spent most of the width on actions wanted
     once each. Shown when the row is hovered or its menu is open — always for a keyboard, which
     cannot hover. */
  .gitacts { display:flex; flex:none; visibility:hidden; }
  .gitline:hover .gitacts, .gitacts:focus-within, .gitline:focus-within .gitacts,
  .gitacts.showing { visibility:visible; }
  .gitacts md-icon-button { --md-icon-button-icon-size:16px; width:28px; height:28px; }
  /* The commit workbench, in the slot above the transcript: what is going in on the left, what
     changed in it on the right, the message under both. Two columns because that is the reading
     order — pick a file, read the diff, write the line — and a dialog cannot hold it: the guide
     caps a basic dialog at 560dp and a unified diff is wider than that on its own. */
  /* Rows that give way, not boxes with heights of their own. The list and the diff shared the top
     row and each carried a cap in vh, so together they could be taller than the card and the whole
     workbench scrolled — the message box included, which is the one thing that must not move. Now
     the top row takes what is left and the two boxes in it scroll; the message row is auto. */
  .commitbox {
    /* The file list takes its content's width, not a fixed 16rem — capped at 16rem so a deep path
       does not run away, floored at 8rem so a one-line list still reads. It resolved to its 16rem
       MAX in every case before, spending 256px on two or three short names while the diff, the
       thing the workbench is for, was squeezed to 67 characters at 1280 with both panes open. */
    display:grid; grid-template-columns:minmax(8rem, min(16rem, max-content)) minmax(0, 1fr);
    grid-template-rows:minmax(3rem, 1fr) auto;
    gap:var(--magi-sys-space-200); align-items:stretch; min-height:0; flex:1 1 auto;
    /* A floor low enough that the message always fits above the fold, and a scroller of last
       resort: forced into a 300px card the whole thing used to overflow and the message box was
       the part that fell off the bottom — the one thing here that cannot be cut. */
    overflow:auto;
  }
  .commitfoot { min-height:0; }
  .commitfiles { display:grid; align-content:start; min-height:0; overflow:auto; }
  .commitbox .commitdiff {
    margin:0; min-height:0; overflow:auto; min-width:0;
    font:var(--md-sys-typescale-body-small-size)/1.6 var(--magi-ref-mono); tab-size:4;
  }
  /* The message spans both columns: it is about all of it, not about the file on the right. */
  .commitfoot { grid-column:1 / -1; display:grid; gap:var(--magi-sys-space-150); }
  .commitfoot .commitmsg { width:100%; }
  .commitacts { display:flex; gap:var(--magi-sys-space-150); justify-content:flex-end; }
  /* The inline draft-rules editor: the field full width, its Save under it at the trailing end. */
  .commitruleswrap { display:grid; gap:var(--magi-sys-space-100); }
  .commitruleswrap .commitrules { width:100%; }
  .commitruleswrap .commitrulesrow { display:grid; gap:var(--magi-sys-space-100); justify-items:end; }
  @media (max-width:52.4375em) {
    /* One column on a phone, the list first: at 390px a 10rem column beside a diff is neither. */
    .commitbox { grid-template-columns:minmax(0, 1fr); }
  }

  /* Full width, which is what it was when there was a box above it. Ranged right it read as one
     more small control in a column of them; across the card it is the thing this card is FOR —
     the reader's call, and the same one they made about the convene button. */
  .gitcommit { display:grid; margin-top:var(--magi-sys-space-150); }
  .gitrow .gitkind {
    flex:none; min-width:4.5rem;
    font:var(--md-sys-typescale-label-small-size)/1.4 var(--magi-ref-display); color:var(--magi-ref-muted);
  }
  .gitrow.staged .gitkind { color:var(--magi-ref-primary); }
  .gitrow.both .gitkind, .gitrow.conflict .gitkind { color:var(--magi-ref-warn); }

  /* The box and the two chips over the tree. The chips are one set with one selection — which of
     the two searches this is — and they are md-filter-chip because that is what they do: narrow a
     collection, with the selected one carrying the check. */
  /* The room a slow answer will land in. A height, so the pane does not jump when the tree
     arrives — measured on a live console the two cards settle around here — and the bar at the top
     of it where the first row will be. */
  .paneloading { display:grid; gap:var(--magi-sys-space-150); padding:var(--magi-sys-space-200) 0; min-height:6rem; align-content:start; }

  /* One line: what is being searched for, and the two ways out of it. The box itself is a dialog
     now — in an 18rem pane it was three rows of control above the forty filenames somebody came to
     read. */
  .filefind { display:flex; align-items:center; flex-wrap:wrap; gap:var(--magi-sys-space-100); }
  .filefind .findnow {
    flex:1 1 8rem; min-width:0; color:var(--magi-ref-muted);
    font:var(--md-sys-typescale-label-medium-size)/1.5 var(--magi-ref-display);
    overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  }
  /* Which of the two searches this is, ranged right for the same reason the git buttons are: they
     belong to the box above them rather than to the list below, and the list starts at the left. */
  .filefind md-chip-set { justify-content:flex-end; }
  /* A hit is a row like any other in this pane, plus the line it was found on. The line is set in
     the same mono as the file it came from and clipped: this is a pointer into a file, not a
     reading of it — pressing it opens the file, which is where reading happens. */
  .treerow.hit { align-items:flex-start; padding-top:var(--magi-sys-space-50); padding-bottom:var(--magi-sys-space-50); }
  .hitwhat { display:grid; gap:2px; min-width:0; }
  .hitline {
    font:var(--md-sys-typescale-label-small-size)/1.5 var(--magi-ref-mono); color:var(--magi-ref-muted);
    overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  }
  /* A row is a button, because it does something when pressed — and buttons here carry .state,
     which brings the layer and the 48dp floor every other hand-built control on this page has. */
  /* A tree row: dense, and the shape of a tree.
     Smaller than the page's body text and tighter than its spacing, because this is a list of forty
     names scanned for one of them — the type scale's label size is what that is for. The indent and
     the guide line beside it both come from --d, the row's depth, so they cannot drift apart. */
  .treerow {
    position:relative;
    display:flex; align-items:center; gap:var(--magi-sys-space-50);
    width:100%; border:0; background:none; color:var(--magi-ref-fg); text-align:left;
    font:var(--md-sys-typescale-label-medium-size)/1.5 var(--magi-ref-mono);
    border-radius:var(--magi-sys-shape-xs); cursor:pointer;
    min-height:1.65rem;
    /* The indent is a MEASURE, not spacing off the scale: it is one step of a tree's depth, the
       same number the guide line beside it is drawn at, and rounding it to the spacing scale would
       make the two disagree by a pixel at every level. */
    padding:1px var(--magi-sys-space-50) 1px calc(var(--d, 0) * var(--magi-comp-tree-step) + var(--magi-sys-space-50));
  }
  /* A row and the control that opens its menu. The control appears when the row is hovered or the
     menu is open, because a permanent one puts "delete" beside every filename in an 18rem column —
     and it is always there for a keyboard, which cannot hover. */
  .treeline { display:flex; align-items:center; gap:0; min-width:0; position:relative; }
  .treeline .treerow { flex:1 1 auto; min-width:0; }
  /* Present when the row is, drawn when it is wanted. visibility rather than a fade: a control at
     0.08 opacity is a control somebody can see and cannot read, and this page's own contrast guard
     refuses dimmed roles for exactly that reason. Hidden this way it also leaves the layout alone,
     so no row changes width when the pointer arrives. */
  .rowmenu { flex:none; visibility:hidden; }
  /* …and while its menu is open, wherever the pointer has gone. The menu is in the top layer now
     but it is still this element's child, so hiding the container hides the menu: moving the mouse
     off the row to reach the menu made the menu disappear on the way. */
  .treeline:hover .rowmenu, .rowmenu:focus-within, .treeline:focus-within .rowmenu,
  .rowmenu.showing { visibility:visible; }
  /* A menu item is 48dp in the guide and the bundle draws it at 56 — the list item's height,
     which is what it is built from. Measured on the live page before and after: 56 → 48. */
  md-menu-item {
    --md-menu-item-one-line-container-height:48px;
    --md-list-item-one-line-container-height:48px;
  }
  .rowmenu md-icon-button { --md-icon-button-icon-size:16px; width:28px; height:28px; }

  /* The line that makes it read as a tree rather than as text with spaces in front of it. One
     guide, at the parent's level: it is what says "these belong to the folder above" without
     drawing a diagram nobody asked for. */
  .treerow[style*="--d"]::before {
    content:''; position:absolute; top:0; bottom:0;
    left:calc(var(--d, 0) * var(--magi-comp-tree-step) - 2px);
    border-left:1px solid var(--magi-ref-outlineVariant);
  }
  .treerow[style*="--d: 0"]::before, .treerow[style*="--d:0"]::before { border-left:0; }
  /* Where a finger is the input, the row is a target rather than a line of text.
     ⚠ Chosen, not conformant, and the numbers said plainly: the guide asks for 48×48dp under a
     finger and 44×44 under a pointer, with 8dp between targets. This row is 26.4dp under a mouse
     and 40 under a finger, because the pane is 18rem wide and holds a list of forty names scanned
     for one of them — at 44 it holds eight. The reader was shown both and picked density
     (2026-08-14). The comment here used to claim the guide allowed 40 for a mouse; it does not. */
  @media (hover:none) {
    .treerow { min-height:40px; }
    /* …and the controls a pointer reveals by hovering are simply there.
       A finger cannot hover, and the other way in — right-click — is not a gesture either. So on a
       phone the row menu and the per-file git actions existed on the page and could not be reached
       at all: measured on an iPhone 13, twelve row menus and twenty-nine git actions, none of them
       visible. Everything they offer — rename, copy path, discard, stage, unstage, the diff — was
       desktop-only by accident.
       Sized to the row rather than to the 48 the guide asks of a touch target: the row itself is
       the recorded 40dp compromise (deviations.md), and a 48 control inside a 40 row would push
       the list back to eight names a screen, which is the trade that was already made and settled. */
    .rowmenu, .gitacts { visibility:visible; }
    .rowmenu md-icon-button, .gitacts md-icon-button { width:40px; height:40px; }
  }
  .treerow .treename { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0; }
  .treerow.dir .treename { color:var(--magi-ref-fg); font-weight:600; }
  .treerow:not(.dir) .treename { color:var(--magi-ref-muted); }
  /* The one whose tab is showing. Marked on the row as well as on the tab: the two are the same
     fact seen from the tree and from the slot, and a reader scrolling the tree should be able to
     find where they are without looking up. */
  .treerow.now .treename { color:var(--magi-ref-primary); font-weight:600; }
  .treerow .treemark { flex:none; color:var(--magi-ref-muted); }
  /* The chevron turns rather than being exchanged for a second glyph: one shape, one meaning, and
     the movement is what says which way it went. */
  .treerow .treemark { transition:transform var(--magi-sys-dur-short2) var(--magi-sys-ease-standard); }
  .treerow .treemark.open { transform:rotate(90deg); }

  /* The file in the slot the facts card shares. */
  /* The bar over the file: where it is, and the way into editing it. */
  /* The bar stays at the top of the card while the file scrolls under it: it holds the path
     somebody copies and the buttons that end an edit, and both are wanted at every scroll
     position. The card is what scrolls, so this sticks to the card. */
  .filebar {
    margin-bottom:var(--magi-sys-space-150); display:flex; align-items:center;
    gap:var(--magi-sys-space-150); flex-wrap:wrap;
    position:sticky; top:0; z-index:2; background:var(--magi-ref-surface-container-low);
    padding-bottom:var(--magi-sys-space-100);
  }
  .filebar .filedir { flex:1 1 auto; min-width:0; }
  /* The editor. Monospace at the code's own size, so a line wraps where it wraps in the file, and
     tall enough to work in without being the whole page. */
  .fileedit { display:grid; gap:var(--magi-sys-space-150); }
  /* The component draws the box, the outline, the focus ring and the label; what is said from here
     is the face and the size. Mono because it is code, and 16px because anything smaller makes iOS
     Safari zoom the page in on focus and never zoom back — the same floor every other field on this
     page keeps. */
  /* Bounded by the room there IS. The card is already capped at the viewport's height (it sticks
     to the top while the conversation scrolls), so an editor with a fixed 28rem minimum pushed the
     buttons out of that box on every screen shorter than it. Half the card, at most. */
  /* The colour and the field, stacked. The field is transparent and sits over a copy of the text
     that carries the marks; both are the same face at the same size and the same padding, or the
     colour drifts off the letters it belongs to. */
  /* The editor is the reading view with a caret in it. Same grid, same gutter, same face, same
     size — anything that differs is a line breaking somewhere else the moment you press edit, and
     then the thing on screen has stopped being the thing you were looking at. */
  .editbody { max-height:min(60vh, 34rem); }
  /* The ghost is in flow and the field is over it, not the other way round: the coloured copy is
     the one that knows how big the file is, so it sizes the box and the field inherits that. Grown
     to the longest line, so a file with one wide line scrolls the pane sideways under a gutter
     that stays where it is. */
  .editstack { position:relative; min-width:100%; width:max-content; }
  .editstack .editghost {
    margin:0; pointer-events:none; white-space:pre; tab-size:4; color:var(--magi-ref-fg);
  }
  /* The text being typed is invisible; the caret and the selection are not. Sized by JS to its own
     content, because the box that scrolls is the pane around it — that is what keeps the numbers
     beside their own lines with nothing listening for a scroll. */
  .fileeditarea {
    position:absolute; inset:0; width:100%; height:100%;
    margin:0; padding:0; border:0; outline:0; background:transparent; resize:none;
    color:transparent; caret-color:var(--magi-ref-primary);
    white-space:pre; overflow:hidden; tab-size:4; font:inherit; display:block;
  }
  .fileeditarea:focus { outline:0; }
  /* Selection has to be visible through transparent text, and it is the one thing here the reading
     view does not have to draw. */
  .fileeditarea::selection { background:var(--magi-ref-primary); color:transparent; }
  /* Except where a finger is the input. Below 16px, iOS Safari zooms the page in when a field
     takes focus and does not zoom back — so on touch the whole pane takes the larger size, editor
     and reading view together, which keeps the two identical rather than trading one jump for
     another. */
  @media (hover:none) {
    .editbody, .editbody .fileeditarea, .editbody .editghost {
      font-size:var(--md-sys-typescale-body-large-size);
    }
  }

  /* This file's controls, in one corner and in every mode: "edit" becomes "save · cancel" where
     the edit button was. At the foot of the editor they were below the fold on an ordinary wide
     screen — somebody had to scroll a 28rem box past what they had just typed to reach the button
     for it — and a control that moves between modes is one you have to look for twice. */
  /* Centred, not stretched: two icon buttons stand beside a filled button and a text button, and
     the default cross-axis stretch made the small ones as tall as the tallest thing in the row —
     a control whose target grows to fill a bar is one whose centre is not where its mark is. */
  .fileacts { display:flex; align-items:center; gap:var(--magi-sys-space-100); flex:none; margin-left:auto; }
  /* Ask now, rather than at the next pause in the typing. Smaller than save and cancel because it
     is a smaller act — the same request the timer already makes — and it must not read as a third
     control of that weight. The button keeps its own 48dp target; only the mark inside it shrinks,
     which is the same trade the tree's reload control makes (.paneagain). */
  .fileacts .editask { flex:none; --md-icon-button-icon-size:18px; }
  /* Armed. While the preference is on, the help also runs itself on every pause in the typing —
     which is a thing worth seeing before the answer arrives rather than after. The primary role,
     the same colour the caret is drawn in, against the muted mark of a control that is only there
     to be pressed once. The word in the tooltip and the accessible name says it too: colour is
     never the only telling. */
  .fileacts .editask.on { --md-icon-button-icon-color:var(--magi-ref-primary); color:var(--magi-ref-primary); }
  /* The switch that lets the model read over your shoulder, and what it said. The switch is a
     preference and reads like one; what it says is a note above the buffer.
     The warning colour was written on the premise that this cell only ever speaks when something
     is wrong — silence being the other answer. The ask buttons broke that premise: a press that
     found nothing has to say so, because silence and a dead control look the same, and "nothing to
     suggest here" is an ordinary answer wearing the colour of a fault. So the fault colouring is
     the default this cell keeps, and .plain is the ordinary answer — same box, the muted ink every
     other quiet reading on this page uses. */
  .looksaid {
    font:var(--md-sys-typescale-body-small-size)/1.6 var(--magi-ref-display); color:var(--magi-ref-warn);
    white-space:pre-wrap; padding:var(--magi-sys-space-100) var(--magi-sys-space-150);
    border-left:2px solid var(--magi-ref-warn); background:var(--magi-ref-surface-container);
    border-radius:var(--magi-sys-shape-xs);
  }
  /* An answer, not a fault. Kept in the same box and the same place so a reader does not have to
     learn a second spot for the same kind of remark — only the colour stops claiming something
     went wrong. */
  .looksaid.plain { color:var(--magi-ref-muted); border-left-color:var(--magi-ref-outlineVariant); }
  /* What the update button's daemon said — its account, or why it could not. Muted and small; it
     takes the button's own place under the version in the facts card. */
  .updsay { display:block; margin-top:var(--magi-sys-space-100); color:var(--magi-ref-muted); font-size:.82rem; }
  /* The build number, with the update button (only when behind) or the daemon's account stacked
     under it — one cell, version on top, the action or its outcome below. */
  .vnum { display:block; }
  .vnum + md-text-button { margin-top:var(--magi-sys-space-50); margin-left:calc(-1 * var(--magi-sys-space-100)); }

  /* The path, whole, in a bar of its own. Whole because this is the line somebody copies into a
     message or a command and half a path pastes as nothing; a bar because a path sitting directly
     over the code read as the file's first line. A surface one step up and a rule under it, which
     is how this page separates two layers everywhere else. */
  .filedir {
    font:var(--md-sys-typescale-label-small-size)/1.5 var(--magi-ref-mono); color:var(--magi-ref-muted);
    overflow-wrap:anywhere; user-select:all;
    padding:var(--magi-sys-space-100) var(--magi-sys-space-150);
    background:var(--magi-ref-surface-container); border-radius:var(--magi-sys-shape-s);
    border:1px solid var(--magi-ref-outlineVariant);
  }
  /* A diff, coloured by what each line does. Not a red/green wash across the row — a line that is
     entirely one colour is unreadable in the light theme and unprintable in either; the marker and
     the text carry it, and the background is a tint you can read through. */
  .diffbody { display:block; }
  /* A rule of colour at the line's start, on every line so the columns do not shift. In the light
     theme the two washes measure 1.00:1 apart — two tints that mean opposite things and carry the
     same luminance — so the edge is the indicator that survives any palette, on top of the +/-
     the diff itself spells. */
  .diffbody .dl { display:block; border-inline-start:3px solid transparent; }
  .diffbody .add { color:var(--magi-ref-fg); border-inline-start-color:var(--magi-ref-primary); background:color-mix(in srgb, var(--magi-ref-primary) 12%, transparent); }
  /* fg, not muted: muted is the on-role of the plain surface, and this line sits on a warn wash —
     label and container from different pairs is the one the colour page calls a hard rule. */
  .diffbody .cut { color:var(--magi-ref-fg); border-inline-start-color:var(--magi-ref-warn); background:color-mix(in srgb, var(--magi-ref-warn) 12%, transparent); }
  .diffbody .at { color:var(--magi-ref-accent); }
  /* File headers (+++/---/diff/index), not content. Painted as add/cut they made every red line a
     header on a real multi-file diff; quiet and bold like the transcript's own diff renderer. */
  .diffbody .dfile { color:var(--magi-ref-muted); font-weight:600; background:none; border-inline-start-color:transparent; }

  /* The parts of a file that are not code. Three marks and a gutter — see paintCode(): this is a
     scan, not a parser, and it colours only what cannot be got wrong in a way that misleads.
     The gutter is the quietest thing on the screen: it is there to be referred to, not read. */
  .filecode .tok-note { color:var(--magi-ref-muted); font-style:italic; }
  .filecode .tok-text { color:var(--magi-ref-accent); }
  .filecode .tok-num { color:var(--magi-ref-primary); }
  /* The inline completion the model offered, drawn in the mirror the caret sits in front of: muted,
     so it reads as a suggestion the way every editor's ghost text does, and Tab takes it. */
  .filecode .editcomplete { color:var(--magi-ref-muted); opacity:.75; }
  /* The look-over remark, drawn at the end of the line it is about — amber, the way an editor marks
     something worth a second look, and never so loud it competes with the code it trails. It rides
     the mirror behind the caret, so it scrolls with the line and clears the moment you type.
     The translucency reads as "annotation, not code" — a deliberate trade below strict AA, made
     by the operator and carried as a named exception in the contrast test with its own floor
     (3.5:1), so it can be softer than body text but never fade past identification. */
  .filecode .linenote { color:var(--magi-ref-warn); font-style:italic; opacity:0.85; }
  /* Line numbers come from the tool, so the body is one preformatted block and the numbers are
     part of the text. Scrolled in both directions inside its own box: a long line in a source file
     is not something to reflow, and the page must not scroll sideways because of it. */
  /* Two columns that scroll as one: the numbers, then the file. Both are <pre> with the same
     metrics, so line 40 in one is line 40 in the other without anything measuring anything. */
  .filebody {
    display:grid; grid-template-columns:auto minmax(0, 1fr);
    max-height:min(60vh, 34rem); overflow:auto; overscroll-behavior:contain;
    font:var(--md-sys-typescale-body-small-size)/1.6 var(--magi-ref-mono);
    color:var(--magi-ref-fg); tab-size:4;
  }
  /* The diff has no number column, so its scroller is the same box without the grid. */
  .filebody.diffscroll { display:block; }
  .filegutter, .filecode { margin:0; white-space:pre; font:inherit; }
  /* Not selectable, and not scrolled away. A drag across the code copies the code — with one run
     of text the numbers came with it and every pasted line had a number welded to its front — and
     the column stays at the left edge while a long line scrolls, which is exactly when a reader
     needs to know which line they are on. */
  .filegutter {
    user-select:none; -webkit-user-select:none; text-align:right;
    padding-right:var(--magi-sys-space-150); position:sticky; left:0;
    background:var(--magi-ref-surface-container-low); color:var(--magi-ref-outline);
  }
  /* The close mark. Not faded — a muted role at .7 measures 3.99:1 in the light theme and this
     page's guard refuses that — so it is the muted colour at full strength and takes the text
     colour when the pointer is on it. */
  /* Which tab is showing, said in more than the indicator under it. The strip sits above a card
     that fills the width, so the 2px line at the bottom of one tab is a long way from the content
     it belongs to — the label carries it too. */
  #cardtabs md-secondary-tab[active] { --md-secondary-tab-label-text-weight:700; }
  #cardtabs md-secondary-tab {
    --md-secondary-tab-label-text-font:var(--magi-ref-mono);
    --md-secondary-tab-active-label-text-color:var(--magi-ref-primary);
    --md-secondary-tab-active-indicator-color:var(--magi-ref-primary);
  }
  /* A real button now, drawn as the glyph it always was — it used to be an svg with a handler on
     it, 14px of target inside a 48dp tab, unreachable by keyboard and hidden from the tree. */
  #cardtabs .tabclose {
    margin-left:var(--magi-sys-space-100); color:var(--magi-ref-muted);
    background:none; border:0; padding:0; cursor:pointer; display:inline-flex; align-items:center;
  }
  #cardtabs .tabclose:hover { color:var(--magi-ref-fg); }

  /* ── how the fleet is laid out ─────────────────────────────────────────────
     Boxes for instances, nodes for companions, and curves for what is crossing between them. The
     boxes are ordinary flow content — a grid that wraps — and the wires are an SVG laid over the
     top and measured after layout, because where a node ended up is something only the browser
     knows. */
  .mapcanvas { position:relative; }
  /* Over the boxes, not under them. The wires are drawn between node edges, so a stacking order
     that put them behind the boxes hid every line the moment it crossed one — which is most of
     them. Inert to the pointer, so being on top costs the nodes nothing. */
  .wires { position:absolute; inset:0; width:100%; height:100%; pointer-events:none; overflow:visible; z-index:1; }
  /* The machine, drawn. machineBox() builds .machine / .machinetop / .machinename / .machineaddr /
     .accounts and the stylesheet had a rule for none of them — so the outer boundary the map exists
     to show ("this account and that one are on the same kernel") was a line of text above an
     ungrouped stack, with the two accounts' rounded corners touching at 0px. */
  .machine {
    border:1px solid var(--magi-ref-outlineVariant); border-radius:var(--magi-sys-shape-m);
    padding:var(--magi-sys-space-200); background:var(--magi-ref-surface-container-lowest);
  }
  .machinetop {
    display:flex; align-items:baseline; gap:var(--magi-sys-space-200); flex-wrap:wrap;
    margin-bottom:var(--magi-sys-space-200);
  }
  .machinename { font:600 var(--md-sys-typescale-title-small-size)/1.4 var(--magi-ref-display); color:var(--magi-ref-fg); }
  .machineaddr { font:var(--md-sys-typescale-label-small-size)/1.5 var(--magi-ref-mono); color:var(--magi-ref-muted); }
  .accounts { display:grid; gap:var(--magi-sys-space-200); }
  .places { position:relative; }
  .wire { fill:none; stroke-width:2; stroke-linecap:round; }
  /* Answered, in flight, and unreachable — three states somebody acts on differently, so three
     appearances rather than one line in three shades. The dashed one is the only one that is not a
     solid connection, which is the reading it should have before any colour is noticed. */
  .wire.ok { stroke:var(--magi-ref-outline); }
  .wire.flight { stroke:var(--magi-ref-primary); }
  .wire.down { stroke:var(--magi-ref-warn); stroke-dasharray:5 5; }
  /* Room under the boxes for the lane the crossing wires travel in. Reserved rather than
     discovered: a lane drawn into space the layout did not leave lands on the legend. */
  .mapcanvas { padding-bottom:var(--magi-sys-space-700); }
  .places {
    /* A track has a ceiling. auto-fit collapses the empty tracks, so a console with a single host
       drew one 1385px-wide box and twelve full-width bars inside it, each carrying a name and a
       state word with 1235px of nothing between them. */
    display:grid; grid-template-columns:repeat(auto-fit, minmax(min(22rem, 100%), 34rem));
    justify-content:start;
    gap:var(--magi-sys-space-300); align-items:start;
  }
  /* One magi. The outline is the boundary the whole system is arranged around — one config
     directory, one policy, one key — so it is drawn as a boundary and not as a heading with rows
     under it. */
  .place {
    border:1px solid var(--magi-ref-outlineVariant); border-radius:var(--magi-sys-shape-m);
    padding:var(--magi-sys-space-200); display:grid; gap:var(--magi-sys-space-150);
    background:var(--magi-ref-surface-container-low);
  }
  /* Yours reads as yours. Not a colour on its own: the word beside the name says it too. */
  .place.own { border-color:var(--magi-ref-primary); }
  .place.unknown { border-style:dashed; }
  .placetop { display:flex; align-items:baseline; gap:var(--magi-sys-space-150); flex-wrap:wrap; }
  .placename {
    font:600 var(--md-sys-typescale-title-small-size)/1.4 var(--magi-ref-mono);
    color:var(--magi-ref-fg); overflow-wrap:anywhere; min-width:0;
  }
  .placetrust {
    font:600 var(--md-sys-typescale-label-small-size)/1.4 var(--magi-ref-display);
    letter-spacing:.06em; text-transform:uppercase; color:var(--magi-ref-muted);
  }
  .placetrust.own { color:var(--magi-ref-primary); }
  .placetrust.unknown { color:var(--magi-ref-warn); }
  .placeseen {
    font:var(--md-sys-typescale-label-medium-size)/1.5 var(--magi-ref-display); color:var(--magi-ref-muted);
  }
  .placeseen.down { color:var(--magi-ref-warn); }
  /* The team a run of nodes belongs to. A label rather than a box: a team crosses machines and
     accounts, so a box around its members here would have to be drawn in three places and would
     say they are together in a way the two boundaries above it deny. */
  .teamlabel {
    margin-top:var(--magi-sys-space-100);
    font:600 var(--md-sys-typescale-label-small-size)/1.4 var(--magi-ref-display);
    letter-spacing:.08em; text-transform:uppercase; color:var(--magi-ref-accent);
  }
  .place > .teamlabel:first-of-type { margin-top:0; }
  /* A companion. The same state vocabulary as the table — .state carries the state layer and the
     48dp floor, and the state class colours the mark. */
  .node {
    display:flex; align-items:center; gap:var(--magi-sys-space-150);
    padding:var(--magi-sys-space-100) var(--magi-sys-space-150);
    border-radius:var(--magi-sys-shape-s); text-decoration:none; color:var(--magi-ref-fg);
    background:var(--magi-ref-surface-container);
  }
  .node .nodename { font:600 var(--magi-sys-body-m) var(--magi-ref-mono); overflow-wrap:anywhere; min-width:0; flex:1 1 auto; }
  .node .nodestate { font:var(--md-sys-typescale-label-medium-size)/1.4 var(--magi-ref-display); color:var(--magi-ref-muted); }
  /* How old this companion's record is. Beside the state and quieter than it: the state is what it
     is doing, this is how sure we are that the state is still true. */
  .node .nodeage { font:var(--md-sys-typescale-label-small-size)/1.4 var(--magi-ref-mono); color:var(--magi-ref-muted); }
  .node .nodeage.down { color:var(--magi-ref-warn); }
  /* on-surface, not primary: the same pair the navigation bar's selected label had. Primary at
     label-small on a surface-container tile is 4.27:1 in the light theme, under the 4.5:1 floor. */
  .node .nodehub { font:600 var(--md-sys-typescale-label-small-size)/1.4 var(--magi-ref-display); color:var(--magi-ref-fg); }
  .node .nodemark { flex:none; }
  /* Not a link, and it should not look like one: a companion on another machine is drawn because
     it exists and cannot be opened from here — its socket is a path on its own filesystem. */
  .node.faroff { cursor:default; }
  .maplegend { display:flex; flex-wrap:wrap; gap:var(--magi-sys-space-300); margin-top:var(--magi-sys-space-300); }
  .mapkey { display:flex; align-items:center; gap:var(--magi-sys-space-100);
    font:var(--md-sys-typescale-label-medium-size)/1.4 var(--magi-ref-display); color:var(--magi-ref-muted); }
  .wirekey { width:2rem; height:0; border-top:2px solid var(--magi-ref-outline); }
  .wirekey.flight { border-color:var(--magi-ref-primary); }
  .wirekey.down { border-top-style:dashed; border-color:var(--magi-ref-warn); }

  /* ── who may use this console ──────────────────────────────────────────────
     Rebuilt on list anatomy, because the screen had none: every line was the same size in the same
     colour, so "kim@corp.com", "read answer" and the group above them read as three peers. The
     type scale is what says which is which, and each line here is a role from it — headline for
     the name, body for what it buys, label for the scope under that.
     Groups and people are built from the same two lines so a field never moves between one row and
     the next, which is the one thing the lists guidance asks of a list holding two kinds of item. */
  /* A subheading over each half. title-small: the section's own h2 is the page's heading, and a
     second one at that weight would say the two are peers. */
  .rosterhead {
    margin:var(--magi-sys-space-400) 0 var(--magi-sys-space-150);
    font:600 var(--md-sys-typescale-title-small-size)/1.4 var(--magi-ref-display);
    letter-spacing:.01em; color:var(--magi-ref-muted);
  }
  /* Gap between rows, not rules: the lists guidance keeps dividers for lists that are complex or
     uncontained, and a short repeating list is neither. The people below are the exception and say
     so themselves. */
  .acclist { display:grid; gap:var(--magi-sys-space-200); }
  .acc { display:grid; gap:var(--magi-sys-space-100); }
  /* Ruled between, not around. Each row is now two lines and a set of chips, which is the "complex"
     the lists guidance keeps dividers for — and a group's grant is a thing somebody reads one at a
     time rather than as a block. Between only: a rule under the last row would close a list that
     the next heading already closes. */
  .acclist .acc + .acc { border-top:1px solid var(--magi-ref-outlineVariant); padding-top:var(--magi-sys-space-200); }
  /* A break between the two lists has to be stronger than a break between two rows, and it was
     weaker: rows were ruled and the sections were separated by a margin, so the roster read as one
     long list with a bold line in the middle of it. Two weights and two amounts of room — the
     hairline between rows is outline-variant, the rule between sections is outline, and the
     section gets the space of a new beginning. Only when something precedes it: a rule above the
     first heading would close nothing. */
  .acclist + .rosterhead {
    margin-top:var(--magi-sys-space-600); padding-top:var(--magi-sys-space-400);
    border-top:1px solid var(--magi-ref-outline);
  }
  /* Complex — each one carries a form — so these get the stronger separation the guidance reserves
     for exactly that, and the room to go with it. */
  /* On the scale. It asked for 250, which this page does not have — the scale steps 200 then 300 —
     so the padding under every person's row has been zero and their rule sat against their text. */
  .acclist .person { padding-bottom:var(--magi-sys-space-200); border-bottom:1px solid var(--magi-ref-outlineVariant); }
  .acclist .person:last-child { border-bottom:0; padding-bottom:0; }
  .accwho { display:flex; align-items:baseline; gap:var(--magi-sys-space-150); flex-wrap:wrap; }
  /* The headline. Monospace because it is a literal somebody types into a file and compares by
     eye — the same reason every other identifier on this page is. */
  .accwho .who {
    font:600 var(--md-sys-typescale-body-large-size)/1.4 var(--magi-ref-mono);
    color:var(--magi-ref-fg); overflow-wrap:anywhere; min-width:0;
  }
  /* Trailing, and a label rather than a heading: it classifies the row, it does not name it. */
  .accwho .role {
    font:600 var(--md-sys-typescale-label-large-size)/1.4 var(--magi-ref-display);
    letter-spacing:.02em; color:var(--magi-ref-primary);
  }
  .accwho .you {
    font:600 var(--md-sys-typescale-label-small-size)/1.4 var(--magi-ref-display);
    letter-spacing:.06em; text-transform:uppercase; color:var(--magi-ref-accent);
  }
  .acccaps { display:grid; gap:var(--magi-sys-space-50); }
  /* Supporting text: body, one step down from the headline and in the muted role. Monospace for
     the same reason — these are the words that go into the file. */
  .acccaps .caps { display:flex; flex-wrap:wrap; gap:var(--magi-sys-space-100); }
  /* A capability is an md-filter-chip and the component draws it — the shape, the ripple, the
     focus ring, the 48dp target and the check mark when it is on. Nothing here paints over that.
     The one thing said from outside is the face: these words are literals from auth.toml, and the
     rest of the page sets literals in mono. */
  .capchip { --md-filter-chip-label-text-font:var(--magi-ref-mono); }
  /* A display tag on a row — not a control. An outlined pill in the chip's own shape and the mono
     face the file uses, but nothing pressable: it says what this row may do, and the legend below
     is where a capability is chosen to filter. Admin reads in warn, the one grant that hands out. */
  .captag {
    display:inline-flex; align-items:center; min-height:2rem; padding:0 var(--magi-sys-space-150);
    border:1px solid var(--magi-ref-outline); border-radius:var(--magi-sys-shape-s);
    font:var(--md-sys-typescale-label-large-size)/1 var(--magi-ref-mono); color:var(--magi-ref-fg);
  }
  .captag[data-cap="admin"] { color:var(--magi-ref-warn); border-color:var(--magi-ref-warn); }
  /* The one that can grant the others reads differently, because it IS different: everything else
     on this screen can be handed back, and this one hands out. Through the component's own tokens
     rather than a colour on the host, which would not reach the label inside its shadow root. */
  .capchip[data-cap="admin"] {
    --md-filter-chip-label-text-color:var(--magi-ref-warn);
    --md-filter-chip-outline-color:var(--magi-ref-warn);
  }
  /* What the screen is showing while one is pressed, and the way back to all of it. */
  .capnote {
    display:flex; align-items:center; gap:var(--magi-sys-space-150); flex-wrap:wrap;
    margin-bottom:var(--magi-sys-space-200);
    font:var(--md-sys-typescale-body-medium-size)/1.6 var(--magi-ref-display); color:var(--magi-ref-fg);
  }
  /* And the scope under it, smaller again: it is a qualifier on the line above, not a line of its
     own standing. */
  .acccaps .scope {
    font:var(--md-sys-typescale-label-medium-size)/1.5 var(--magi-ref-display);
    color:var(--magi-ref-muted);
  }
  /* The lead sentence, and the one under each heading: prose, so the display face and a body size.
     A generous reading cap, not a narrow strip. Measured, the map's lead ran 162 characters on one
     line at 1512/1920 and people's 146 — the old comment claimed "the container already does it at
     every width," which the measurement disproved. 92ch of this face renders comfortably under the
     guide's 120-character ceiling while staying wide enough that it reads as the panel's own lead,
     not a caption indented from the rows below it — the narrow-strip failure a 60ch cap had. */
  .accsay, .rosterhead .why {
    font:var(--md-sys-typescale-body-medium-size)/1.6 var(--magi-ref-display);
    max-width:92ch;
    color:var(--magi-ref-muted);
  }
  .accsay { margin:calc(-1 * var(--magi-sys-space-100)) 0 var(--magi-sys-space-200); }
  /* What the server said when it turned a press down -- any screen, not just this one. It goes
     above the list, because the row that was pressed looks exactly as it did before: there is
     nothing down there for this line to point at. Error colour and a rule down the side: this is
     the one line on a screen that says the console asked for something and did not get it. The
     text is the server's own sentence, so it is left at the body face and wrapped, not clipped. */
  .refused {
    margin:calc(-1 * var(--magi-sys-space-100)) 0 var(--magi-sys-space-200);
    padding:var(--magi-sys-space-100) var(--magi-sys-space-150);
    border-left:2px solid var(--magi-ref-error);
    color:var(--magi-ref-error);
    font:var(--magi-sys-body-s) var(--magi-ref-display);
    max-width:92ch;
  }
  /* Under the heading rather than beside it: a heading and a sentence on one line is two type
     roles competing for the same baseline, and the sentence loses. */
  .rosterhead .why { display:block; margin-top:var(--magi-sys-space-50); font-weight:400; letter-spacing:0; }
  /* The legend: what each word on a chip means, once. Two columns so the chips make a column of
     their own and the sentences line up beside them — a list of "word — sentence" run together is
     the shape that gets skipped. */
  .caplegend { display:grid; gap:var(--magi-sys-space-150); }
  .capdef { display:grid; grid-template-columns:minmax(6rem, auto) minmax(0, 1fr);
    gap:var(--magi-sys-space-200); align-items:baseline; }
  .capdef .capsay {
    font:var(--md-sys-typescale-body-medium-size)/1.6 var(--magi-ref-display);
    color:var(--magi-ref-muted);
  }
  /* A person's scope: which companions their role applies to. A section under them rather than a
     field beside them — it is a list somebody adds to and takes from one at a time, and it was a
     comma-separated text box they had to retype in full to drop one name from three. */
  .scopes { display:grid; gap:var(--magi-sys-space-100); margin-top:var(--magi-sys-space-150); }
  .scopek {
    font:var(--md-sys-typescale-label-medium-size)/1.5 var(--magi-ref-display); color:var(--magi-ref-muted);
  }
  /* The names and the way to add one are ONE box, the way an address field is.
     They were two: a row of chips, and an outlined field under it — two outlines for one act, and
     the field read as a setting of its own rather than as the end of the list above it. So the box
     carries the outline and the field gives its own up, which is the only part of the mail-client
     shape that can be had without leaving the component (its input area has no slot: the bundle's
     text field renders that element itself and opens only leading-icon, trailing-icon, container).
     Chips and cursor flow in one wrapping line; the field keeps a floor so a long label is not cut. */
  .scopebox {
    display:flex; flex-wrap:wrap; align-items:center; gap:var(--magi-sys-space-100);
    padding:var(--magi-sys-space-100);
    border:1px solid var(--magi-ref-outline); border-radius:var(--magi-sys-shape-s);
  }
  /* Focus belongs to the box now. With the field's outline off, the component's focus outline goes
     with it — and a field you cannot see you are typing in is worse than two outlines. Drawn as a
     ring rather than a thicker border so nothing inside moves when it appears. */
  .scopebox:focus-within {
    border-color:var(--magi-ref-primary); box-shadow:0 0 0 1px var(--magi-ref-primary);
  }
  .scopebox md-chip-set { display:flex; flex-wrap:wrap; gap:var(--magi-sys-space-100); }
  .scopes md-outlined-text-field {
    flex:1 1 14rem; min-width:0; max-width:16rem;
    --md-outlined-text-field-outline-width:0px;
    --md-outlined-text-field-focus-outline-width:0px;
  }

  /* All four states, not one. The component swaps the label to on-surface-variant on hover and
     on press, so the error colour — the only colour saying this press revokes somebody's access —
     was cancelled at exactly the moment before the press. */
  .scopechip.armed {
    --md-input-chip-label-text-color:var(--magi-ref-error);
    --md-input-chip-hover-label-text-color:var(--magi-ref-error);
    --md-input-chip-pressed-label-text-color:var(--magi-ref-error);
    --md-input-chip-focus-label-text-color:var(--magi-ref-error);
  }
  .scopechip { --md-input-chip-label-text-font:var(--magi-ref-mono); }
  .acccontrols {
    display:flex; flex-wrap:wrap; align-items:center;
    gap:var(--magi-sys-space-150); margin-top:var(--magi-sys-space-150);
  }
  /* A person is a ROW, not a form.
     Four full-width blocks stacked — the name, the capabilities, the role and remove, the scope —
     made one person 250 to 300 pixels tall in a 1350px row with nothing on the right of any of
     them. Measured on the published console: 249px for the first, 298 for the second.
     The name and the controls that act on that name share the top line, which is where the eye
     looks for "who, and what are they"; the capabilities and the scope sit under it, full width,
     because they wrap. Everything keeps the order it has in the markup, so nothing moves for a
     screen reader. */
  .acclist .person {
    display:grid; grid-template-columns:minmax(0, 1fr) auto;
    column-gap:var(--magi-sys-space-300); align-items:center;
  }
  .acclist .person > .accwho { grid-column:1; grid-row:1; }
  .acclist .person > .acccontrols { grid-column:2; grid-row:1; justify-self:end; margin-top:0; }
  .acclist .person > .acccaps { grid-column:1 / -1; grid-row:2; }
  .acclist .person > .scopes { grid-column:1 / -1; grid-row:3; }
  /* Narrow: the controls go back under the name rather than squeezing it to nothing. */
  @media (max-width:52.4375em) {
    .acclist .person { grid-template-columns:minmax(0, 1fr); }
    .acclist .person > .acccontrols { grid-column:1; grid-row:auto; justify-self:start; }
    .acclist .person > .acccaps, .acclist .person > .scopes { grid-row:auto; }
  }
  .acccontrols md-outlined-select { flex:0 0 11rem; }
  .acccontrols md-outlined-text-field { flex:1 1 14rem; min-width:0; }

  /* Whose list this is, under the heading and above the rows. Quiet, because it is not a row and
     not a setting — but monospace, because a path a person is about to compare with what they
     typed is read character by character, and the account and machine beside it are literals too. */
  .instance { font:var(--md-sys-typescale-label-medium-size)/1.7 var(--magi-ref-mono);
    color:var(--magi-ref-muted); margin:0 0 var(--magi-sys-space-200); overflow-wrap:anywhere; }
  .instance b { color:var(--magi-ref-fg); font-weight:600; }
  /* The sentence under it is prose and is set as prose: a reason to read, not a value to compare. */
  .instance .why { display:block; font:var(--md-sys-typescale-body-small-size)/1.6 var(--magi-ref-display); }

  /* The empty state takes the whole panel and stands in the middle of it.
     A measure is for prose somebody READS — a paragraph after paragraph, where the eye has to find
     the next line and 52 characters is where it stops missing. This is two sentences on a screen
     with nothing else on it, and held to 52ch on the left edge of a 1400px panel it read as a
     caption for the emptiness rather than as the whole of what the screen is saying. Centred and
     given the width, it is what is there. */
  .empty { font:var(--md-sys-typescale-body-large-size)/1.7 var(--magi-ref-display); color:var(--magi-ref-muted);
    padding:var(--magi-sys-space-700) var(--magi-sys-space-300); text-align:center; }
  .empty code { font:var(--md-sys-typescale-body-medium-size)/1 var(--magi-ref-mono); color:var(--magi-ref-accent); }

  /* ── the agent's own header, so a detail page says what it is looking at ──── */
  /* The three panels on a companion's page are md-outlined-card: each one groups what is true
     about a single subject, which is what a card is for, and the outline replaces the hairline
     rule that used to separate them. The rows in the fleet keep their rule and are NOT cards —
     they are links, and this component has no ripple, no focus ring and no role, so making one a
     card would trade the keyboard for a box.

     A card lays its slotted children out itself (:host is flex, and a slot is display:contents),
     so a display of ours on the host wins and the children stay grid items. */
  /* No margin. Every place a card is put is a flex column with a gap, so a margin under it was
     that gap twice — 48px below the detail card where 24 was asked for — and #side already
     carried a margin-bottom:0 to undo it in the one place somebody noticed. The container
     spaces its children; the child does not space itself. */
  md-outlined-card { padding:var(--magi-sys-space-200) var(--magi-sys-space-200); }
  /* A card that scrolls must paint its own frame. The component draws its outline and its surface
     on absolute inset:0 children, which size to the VISIBLE box — make the card its own scroll
     container (both of these are, on a companion's page) and the frame and ground stop at the
     first viewport-full while the content keeps going: measured, the facts card's border ended
     mid-panel the moment it scrolled. Painting the HOST's border and background instead pins the
     frame to the scroll container's box — which is what a scrolling card looks like everywhere
     else — and the component's own layers are switched off so the two never disagree. */
  #detail, #fileview {
    --md-outlined-card-outline-width:0px;
    --md-outlined-card-container-color:transparent;
    border:1px solid var(--magi-ref-outlineVariant);
    border-radius:var(--magi-sys-shape-m);
    background:var(--magi-ref-bg);
    box-sizing:border-box;
  }
  #detail { display:flex; flex-direction:column; gap:var(--magi-sys-space-200); }
  /* An open file stays at the top of the conversation while the conversation scrolls under it.
     It is being read ALONGSIDE the transcript — that is the whole reason it is on this page and
     not a screen of its own — and a file that scrolls away the moment somebody reads three
     messages is one they have to go and find again. The card has its own scroll inside it, so
     what is stuck is a bounded box and never the whole file.
     Only where the panes are beside the conversation: stacked, the page is one column and a
     sticky block in the middle of it would sit on top of the messages. */
  @media (min-width:52.5em) {
    #fileview { overflow:auto; overscroll-behavior:contain; }
  }
  #detail .grid {
    /* auto-fit at 9rem packed a 60-character workspace path into the same cell as a four-letter
       step count. 14rem is the width of the longest SHORT field (the context reading), so the long
       ones take a whole row of their own instead of squeezing the rest.

       The column gap is a scale step wider than the row gap on purpose: auto-fit spends every
       pixel it saves on another column, so narrowing this one from 25.6dp to 24dp fitted a
       fourth column, took 119px off each cell and wrapped the long fields it was drawn to keep
       whole. Snapping to the scale is not the same as snapping to the nearest value. */
    display:grid; grid-template-columns:repeat(auto-fit, minmax(14rem, auto)); gap:var(--magi-sys-space-200) var(--magi-sys-space-400);
  }
  #detail .f { display:flex; flex-direction:column; gap:var(--magi-sys-space-50); }
  /* The long fields take TWO columns, not the whole row. A session id and a workspace path are
     strings nobody reads in a 14rem column — the id wraps mid-hash, the path ellipses at the part
     that says which project it is — but a full-row span breaks the packing on both sides of it,
     and the card grew three near-empty rows: the state on one, the path on the next, the session
     on a third. Two columns is enough for either string and leaves the short readings packed
     beside them. Below the two-column width there is only one column and this is moot. */
  #detail .f.wide { grid-column:span 2; }
  /* 52.4375em, not 52.5: 840px is where expanded BEGINS, so a max-width of 52.5em puts one CSS
     pixel in both classes — measured, at exactly 840 the grid had already gone two-column while
     this rule was still spanning a field across the whole row. Every other max-width in this file
     is written the same way for the same reason. */
  @media (max-width:52.4375em) { #detail .f.wide { grid-column:1 / -1; } }
  /* The menu on a full-width row takes the width: a select sized to its own content puts a long
     session title behind an ellipsis in the middle of an empty row.
     No max-width. `--magi-sys-measure` is 74ch and its own definition says `/* prose */` — it is
     the line length a paragraph is comfortable to READ at, and a dropdown is not a paragraph.
     Capping the control at it also contradicted the width:100% on the same line, which is there
     precisely so the control fills the row it was given. */
  #detail .f.wide .permsel { width:100%; }
  #detail .f .k {
    font:600 var(--md-sys-typescale-label-small-size)/1.4 var(--magi-ref-mono); letter-spacing:0.06em; color:var(--magi-ref-muted);
  }
  #detail .f .v { font:var(--magi-sys-body-m) var(--magi-ref-mono); color:var(--magi-ref-fg); overflow-wrap:anywhere; }
  #detail .f .v.state { font-weight:600; letter-spacing:0.05em; font-size:var(--md-sys-typescale-label-small-size); }
  /* The window, as a rule under the number rather than a gauge beside it: this is a fill level and
     the page already spends its colour on state. Unknown windows draw no bar at all — an empty
     track reads as "nearly empty", which is the opposite of "we do not know". */
  #detail .f .bar { height:2px; background:var(--magi-ref-outlineVariant); margin-top:var(--magi-sys-space-50); }
  #detail .f .bar i { display:block; height:100%; background:var(--magi-ref-primary); }
  #detail .f .bar.tight i { background:var(--magi-ref-warn); }
  #detail .f .v small { color:var(--magi-ref-muted); font-size:var(--md-sys-typescale-label-small-size); }
  /* Disabled is the component's own fade now, not a rule here. The contrast check reads this
     stylesheet and cannot see into a shadow root, so that opacity is not covered — which is the
     right answer rather than a gap: WCAG exempts inactive controls, and the repo's own rule
     against dimming was about text somebody still has to read. */
  #detail .f .fold { justify-self:start; }

  /* The conversation and the facts about it, side by side where there is room. The transcript is
     the wider of the two because its lines are code; the aside is a reading column of short
     labelled facts and does not want more than it needs. */
  #agentview { display:grid; grid-template-columns:minmax(0, 1fr); gap:var(--magi-sys-space-300); }
  /* The facts fold away. On this page they answer "what am I looking at" once, and after that they
     are 380px of masthead between the reader and the conversation they came for — measured at
     430px wide, the transcript began 1073px down a 900px screen. Folded, the summary line still
     carries the state and the workspace, which is what somebody glancing back actually wants. */
  #detail .foldbar {
    display:flex; align-items:baseline; gap:var(--magi-sys-space-150); width:100%; cursor:pointer;
    background:none; border:0; padding:0; color:inherit; text-align:left; font:inherit;
  }
  /* It answers the pointer. Rest, hover and press were the same image — a control that folds the
     whole card and looked exactly like a heading. The layer is the surface's own ink at the two
     opacities everything else on this page uses. */
  #detail .foldbar:hover { background:color-mix(in srgb, var(--magi-ref-fg) 8%, transparent); }
  #detail .foldbar:active { background:color-mix(in srgb, var(--magi-ref-fg) 12%, transparent); }
  #detail .foldbar .caret { color:var(--magi-ref-muted); transition:transform 200ms var(--magi-sys-ease-standard); }
  #detail[folded] .foldbar .caret { transform:rotate(-90deg); }
  #detail .foldbar .sum {
    font:var(--magi-sys-body-s) var(--magi-ref-mono); color:var(--magi-ref-muted); margin-left:auto;
    overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  }
  /* Folding is a movement, not a disappearance.
     display:none takes the facts away between two frames, which reads as the card being replaced
     rather than closed — and the caret above it animates, so half the control moved and half
     blinked. A grid row from 1fr to 0fr is the one way to transition to content height without
     guessing what that height is; the child clips itself while it happens.
     Reduced motion is already handled globally: transitions there are restricted to opacity and
     colour, so this collapses instantly for anybody who asked for that. */
  #detail .foldwrap {
    display:grid; grid-template-rows:1fr;
    transition:grid-template-rows var(--magi-sys-dur-short4) var(--magi-sys-ease-standard);
  }
  #detail[folded] .foldwrap { grid-template-rows:0fr; }
  /* The file in the slot folds with the same movement and the same stored answer. */
  #fileview .foldwrap {
    display:grid; grid-template-rows:1fr;
    transition:grid-template-rows var(--magi-sys-dur-short4) var(--magi-sys-ease-standard);
  }
  #fileview[folded] .foldwrap { grid-template-rows:0fr; }
  /* min-height:0 always, so the grid row can shrink to nothing during the fold; but overflow is
     hidden ONLY while folded. Left hidden in every state it clipped the open file to its box at
     desk widths — no scrollbar on either axis, a long line's tail and any file past ~544px
     unreachable — because the pre-wrap escape that saves the phone is scoped below 840px. Open, the
     filebody keeps its own max-height and overflow:auto (its base rule) and scrolls; folded, it
     clips so the collapsing row does not spill. */
  #fileview .foldwrap > .filebody { min-height:0; }
  #fileview[folded] .foldwrap > .filebody { overflow:hidden; }
  .foldcaret {
    background:none; border:0; padding:0; margin-inline-end:var(--magi-sys-space-100);
    color:var(--magi-ref-muted); cursor:pointer; display:inline-flex; align-items:center;
  }
  .foldcaret .caret { transition:transform 200ms var(--magi-sys-ease-standard); }
  #fileview[folded] .foldcaret .caret { transform:rotate(-90deg); }
  /* Shut, the card is a bar and its own padding, and nothing between them.
     The gap belongs to the card — 16px between the bar and the fields — and a collapsed wrapper is
     still a flex item, so the gap stayed and the shut card measured 71px for a 23px line: 16 above,
     the bar, 16 to an empty box, 16 below. Read as an odd tall band that says one sentence, which
     is how it was reported. Without the gap it is 55, which is the bar in its padding. */
  #detail[folded] { gap:0; }
  #detail .foldwrap > .grid { overflow:hidden; min-height:0; }
  /* Two panels on one screen instead of six cards in a column.
     Below the two-column width the page stacked: the facts, then the conversation, then four cards
     of which three are history — so the transcript was off the first screen and the composer, fixed
     at the foot, was nowhere near the words it answers. The tabs put the conversation on its own
     screen and everything else on the other. Above it, both columns are visible and there is
     nothing to switch between. */
  /* Secondary tabs, drawn with primary tabs' tokens. These switch content INSIDE the companion
     destination, which is what the guide calls a secondary tab — and the second level of tabs on a
     page is exactly the case it says needs them. The bundle has no md-secondary-tab (checked), so
     the difference is made here: a 2dp indicator instead of 3dp, and on-surface for the active
     label and icon instead of primary. The one part no token reaches is the indicator spanning the
     whole tab rather than hugging the label; that is set as a property in paint(). */
  #ptabs {
    margin:0 0 var(--magi-sys-space-200); border-bottom:1px solid var(--magi-ref-outlineVariant);
    --md-primary-tab-active-indicator-height:2px;
    --md-primary-tab-active-label-text-color:var(--magi-ref-on-surface);
    --md-primary-tab-active-icon-color:var(--magi-ref-on-surface);
    --md-primary-tab-active-hover-label-text-color:var(--magi-ref-on-surface);
    --md-primary-tab-active-hover-state-layer-color:var(--magi-ref-on-surface);
    --md-primary-tab-active-pressed-label-text-color:var(--magi-ref-on-surface);
    --md-primary-tab-active-pressed-state-layer-color:var(--magi-ref-on-surface);
  }
  /* Hidden until there is room for a pane to toggle, and declared HERE — before the width rule
     that shows it. It used to sit below, and two rules of one id each are decided by source order,
     so the later "none" won at every width: the pane defaults to shut and the only control that
     opens it was invisible, which made the plan, the handoffs and the history unreachable on this
     page. Nothing said so — a control that is not drawn asks no questions. */
  /* Both pane handles live in the masthead now, and are drawn only where there are panes.
     In the columns they were two controls floating at the top of two columns with nothing to line
     up against; here they are on the one line that already holds the preferences, which is what
     they are — how this window is arranged. */
  #sideToggle, #filesToggle { display:none; }
  body[at="agent"] #sideToggle, body[at="agent"] #filesToggle { display:inline-flex; flex:none; }
  /* 840px, the start of the expanded breakpoint, where the guide recommends two panes. It was
     1100 — a number nobody's scale has. The five are compact <600, medium 600-839, expanded
     840-1199, large 1200-1599, extra-large 1600+, and two panes are recommended from expanded. */
  @media (min-width:52.5em) {
    #ptabs { display:none !important; }
    /* ── the companion's page is an app shell ───────────────────────────────
       One window scroller for three columns of independent content made them fight: the cards
       above the conversation and the panes either side are sticky or not, and whichever way that
       is decided per element, scrolling the transcript moved something that had no reason to move.
       So the page itself stops scrolling beside a companion, and each column scrolls its own
       contents — which is what every editor and every chat client does with this layout, and what
       makes "stay at the bottom" a question about the transcript rather than about the window.
       Only here: the fleet, the board and the phone are one column of page and scroll as a page. */
    body[at="agent"] main {
      /* From where the content actually starts — measured, because what sits above it is the
         masthead plus this page's own padding plus, in the demo, a banner. Sized against the
         masthead alone, the columns ended below the bottom of the window and the last card in
         them was cut off by the screen edge. */
      height:calc(100dvh - var(--magi-comp-shelltop, 5.5rem));
      /* And a little room under them, so the bottom card ends before the window does rather than
         against it. */
      overflow:hidden; padding-bottom:var(--magi-sys-space-200);
      /* And barely any at the top. The panes used to be stuck to a line 5.5rem down the WINDOW,
         which is just under the masthead; in the shell they start where main's content starts, so
         the page's own 24px of breathing room put them back down the screen — the gap the sticky
         rule existed to remove. The columns hold their own padding now. */
      padding-top:var(--magi-sys-space-100);
    }
    body[at="agent"] #agentview { height:100%; min-height:0; }
    /* The columns stop being sticky: there is nothing to stick to, and they are simply as tall as
       the shell. */
    body[at="agent"] #filecol, body[at="agent"] #sidecol {
      position:relative; z-index:2; height:100%; max-height:none; top:auto;
    }
    body[at="agent"] #stream { height:100%; min-height:0; overflow:hidden; }
    /* What is above the transcript keeps its size while the transcript grows.
       The column is a flex box and everything in it was shrinkable, so a conversation getting
       longer squeezed the things over it: measured, sixty new lines took the tab strip from 44px
       to 6 and the open file from 412 to 85. The tabs are chrome — they are the same height with
       one line of conversation or a thousand — and the file has its own cap and its own scroll.
       The transcript is the one that takes what is left, which it already asks for. */
    body[at="agent"] #cardtabs, body[at="agent"] #fileview {
      flex:0 0 auto;
    }
    /* The facts card holds its size only while the column can afford it.
       Written flex:0 0 auto with the others, a card taller than the column — 860px of facts in a
       770px column at 1280 with both panes open — pushed the transcript to literally nothing:
       #log's box was exactly its own dock padding, the card's last rows were clipped by the
       column's overflow:hidden with no scrollbar anywhere, and no combination of panes brought
       the conversation back. This is the same defect as the git card starving the file tree, one
       column over. The tabs are chrome and keep their size; the card yields past the transcript's
       floor and scrolls inside itself for the rest. */
    /* scrollbar-gutter, for the same reason `html` has it one screen up: this card's scrollbar
       comes and goes as its content grows — a late row arrives, the context reading appears — and
       the grid inside is `auto-fit, minmax(14rem, auto)`, which spends every pixel it is given on
       another column. Measured: with the scrollbar the grid is 952px, without it 967. Fifteen
       pixels is a whole column at that minimum, and with `auto` as the maximum the surviving
       tracks then redistribute to unequal widths — so every field on the card moves, twice, for a
       scrollbar nobody was looking at. Reserving the gutter holds it at 952 either way. */
    body[at="agent"] #detail { flex:0 1 auto; min-height:0; overflow:auto; overscroll-behavior:contain;
                               scrollbar-gutter:stable; }
    /* …but an OPEN card must stay visibly taller than a shut one, or the control above it is a
       lie. Yielding with no floor, a long conversation squeezed the card to about the height of
       its own bar — the measured shut size is 55px, the bar in its padding — so folding and
       unfolding drew the same picture and the caret was the only thing that moved.
       7rem is that bar plus the gap plus one row of facts: the smallest open card where opening
       it still shows something. Below the transcript's floor it scrolls inside itself, which is
       what the rule above already arranges. Not applied while folded: shut, the card is supposed
       to collapse to the bar. */
    body[at="agent"] #detail:not([folded]) { min-height:7rem; }
    /* The transcript is the one thing in the middle column that moves. Everything above it — the
       tabs, the card, an open file — stays where it was put. */
    body[at="agent"] #log {
      /* The dock's height is kept clear with MARGIN, not padding. Padding put the clearance
         inside the scrollport, which kept the content above the bay but drew the scrollbar's
         track down behind it — scrolled to the bottom, the thumb ran under the bay and its tail
         was clipped. Margin ends the box (and the track) above the bay, which is where a
         scrollbar for things above the bay should end. The floor is plain 8rem now that the box
         no longer contains the dock's space — four lines of transcript at the worst. */
      /* Basis ZERO, not auto — this is what decides the split with the facts card above.
         `auto` makes a flex item's basis its own CONTENT height, and a transcript's content is the
         whole conversation: measured live at 7885px against a 1235px column. The shrink flexbox
         then distributes is proportional to that basis, so the longer the conversation the harder
         it squeezed everything sharing the column — the facts card was measured at 110px against
         the 507px it needed, one row of a card whose fold control then did nothing visible, which
         is exactly how it was reported ("the more chat, the smaller it gets").
         From zero, the card takes the height it needs and the transcript takes the REST, however
         long it grows. When the card genuinely cannot fit, min-height below is what stops it
         taking the transcript with it — the floor the card yields to. */
      flex:1 1 0; min-height:8rem; overflow:auto; overscroll-behavior:contain;
      margin-bottom:calc(var(--dock, 0px) + var(--magi-sys-space-300));
    }
    /* A deep screen is a page of its own inside the column. */
    body[at="agent"] #agentdetail { overflow:auto; min-height:0; }
    /* And the file card stops needing to be sticky: nothing under it scrolls past it any more.
       RELATIVE, not static. A card paints its own surface with an absolutely-positioned box inset
       to zero, and inset:0 answers to the nearest POSITIONED ancestor — with the card itself
       static, that was the whole column. So the card's background was painted from the top of the
       column to the bottom of it, over the tab strip above it: the tabs were there, the right
       size, the right colour, and buried under an opaque surface belonging to the thing below
       them. Which is why opening a file made the tabs disappear. */
    /* Half the column at most, and never more than a screenful.
       It was 60vh — which on a 900px window is 540 of the 777 the column has, leaving the
       conversation 223px: two messages under a file that fills the screen. The file and the
       conversation share this column, and neither is the other's guest, so the file takes what it
       needs up to half of it and scrolls inside itself for the rest. The vh ceiling stays for the
       tall window, where half a column is more file than anybody reads at once. */
    /* One scroller, and it is the file.
       The card and the box inside it were capped by two numbers that did not know about each
       other: the card at min(50%, 60vh) — 571px in an 1267px window — and the body at
       min(60vh, 34rem), which is 544. Add the bar (48) and the card's own padding (32) and the
       content is 624 in a 571 box, so the CARD scrolled too: 66px of travel next to the file's
       3300, which is the second scrollbar. Measured on a 200-line file at 1440x900.
       The commit workbench two rules down has always been written the other way — flex column,
       nothing scrolls but the boxes that should — and this is the same shape. The card holds the
       bar at its size and gives the rest to the file; the file's own cap comes off, because the
       room the card has IS the cap. Short files still measure their own height: the body may
       shrink, it does not grow. */
    body[at="agent"] #fileview {
      position:relative; max-height:min(50%, 60vh);
      display:flex; flex-direction:column; min-height:0; overflow:hidden;
    }
    body[at="agent"] #fileview > .filebar { flex:0 0 auto; }
    /* Both wrappers a file can arrive in. .foldwrap stays a grid — the fold is a grid row going
       1fr → 0fr and nothing else transitions to content height — so it only learns to shrink. */
    body[at="agent"] #fileview > .foldwrap { min-height:0; }
    body[at="agent"] #fileview > .fileedit { min-height:0; display:flex; flex-direction:column; }
    body[at="agent"] #fileview .filebody { max-height:none; min-height:0; }
    /* Except while it is holding the commit workbench, which is three things at once — the list,
       the diff and the message — and half a column is not enough for them: at 1080 the whole
       workbench scrolled inside a 400px box while the transcript below it had room to spare. It
       takes most of the column and nothing inside it scrolls except the two boxes that should. */
    body[at="agent"] #fileview.commitmode {
      max-height:min(78%, 78vh); display:flex; flex-direction:column; min-height:0; overflow:hidden;
    }
  }
  /* One thing at a time where there is only room for one.
     The card slot sits above the transcript, which is right on a screen wide enough for both and
     wrong on a phone: opening a file put 0px of conversation on screen and 1636px of page under
     the thumb, with the file's own Save five hundred pixels above the composer. Below the pane
     breakpoint the strip switches between them — the conversation is one of its tabs — and the
     other one is not drawn. */
  /* The workspace is a list you drill into, one card at a time.
     Both cards stacked meant the git card began under a tree of forty names and nobody scrolled to
     it — and the guide is plain about the width: "Compact and medium breakpoints: A single pane
     works best", with list-detail's compact form being the list and the detail taking turns. */
  @media (max-width:52.4375em) {
    #files[data-shows="files"] .pane-git,
    #files[data-shows="git"] .pane-files { display:none; }
    /* The tree, with git one row above it — and when git is what is showing, the way back is that
       row's place. Two secondary tabs used to live here, one of them repeating the word on the
       primary tab above it. */
    /* No margin on the child: the column already has a 24dp gap, and the two together made 36 —
     which is not on the 8dp scale and is not what either of them meant. */
    /* The way back sits at the leading edge of the bar it is on, before the path it leaves. */
    .fileback { flex:none; }
  }
  /* The Shared destination's own switch — experience, and the servers. One layer of tabs on a
     screen that has no others, which is what tabs are for; the destinations moved to the bar at
     the foot of the phone, so nothing sits above this. */
  /* A tab strip's own anatomy: the window's width, equal shares, and a divider under it.
     Two tabs sitting at their content width used 215px of a 358px strip inset 16px from each edge,
     with no line separating them from what they switch — three of the things the guide names in
     one sentence about the container. */
  .onetabs {
    display:flex; gap:0;
    margin-inline:calc(-1 * var(--magi-sys-space-200));
    border-bottom:1px solid var(--magi-ref-outlineVariant);
  }
  .onetabs > * { flex:1 1 0; min-width:0; }

  /* Above the breakpoint there is room for both cards and nothing to switch between. */


  /* The pane handles are what the tabs are, below the breakpoint. Two controls for one thing is a
     screen that cannot say which of them is the truth — and it showed: with the files pane toggled
     shut, its tab held nothing at all. */
  @media (max-width:52.4375em) {
    body[at="agent"] #filesToggle, body[at="agent"] #sideToggle { display:none; }
  }

  /* How many words arrived on the other half. A badge is the guide's own answer to "something
     happened over there": the large one, 16dp, label-small, in the error role — which is the only
     place this page uses that role for something that is not an error, because "unread" is what a
     badge IS and inventing a second colour for it would be inventing a second badge. */
  .tabbadge {
    display:inline-flex; align-items:center; justify-content:center; margin-left:var(--magi-sys-space-100);
    min-width:16px; height:16px; padding:0 4px; border-radius:var(--magi-sys-shape-full);
    background:var(--magi-ref-primary); color:var(--magi-ref-bg);
    font:500 var(--md-sys-typescale-label-small-size)/1 var(--magi-ref-display); letter-spacing:.02em;
  }

  /* The box for talking to the companion stays on both halves, and gets smaller on the one it is
     not the point of.
     Hiding it while somebody edits a file was wrong: telling the agent something WHILE reading its
     work is half of why the file is on the screen at all, and the model reading over your shoulder
     answers into the same conversation. So on the workspace half of a stacked companion the
     composer keeps its line and gives up what belongs to the other half — the interrupt, which is
     about a turn you are watching, not about the file you are in. */
  @media (max-width:52.4375em) {
    body[at="agent"][panel="state"] #stop,
    body[at="agent"][panel="facts"] #stop,
    body[at="agent"][panel="files"] #stop,
    body[at="agent"][panel="plan"] #stop { display:none; }
    /* The bar where the companion is ASKING something stays on every screen. That is a question
       waiting for an answer, and hiding it behind a tab is how a fleet stalls. (What is RUNNING
       used to be hidden here as well, because the dock held it and the dock is 535px of a 390x844
       phone. It is in the pane now, which is the same answer said once instead of per screen.) */
  }

  @media (min-width:52.5em) {
    #agentview {
      /* The conversation has a floor, and the panes give way to it.
         Both panes were fixed tracks, so the middle took the whole cost of opening one: at 1280 the
         conversation went from 1088px to 736 with the state pane, and to 448 — fifty-three
         characters a line — with both. The panes are reference; this is the page. So the middle
         asks for 34rem before either of them gets its full width, and what is left is shared
         between them. Dragging a pane wider still works: this is the floor under the automatic
         arrangement, not a cap on what somebody chooses.

         The panes have a floor of their own, or the rule above would starve them: at 1024 with
         both open they fell to 144px each, which is a column too narrow to read a filename in.
         A pane dragged narrower than its floor keeps the width it was given — the min() takes
         whichever is smaller.

         The middle's floor is 20rem and NOT the 33 it asks for above.
         11 + 33 + 11 rem is 880px, and this layout begins at 840 — where the rail and the page's
         margins leave 696. The tracks did not shrink to fit; they overflowed a box whose overflow
         is hidden, so with both panes open at 840 the trailing pane was drawn from x=824 to 1000
         in an 840px window: 160px of it outside the screen, with no scrollbar to reach it, and
         elementFromPoint answering null. Measured at every width to 1000, where it finally came
         inside. A narrow transcript is what somebody asked for when they opened both panes; a pane
         off the edge of the window is not. */
      grid-template-columns:
        minmax(min(11rem, var(--magi-comp-files-w)), var(--magi-comp-files-w))
        minmax(min(20rem, 100%), 1fr)
        minmax(min(11rem, var(--magi-comp-side-w)), var(--magi-comp-side-w));
      align-items:start;
      /* No column gap, because a gap is drawn between tracks whether or not the track has any
         width — so with both panes shut the conversation sat 24px in from each edge while the
         composer, fixed to the page, sat where the page's content starts. The two were out of line
         by exactly that gap, and what a reader sees is the footer indented under everything above
         it. The gutter is put on the middle column instead, on the side where a pane actually is. */
      column-gap:0;
      /* The pane opens and closes rather than appearing and vanishing. Both tracks are lengths so
         the change interpolates — with auto on the shut side there is nothing to interpolate to,
         which is why this was a jump. */
      transition:grid-template-columns var(--magi-sys-dur-medium2) var(--magi-sys-ease-emphasized);
    }
    /* The handle travels WITH the pane it opens, in a column of their own.
       It used to be a sibling of the two panes, and a grid places children in order: the handle
       took the second column's first row and the pane it opens was pushed onto the next row, under
       the conversation, at full width. Which is not a side pane at all — it was the conversation
       with its reference material dumped underneath, and the plan somebody opened the pane to see
       was off the bottom of the page. Shut, the handle stays where the pane was, which is the one
       place somebody looks for the way back. */
    /* The facts stay put while the conversation scrolls: on this page they are the thing you keep
       glancing back at, and a plan that scrolls away is one you re-find rather than read. */
    /* Stuck, and bounded. A sticky pane does not take main's bottom padding — that space is
       reserved for the composer and the pane sat over it, so the last control in the facts was
       three pixels under the dock and the bottom of its 48dp reach was not pressable. It ends
       where the dock begins and scrolls inside itself if there is more than fits. */
    /* The column is bounded and does not scroll; the CARDS inside it do. The handle sits above
       them and stays put — with the scroll on the column it was the first thing inside the
       scrolling box, so the scrollbar ran down the side of the one control that is always there. */
    #sidecol {
      position:sticky; top:5.5rem;
      /* The 5.5rem is where it STICKS; unstuck it starts lower — below the masthead's own block —
         so a cap measured from the stuck position is short by that difference at the top of the
         page, which is exactly where somebody reads it. The extra 3rem covers it.
         No room left for the dock: it is inset between the two panes now. */
      max-height:calc(100dvh - 5.5rem - 3rem - var(--magi-sys-space-300));
    }
    #side {
      flex:1 1 auto; min-height:0;
      /* Pinned to the pane's full width, so the column narrowing past it CLIPS rather than
         reflows. Left to shrink, every line in the pane re-wrapped on the way through — the
         content got taller as it got narrower and a scrollbar flashed in and out for the length
         of the movement, which is the one thing an animation must not do. */
      /* Pinned, but never wider than the column it is in. The pin is what stops every line in the
         pane re-wrapping while the column animates open or shut — content getting taller as it
         gets narrower, with a scrollbar flashing in and out for the length of the movement. What
         it must not do is hide half the pane when the column is at its floor: at 1024 the column
         is 176px and the pane was still 352, so 215px of it was off the side of the screen with
         no way to reach it. The max() keeps the pin during the animation, where 100% is on its
         way to zero, and the min() gives the column its say when the column is what is small. */
      width:max(11rem, min(var(--magi-comp-side-w), 100%)); box-sizing:border-box;
      /* Down, never sideways. A session id and a workspace path are long unbreakable strings, and
         with overflow:auto they gave this column a horizontal scrollbar — drawn across the bottom
         of the pane, scrolling content that has somewhere better to go: the cards wrap it. A
         sideways scrollbar in a 22rem column is a control nobody wants. */
      overflow:hidden auto; overscroll-behavior:contain;
    }
    /* A way out. Whichever it is — a fixed pane or a side sheet — the guide asks for one: a fixed
       pane gets a handle that collapses it, a side sheet "requires that a close affordance is
       always present". Without it nobody can tell whether the pane is transient or permanent, and
       the conversation cannot have the width back. */
    /* Shut, the second column is only as wide as the handle. */
    /* 2.5rem is the handle: the column shrinks to exactly the control that reopens it. */
    /* Shut is nothing at all now. The 2.5rem was the width of a handle standing in the column, and
       the handles are in the masthead: a shut pane that still held a 40px strip was 40px of nothing
       down each side of the conversation. */
    /* The gutter belongs to whoever is standing in the middle column, and one level in that is not
       #stream — it is the decision screen, which REPLACES it (the transcript is hidden while a
       report or a verdict is open, so the section takes its track). Named for #stream alone, the
       report ran flush against an open pane on whichever side was open, while the same screen with
       both panes shut looked right. Same two rules, same column, both occupants. */
    body[files="open"] #stream,
    body[files="open"] #agentdetail { padding-inline-start:var(--magi-sys-space-300); }
    body[side="open"] #stream,
    body[side="open"] #agentdetail { padding-inline-end:var(--magi-sys-space-300); }
    body[side="shut"] #agentview {
      grid-template-columns:
        minmax(min(11rem, var(--magi-comp-files-w)), var(--magi-comp-files-w))
        minmax(min(33rem, 100%), 1fr) 0;
    }
    body[files="shut"] #agentview {
      grid-template-columns:0 minmax(min(33rem, 100%), 1fr)
        minmax(min(11rem, var(--magi-comp-side-w)), var(--magi-comp-side-w));
    }
    body[files="shut"][side="shut"] #agentview { grid-template-columns:0 minmax(0, 1fr) 0; }
    /* The same column the state pane is, mirrored. Sticky is the fallback for a browser without
       dvh or without the shell above applying; where the shell does apply it overrides both of
       these, and the column is simply as tall as the shell. */
    #filecol {
      position:sticky; top:5.5rem;
      max-height:calc(100dvh - 5.5rem - var(--magi-sys-space-300));
      overflow:hidden;
    }
    #files {
      flex:1 1 auto; min-height:0; box-sizing:border-box;
      /* Same pin, same escape: see #side. */
      width:max(11rem, min(var(--magi-comp-files-w), 100%));
      /* The COLUMN does not scroll; the cards in it do. A scrollbar out here would scroll the two
         together, which is the thing having two cards is for. */
      overflow:hidden;
      transition:opacity var(--magi-sys-dur-short4) var(--magi-sys-ease-standard), visibility 0s;
    }
    body[files="shut"] #files {
      opacity:0; visibility:hidden;
      transition:opacity var(--magi-sys-dur-short4) var(--magi-sys-ease-standard),
                 visibility 0s var(--magi-sys-dur-medium2);
    }
    /* Clipped by the column as it narrows, and faded out on the way. Not display:none: a box that
       stops existing cannot be animated, and the pane would vanish a fifth of a second before the
       space it occupied closed. Hidden at the END, so nothing inside it is tabbable once it is
       gone — visibility is delayed by the length of the movement, and comes back instantly. */
    #sidecol { overflow:hidden; }
    #side { transition:opacity var(--magi-sys-dur-short4) var(--magi-sys-ease-standard), visibility 0s; }
    body[side="shut"] #side {
      opacity:0; visibility:hidden;
      transition:opacity var(--magi-sys-dur-short4) var(--magi-sys-ease-standard),
                 visibility 0s var(--magi-sys-dur-medium2);
    }
    /* And the conversation actually takes the room.
       The column widened and the transcript did not: it is capped at the wide measure, and on any
       screen big enough to show a 22rem pane that cap was already the binding constraint — so
       collapsing the pane moved the right-hand margin and nothing else. Measured at a 1617px
       window: 874px of text with the pane open, 874px with it shut — not a pixel. It is 1268px
       now.
       Closing the pane is somebody asking for the width, so it is given, up to the page's own
       bound. Unbounded it would run to 300 characters on a large monitor, which is not a wider
       conversation — it is one nobody can read a line of. */
    /* One cap, not two. Tying it to the pane tightened it from 170ch to 108ch at the very moment
       the pane took the room away, so opening the side pane narrowed the conversation TWICE: the
       column lost the pane's width and then the text lost 91px more to a cap that only bit once
       the column was small. Measured at 1512: column 120..1121, text 120..1030. The pane does the
       narrowing; the cap is only here to stop a line running to 300 characters on a big monitor,
       which is what it still does with the panes shut. */
    /* Only where there is a pane to toggle. On the fleet list there is none, and a control that
       opens nothing is a control somebody presses once and distrusts afterwards. */
    /* flex:none, because the column it sits in is a flex column and the pane beside it wants the
       height: the toggle was squeezed to 40x24 — an icon button is 40dp square, with a 48dp target
       around it, and 24 is neither. Measured on the live page before this line existed. */

  }

  /* There is no list beside a companion.
     There was: at 1200px the roster took an 18rem column on the left and the companion filled the
     rest, the shape the guide calls list-detail. Two reasons it is gone, and the second settles it.
     A roster grouped by team, badged with who is waiting and sorted so the loudest group rises, is
     a monitoring view — and there is a screen whose whole job is that. And it is the screen you
     came from: reproducing it in a column beside the thing it led you to is drawing the previous
     step over the current one. The way back is the way in. */

  /* What is running beside the turn: one chip per thing, wrapping rather than scrolling. A card in
     the pane like the plan and the queue beside it — it was a strip in the dock, on screen at every
     width whatever you were doing, and the dock is not where a thing you consult belongs. */
  /* Chips, to the spec rather than to the eye.
     The guide gives one set of measurements for all four chip variants and this had none of them:
     a pill instead of the 8dp corner, whatever height the text made instead of 32dp, and paddings
     picked from the spacing scale rather than from the component. 8dp between chips, 8dp between
     the elements inside one, 16dp at the ends — 8dp on the end that has the leading dot, which is
     what the guide calls "left/right padding with icon". */
  /* A pane card, laid out like the others: the heading on its own line, the chips wrapping under
     it. The ground and the fade that used to be here belonged to the dock — the chips sat in its
     transparent top edge and the transcript scrolled through them. There is no edge to sit in now.

     A GRID, and the chips in a box of their own. As a flex row the heading and the chips shared a
     line: the heading took 146px of a 352px card and the chips ran off the right edge, clipped
     mid-word. `flex-basis:100%` on the heading did not save it — this card is one column of
     things, which is what the plan and the queue beside it are, and saying so is shorter than
     making a row behave like one. */
  #strip:not([hidden]) { display:grid; gap:var(--magi-sys-space-100); }
  #strip > .k { margin-bottom:var(--magi-sys-space-50); }
  .stripjobs { display:flex; flex-wrap:wrap; gap:var(--magi-sys-space-100); min-width:0; }
  /* Scoped to the strip. A scheduled job in the pane is also .job — an unscoped rule here gave
     every row of the schedule a chip's height and border, which is a list turned into a row of
     pills by a name collision. */
  #strip .job {
    display:inline-flex; align-items:center; gap:var(--magi-sys-space-100);
    height:2rem; box-sizing:border-box;
    padding-inline:var(--magi-sys-space-200);
    border:1px solid var(--magi-ref-outlineVariant); border-radius:var(--magi-sys-shape-s);
    font:var(--magi-sys-body-s) var(--magi-ref-mono); color:var(--magi-ref-muted);
    background:transparent; max-width:100%; min-width:0;
  }
  /* The leading dot is this chip's icon, and the guide narrows the padding on that side for one. */
  #strip .job:has(.jdot) { padding-inline-start:var(--magi-sys-space-100); }
  /* The pressable one is the page's own button component, so it brings its own shape, ripple and
     focus ring; what it takes from here is the size, because a strip of full-size buttons would be
     a toolbar and this is a status line. */
  /* The pressable one is the page's own button component, which brings the ripple, the focus ring
     and the shape. A rule on the host can only draw a box AROUND it — which is exactly the outline
     the other chips have, so the two sit together instead of one looking like a stray link. */
  /* The pressable one is the same chip, with the states the guide asks a pressable thing to have:
     a state layer at .08 on hover and .12 on press, over the container. */
  /* By class, never by tag: the page styles its hand-built controls by what they are for, and a
     tag selector here would be the one rule that names an element the markup does not have. */
  #strip .job.press {
    cursor:pointer; position:relative; overflow:hidden;
    /* A button element brings the platform's own font and background with it, and neither is this
       page's. The chip rule above sets the box; this undoes what the element assumed. */
    background:transparent; font:inherit; font-size:inherit; color:inherit; text-align:left;
  }
  #strip .job.press::after {
    content:''; position:absolute; inset:0; border-radius:inherit; pointer-events:none;
    background:currentColor; opacity:0; transition:opacity var(--magi-sys-dur-short2) var(--magi-sys-ease-standard);
  }
  #strip .job.press:hover::after { opacity:.08; }
  #strip .job.press:active::after, #strip .job.press:focus-visible::after { opacity:.12; }
  #strip .job.press:hover { color:var(--magi-ref-primary); border-color:var(--magi-ref-primary); }
  /* The pressable chip is one text node, so the ellipsis goes on the chip itself. Without it the
     words wrapped inside a box fixed at 32dp and came out cut through the middle of a line —
     measured on the demo, in the card this now lives in. */
  #strip .job.press { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; display:block;
    line-height:calc(2rem - 2px); text-align:start; }
  /* One line each, and the name keeps its space before the tail gives way.
     In the dock this chip sat on a bar as wide as the page; in a 352px card the label wrapped
     inside a box fixed at 32dp and came out cut through the middle of a word — measured on the
     demo, two lines of text in a one-line chip. The name is what identifies the thing running, so
     it is the last to be shortened; the tail (what it last said) is what gives way. */
  #strip .job .jname {
    color:var(--magi-ref-fg); font-weight:600; flex:0 1 auto; min-width:0;
    overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  }
  #strip .job .jsay {
    flex:1 1 auto; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0;
  }
  /* Finished, and quieter for it — but not below what the muted role can carry: this text is the
     only thing saying which command it was. */
  #strip .job.done { opacity:.75; }
  #strip .job.bad { border-color:var(--magi-ref-error); color:var(--magi-ref-error); }
  /* Running, on the chip that cannot hold a dot — the pressable one, whose label is one string —
     said by its outline, which breathes the same way the dot does. */
  /* The OUTLINE breathes, not the chip: animated as whole-chip opacity, the label rested at
     2.79:1 (dark) and 2.18:1 (light) twice every 1.6s, forever — an infinite animation has no
     "on the way somewhere", its trough is a resting state. The words never dim now. */
  #strip .job.live:not(:has(.jdot)) { border-color:var(--magi-ref-primary); animation:jbreathe 1.6s ease-in-out infinite; }
  @media (prefers-reduced-motion:reduce) { #strip .job.live:not(:has(.jdot)) { animation:none; } }
  @keyframes jbreathe {
    0%, 100% { border-color:color-mix(in srgb, var(--magi-ref-primary) 35%, transparent); }
    50% { border-color:var(--magi-ref-primary); }
  }
  /* Running, said by something that moves: a strip of still chips does not distinguish the build
     that is going from the one that ended four minutes ago. */
  /* The dot breathes in colour, like the border above: opacity was how the WHOLE chip once
     pulsed, and the contrast test now refuses any forever-animation of opacity outright — colour
     cannot dim a label that happens to share the rule. */
  #strip .job .jdot {
    width:.5rem; height:.5rem; border-radius:var(--magi-sys-shape-full); background:var(--magi-ref-primary); flex:none;
    animation:jdotpulse 1.6s ease-in-out infinite;
  }
  @keyframes jdotpulse {
    0%, 100% { background-color:color-mix(in srgb, var(--magi-ref-primary) 35%, transparent); }
    50% { background-color:var(--magi-ref-primary); }
  }
  @media (prefers-reduced-motion:reduce) { #strip .job .jdot { animation:none; } }


  #stream, #side, #sidecol, #files, #filecol { min-width:0; display:flex; flex-direction:column; gap:var(--magi-sys-space-300); }
  /* The columns hold panes and nothing else now. That gap was the space between a handle and the
     pane under it; with the handles in the masthead it was a strip of nothing at the top of both
     columns, and the cards no longer lined up with the conversation beside them. */
  #sidecol, #filecol { gap:0; }
  #sidecol, #filecol { position:relative; }
  /* And the column with it: a grid track holding one hidden control still takes its gap, which on
     the list screen is a 24px hole down the leading edge of the page. #sidecol goes with it — the
     Going-on aside belongs to a companion, and refreshSideToggle draws its "nothing is going on"
     empty card unconditionally, so off a companion it hung a stray card (and an extra labelled
     complementary landmark) off the bottom of the fleet/board/skills lists. */
  body:not([at="agent"]) #filecol, body:not([at="agent"]) #sidecol { display:none; }
  #side #plan, #side #handoffs { max-width:none; }

  /* ── a panel you drill into, on a phone ──────────────────────────────────── */
  /* A list of what the panel holds, each row opening as its own screen. It replaced a strip of up
     to five secondary tabs under the four primary ones: the guide gives secondary tabs for a second
     layer, and in the same breath says never more than four in a strip. The outer four are already
     at that ceiling. */
  .panelist { display:grid; gap:var(--magi-sys-space-100); }
  /* A tappable tile, not a divider row. It was a full-width button with a transparent ground and a
     hairline under it — indistinguishable from a heading, so the one press this screen offers
     "누르는것처럼 생기지도 않았다". Now it carries a resting surface and a rounded box the way a card
     does, a leading mark for what it is (the changes behind it), and a state layer for the press —
     an M3 list item that navigates: [mark] word · count [›]. */
  .panelrow {
    display:grid; grid-template-columns:auto 1fr auto auto; align-items:center;
    gap:var(--magi-sys-space-150); min-height:56px;
    padding:var(--magi-sys-space-150) var(--magi-sys-space-200);
    border:0; border-radius:var(--magi-sys-shape-m); background:var(--magi-ref-surface-container-high);
    text-align:start; font:inherit; color:var(--magi-ref-fg); cursor:pointer; width:100%;
    position:relative; overflow:hidden;
  }
  /* State layer, the on-surface colour over the tile at the guide's opacities — not a colour swap. */
  .panelrow::after {
    content:''; position:absolute; inset:0; border-radius:inherit; pointer-events:none;
    background:currentColor; opacity:0; transition:opacity var(--magi-sys-dur-short2) var(--magi-sys-ease-standard);
  }
  .panelrow:hover::after { opacity:.08; }
  .panelrow:active::after, .panelrow:focus-visible::after { opacity:.12; }
  .panelrow .panelmark { width:1.25em; height:1.25em; color:var(--magi-ref-muted); flex:none; }
  .panelrow .panelword { font-size:var(--md-sys-typescale-body-large-size); }
  .panelrow .panelcount {
    font:600 var(--md-sys-typescale-label-small-size)/1.5 var(--magi-ref-mono);
    color:var(--magi-ref-muted);
  }
  .panelrow .panelgo { color:var(--magi-ref-muted); flex:none; }
  .panelback { display:flex; }

  /* ── the palette ─────────────────────────────────────────────────────────── */
  /* A field and a list of what it found. The list scrolls inside the dialog rather than growing it:
     a dialog that resizes as you type moves the row under the cursor while you are aiming at it. */
  #palDialog md-outlined-text-field { width:100%; }
  /* The cap is what is LEFT, not a round number. At min(50vh, 22rem) the list took 352px, the
     field above it took 88, and the dialog gives its content 408 — so the content was 32px too
     tall and md-dialog's own scroller started scrolling behind the list's. Two scrollbars in one
     dialog, one of them moving a field the palette exists to keep still.

     Measured (2026-08-20, viewport 757): scroller clientHeight 408, content 440. md-dialog caps
     itself at min(560px, 100dvh - 48px) and spends 152 of it on headline and actions, and the
     field block inside the scroller costs 88 — so the list may have

       min(560, 100dvh - 48) - 152 - 88  =  min(320px, 100dvh - 288px)

     which is what is written below. Verified in the browser: the dialog's own scroller goes to
     zero overflow and the list still scrolls, which is the one scrollbar this should have.

     ⚠ The two constants are md-dialog's chrome and this dialog's own field. A flex column that
     simply absorbed the leftover would need no numbers, and does not work here — `height:100%` on
     the slotted content does not resolve against a scroller bounded by max-height (tried: the list
     grew to 584 and the overflow got worse), and md-dialog exposes no ::part to reach the scroller
     with. If either constant changes, this is where it shows. */
  #palList {
    display:grid; gap:var(--magi-sys-space-50); margin-top:var(--magi-sys-space-200);
    max-height:min(20rem, calc(100dvh - 18rem)); overflow-y:auto;
  }
  .palrow {
    display:grid; grid-template-columns:5.5rem 1fr; align-items:baseline;
    gap:var(--magi-sys-space-150) var(--magi-sys-space-200);
    /* 48dp: this is a list of things one press away, which is the size the guide gives a list item
       and a touch target both. */
    min-height:48px; padding:var(--magi-sys-space-100) var(--magi-sys-space-200);
    border:0; border-radius:var(--magi-sys-shape-s); background:transparent; cursor:pointer;
    text-align:start; font:inherit; color:var(--magi-ref-fg); width:100%;
  }
  /* The row the keyboard is on, and the row the pointer is on, are the same state drawn one way. */
  /* Hover is a layer and selection is a fill, so the two can be told apart AND stack.
     They were one rule — the same tonal swap for both — which read wrong twice: in dark the fill
     was a step DOWN the elevation scale, so hovering made a row darker than the sheet it sits on,
     and with the pointer anywhere on the list there was no telling which row Enter would run,
     because hover and selection were the same pixel value. A state layer over its own surface is
     what the guide gives hover; selected keeps its fill and takes the layer on top like anything
     else ("상태는 겹칠 수 있다 — 선택 + 호버"). */
  .palrow:hover { background:color-mix(in srgb, var(--magi-ref-fg) 8%, var(--magi-ref-surface-container-high)); }
  .palrow:active { background:color-mix(in srgb, var(--magi-ref-fg) 12%, var(--magi-ref-surface-container-high)); }
  /* A tint of the role whose outline already marks it — not a lower tonal surface, which in dark
     inverted the elevation order. */
  .palrow.at {
    background:color-mix(in srgb, var(--magi-ref-primary) 12%, var(--magi-ref-surface-container-high));
    outline:2px solid var(--magi-ref-primary); outline-offset:-2px;
  }
  .palrow.at:hover {
    background:color-mix(in srgb, var(--magi-ref-fg) 8%,
               color-mix(in srgb, var(--magi-ref-primary) 12%, var(--magi-ref-surface-container-high)));
  }
  .palrow.at:active {
    background:color-mix(in srgb, var(--magi-ref-fg) 12%,
               color-mix(in srgb, var(--magi-ref-primary) 12%, var(--magi-ref-surface-container-high)));
  }
  .palrow .palkind {
    font:600 var(--md-sys-typescale-label-small-size)/1.5 var(--magi-ref-mono); letter-spacing:0.06em;
    color:var(--magi-ref-muted); text-transform:lowercase;
  }
  .palrow .palname { font-size:var(--md-sys-typescale-body-medium-size); overflow-wrap:anywhere; }
  .palrow .palhint {
    grid-column:2; font:var(--md-sys-typescale-label-small-size)/1.4 var(--magi-ref-mono);
    color:var(--magi-ref-muted); overflow-wrap:anywhere;
  }
  #palNone { margin-top:var(--magi-sys-space-200); color:var(--magi-ref-muted); }

  /* ── one level in: a verdict, or a child ─────────────────────────────────── */
  /* A page, not a card: what is in here is a transcript or the material a vote stood on, and both
     are things you scroll and copy out of. It takes the SAME width as the conversation it was
     opened from — the cap below and the wide-screen rule further down are both shared with #log.
     They were not: this kept 108ch while the transcript was later widened to the page, so opening
     a decision from a conversation narrowed the column under the reader for no reason they could
     see. The prose inside still keeps its own measure (.dbody, .gsec), which is where a reading
     width belongs. */
  #agentdetail { max-width:var(--magi-sys-page); display:flex; flex-direction:column; gap:var(--magi-sys-space-200); min-width:0; }
  #agentdetail .dhead { display:flex; align-items:baseline; gap:var(--magi-sys-space-200); flex-wrap:wrap; }
  #agentdetail .dwho { font:600 var(--md-sys-typescale-title-medium-size)/1.4 var(--magi-ref-display); color:var(--magi-ref-primary); }
  #agentdetail .dchip, .dinsp .dchip { font:var(--magi-sys-body-s) var(--magi-ref-mono); color:var(--magi-ref-muted); }
  /* Second scope: .dinsp is the same content in a CARD rather than on the detail screen — the
     new console opens a verdict beside the transcript instead of one level in. These rules were
     written against an id, so in the card every one of them missed: measured, .dk there computed
     to no font at all and the body colour, which is a heading that looks exactly like the text
     under it. Same cells, same look, wherever the surface chose to put them. */
  /* The section headings. Small and quiet: they are labels on the things below them, and the
     things below them are the content. */
  #agentdetail .dk, .dinsp .dk {
    font:600 var(--md-sys-typescale-label-small-size)/1.9 var(--magi-ref-mono);
    letter-spacing:.05em; color:var(--magi-ref-muted); text-transform:none;
  }
  /* The evidence heading leads, because the order is the argument: what was judged, then the
     judgement. A verdict read first can only be believed. */
  #agentdetail .dhero, .dinsp .dhero { color:var(--magi-ref-accent); border-top:1px solid var(--magi-ref-outlineVariant); padding-top:var(--magi-sys-space-200); }
  #agentdetail .dbody, .dinsp .dbody { max-width:var(--magi-sys-measure); overflow-wrap:anywhere; }
  #agentdetail .dbody pre, .dinsp .dbody pre {
    font:var(--magi-sys-body-s) var(--magi-ref-mono); white-space:pre; max-width:100%;
    overflow-x:auto; overscroll-behavior-x:contain; background:var(--magi-ref-surface);
    padding:var(--magi-sys-space-150); border-radius:var(--magi-sys-shape-xs);
  }
  #agentdetail .dnote, .dinsp .dnote { color:var(--magi-ref-muted); font:var(--magi-sys-body-s) var(--magi-ref-mono); }
  #agentdetail .dseen, .dinsp .dseen { display:flex; flex-direction:column; gap:var(--magi-sys-space-100); }
  /* The decision's own controls, at the size a decision deserves. The bar under the transcript
     keeps its compact ones; this is the screen somebody opened BECAUSE the compact ones were not
     enough to answer from. */
  #agentdetail .askact { display:flex; flex-wrap:wrap; gap:var(--magi-sys-space-200); align-items:center; }
  #agentdetail .askact .answer { display:flex; flex-wrap:wrap; gap:var(--magi-sys-space-150); align-items:center; width:100%; }
  #agentdetail .askact .answer.choices { justify-content:center; }
  #agentdetail .askact md-outlined-text-field { flex:1 1 22rem; min-width:0; }
  /* The grounds on this screen are the content, not an aside.
     The section name goes in a gutter to the left, which is the shape this page uses everywhere a
     short label stands beside something being read — the transcript's speakers, the facts card,
     the strip's own keys. Stacked above the text it took a line each and pushed the reasoning down
     the screen it exists to be read on; beside it, the whole report is one scan.
     The text keeps the page's reading measure. It is not narrower here than in the dock — the
     gutter is outside it — and it is not wider, because 74 characters is where prose stops being
     comfortable and a decision screen is not the place to prove otherwise. */
  #agentdetail .grounds { display:grid; grid-template-columns:1fr; gap:var(--magi-sys-space-150); max-width:none; }
  #agentdetail .grounds .gsec {
    display:grid; grid-template-columns:7rem minmax(0, var(--magi-sys-measure));
    gap:var(--magi-sys-space-150) var(--magi-sys-space-300); align-items:baseline;
  }
  /* Centred in the gutter, not pushed against the text. Right-aligned, each label ended at a
     different distance from its own word depending on how long the label was — "tried" and
     "recommendation" sat on two different left edges, and the column of keys had no edge of its
     own to read down. Centred, the gutter reads as one column. Below 840 the label stacks ABOVE
     the text rather than beside it (further down), and there it stays left, where a line of text
     starts. */
  #agentdetail .grounds .gk {
    font:600 var(--md-sys-typescale-label-small-size)/1.9 var(--magi-ref-mono);
    letter-spacing:.05em; color:var(--magi-ref-accent); text-align:center;
  }
  /* 839, not 640. 40em is a number nobody's scale has — it fires in the middle of the medium
     class, which M3 treats as one thing. The measurement behind it was taken at 390px and the
     band it belongs to is "one pane", which ends at 840. */
  @media (max-width:52.4375em) {
    #agentdetail .grounds .gsec { grid-template-columns:1fr; gap:var(--magi-sys-space-50); }
    #agentdetail .grounds .gk { text-align:left; margin-top:var(--magi-sys-space-100); }
  }
  #agentdetail .grounds .gv { font-size:var(--md-sys-typescale-body-medium-size); overflow-wrap:anywhere; }
  #agentdetail .dlog { container-type:inline-size; container-name:transcript; min-width:0; }
  /* A mark in a gutter: the tick beside a finished step, the caret beside the one in hand. Sized
     to the line it labels rather than to the icon's own idea of a size. */
  .mk { width:.9em; height:.9em; }
  /* A heading's mark needs the space a word after it would have had; a mark inside a button sits
     beside text that brought its own. */
  .hk { margin-inline-end:.4em; }

  /* The panel button draws what the panel is doing.
     The divider sits where the panel's edge is when it is open, and slides out past the frame's
     right side when it shuts — the same curve and the same 200ms the pane fades in, so the two
     read as one movement rather than a button that changed after the fact. Clipped by the frame, so "out" is genuinely gone rather than a line hanging in space. */
  .panelic { overflow:hidden; }
  .panelic .split {
    transition:transform var(--magi-sys-dur-short4) var(--magi-sys-ease-emphasized);
  }
  /* Each handle draws ITS OWN pane. One rule drove both: the state pane shutting slid the divider
     in the file pane's icon as well, and the file pane's own state moved nothing at all — so the
     left-hand button was a readout of the right-hand pane.
     And they slide opposite ways, because the two icons are mirror images: the leading pane's
     divider leaves past the left edge, the trailing one past the right. */
  body[side="shut"] #sideToggle .panelic .split { transform:translateX(5.6px); }
  body[files="shut"] #filesToggle .panelic .split { transform:translateX(-5.6px); }
  /* The one icon that turns, and only while something is actually running — a spinner on a still
     screen is a screen pretending to work. Reduced motion is handled globally: the rule there
     restricts transitions and animations to a hair, which stops this without a second switch. */
  .spin { animation:spin 1.1s linear infinite; }
  @keyframes spin { to { transform:rotate(360deg); } }

  /* An icon from the sprite. Sized by the text it sits in rather than in pixels, so it grows with
     a reader who raised their default, and coloured by the line — every symbol is drawn with
     fill:currentColor, which is the whole reason a two-tone icon was not worth a third file.
     ⚠ Not .ic: that class is the rail's, is older, and belongs to svgs sized by a width attribute.
     A rule named .ic here set width:1em on those and a stylesheet beats an attribute — every rail
     icon shrank from 24px to 14, silently, in the same commit that gave them their drawings. */
  .sic { width:1em; height:1em; fill:currentColor; vertical-align:-0.125em; flex:none; }
  /* The mark on a detail heading: a shade larger than the words, and never the thing that sets the
     line's height. */
  .hic { width:1.1em; height:1.1em; }
  /* The stand-in, when this build has no sprite. Not styled to imitate an icon — it IS the mark the
     page drew before, and pretending otherwise would make a missing licence look like a bug. */
  .gl { font-variant-emoji:text; }

  /* The way one level in. A quiet control: it is beside content, not over it, and the row it sits
     under is the thing being read. */
  /* Block and fit-content, so the `auto` margins below actually do something. An auto margin was
     here from the start and `inline-block` made it inert: an inline box has no free space to distribute,
     so the margin computed to 0 and the control sat at the left edge under the report it opens.
     fit-content keeps the box the width of its word; a plain block would fill the column and put
     the border round the whole row.
     Auto on BOTH sides, so it sits in the MIDDLE of what it is a way out of. The report above it is
     a block of sections, and a control hard against either edge of that reads as belonging to the
     edge rather than to the thing it opens. The two places that want it elsewhere still say so: the
     transcript row pins it with justify-self, and the button group overrides margin and display. */
  .deeper {
    background:none; border:1px solid var(--magi-ref-outlineVariant); border-radius:var(--magi-sys-shape-xs);
    color:var(--magi-ref-primary); font:var(--magi-sys-body-s) var(--magi-ref-mono);
    padding:2px var(--magi-sys-space-150); cursor:pointer; margin:var(--magi-sys-space-50) auto 0;
    display:block; width:fit-content;
  }
  .deeper:hover { background:color-mix(in srgb, var(--magi-ref-primary) 8%, transparent); }
  .deeper:active { background:color-mix(in srgb, var(--magi-ref-primary) 12%, transparent); }
  .deeper:focus-visible { outline:2px solid var(--magi-ref-primary); outline-offset:2px; }
  /* The handle says which way it is. It carried aria-expanded from the start, so a screen reader
     was told and an eye was not: the same icon in the same colour whether the pane was open or
     shut, which makes it a button you press to find out. M3 draws a selected toggle in the primary
     colour over its container tint, which is the same treatment the rail's current destination
     gets — one vocabulary for "this is on". */
  /* BOTH handles, not one. The paragraph above was written about the state pane's handle and the
     rule was given only to it, while the workspace handle — the same control, built by the same
     paneHandle(), reporting the same aria-expanded — kept drawing identically whether its pane was
     open or shut. Two controls side by side in the masthead, saying "this is on" in two different
     vocabularies. Found by scanning the live page for adjacent buttons (2026-08-20). */
  #sideToggle[aria-expanded="true"], #filesToggle[aria-expanded="true"] {
    color:var(--magi-ref-primary);
    background:color-mix(in srgb, var(--magi-ref-primary) 14%, transparent);
    border-radius:var(--magi-sys-shape-s);
  }
  /* ── a standard button group ─────────────────────────────────────────────
     M3's standard button group: related buttons sitting together, no selection among them.
     NOT the segmented button (withdrawn in the expressive update) and NOT its replacement, the
     connected group, which the guidance reserves for toggles — "Avoid using a connected group
     when none of the buttons can be toggled." These three leave for another screen or open a
     dialog; none of them is ever the chosen one.

     No width is set. The standard group "hugs the width of the buttons inside", where the
     connected one spans its surface — so the group is as wide as its buttons and no wider.

     nowrap because a button group "shouldn't wrap to a second line". That is a real constraint in
     a narrow pane, and it is what decided the third button's label rather than the other way
     round.

     The gap is 8px, not 4. Two reasons, and neither is taste: the 48px press target each button
     carries sits centred on the button, so adjacent targets need room not to meet; and a pressed
     button grows on each side, which at a 4px gap would have it touching its neighbour before the
     neighbour had moved. */
  .bgroup { display:flex; flex-wrap:nowrap; align-items:center; gap:var(--magi-sys-space-100); }
  /* The one group allowed to wrap: the four decisions in the answer strip. M3's answer to a group
     that does not fit is an overflow menu, and a hidden "deny" is a decision somebody cannot see
     to make — on the highest-stakes control on the page, wrapping is the lesser harm. */
  .answer .bgroup, .askact .bgroup { flex-wrap:wrap; }

  /* An M3 button, at XS, outlined — read off the spec's own measurement drawing rather than
     recalled, because the third-party numbers in circulation disagree with it (one puts XS at
     36dp; the drawing says 32):

       size   height   side padding
       XS       32         12
       S        40         16
       M        56         24
       L        96         48
       XL      136         64

     Shape is Full, which is the default for every size — M3 buttons are pills unless they are
     deliberately square, and the 4px corner the plain `.deeper` carries is neither. Pressed morphs
     to 8dp, the value the spec gives for XS and S ("Both round and square buttons should have the
     same pressed shape").

     Colour is OUTLINED, and the outline is the `outline` role. This was got wrong twice before it
     was measured, and the file already held the answer both times.

     M3 asks a button's container (a fill) or its outline to reach 3:1 against the surface behind
     it — that edge is the only thing saying where the control is. The rule is CONTAINER against
     SURFACE. An earlier version of this comment quoted the same 3:1 and applied it to LABEL
     against container, which every colour passes and which was never the question. Measured
     against the page background, both themes:

       primaryContainer  #4A2E0B / #F8D9A8   1.51 : 1   1.28 : 1   FAIL   (the tonal cut)
       outlineVariant    #463E34 / #D8CFC0   1.79 : 1   1.46 : 1   FAIL   (the first cut)
       outline           #72675C / #8A7E6E   3.41 : 1   3.75 : 1   PASS

     Two other comments in this file already carried that finding — the approval buttons' and the
     one above them — both reporting a tonal container at 1.24:1 dark and 1.18:1 light and choosing
     outlined for it.

     Tonal was reached for because M3 warns an outlined button reads as a chip and this page is
     full of chips. That worry is real and it is answered by the ICONS rather than by the fill: a
     chip in this console carries a count or a state, never a leading glyph, and a wrench in front
     of a word is a button asking to be pressed. Which is the better answer anyway — it survives a
     palette change, and a contrast floor does not bend.

     The label is `muted` (on surface variant): 10.66:1 dark and 8.98:1 light against the page, far
     past the 4.5:1 text asks. NOT primary — primary on a bordered container is M3's vocabulary for
     a SELECTED toggle, and at rest all three would be claiming to be on. */
  .bgroup .deeper {
    margin:0; white-space:nowrap;
    height:32px; box-sizing:border-box;
    /* XS with a leading icon: 12 before, 4 between, 12 after — the spec drawing's own numbers. */
    display:inline-flex; align-items:center; gap:4px; padding:0 12px;
    border:1px solid var(--magi-ref-outline); border-radius:var(--magi-sys-shape-full);
    background:transparent; color:var(--magi-ref-muted);
    font:var(--magi-sys-label-l) var(--magi-ref-mono);
    transition:padding .12s ease, border-radius .12s ease;
  }
  /* 20dp, which is what the spec gives the icon at XS and S alike. currentColor so the glyph and
     the word are one thing, and flex:none so a long label never squeezes it. */
  .bgroup .deeper .sic { width:20px; height:20px; flex:none; fill:currentColor; }
  /* An outlined button's fill is invisible at rest, and its state layers behave like every other
     style's — drawn in the label's own colour over nothing. */
  .bgroup .deeper:hover { background:color-mix(in srgb, var(--magi-ref-muted) 8%, transparent); }
  /* "When a button is pressed, it changes width and shape. In a standard button group, pressing a
     button also affects the width of adjacent buttons." The shape is the 8dp morph the spec gives
     XS and S; the width is the padding, and the neighbours move because they are flex items being
     pushed — the second sentence falls out of the first rather than needing script to stage it. */
  .bgroup .deeper:active { padding:0 15px; border-radius:var(--magi-sys-shape-s);
    background:color-mix(in srgb, var(--magi-ref-muted) 12%, transparent); }
  @media (prefers-reduced-motion: reduce) { .bgroup .deeper { transition:none; } }

  .row .deeper { grid-column:2; justify-self:start; }
  #agentdetail .dlog .row .deeper { display:none; } /* one level in is as deep as it goes */

  /* ── the agent's own plan ───────────────────────────────────────────────── */
  /* Wrapped, not scrolled. A plan step is a sentence and a progress bar is a box — neither has a
     reason to be wider than the card, and in a 22rem column the two of them were producing a
     horizontal scrollbar along the bottom of the pane. min-width:0 because the card lays its rows
     out in a grid, whose tracks otherwise take their content's width as a floor. */
  #plan { max-width:var(--magi-sys-measure); min-width:0; overflow-x:clip; }
  #plan .td, #plan .what, #plan .plancount { min-width:0; overflow-wrap:anywhere; }
  /* The plan's own progress. Linear, at the top edge of the card it belongs to, spanning its
     width — where the guide puts a bar for the container that is progressing. The track is
     outline-variant, which is under 3:1 against the surface, so the spec makes the end stop
     mandatory rather than decorative. */
  #plan .planbar {
    display:block; margin:var(--magi-sys-space-50) 0 var(--magi-sys-space-50);
    /* The spec's numbers, which are also the component's defaults — set here so a change to either
       is visible as a change to this line rather than a silent drift. */
    --md-linear-progress-track-height:4px;
    --md-linear-progress-active-indicator-height:4px;
    --md-linear-progress-active-indicator-color:var(--magi-ref-primary);
    --md-linear-progress-track-color:var(--magi-ref-outlineVariant);
  }
  #plan .plancount { font:var(--md-sys-typescale-label-small-size)/1.5 var(--magi-ref-mono); color:var(--magi-ref-muted); margin-bottom:var(--magi-sys-space-100); }
  /* The five Going-on section headings share one label: plan, running (in the strip), waiting to
     run, scheduled, handed out. They were styled per container and only #plan and #handoffs had a
     rule, so the other three fell back to an unstyled .k and inherited body size — the same kind of
     heading at three different sizes down one column, which is what a reader sees as "제각각". One
     rule now, so they cannot drift apart again. */
  #plan .k, #strip > .k, #queued .k, #cron .k, #handoffs .k {
    font:600 var(--md-sys-typescale-label-small-size)/1.4 var(--magi-ref-mono); letter-spacing:0.06em;
    color:var(--magi-ref-muted);
  }
  /* Space below the heading. The strip is a grid with its own gap so it asks for less; the block
     cards carry the whole gap on the heading. */
  #plan .k, #queued .k, #cron .k, #handoffs .k { margin-bottom:var(--magi-sys-space-100); }
  .td { display:grid; grid-template-columns:1.2rem 1fr; gap:var(--magi-sys-space-100); padding:var(--magi-sys-space-50) 0; }
  .td .mark { font:var(--md-sys-typescale-label-medium-size)/1.6 var(--magi-ref-mono); color:var(--magi-ref-muted); text-align:center; }
  .td .what { font-size:var(--md-sys-typescale-body-medium-size); color:var(--magi-ref-fg); overflow-wrap:anywhere; }
  .td.completed .what { color:var(--magi-ref-muted); text-decoration:line-through; }
  .td.in_progress .mark { color:var(--magi-ref-primary); }
  .td.in_progress .what { color:var(--magi-ref-primary); }

  /* ── work handed to other companions ────────────────────────────────────── */
  #handoffs { max-width:var(--magi-sys-measure); }

  /* ── the shape of a report ───────────────────────────────────────────────── */
  /* The contract, as pairs: what a section is called, and the sentence the agent has to answer
     there. Both were the same size, the same weight and the same colour — the .f/.k/.v rules the
     card is built from are scoped to #detail and never reached this pane, so the card arrived
     unstyled and read as one paragraph of six half-sentences.
     The pair is the guide's overline-and-body: the name is a label in the variant colour, small
     and spaced, and the sentence is body text in the plain one. Two roles, so the eye can take
     the names alone down the left and stop at the one it wants. */
  /* One row per section: the name, what belongs in it, and a way to take it out. The name is short
     by nature and the sentence is not, so they are not given equal room. Aligned to the TOP, not
     the middle: the prompt is a two-line field and centring left its label floating beside the
     name's second half.
     The gaps are the guide's — 16dp between the things inside a dialog, 24dp between the blocks
     of it — rather than whatever the spacing scale's smallest step happened to be. */
  /* The two fields are one row and are drawn as one: stretched, so the short name is a box the
     same height as the sentence beside it rather than a stamp floating at the top of it. */
  .fmtrow {
    display:grid; grid-template-columns:8rem minmax(0, 1fr) 2.5rem;
    gap:var(--magi-sys-space-200); align-items:stretch;
  }
  /* Wider than a basic dialog, deliberately, and the only dialog here that is.
     M3 caps a basic dialog at 560dp and md-dialog writes that on its own :host — reachable from
     here because a page rule on the element beats a rule inside its shadow. The cap is written for
     a dialog that asks one question; this one is an editor with three columns, and at 560 the
     middle one — the sentence that tells the agent what the section is for — had about 40
     characters to say it in. 720 gives it around 60, which is where the rest of this page's prose
     sits (--magi-sys-measure). Still short of the viewport on a phone, where the subtraction wins.
     ⚠ Do not widen the others to match: they ask one thing, and 560 is right for them. */
  #fmtDialog { max-width:min(720px, 100% - 48px); }
  /* Fits the dialog rather than pushing past it. The content box does not grow with what is put in
     it — a form wider than the container is simply cut off on the right, which is where the remove
     control was: measured 608px of form inside 560px of dialog, and the last 48 were the column
     nobody could see. */
  #fmtForm {
    display:grid; gap:var(--magi-sys-space-200);
    /* An asked-for width, not a percentage. The dialog's container is fit-content, so a form at
       100% asks the container how wide it is and the container asks back — the pair settled on
       whatever the longest sentence happened to be, and raising the container's cap moved nothing.
       Naming the inline size here is what makes the dialog grow to it, up to the cap above; the
       viewport subtraction is the phone, where the cap never applies. 672 + the dialog's own
       24 either side is the 720 up there, so the two numbers stay one decision. */
    inline-size:min(672px, calc(100vw - 96px));
    padding-top:var(--magi-sys-space-100);
  }
  /* The remove control sits on the field's first line rather than in the middle of a two-line row,
     which is where the thing it removes starts. */
  .fmtdrop { align-self:start; margin-top:var(--magi-sys-space-100); --md-icon-button-icon-color:var(--magi-ref-muted); }
  .fmtdrop:hover { --md-icon-button-icon-color:var(--magi-ref-error); }
  /* A dialog's supporting text: the sentence under the headline that says what this is. Body, not
     label — it is read once, in full, by somebody deciding whether to change something. */
  .dlgsup {
    font:var(--md-sys-typescale-body-medium-size)/1.5 var(--magi-ref-display);
    color:var(--magi-ref-on-surface-variant); max-width:var(--magi-sys-measure);
  }
  /* The add control takes the width of a row, because that is what pressing it makes.
     Hugging its label it sat alone under the left column with the form's whole width beside it,
     reading as something left over rather than as the next row of the list. */
  #fmtForm > md-text-button { width:100%; }

  /* ── what has not started yet ────────────────────────────────────────────── */
  #queued { max-width:var(--magi-sys-measure); }
  .qrow {
    display:grid; grid-template-columns:1.5rem minmax(0, 1fr); gap:var(--magi-sys-space-100);
    padding:var(--magi-sys-space-100) 0; border-top:1px solid var(--magi-ref-outlineVariant);
  }
  .qrow:first-of-type { border-top:0; }
  .qn {
    font:600 var(--md-sys-typescale-label-small-size)/1.6 var(--magi-ref-mono);
    color:var(--magi-ref-muted); text-align:right;
  }
  /* Whose it is, said in the colour the page already uses for the person's own words. Order alone
     would leave a reader counting rows to work out which of these they wrote. */
  .qwho { font:600 var(--md-sys-typescale-label-small-size)/1.6 var(--magi-ref-mono); color:var(--magi-ref-accent); }
  .qrow.mine .qwho { color:var(--magi-ref-primary); }
  .qsaid { font:var(--magi-sys-body-s) var(--magi-ref-mono); color:var(--magi-ref-fg); overflow-wrap:anywhere; }

  /* ── the schedule ────────────────────────────────────────────────────────── */
  #cron { max-width:var(--magi-sys-measure); }
  #cron .job {
    display:grid; grid-template-columns:1fr auto; gap:0 var(--magi-sys-space-200);
    padding:var(--magi-sys-space-150) 0; border-top:1px solid var(--magi-ref-outlineVariant);
  }
  #cron .jname { font-weight:600; }
  #cron .jwhen { font:var(--magi-sys-body-s) var(--magi-ref-mono); color:var(--magi-ref-muted); text-align:right; }
  #cron .jnext, #cron .jask, #cron .jfile { grid-column:1 / -1; }
  #cron .jnext { font:var(--magi-sys-body-s) var(--magi-ref-display); color:var(--magi-ref-muted); }
  #cron .jask { color:var(--magi-ref-fg); margin-top:var(--magi-sys-space-50); }
  #cron .jfile {
    font:var(--magi-sys-body-s) var(--magi-ref-mono); color:var(--magi-ref-muted);
    margin-top:var(--magi-sys-space-50); overflow-wrap:anywhere;
  }
  /* Switched off reads as switched off, and never-runs reads as wrong — by more than colour, since
     a state told only in colour is a state some readers are not told. */
  #cron .job.off .jname { color:var(--magi-ref-muted); text-decoration:line-through; }
  #cron .job.broken .jnext { color:var(--magi-ref-error); }

  /* ── a meeting: who has the floor, and what was said ─────────────────────
     One column and not a chat: a meeting is read top to bottom afterwards as much as it is watched
     while it happens, and the thing a reader is looking for is who said what in which round. The
     token is the mechanism, so it is the loudest thing on the screen — everything else here is
     ordinary text at ordinary weight. */
  #meet { display:block; max-width:var(--magi-sys-page); }
  .meetbox { display:grid; gap:var(--magi-sys-space-200); }
  /* Capped to the house prose measure like .accsay: uncapped these helper sentences ran 132–163ch
     on a wide screen, past the guide's 120ch ceiling for large displays. */
  .meetwhy, .meetnote { color:var(--magi-ref-muted); max-width:var(--magi-sys-measure); }
  .meetlbl { color:var(--magi-ref-muted); margin-top:var(--magi-sys-space-100); }
  /* A heading, so it is drawn as one: the h3 default would be a browser size off the type scale
     entirely. */
  .meettopic {
    margin:0; color:var(--magi-ref-fg); font-weight:600;
    font-size:var(--md-sys-typescale-title-medium-size);
    /* Two lines, which is the most the guide gives a headline that will not fit its bar: "길면
       medium/large flexible로 바꿔 두 줄까지". This head is sticky, so an unbounded topic does not
       merely take room — measured on a 320px screen, a 300-character topic made the head 90% of
       the window, taller than the space it sticks in, and the topic scrolled off the top again.
       The whole of it is still one press away in the meeting's own record. */
    display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
  }
  .meetbar-progress { --md-linear-progress-active-indicator-color:var(--magi-ref-primary); }
  .meetmeta { color:var(--magi-ref-muted); font-size:var(--md-sys-typescale-label-medium-size); }
  .meettrouble { color:var(--magi-ref-warn); }
  /* Groups down, chips across. */
  .meetwho { display:grid; gap:var(--magi-sys-space-100); }
  /* 16dp between ROWS, like the roster next door. The set's own 8dp gap is right across a row and
     wrong down one: a 32dp chip carries a 48dp press area, so two wrapped rows 8dp apart overlap by
     8 — measured, a press three pixels under "design" selected "ops". */
  .meetwho md-chip-set { display:flex; flex-wrap:wrap; row-gap:var(--magi-sys-space-200); }
  .meetowner {
    font:var(--md-sys-typescale-label-small-size)/1.5 var(--magi-ref-mono); color:var(--magi-ref-muted);
    margin-top:var(--magi-sys-space-50);
  }
  /* A team is a colour. Four of them, cycled, and the name is in the tooltip as well — a colour
     that is the only telling is one nobody can read out, and this page has been caught doing that
     before. On the label and the outline while the chip is unselected, and as the container once
     it is picked, where the label takes the page's own ground colour: every one of these hues is
     bright against the dark theme's background and dark against the light one, so the pair holds
     both ways round. */
  .meetwho md-filter-chip.tm0 { --md-filter-chip-outline-color:var(--magi-ref-primary); --md-filter-chip-label-text-color:var(--magi-ref-primary); --md-filter-chip-selected-container-color:var(--magi-ref-primary); }
  .meetwho md-filter-chip.tm1 { --md-filter-chip-outline-color:var(--magi-ref-accent); --md-filter-chip-label-text-color:var(--magi-ref-accent); --md-filter-chip-selected-container-color:var(--magi-ref-accent); }
  .meetwho md-filter-chip.tm2 { --md-filter-chip-outline-color:var(--magi-ref-warn); --md-filter-chip-label-text-color:var(--magi-ref-warn); --md-filter-chip-selected-container-color:var(--magi-ref-warn); }
  .meetwho md-filter-chip.tm3 { --md-filter-chip-outline-color:var(--magi-ref-muted); --md-filter-chip-label-text-color:var(--magi-ref-muted); --md-filter-chip-selected-container-color:var(--magi-ref-muted); }
  .meetwho md-filter-chip[class*="tm"] {
    --md-filter-chip-selected-label-text-color:var(--magi-ref-bg);
    --md-filter-chip-icon-selected-color:var(--magi-ref-bg);
  }
  .meetends { color:var(--magi-ref-muted); max-width:var(--magi-sys-measure); }
  /* The action on its own line, centred, and wide.
     It has been every way round: flush left and small, which read as another field that had lost
     its label; pushed to the trailing end, alone at the far side of a wide screen with the note
     that explains it a screen away; flush left and wide, which on a 1385px card still left it
     hanging off the leading edge with the whole card empty beside it. Centred, it is the one
     control on the screen and sits where the eye already is at the end of the form. The note that
     says why it cannot be pressed yet sits under it. */
  .meetgo { justify-self:center; min-width:min(26rem, 100%); }
  /* And what is under it lines up with it rather than with the form's edge. */
  .meetbox .meetnote { text-align:center; }

  /* The room, as one line of chips.
     Rows first, and four participants in four full-width rows spent most of the screen saying one
     word each about their state — a state is what a colour is for. Each chip is also the control
     that calls on that participant, so the roster is where the token is moved rather than a
     read-out beside it.
     Filter chips because they are what the bundle has, and the semantic survives: the set has one
     selected member and the selected one is whoever holds the floor, so pressing another is
     changing that selection. Colour carries the state and the tooltip carries the word — the pair
     the guide asks for wherever a colour would otherwise be the only telling. */
  /* Centred. The roster is the meeting's own instrument rather than a list to read down, and the
     transcript under it is a column of text with its own left edge — chips ranged against that
     edge read as the first line of the transcript. */
  /* 16dp between ROWS, 8 between chips. The press area is 48dp around a 32dp chip, so two wrapped
     rows 8dp apart have targets that overlap by 8 — measured, and with five companions a press
     aimed at the foot of one chip called on the one below it, which spends a model turn on the
     wrong companion. The board's cards already carry this fix and its reasoning; the roster is the
     same shape and did not. */
  .meetroster { display:flex; flex-wrap:wrap; gap:var(--magi-sys-space-300) var(--magi-sys-space-100); justify-content:center; }
  /* The topic and the roster stay put while the transcript scrolls under them: five rounds of four
     companions is several screens, and both are things a reader needs at every point of it.
     Its own ground, or the lines would show through as they pass behind. */
  .meethead {
    position:sticky; top:0; z-index:2;
    display:grid; gap:var(--magi-sys-space-150);
    padding-bottom:var(--magi-sys-space-150);
    background:var(--magi-ref-bg);
    border-bottom:1px solid var(--magi-ref-outlineVariant);
  }
  /* A participant is a colour, in the roster and on everything it says. Six, cycled in the order
     they are in the room. Never the only telling: the name is on the line and on the chip, and the
     tooltip says what the state is — a colour nobody can read out is a colour that says nothing to
     a reader who cannot see it. All six are hues this page already uses as text on its own ground,
     both themes, so the pairs hold either way round. */
  .meetsp.sp0 { --md-filter-chip-outline-color:var(--magi-ref-primary); --md-filter-chip-label-text-color:var(--magi-ref-primary); }
  .meetsp.sp1 { --md-filter-chip-outline-color:var(--magi-ref-accent); --md-filter-chip-label-text-color:var(--magi-ref-accent); }
  .meetsp.sp2 { --md-filter-chip-outline-color:var(--magi-ref-warn); --md-filter-chip-label-text-color:var(--magi-ref-warn); }
  .meetsp.sp3 { --md-filter-chip-outline-color:var(--magi-ref-success); --md-filter-chip-label-text-color:var(--magi-ref-success); }
  .meetsp.sp4 { --md-filter-chip-outline-color:var(--magi-ref-error); --md-filter-chip-label-text-color:var(--magi-ref-error); }
  .meetsp.sp5 { --md-filter-chip-outline-color:var(--magi-ref-muted); --md-filter-chip-label-text-color:var(--magi-ref-muted); }
  .meetline.sp0 { --spoke:var(--magi-ref-primary); }
  .meetline.sp1 { --spoke:var(--magi-ref-accent); }
  .meetline.sp2 { --spoke:var(--magi-ref-warn); }
  .meetline.sp3 { --spoke:var(--magi-ref-success); }
  .meetline.sp4 { --spoke:var(--magi-ref-error); }
  .meetline.sp5 { --spoke:var(--magi-ref-muted); }
  /* Getting ready, ready, and could not. Three states before the room opens, and the words are in
     the tooltip as well — a colour that is the only telling is one nobody can read out. */
  .meetsp.getting { opacity:.75; }
  .meetsp.set { --md-filter-chip-outline-color:var(--magi-ref-ok, var(--magi-ref-accent)); }
  .meetsp.lost { --md-filter-chip-outline-color:var(--magi-ref-warn);
                 --md-filter-chip-label-text-color:var(--magi-ref-warn); }
  .meetgetting { color:var(--magi-ref-muted); }

  /* Whoever holds the floor: a container role and the `on-` colour that belongs to it, rather than
     two colours that happen to sit together. Written as primary-on-surface first, and the light
     theme measured 4.01:1 on the one name a reader is looking for. */
  .meetsp.holding {
    --md-filter-chip-selected-container-color:var(--md-sys-color-secondary-container);
    --md-filter-chip-selected-label-text-color:var(--md-sys-color-on-secondary-container);
    --md-filter-chip-selected-outline-color:var(--magi-ref-primary);
    --md-filter-chip-selected-outline-width:2px;
  }
  /* Next in line, outlined in the accent: "who is being waited on" without reading anything. */
  .meetsp.next { --md-filter-chip-outline-color:var(--magi-ref-accent); }
  /* The person's own colour, and not one of the six: they are in the room differently from the
     companions and the roster has always said so. (It also collided — with four companions the
     fourth wore the person's hue.) */
  .meetsp.person {
    --md-filter-chip-label-text-color:var(--magi-ref-fg);
    --md-filter-chip-outline-color:var(--magi-ref-fg);
  }
  .meetline.you { --spoke:var(--magi-ref-fg); }
  /* Asked twice, said no twice: in the room and not being asked. Dimmer, and the tooltip says why
     and what pressing it would do. */
  .meetsp.resting { --md-filter-chip-label-text-color:var(--magi-ref-muted); }

  .meetsaid { display:grid; gap:var(--magi-sys-space-150); }
  .meetlap {
    color:var(--magi-ref-muted); font-size:var(--md-sys-typescale-label-small-size);
    letter-spacing:.05em; border-top:1px solid var(--magi-ref-outlineVariant);
    padding-top:var(--magi-sys-space-150);
  }
  .meetline {
    display:grid; grid-template-columns:minmax(6rem, 9rem) 1fr; gap:var(--magi-sys-space-200);
  }
  /* The speaker's own colour, on their name and as a rule down the side of what they said. The
     rule is what makes a run of turns readable at a glance; the name is what makes it readable at
     all. Falls back to the one colour everybody used to share, for a line whose speaker is not in
     the roster — somebody who left the room. */
  .meetwho2 { color:var(--spoke, var(--magi-ref-accent)); overflow-wrap:anywhere; font-weight:600; }
  /* The speaker's rule down the side. Its own declaration rather than folded into the grid rule
     above, because that one is overridden at the narrow breakpoint and this must not be. */
  .meetline {
    border-inline-start:2px solid var(--spoke, var(--magi-ref-outlineVariant));
    padding-inline-start:var(--magi-sys-space-200);
  }
  /* What whoever has the floor is doing, while they do it. Its own block above the transcript
     rather than a row in it: it is not something that was said, and it is replaced rather than
     added to. */
  .meetnow {
    display:grid; gap:var(--magi-sys-space-100);
    padding:var(--magi-sys-space-200);
    border:1px solid var(--magi-ref-outlineVariant); border-radius:var(--magi-sys-shape-m);
    background:var(--magi-ref-surface-container-low);
  }
  .meetnowrows { display:grid; gap:var(--magi-sys-space-100); max-height:16rem; overflow-y:auto; }
  /* What the participant read and thought before it spoke, behind a control. Quiet: it is the
     working, not the answer. */
  .meetwork { grid-column:2; }
  .meetworkrows { display:grid; gap:var(--magi-sys-space-100); margin-top:var(--magi-sys-space-100); }
  .meettext { white-space:pre-wrap; overflow-wrap:anywhere; }
  /* A pass is quieter than a sentence and still there. Muted rather than hidden: silence from
     somebody who read the discussion is information. */
  .meetline.passed .meettext { color:var(--magi-ref-muted); }
  .meetwait { color:var(--magi-ref-muted); }

  /* The box and its two buttons are one control on one line: the field takes the room and they sit
     at the end of it, the same shape the page's composer has. Wrapping, because on a phone three of
     them do not fit and the buttons belong under the box rather than squeezed beside it. */
  .meetsay {
    /* 16dp down, 12 across: these buttons are 40dp with a 48dp press area, so a 12dp row gap
       leaves 4 between targets where the guide asks for 8. */
    display:flex; gap:var(--magi-sys-space-200) var(--magi-sys-space-150); align-items:center; flex-wrap:wrap;
  }
  .meetsay > md-outlined-text-field { flex:1 1 18rem; min-width:0; }

  .meettasks { display:grid; gap:var(--magi-sys-space-150); }
  .meettask {
    display:grid; grid-template-columns:minmax(6rem, 9rem) 1fr auto;
    gap:var(--magi-sys-space-200); align-items:start;
    border-top:1px solid var(--magi-ref-outlineVariant); padding-top:var(--magi-sys-space-150);
  }
  /* Top, not centre: what a participant leaves with is often a list, and a name floating at the
     middle of six bullets reads as though it belongs to the third one. */
  .meettaskwho { color:var(--magi-ref-accent); align-self:start; }
  .meettaskwhat { white-space:pre-wrap; overflow-wrap:anywhere; }
  .meettask.nothing .meettaskwhat { color:var(--magi-ref-muted); }
  /* The receipt and, when the socket is known, the way to the work it made — one line, the two
     read as one thing. The link takes the accent so it is legibly a control beside quiet text. */
  .meetsent { color:var(--magi-ref-muted); font-size:var(--md-sys-typescale-label-small-size);
    display:flex; align-items:center; gap:var(--magi-sys-space-100); flex-wrap:wrap; }
  .meetsent .sentgo { color:var(--magi-ref-primary); text-decoration:underline;
    text-underline-offset:2px; border-radius:var(--magi-sys-shape-xs); }
  .meetsent .sentgo:focus-visible { outline:3px solid var(--magi-ref-primary); outline-offset:2px; }

  /* And the meetings themselves, for the same reason: a list of things to open is a list. The
     whole row is the one action — single-action, in the lists guidance's five modes — so the state
     layer covers all of it rather than sitting on a word inside it. */
  .meetlist { display:grid; }
  .meetrow {
    display:grid; gap:var(--magi-sys-space-50); text-decoration:none; color:inherit;
    padding:var(--magi-sys-space-200);
  }
  .meetlist .meetrow + .meetrow { border-top:1px solid var(--magi-ref-outlineVariant); }
  /* A title, drawn as one. It was body text at body weight over a muted line, so three meetings in
     a list read as three sentences rather than as three things — measured 14px/400 against a 12px
     supporting line. The room's own topic is title-medium at 600 and this is the same thing in a
     list. */
  .meettitle {
    color:var(--magi-ref-fg); font-weight:600;
    font-size:var(--md-sys-typescale-title-medium-size);
  }

  /* The About tab is the disclosure wherever the tab strip is drawn — which is every width below
     840, not only a phone. In landscape (664×390, the medium class) the fold was still there and
     the panel opened as a single 55px line with nothing under it: the exact picture these two
     rules were written to end, in the band they did not cover. */
  @media (max-width:52.4375em) {
    /* The live tail is not a box that scrolls on a phone — in either orientation. Eight tool calls
       is 543px through a 256px window, a scroller inside a scrolling page, and in landscape that
       nested box is two thirds of the window. The page scrolls; this grows. */
    .meetnowrows { max-height:none; overflow-y:visible; }
    body[at="agent"] #detail .foldbar, body[at="agent"] #fileview .foldcaret { display:none; }
    body[at="agent"] #detail[folded] .foldwrap,
    body[at="agent"] #fileview[folded] .foldwrap { grid-template-rows:1fr; }
    /* P3: a diff is code and it wraps here too, for the same reason an open file does — measured
       at 664, one diff took the document to 1156px and tipped the page into the desk layout. */
    #fileview .diffbody, #fileview .filecode { white-space:pre-wrap; overflow-wrap:anywhere; }
    /* And the line-number gutter goes: once the code wraps, one number per source line no longer
       sits beside its line — measured, a 240-char line added 8 visual rows and every number below
       it pointed at the wrong line. A number that lies is worse than no number, so on the phone,
       where the code wraps to fit, the gutter is dropped and the code takes the whole width. The
       editor keeps its gutter — it scrolls rather than wraps, so the numbers still track. */
    #fileview .filebody:not(.editbody) { grid-template-columns:minmax(0, 1fr); }
    #fileview .filebody:not(.editbody) .filegutter { display:none; }
    /* The workbenches keep their own scroller and their own cap below 840, where the desk's rule
       stops applying: without it the commit screen is 14,129px tall on a phone and its message box
       and Commit button are twenty-one screens down. */
    body[at="agent"] #fileview.commitmode {
      max-height:min(78%, 78vh); display:flex; flex-direction:column; overflow:hidden;
    }
    .commitfoot { grid-template-columns:minmax(0, 1fr); }
    .commitfiles { overflow-x:auto; }

    /* The name over the words rather than beside them: at 390px a nine-rem column leaves a
       sentence four words wide. */
    .meetline, .meettask { grid-template-columns:1fr; }
    /* And what is in the second column comes with it. Asking for `grid-column:2` in a one-column
       grid makes the grid grow an implicit second column, so a turn WITH a working disclosure was
       laid out differently from the turns around it — measured, an 8.4px dead column and 24px off
       the width of that turn's words alone. */
    .meetwork { grid-column:1; }
    /* The topic sticks under the app bar, not behind it.
       Both were stuck at top:0 and the bar wins on z-index, so the one thing this head exists to
       keep on screen — what the meeting is about — sat inside the bar for the whole length of the
       transcript. Measured: the h3 at y 0–53 under a 65px bar, and a hit test at its centre
       returning a masthead control. */
    .meethead { top:var(--magi-comp-appbar-h, 0px); }
  }

  /* ── the board: a column per companion, a card per piece of work ────────── */
  #board { display:block; max-width:var(--magi-sys-page); }
  .boardhead { display:flex; gap:var(--magi-sys-space-200); align-items:center; margin:0 0 var(--magi-sys-space-200); flex-wrap:wrap; }
  /* The card's one action, so it is a target and not a line of prose. Inline at 16px tall, a press
     at the middle of a four-line title landed on the card behind it and did nothing — measured, the
     centre answered .wcard and only a press on the first line navigated. */
  .wcard .wwhat { color:inherit; text-decoration:none; cursor:pointer; display:block; }
  .wcard .wwhat:hover { text-decoration:underline; background:color-mix(in srgb, var(--magi-ref-fg) 8%, transparent); }
  .wcard .wwhat:active { background:color-mix(in srgb, var(--magi-ref-fg) 12%, transparent); }
  .wcard .wlong { font:var(--md-sys-typescale-label-small-size)/1.5 var(--magi-ref-mono); color:var(--magi-ref-muted); }
  .wcard .wmodel {
    font:var(--md-sys-typescale-label-small-size)/1.5 var(--magi-ref-mono); color:var(--magi-ref-accent); overflow-wrap:anywhere;
  }
  /* A label is pressable, so it is drawn as something that can be pressed — a chip's shape, at the
     size of the line it sits on rather than the size of a control, because a card carrying three
     of them is still a card. */
  .wcard .wlabel { font:inherit; border:0; cursor:pointer; }
  /* Drawn to the chip's own spec rather than to the line it sits on: height 32dp, Label Large,
     8dp between one and the next. It was 23dp tall with 4dp gaps, which is under the 24dp floor
     for any target at all, and the press target is a further 48dp on top — the guide states that
     one separately from the container height, so the chip stays 32 and the reach is 48. */
  .wcard .wlabel {
    display:inline-flex; align-items:center; min-height:2rem; cursor:pointer;
    /* 16dp between rows, not 8: the press target is 48dp on a 32dp chip, and at the 8dp
       the guide gives as a minimum the targets of two wrapped rows overlap by 8dp — the
       lower one takes presses aimed at the upper. Measured, not assumed. */
    margin:var(--magi-sys-space-200) var(--magi-sys-space-100) 0 0;
    font:600 var(--md-sys-typescale-label-large-size)/1.25rem var(--magi-ref-mono); letter-spacing:.06em;
    color:var(--magi-ref-primary); background:color-mix(in srgb, var(--magi-ref-primary) 12%, transparent);
    border-radius:var(--magi-sys-shape-full); padding:0 var(--magi-sys-space-150);
  }
  /* A layer over the container, not a re-tint of it: 22% is not a value on the state scale, and
     swapping the wash is the colour-change the checklist names as not-M3. The layer is the ink's
     own colour over the chip's own 12% wash, and pressed exists. */
  .wcard .wlabel:hover {
    background:color-mix(in srgb, var(--magi-ref-fg) 8%,
               color-mix(in srgb, var(--magi-ref-primary) 12%, transparent));
  }
  .wcard .wlabel:active {
    background:color-mix(in srgb, var(--magi-ref-fg) 12%,
               color-mix(in srgb, var(--magi-ref-primary) 12%, transparent));
  }
  /* The words on that tint are on-surface. Primary on a 12% wash of itself is 3.88:1 in the light
     theme and 3.38 with the hover wash — the tint is the badge, the ink does not have to be. */
  .wcard .wlabel { color:var(--magi-ref-fg); }
  /* The arrows sit level with the field's box, not with the row's centre — the field is 56dp tall
     and carries a floating label above its text, so centring on the row puts them over the label. */
  .boardhead md-icon-button { align-self:end; }
  /* Scrolls sideways, and ONLY here. The page must never do it, but a board of lanes is the one
     shape where sideways is the reading direction, and clipping a lane would hide a companion. */
  .lanes { display:flex; gap:var(--magi-sys-space-300); align-items:flex-start; overflow-x:auto; padding-bottom:var(--magi-sys-space-100); }
  /* A fixed lane width, and it has to be spelled out three ways: flex-basis alone loses to the
     content's own minimum, so a long title widened its lane and a short one narrowed it, and the
     columns stopped lining up — which is the one thing a board is for. */
  /* The lane grows with the window, to a limit. Pinned at 15rem it was 240px at 1280 and 240px at
     2560 — three lanes using 768 of a 1385px strip with 43% of it blank, and a card's line 25
     characters long at every width where the guide asks for 40–60 at all of them. The floor keeps
     the columns lining up when there are enough teams to fill the strip; the ceiling keeps a
     single team from becoming a full-width bar. */
  .lane { flex:1 1 15rem; min-width:15rem; max-width:26rem; }
  /* Only when there is more board than room. Without this a single lane still drew a scrollbar,
     which reads as "there is more over there" when there is not. */
  /* The lanes scroll, so the strip must reach the page's edge rather than stopping at the reading
     column's — a lane half off the right of a 150ch box, with room to its right, reads as clipped
     rather than as scrollable. */
  .lanes { scrollbar-width:thin; }
  #board { padding-right:0; }
  /* Set apart from the chips it sits beside: they filter this list and it leaves it. */
  .lanes::after { content:""; flex:0 0 1.4rem; }   /* the last lane gets a right edge too */
  .lanehead {
    display:flex; gap:var(--magi-sys-space-100); align-items:baseline;
    border-bottom:1px solid var(--magi-ref-outlineVariant); padding-bottom:var(--magi-sys-space-50); margin-bottom:var(--magi-sys-space-100);
  }
  .lanehead .lname {
    font:600 var(--md-sys-typescale-label-medium-size)/1.4 var(--magi-ref-mono); letter-spacing:0.0467em; color:var(--magi-ref-fg);
  }
  .lanehead .lcount { margin-left:auto; font:var(--md-sys-typescale-label-small-size)/1.5 var(--magi-ref-mono); color:var(--magi-ref-muted); }
  .wcard {
    /* 12dp: the card's step on the shape scale. 8 is the chip's. */
    border:1px solid var(--magi-ref-outlineVariant); border-radius:var(--magi-sys-shape-m);
    padding:var(--magi-sys-space-100) var(--magi-sys-space-150); margin-bottom:var(--magi-sys-space-100); background:var(--magi-ref-surface-container-low);
  }
  /* Who did it, beside when. Named in the companion's own colour so it reads as an attribution
     rather than one more grey fact — the card already has two of those under it. */
  .wcard .wwhen { display:flex; align-items:baseline; gap:var(--magi-sys-space-100); }
  .wcard .wwho {
    font:600 var(--md-sys-typescale-label-small-size)/1.4 var(--magi-ref-mono);
    letter-spacing:.05em; color:var(--magi-ref-primary);
  }
  .wcard .wwhen { font:var(--md-sys-typescale-label-small-size)/1.5 var(--magi-ref-mono); color:var(--magi-ref-muted); }
  .wcard .wwhat { font-size:var(--md-sys-typescale-body-medium-size); line-height:1.5; color:var(--magi-ref-fg); overflow-wrap:anywhere; }
  /* The one running now, in the colour the rest of the page uses for that. */
  .wcard.now { border-color:var(--magi-ref-success); }
  .wcard.now .wwhen { color:var(--magi-ref-success); font-weight:600; }

  /* ── what this companion did before now ─────────────────────────────────── */
  #agentdetail .find { display:flex; align-items:center; gap:var(--magi-sys-space-150); margin:var(--magi-sys-space-150) 0; }
  #agentdetail .findin { flex:1 1 auto; min-width:0; }
  #agentdetail .findn { font:var(--magi-sys-body-s) var(--magi-ref-mono); color:var(--magi-ref-muted); }
  #agentdetail .findnone { font:var(--magi-sys-body-s) var(--magi-ref-display); color:var(--magi-ref-muted); padding:var(--magi-sys-space-150) 0; }
  #agentdetail .snip {
    font:var(--magi-sys-body-s) var(--magi-ref-display); color:var(--magi-ref-muted);
    padding:0 0 var(--magi-sys-space-50) var(--magi-sys-space-400); overflow-wrap:anywhere;
  }
  /* Rows that open. They were divs when the list was a card nobody could go into; as buttons they
     keep the same shape and gain the things a control needs — a pointer, a hover, a focus ring and
     a place in the tab order. */
  .hs {
    display:grid; grid-template-columns:5.5rem 1fr;
    gap:var(--magi-sys-space-50) var(--magi-sys-space-200); padding:var(--magi-sys-space-50) 0;
    width:100%; text-align:left; background:none; border:0; border-radius:var(--magi-sys-shape-xs);
    color:inherit; font:inherit; cursor:pointer;
  }
  .hs:hover { background:color-mix(in srgb, var(--magi-ref-fg) 8%, transparent); }
  .hs:active { background:color-mix(in srgb, var(--magi-ref-fg) 12%, transparent); }
  .hs:focus-visible { outline:2px solid var(--magi-ref-primary); outline-offset:2px; }
  .hs + .hs { border-top:1px solid var(--magi-ref-outlineVariant); }
  .hs .when { font:var(--md-sys-typescale-label-small-size)/1.6 var(--magi-ref-mono); color:var(--magi-ref-muted); text-align:right; }
  .hs .what { font-size:var(--md-sys-typescale-body-medium-size); color:var(--magi-ref-fg); overflow-wrap:anywhere; }
  /* The one it is in now is work too, and it is the newest row. Marked rather than left off. */
  .hs.now .when { color:var(--magi-ref-success); font-weight:600; }
  /* #handoffs .k is styled with the other four Going-on headings, up beside #plan .k. */
  /* The name takes what the name needs, and the work takes the rest.
     A fixed 8rem column gave a five-letter companion the same room as the sentence describing what
     it was asked to do — half the card, in a pane 22rem wide, holding one short word. fit-content
     keeps the cap for the rare long name while letting the common short one give its room back. */
  .ho {
    display:grid; grid-template-columns:fit-content(8rem) minmax(0, 1fr);
    gap:var(--magi-sys-space-50) var(--magi-sys-space-200); padding:var(--magi-sys-space-100) 0;
  }
  .ho .to { font:600 var(--md-sys-typescale-label-small-size)/1.6 var(--magi-ref-mono); letter-spacing:.08em; color:var(--magi-ref-accent); text-align:right; }
  /* Both of this card's controls answer the pointer; they answered with nothing — a link to
     another companion and a clipped request that expands, each with only a cursor to say so. */
  a.to:hover, .ho .req:hover { background:color-mix(in srgb, var(--magi-ref-fg) 8%, transparent); }
  a.to:active, .ho .req:active { background:color-mix(in srgb, var(--magi-ref-fg) 12%, transparent); }
  /* Three lines of what was asked, not nine. The guide caps a list item's supporting text at one
     to three lines and says to cut it by screen size; measured on a phone, two handed-out requests
     came to 881px in a 664px window. */
  .ho .req {
    font:var(--magi-sys-body-l) var(--magi-ref-display); color:var(--magi-ref-fg); overflow-wrap:anywhere;
    display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden;
    cursor:pointer;
  }
  /* Pressed, it is the whole request. The clip is the list's rule; this is the other half of it. */
  .ho .req.all { -webkit-line-clamp:none; }
  .ho .ans { grid-column:2; font-size:var(--md-sys-typescale-label-medium-size); color:var(--magi-ref-muted); overflow-wrap:anywhere; }
  .ho.working .to { color:var(--magi-ref-primary); }

  /* ── transcript ─────────────────────────────────────────────────────────── */
  /* Monospace throughout: every line here is something the machine said or did, and a serif would
     be dressing up evidence. The editorial part is the rhythm — a wide gutter of small-caps labels
     against a single column of text. */
  /* A container, so the rows inside can be asked how much room THEY have.
     The speaker gutter is a fixed 6.5rem on every row, which reads well beside a wide column and
     is ruinous beside a narrow one — and how narrow this column is has never been a function of
     the window. It depends on which other panes are open, and judging it by the viewport is what
     produced the measurement that started this: at a 1000px window the text column was six
     characters wide. A container query asks the only question that matters. */
  #log { max-width:var(--magi-sys-page); container-type:inline-size; container-name:transcript; }
  /* minmax(0, 1fr), not 1fr. A grid track's automatic minimum is its content, so one unbreakable
     line — a tool result naming forty tools, a path with no spaces in it — widened the column past
     the page and put a horizontal scrollbar under the whole console. The transcript scrolls
     vertically; anything too wide scrolls inside its own box (below), never by moving the page. */
  .row { display:grid; grid-template-columns:6.5rem minmax(0, 1fr); gap:var(--magi-sys-space-200); align-items:start; padding:var(--magi-sys-space-50) 0;
    /* Off screen, a row costs nothing to lay out or paint. A long conversation is a thousand rows
       of markdown, tables and folded output, and the browser was laying out all of it on every
       change to the last one. The size hint keeps the scrollbar honest before a row has ever been
       rendered; auto lets each row remember its real height once it has been. */
    content-visibility:auto; contain-intrinsic-size:auto 3.5rem; }
  /* 34rem: the gutter and its gap cost 7.5rem, and below this there is not enough left for a line
     of prose to be worth reading. The label goes above the text instead of beside it — still
     present, no longer paid for on every line. */
  @container transcript (max-width:34rem) {
    .row { grid-template-columns:minmax(0, 1fr); gap:0; }
    /* The time goes beside the name once the gutter is a line of its own, so a narrow screen does
       not spend a second row on four digits. */
    .who { text-align:left; display:flex; gap:var(--magi-sys-space-100); align-items:baseline; }
    /* The council name is a button and carries a more specific rule, so it needs telling twice. */
    .who.whoin { text-align:left; }
  }
  /* No blanket opacity on the gutter.
     It was .8 to make the label quieter than the words beside it, and the muted role survives that
     — but the label is not always muted: a person's row paints it `primary`, a tool's `accent`, a
     councillor its own hue. Multiplied by .8 in the LIGHT theme those land at 3.39, 3.52 and
     3.39:1 against the page, under the 4.5:1 the guide sets for small text. The roles are already
     the quiet ones; being quiet is what they are for. */
  .who {
    font:600 var(--md-sys-typescale-label-small-size)/1.9 var(--magi-ref-mono); letter-spacing:0.0533em;
    color:var(--magi-ref-muted); text-align:right; user-select:none;
  }
  /* Under the name, quieter than it. Times are scanned, not read: they answer "when did this
     happen" for a row somebody has already found, and at full weight they compete with the name
     for a column that is 6.5rem wide.
     Quieter by WEIGHT, not by another layer of opacity: the gutter is already at .8, a second
     multiplier lands the muted role at 3.3:1 in the light theme, and a time nobody can read is
     not a quiet time — it is a missing one. */
  .who .when { font-weight:400; letter-spacing:0; }
  /* Quiet until it is wanted, and never wider than the gutter it sits in. It is a button, so it
     brings a focus ring and a keyboard press for free; what it needs from here is to stop looking
     like one. */
  .who .copy {
    background:none; border:0; padding:0; margin-top:var(--magi-sys-space-50);
    font:inherit; font-size:var(--md-sys-typescale-label-medium-size); line-height:1;
    /* No opacity of its own. The gutter is already at .8 and a second multiplier lands the muted
       role at 3.97:1 in the dark theme — a control nobody can see is not a quiet control. */
    color:var(--magi-ref-muted); cursor:pointer;
    /* Wide enough to be pressed, before hit48 makes it tall enough. A glyph's own box is nine
       pixels across and the expander only stretches what is there. */
    min-width:1.5rem;
  }
  .who .copy:hover, .who .copy:focus-visible { color:var(--magi-ref-primary); background:color-mix(in srgb, var(--magi-ref-fg) 8%, transparent); }
  .who .copy:active { background:color-mix(in srgb, var(--magi-ref-fg) 12%, transparent); }
  .who .copy.done { color:var(--magi-ref-success); }
  /* The stand-in for what is not in the page. It has no content and is not read out; its only job
     is to be as tall as the rows it replaces, so the scrollbar means what it says. */
  #log .above { flex:none; }
  .txt { white-space:pre-wrap; overflow-wrap:anywhere; }
  /* Rendered markdown sets its own blocks, so the pre-wrap that keeps plain text's newlines would
     add a blank line after every paragraph. Only the rows that are still plain text keep it. */
  .txt:has(> p), .txt:has(> pre), .txt:has(> ul), .txt:has(> ol),
  .txt:has(> .tablewrap), .txt:has(> blockquote), .txt:has(> h3),
  .txt:has(> h4), .txt:has(> h5), .txt:has(> h6) { white-space:normal; }
  .txt > :first-child { margin-top:0; }
  .txt > :last-child { margin-bottom:0; }
  .txt p { margin:0 0 var(--magi-sys-space-150); }
  .txt h3, .txt h4, .txt h5, .txt h6 {
    margin:var(--magi-sys-space-300) 0 var(--magi-sys-space-100);
    font:var(--magi-sys-title-s) var(--magi-ref-display); color:var(--magi-ref-on-surface);
  }
  .txt ul, .txt ol { margin:0 0 var(--magi-sys-space-150); padding-left:var(--magi-sys-space-400); }
  .txt li { margin:var(--magi-sys-space-50) 0; }
  .txt li > p { margin:0; }
  /* 150, like every other bar in a row: the inset a rule stands off its text by is one measure
     across the transcript, not one per kind of quote. */
  .txt blockquote {
    margin:0 0 var(--magi-sys-space-150); padding-left:var(--magi-sys-space-150);
    border-left:2px solid var(--magi-ref-outlineVariant); color:var(--magi-ref-muted);
  }
  /* A tint of the foreground rather than a surface token: this page's palette has one surface, and
     a code span has to read as raised against whatever it happens to be sitting on — a card, the
     page, or a folded body. color-mix is the idiom the rest of the file already uses for that. */
  .txt code {
    font:var(--magi-sys-body-s) var(--magi-ref-mono);
    background:color-mix(in srgb, var(--magi-ref-fg) 8%, transparent);
    border-radius:var(--magi-sys-shape-xs);
    padding:0 var(--magi-sys-space-50);
  }
  .txt pre {
    margin:0 0 var(--magi-sys-space-150); padding:var(--magi-sys-space-200);
    background:color-mix(in srgb, var(--magi-ref-fg) 5%, transparent);
    border-radius:var(--magi-sys-shape-s);
    /* Its own scroller. A long line of code is the one thing on this page allowed to be wider than
       the column, and letting it push the column is how the whole page ends up scrolling sideways. */
    overflow-x:auto; overscroll-behavior-x:contain;
  }
  .txt pre code { background:none; padding:0; white-space:pre; }
  /* A diff, coloured the way the terminal colours it. The background tint carries the same fact as
     the sign at the start of the line, so somebody who cannot separate the two colours still has
     the character — and somebody scanning has the block. */
  pre.diff { font:var(--magi-sys-body-s) var(--magi-ref-mono); white-space:pre; }
  pre.diff .dadd { color:var(--magi-ref-success); background:color-mix(in srgb, var(--magi-ref-success) 12%, transparent); }
  pre.diff .ddel { color:var(--magi-ref-error); background:color-mix(in srgb, var(--magi-ref-error) 12%, transparent); }
  pre.diff .dhunk { color:var(--magi-ref-accent); }
  pre.diff .dfile { color:var(--magi-ref-muted); font-weight:600; }
  pre.diff .dctx { color:var(--magi-ref-fg); }
  /* Tables scroll inside themselves, for the same reason. */
  .tablewrap { overflow-x:auto; overscroll-behavior-x:contain; margin:0 0 var(--magi-sys-space-150); }
  /* Centred in the column, not stretched across it. A three-column table set flush left in a
     74-character measure leaves a field of empty paper to its right and reads as though something
     failed to load; auto margins put the figure where a figure goes. It keeps its own width — a
     table stretched to the measure would space its columns by the widest cell in the paragraph
     around it, which is not a column width, it is an accident. */
  .txt table { border-collapse:collapse; font:var(--magi-sys-body-s) var(--magi-ref-display); margin-inline:auto; }
  .txt th, .txt td {
    border:1px solid var(--magi-ref-outlineVariant);
    padding:var(--magi-sys-space-50) var(--magi-sys-space-150); text-align:left; vertical-align:top;
  }
  .txt th { background:color-mix(in srgb, var(--magi-ref-fg) 5%, transparent); font-weight:600; }
  .txt hr { border:0; border-top:1px solid var(--magi-ref-outlineVariant); margin:var(--magi-sys-space-200) 0; }
  .txt a { color:var(--magi-ref-primary); }

  /* A folded row: reasoning, and what a tool was asked and answered. */
  .fold > summary {
    cursor:pointer; list-style:none; color:var(--magi-ref-muted);
    font:var(--magi-sys-body-s) var(--magi-ref-mono);
    padding:var(--magi-sys-space-50) 0; border-radius:var(--magi-sys-shape-xs);
    /* 48dp of hit area without 48dp of ink: the row is one line and the guide asks for the target,
       not for the height. It said that and then set 24px — measured, three of six folds in one
       transcript were 24px tall, and this is the only way into a tool call, a diff or a piece of
       reasoning. The ink stays one line; the target is an expander, the same one every other
       hand-built control on this page uses. */
    min-height:var(--magi-sys-space-300); position:relative;
  }
  .fold > summary::after {
    content:''; position:absolute; left:0; right:0; top:50%; transform:translateY(-50%);
    /* And it takes the press. Written pointer-events:none it was drawn, measured, and answered
       nothing — the target stayed the 40px of ink. It belongs to the summary it sits in, which is
       what a press there does anyway. */
    height:48px;
  }
  .fold > summary::-webkit-details-marker { display:none; }
  .fold > summary::before {
    content:"›"; display:inline-block; width:1em;
    transition:transform var(--magi-sys-dur-short2) var(--magi-sys-ease-standard);
  }
  .fold[open] > summary::before { transform:rotate(90deg); }
  /* Declared a step above the scale because the row it lives in carries opacity:.8 — 10% and 15%
     PAINT as the 8% and 12% every component on this page uses. Measured before this: the declared
     6% painted 4.8%, two multiplications under the number the bundle uses. */
  .fold > summary:hover { background:color-mix(in srgb, var(--magi-ref-fg) 10%, transparent); }
  .fold > summary:active { background:color-mix(in srgb, var(--magi-ref-fg) 15%, transparent); }
  .fold > summary:focus-visible { outline:2px solid var(--magi-ref-primary); outline-offset:2px; }
  /* min-width:0 for the same reason as the row above: this is a grid item, and without it the
     preformatted block inside decides how wide the transcript is. */
  .foldbody { padding-left:1em; min-width:0; }
  /* The word above a block of a folded row: what it was asked, what changed, what it answered.
     Quiet and small — it is a label on evidence, not a heading — and it takes the muted role at
     full strength because it is the only thing naming the block under it. */
  .foldk {
    font:600 var(--md-sys-typescale-label-small-size)/1.6 var(--magi-ref-mono);
    letter-spacing:0.05em; color:var(--magi-ref-muted); margin-top:var(--magi-sys-space-150);
  }
  .foldbody > .foldk:first-child { margin-top:0; }
  /* What a tool was asked and what it answered is preformatted and frequently one very long line.
     It keeps its shape and takes its own scrollbar — the alternative is wrapping JSON at arbitrary
     characters, which makes an argument list unreadable in a different way. */
  .foldbody > pre {
    margin-top:var(--magi-sys-space-50); max-width:100%;
    overflow-x:auto; overscroll-behavior-x:contain;
  }
  /* Under the call it belongs to, spanning it — where the guide puts a bar for the container that
     is progressing. Thin, because it is a heartbeat and not a measurement. */
  .runbar { display:block; margin-top:var(--magi-sys-space-100); --md-linear-progress-track-height:2px; --md-linear-progress-active-indicator-height:2px; }
  /* What the running call last said. Monospace because it is the tool's own words, and muted
     because it sits under a bar that has already claimed the eye. */
  .note { margin-top:var(--magi-sys-space-50); font:var(--md-sys-typescale-label-medium-size)/1.45 var(--magi-ref-mono); color:var(--magi-ref-muted); overflow-wrap:anywhere; }

  /* A user turn is the anchor you scan for: set as a lead, with the rule an editorial layout uses
     for a pull quote. */
  .row.user { margin:var(--magi-sys-space-300) 0 var(--magi-sys-space-150); }
  .row.user .txt {
    font:var(--md-sys-typescale-body-large-size)/1.55 var(--magi-ref-display); color:var(--magi-ref-primary);
    /* The same inset as every other barred row (system, pending, a tool's result): the negative
       margin pulls the bar toward the gutter, so a larger inset is a bar sitting CLOSER to the
       name beside it — this one at 200 crowded the label while the rest stood off it by 150. */
    border-left:2px solid var(--magi-ref-primary); padding-left:var(--magi-sys-space-150); margin-left:calc(-1 * var(--magi-sys-space-150));
  }
  .row.user .who { color:var(--magi-ref-primary); }
  .row.assistant .txt { color:var(--magi-ref-fg); }
  .row.thinking .txt { color:var(--magi-ref-muted); font-style:italic; opacity:.8; }
  .row.tool .txt { color:var(--magi-ref-accent); }
  .row.tool .who { color:var(--magi-ref-accent); }
  .row.result .txt, .row.failed .txt {
    color:var(--magi-ref-muted); border-left:1px solid var(--magi-ref-outlineVariant);
    padding:var(--magi-sys-space-50) 0 var(--magi-sys-space-50) var(--magi-sys-space-150);
    max-height:11rem; overflow:auto; max-width:100%; overscroll-behavior:contain;
  }
  .row.failed .who, .row.failed .txt { color:var(--magi-ref-error); border-left-color:var(--magi-ref-error); }
  /* A turn that ended in an error, and an image the agent produced. Both reached the log and
     neither reached this page until now, so neither had anywhere to be drawn. */
  .row.error .who, .row.error .txt { color:var(--magi-ref-error); }
  .row.error .txt { font:var(--magi-sys-body-s) var(--magi-ref-mono); }
  /* The path, not the picture. Serving image bytes means a route that reads a file named by the
     log, and the transcript is the least trustworthy source of a filename on this machine — that
     is its own decision, not one to make by adding an img tag here. */
  .row.image .txt { font:var(--magi-sys-body-s) var(--magi-ref-mono); color:var(--magi-ref-muted); }
  /* The council, in the transcript rather than beside it. It votes on whether a turn is finished,
     so it belongs where the turn it judged is — the terminal has always put it there. */
  /* magi's own voice, addressed to the agent. Set apart from the person's turn, which is the lead
     the page is scanned for, and quieter than the answer — it is neither the question nor the work.
     Until now these rows fell through to the default with no rule at all, which is what a
     compaction summary has looked like since compaction existed. */
  .row.system .who { color:var(--magi-ref-accent); }
  .row.system .txt {
    color:var(--magi-ref-muted); font:var(--magi-sys-body-s) var(--magi-ref-mono);
    border-left:1px solid var(--magi-ref-accent);
    padding-left:var(--magi-sys-space-150); margin-left:calc(-1 * var(--magi-sys-space-150));
  }
  .row.council .who { color:var(--magi-ref-secondary); }
  .row.council .fold > summary { color:var(--magi-ref-secondary); font-weight:600; }
  /* Each councillor in their own hue, the same three the terminal paints them in. Three voices in
     one colour is a wall of identical rows, and which of them said the thing you are reading is
     most of what a council transcript is for. The tokens were declared here from the beginning and
     nothing used them, under a comment saying this console shows no council — it does. */
  /* The name is a control on a council row, so it brings a button's own font and background with
     it and has to be told to be the gutter it looks like. */
  .who.whoin {
    /* The gutter's own type, spelled out — NOT font:inherit. A button inherits from its parent
       .row, whose font is the 14px/400 body face, so every council seat name rendered larger and
       lighter than magi/council/you two rows away. The name is a .who; it must read as one. */
    background:none; border:0; padding:0; margin:0;
    font:600 var(--md-sys-typescale-label-small-size)/1.9 var(--magi-ref-mono);
    color:inherit; letter-spacing:0.0533em; cursor:pointer; width:100%;
    /* Right, spelled out, and NOT inherit. A button's own text-align is center, so it has to be
       overridden — but inherit takes the ROW's value, not the .who beside it, and the row is left.
       So every council name sat at the wrong end of the gutter while every other name lined up on
       the right. The narrow layout flips both, below. */
    text-align:right;
  }
  .who.whoin:hover, .who.whoin:focus-visible { text-decoration:underline; text-underline-offset:2px; }
  /* And the fold below it has to stop reaching up over it. Every fold draws a 48px press target
     centred on a 24px line, so it overhangs 12px in each direction — harmless above a plain gutter
     label, but a council row is the one row whose gutter is a CONTROL, and in the stacked layout
     (the transcript column at 448px, which is what a phone gets) the name sits 21px above that
     summary. Measured: elementFromPoint at the centre of the name answered SUMMARY, and a click on
     the name opened the fold instead. The seat name was unpressable at that width.

     Anchored to its own top the target grows downward into the fold's own space and the name keeps
     its ink — the same move, and the same reason, as the copy control above
     (`.who .copy.hit48::after`). Neither control gets the full 48 in that layout: the two of them
     have 45px of ink between them and cannot both have 48, and two press targets that overlap mean
     one of them answers nothing at all. Partitioned, both answer. */
  .row.council .fold > summary::after { top:0; transform:none; }
  /* Casper was #B3261E in the light theme — the error colour, to the digit — and #FF8A8A in the
     dark one, which is the error colour's neighbour. So one of the three seats was indistinguishable
     from a rejection, on a row whose whole job is to say who is speaking. Violet: no other role on
     this page uses it. */
  .row.m-melchior .who { color:var(--magi-ref-melchior); }
  .row.m-balthasar .who { color:var(--magi-ref-balthasar); }
  .row.m-casper .who { color:var(--magi-ref-casper); }
  /* The verdict still owns the summary line: done or continue is what the row SAYS, and it stays
     green or red whoever said it. The name in the gutter is who. */
  /* A call that ended, on the line you can see with the row shut. */
  .row.toolok .fold > summary { color:var(--magi-ref-success); }
  .row.toolfail .fold > summary { color:var(--magi-ref-error); }
  /* Done, with something attached to read. The caution role, not the error one: the file is on
     disk and the row must not say otherwise. */
  .row.toolnote .fold > summary { color:var(--magi-ref-warn); }
  .row.toolnote .who { color:var(--magi-ref-warn); }
  .row.toolfail .who { color:var(--magi-ref-error); }
  /* The prompt nothing has answered yet. A bar down its left, the way the terminal draws one, and
     the word beside the label — the bar alone would be a state told only in ink. */
  .row.pending .txt {
    border-left:2px solid var(--magi-ref-primary);
    padding-left:var(--magi-sys-space-150); margin-left:calc(-1 * var(--magi-sys-space-150));
  }
  .pendtag { color:var(--magi-ref-primary); font-weight:600; }
  /* A prompt nothing will answer. Quieter than a live one and struck through the way a finished
     plan item is — the request was made and then overtaken, which is what the line says. */
  .row.abandoned .txt { color:var(--magi-ref-muted); border-left-color:var(--magi-ref-outlineVariant); }
  .row.abandoned .who .pendtag { color:var(--magi-ref-muted); font-weight:400; }
  /* Coloured by the verdict, the way the terminal colours it: done is green, a continue is a
     rejection and is red, an abstention is muted. The icon in the summary carries the same fact,
     so a reader who cannot separate the colours is not being told less. */
  .row.v-done .fold > summary { color:var(--magi-ref-success); }
  .row.v-continue .fold > summary { color:var(--magi-ref-error); }
  .row.v-abstain .fold > summary { color:var(--magi-ref-muted); }
  /* The outcome row's gutter takes the verdict's colour; a SEAT's does not — that one belongs to
     the councillor, and this rule beat it for having been written later in the file. */
  .row.v-done:not(.seated) .who { color:var(--magi-ref-success); }
  .row.v-continue:not(.seated) .who { color:var(--magi-ref-error); }
  /* A command somebody ran from here. Its own colour because it is the one row on the page that
     nobody's agent did — and it is not in the log, so it is also the one row a second console
     watching the same session will not have. */
  .row.shell .who, .row.shell .fold > summary { color:var(--magi-ref-warn); }
  .row.shell .fold > summary { font:var(--magi-sys-body-s) var(--magi-ref-mono); }

  /* ── the prompt an agent is blocked on, on that agent's own page ─────────── */
  /* Without this, opening an agent is the one place you CANNOT see that it is waiting for you: the
     prompt is not in the log — it is a question about what should happen, not a record of what did
     — so the transcript shows a run that has simply stopped. It sat above the composer because
     that is where the answer goes. */
  /* One dock, two rows: the prompt above the composer rather than on top of it. Fixed separately
     they both sat at the bottom, and the prompt — being the later one — hid the composer entirely,
     so a blocked agent could be answered and not steered. Which is the wrong way round: "do
     something else instead" is a legitimate reply to being asked. */
  /* Under the panes, over the conversation.
     The dock sat at the same level as the masthead — which was right when it ran the width of the
     page and had nothing beside it. Between two panes it is a third column's worth of furniture,
     and at z-index 2 it drew OVER the cards either side of it: a menu opened from the file tree,
     or a long state card, went behind the composer. It belongs to the middle column, so it stacks
     with the middle column: above the transcript scrolling under it, below the panes. */
  #dock {
    position:fixed; left:0; right:0; bottom:0; z-index:1;
    padding-bottom:env(safe-area-inset-bottom);
    /* Half the window, whatever the window is. A companion blocked on a decision puts its whole
       question in here — the grounds, the options, the report — and the cap that stopped it taking
       the screen was written inside the phone's own media query. So a phone turned SIDEWAYS, where
       there is least height of all, had no cap at all: measured at 664×390 the dock was 278px of a
       390px window, 71%, with the transcript left a 49px band. A permanent band over most of the
       screen is the one thing the guide says a fixed surface may not be. */
    max-height:50vh; overflow:auto; overscroll-behavior:contain;
    /* Full width and takes no clicks. It spans the window so the bay inside it can be measured
       against the same box main is, and a fixed band across the foot of the page would otherwise
       swallow the bottom of both panes — a file tree whose last rows cannot be clicked. Only the
       three boxes that ARE the dock take the pointer back. */
    pointer-events:none;
  }
  #strip, #prompt, #dock form { pointer-events:auto; }
  /* A <form> carries the browser's own 1em block margin, and nothing here had ever taken it off:
     in a dock that establishes its own formatting context it cannot collapse away, so the composer
     floated 14px above the window with the transcript showing through the band — and the band grew
     with the reader's font size. The dock owns the spacing of what is in it. */
  #dock form { margin-block:0; }
  /* The dock belongs to the CONVERSATION, so beside a companion it stands between the two panes
     rather than across all three columns. It used to run the full width under them: the composer
     answered a transcript in the middle while starting under the file tree, and the panes had to
     stop short of the bottom to keep clear of it — which is why they ended in a strip of empty
     column. Inset by the two tracks, and only where the tracks are beside the conversation. */
  @media (min-width:52.5em) {
    /* Inset the BAY by the pane tracks, not the dock by the window.
       Insetting the dock was right about where the composer belongs and wrong about what it is
       measured against: the panes are tracks inside main's grid, and main centres itself once the
       page is wider than its cap, so on a 2488px monitor the conversation sat at 651 and the
       composer under it at 432. These are the same two variables the grid uses, applied to a box
       that is already main's box — which is what makes the composer land on the middle track
       rather than near it. */
    body[at="agent"] #dock .bay {
      padding-left:calc(var(--magi-comp-files-now, var(--magi-comp-files-w)) + var(--magi-sys-space-300));
      padding-right:calc(var(--magi-comp-side-now, var(--magi-comp-side-w)) + var(--magi-sys-space-300));
    }
    body[at="agent"][files="shut"] #dock .bay { padding-left:0; }
    body[at="agent"][side="shut"] #dock .bay { padding-right:0; }
  }
  /* Main's rule, on purpose, written the same way it is written for main: the dock is a fixed box
     across the window and main is a centred column, and the only way the two stay in one column is
     for the dock's contents to be measured the way main is. */
  #dock .bay {
    max-width:var(--magi-sys-page); margin-inline:auto;
    /* The fade is the conversation's, so it is drawn on the conversation's box and clipped to it:
       over a pane it would put a wash across the last rows of a list somebody is reading. */
    /* …and it fades to the tone the boxes on it take, not to the page. Faded to the page colour,
       the composer — the half of this bar that is always there — was still painted the page while
       the question box beside it had a surface, so the dock banded down the middle. */
    /* The page's own ground, not a tone of its own.
       A filled bar between two outlined panels is a third material on a screen that has two: the
       facts and workspace cards beside it paint --magi-ref-bg inside a hairline, and the bay was
       the one box carrying a container tone — so it read as a strip laid over the page rather
       than as part of it, which is what "the bay does not sit well with the panels" is describing.
       Fully transparent is the other wrong answer: the transcript scrolls UNDER this, and text
       passing behind the composer is worse than a seam. So it takes the ground the panels take,
       and the gradient keeps its job of letting the last line fade rather than end at an edge. */
    background:linear-gradient(to top, var(--magi-ref-bg) 88%, transparent);
    background-clip:content-box;
  }
  /* The answer stays under the thumb however long the question is.
     A companion can ask about a 3600-character command; measured on a phone that is 2121px of
     question in a dock capped at half the screen, with Allow six and a half dock-screens down. The
     question is what you read and the buttons are what you do — so the buttons stop scrolling away
     from it. Sticky rather than fixed, so they sit at the foot of the dock and not of the window. */
  #prompt .answer {
    position:sticky; bottom:0; z-index:1;
    background:var(--magi-ref-surface-container-low);
    padding-top:var(--magi-sys-space-100);
  }
  #prompt {
    /* The third bar on the screen, and the last one painted the page's own background: the rail
       and the phone's navigation bar take a container tone, and a fixed band at the foot separated
       by a divider-coloured rule alone is 1.46:1 from the transcript in the light theme. */
    /* Same ground as the panels beside it (see the bay's own comment): the three boxes on a
       companion's page are one material now, and this bar is separated by its rule rather than by
       being a different colour. */
    background:var(--magi-ref-bg);
    /* The side padding lives HERE, not on the box that draws the rule. Inside .inner it was part
       of that box, so the 2px line ran 24px wider than the conversation at each end — the bottom
       of the page a little broader than everything above it, which reads as the footer being
       indented. Measured on the published console: the bar 96 to 1497, the transcript 120 to 1473. */
    padding:var(--magi-sys-space-200) var(--magi-sys-space-300) var(--magi-sys-space-150);
  }
  /* The rule belongs to the column, not to the window. Drawn on #prompt it ran the full width of
     the viewport while the words under it started 514px in, which reads as a divider for the whole
     page rather than the top of one bar. */
  #prompt .inner { border-top:2px solid var(--magi-ref-warn); padding-top:var(--magi-sys-space-200); }
  /* The dock stands in the same column as the page. It used to centre its own narrower measure in
     whatever space was left, which put the composer 235px to the right of the text it answers — the
     footer read as belonging to a different page. Same max-width and same centring as main, and the
     reading measure applies INSIDE that, pinned left. */
  /* The dock stands in the same column as the page: same max-width, same centring, same inner
     padding as main. It used to centre a narrower measure of its own in whatever space was left,
     which put the composer 235px right of the text it answers, and matching only the OUTER box
     left it 25px off — the padding is part of where a column starts. */
  /* The strip stands in the same column as the composer under it, for the reason written above:
     the padding is part of where a column starts, and a strip aligned to the dock instead sat 44px
     left of everything it belongs with. */
  #prompt .inner { max-width:var(--magi-sys-page); margin-inline:auto; width:100%; box-sizing:border-box; }
  /* content-box, and the cap counts the TEXT. Everywhere else in this file a box is measured to
     its border, but a reading measure is a count of characters, and the 24px on either side is not
     text. Measured to the border these two came out 48px narrower than the transcript and started
     24px left of it — the composer visibly not under the words it answers. The prompt bar above is
     padded by its own box, so its inner needs none of this. */
  form, #strip:not([hidden]) {
    max-width:var(--magi-sys-page); margin-inline:auto; box-sizing:content-box;
    padding-left:var(--magi-sys-space-300); padding-right:var(--magi-sys-space-300);
  }
  /* Beside a companion the dock IS the middle column, so what is in it takes that column's width
     and the transcript's measure — not the page's.
     Moving the dock between the panes was half the job: the boxes inside it were still capped at
     the whole page's 170ch and centred in whatever the inset left, so the composer sat narrower
     than the conversation and off its axis. Same cap and same left edge as #log, which is what
     "under the text it answers" means. */
  @media (min-width:52.5em) {
    /* The transcript's reading measure, deliberately overridden to FILL the column when the room is
       there. The guide (deviations.md) sanctions the 108ch cap, and its whitespace at the very wide
       end is the guide's own answer — but the operator asked for the width instead: a wide, mostly-
       empty right on a big monitor read to them as a layout fault, not as intended breathing room,
       and it left the prose narrower than the full-width cards above it. So below 108ch of room the
       cap still holds (a laptop keeps the reading measure), and above it the transcript grows to the
       column, bounded at 132rem so a truly enormous monitor does not run one line clear across it.
       The trade the guide warned about — a line past ~110ch is harder to read — is taken knowingly.
       The composer and strip still take the whole track: they are controls, not prose. */
    body[at="agent"] #log,
    /* …and the decision screen with it. It is one level in from this same conversation, so a width
       the transcript takes and it does not is a column that changes under the reader on the way in. */
    body[at="agent"] #agentdetail { max-width:max(var(--magi-sys-wide), min(100%, 132rem)); }
    body[at="agent"] #prompt .inner,
    body[at="agent"] form,
    body[at="agent"] #strip:not([hidden]) { max-width:none; margin-inline:0; }
  }
  #prompt .asking { font:600 var(--md-sys-typescale-body-medium-size)/1.5 var(--magi-ref-mono); color:var(--magi-ref-warn); overflow-wrap:anywhere; }

  /* ── composer ───────────────────────────────────────────────────────────── */
  form {
    padding-top:var(--magi-sys-space-200); padding-bottom:var(--magi-sys-space-200); display:block;
  }
  /* Under the row, not inside the field. As the field's own supporting text it added 20px to the
     field's height, and the buttons — bottom-aligned so they stay put as the box grows — sat 20px
     below the box they belong to. It also reads better here: the note is about what pressing send
     will do, which is the row's business and not the box's. */
  #cnote {
    font:var(--magi-sys-body-s) var(--magi-ref-mono); color:var(--magi-ref-muted); margin-top:var(--magi-sys-space-100);
    padding-left:var(--magi-sys-space-50); overflow-wrap:anywhere;
  }
  .composer {
    display:flex; gap:var(--magi-sys-space-200); align-items:center;
    /* outline-variant, which is the divider role. Drawn in the on-surface role this line was the
       page's TEXT colour used as a rule — 14.6:1, louder than the words it separates. */
    border-top:1px solid var(--magi-ref-outlineVariant); padding-top:var(--magi-sys-space-150);
  }
  /* The composer's field is an M3 outlined text field. Its outline, focus behaviour, floating
     label slot and 56dp height are the component's; what is said here is that it takes the row and
     which colours magi uses. Its own scrolling replaces the auto-grow this page used to do by
     measuring scrollHeight — a measurement that stopped being possible when the textarea moved
     into a shadow root, and one the component already does. */
  /* The small acting words on a row — open a companion, stop a turn, drop a lesson, remove a
     server, fold a transcript. Every one of them is md-text-button now, and it is told what to be
     through the component's tokens: a rule on the host can only draw a second box AROUND the
     button (the border-bottom that used to be here did exactly that), and the font it set never
     reached the label, which lives in a shadow root. letter-spacing and text-transform are the
     two that do cross the boundary, being inherited properties, so those stay as they are. */
  /* An armed control asks rather than warns, and it is the asking that is red. Before this the
     error colour was on :hover, which a touch screen does not have — so the surface where a
     misplaced thumb is likeliest carried no signal at all. */
  md-text-button.armed {
    --md-text-button-label-text-color:var(--magi-ref-error);
    --md-text-button-hover-label-text-color:var(--magi-ref-error);
    --md-text-button-hover-state-layer-color:var(--magi-ref-error);
  }
  md-text-button {
    --md-text-button-label-text-font: var(--magi-ref-mono);
    /* label-large, the role M3 assigns to a button. It was 11px — label-SMALL, a scale value in the
       wrong role — on eight of the page's twelve buttons. The editorial identity is the face and
       the letterspacing, both of which stay; M3 asks for a different typeface to keep the scale,
       not for the scale to be shrunk to fit a look. */
    --md-text-button-label-text-size: var(--md-sys-typescale-label-large-size);
    --md-text-button-label-text-line-height: var(--md-sys-typescale-label-large-line-height);
    --md-text-button-label-text-weight: 500;
    --md-text-button-label-text-color: var(--magi-ref-muted);
    --md-text-button-hover-label-text-color: var(--magi-ref-primary);
    --md-text-button-focus-label-text-color: var(--magi-ref-primary);
    --md-text-button-pressed-label-text-color: var(--magi-ref-primary);
    --md-text-button-hover-state-layer-color: var(--magi-ref-primary);
    --md-text-button-pressed-state-layer-color: var(--magi-ref-primary);
    letter-spacing:0.0467em;
  }
  /* Removing something reads in the error colour on the way to being pressed, and only there: a
     control that is red at rest is a warning, and these are ordinary. */
  md-text-button.drop {
    --md-text-button-hover-label-text-color: var(--magi-ref-error);
    --md-text-button-focus-label-text-color: var(--magi-ref-error);
    --md-text-button-pressed-label-text-color: var(--magi-ref-error);
    --md-text-button-hover-state-layer-color: var(--magi-ref-error);
    --md-text-button-pressed-state-layer-color: var(--magi-ref-error);
  }
  /* Interrupting reads the same way, and it is an ICON button — named md-text-button.stop it was
     styling nothing, so the one control on the fleet that halts a running turn was the only
     destructive-feeling thing on the page that stayed grey under the cursor. */
  /* The error role rather than the accent: this is the one control on the page that destroys work
     in progress, and M3 expresses a destructive action through the colour role, not through a
     variant of its own. The container is the quiet end of that role so a list of three working
     companions is not three red blocks; the glyph carries the colour. */
  md-icon-button.stop {
    --md-icon-button-icon-color: var(--magi-ref-error);
    --md-icon-button-hover-icon-color: var(--magi-ref-error);
    --md-icon-button-focus-icon-color: var(--magi-ref-error);
    --md-icon-button-pressed-icon-color: var(--magi-ref-error);
    --md-icon-button-hover-state-layer-color: var(--magi-ref-error);
    --md-icon-button-pressed-state-layer-color: var(--magi-ref-error);
  }
  md-outlined-text-field#t { flex:1; }
  /* The composer's suggestion, dimmed under the box: how the model thinks the instruction ends,
     with the key that takes it. One line — a suggestion that fills the screen is one nobody reads. */
  .sughint {
    flex-basis:100%; min-width:0; margin:2px 6px 0; padding:2px 8px;
    font-size:.82rem; line-height:1.35; color:var(--magi-ref-muted);
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
    border-left:2px solid var(--magi-ref-outline);
  }
  .sughint::before { content:"⇥ "; opacity:.7; font-weight:600; }
  md-outlined-text-field {
    --md-sys-color-primary: var(--magi-ref-primary);
    --md-sys-color-on-surface: var(--magi-ref-on-surface);
    --md-sys-color-on-surface-variant: var(--magi-ref-on-surface-variant);
    --md-sys-color-outline: var(--magi-ref-outline);
    --md-sys-color-surface: transparent;
    --md-outlined-text-field-input-text-font: var(--magi-ref-mono);
    /* 16px, and not because the scale says so: under 16 iOS Safari zooms the page when a field
       takes focus and does not zoom back. The component's own default is smaller. */
    --md-outlined-text-field-input-text-size: 16px;
    --md-outlined-text-field-label-text-font: var(--magi-ref-mono);
  }
  /* The select is a text field wearing a menu, and it reads its own copy of these. */
  md-outlined-select {
    --md-sys-color-primary: var(--magi-ref-primary);
    --md-sys-color-on-surface: var(--magi-ref-on-surface);
    --md-sys-color-on-surface-variant: var(--magi-ref-on-surface-variant);
    --md-sys-color-outline: var(--magi-ref-outline);
    --md-sys-color-surface-container: var(--magi-ref-surface-container);
    --md-outlined-select-text-field-input-text-font: var(--magi-ref-mono);
    --md-outlined-select-text-field-input-text-size: 16px;
    --md-outlined-select-text-field-label-text-font: var(--magi-ref-mono);
  }
  /* The composer's two are Material Web buttons. Their shape, state layers, ripple and touch
     target come from the component — this page only tells them which colours magi uses, through
     the --md-sys-* properties the library reads. Writing any of the rest here again is how two
     descriptions of one button start to disagree. */

  /* State layers, not colour swaps: M3 puts the on- colour over the surface at a fixed opacity.
     Doing it with a pseudo-element keeps the label's own contrast untouched, which dimming or
     recolouring the text does not. */

  /* ── the table, when the table does not fit ──────────────────────────────
     A separate breakpoint from the navigation's, because it answers a different question. 768px is
     where a rail stops being worth its width; this is where seven columns stop fitting, which is a
     fact about the columns. Tying the two together would mean moving one every time the other's
     reason changed.

     The row's own comment used to say it "collapses to two lines on a phone". Nothing collapsed it
     — the comment described a mechanism that was never written, and the page scrolled sideways at
     every width instead. */
  @media (max-width:74.9375em) {
    .thead { display:none; }   /* no columns left to label */
    .card {
      grid-template-columns:auto auto 1fr;
      gap:var(--magi-sys-space-50) var(--magi-sys-space-200); padding:var(--magi-sys-space-200) var(--magi-sys-space-150) var(--magi-sys-space-200);
    }
    /* Everything takes the full width unless it is placed. The cells stay exactly as they are —
       a row still has as many cells as the head has columns — and only where they sit changes. */
    .card > * { grid-column:1 / -1; }
    .card .badge { grid-column:1 / 3; grid-row:1; }
    .card .actions { grid-column:3; grid-row:1; justify-content:flex-end; }
    /* The three short readings share one line at the foot of the row rather than taking three. */
    .card .num, .card .host { grid-column:auto; }
    .card .num.r { text-align:left; }
  }

  /* 839 for the same reason as the grounds grid above: 640 is not on M3's scale, and the single-
     pane class it belongs to runs to 840. Measured at 640 the dock jumped 171px → 115px at a
     boundary the guide does not have. */
  @media (max-width:52.4375em) {
    /* The two buttons and a text box do not fit across 390px: measured, the box was left with
       about a third of the row and the placeholder was cut mid-sentence. They take their own line,
       which also puts them under the thumb rather than beside it. */
    /* One row. The field flexes and the two buttons keep their size beside it — the field can go
       narrow because a textarea scrolls its own text, and on a phone the keyboard covers the
       transcript anyway, so the room the field needs is a few words and not the whole message.
       It used to give the field a row of its own (flex:1 0 100%) and drop the buttons below, which
       read as a two-line dock for what is one action. Vertically centred on the field, not
       bottom-aligned. The markup order (field → send → interrupt) is the tab order, kept. */
    .composer { flex-wrap:nowrap; align-items:center; }
    .composer md-outlined-text-field#t { flex:1 1 0; min-width:0; }
    .composer .bgroup, .composer md-filled-button, .composer md-filled-tonal-button { flex:0 0 auto; }
    header { padding-left:var(--magi-sys-space-200); padding-right:var(--magi-sys-space-200); }
    main { padding:var(--magi-sys-space-200) var(--magi-sys-space-200) calc(var(--dock, var(--magi-sys-space-1600)) + var(--magi-sys-space-300)); }
    .card .name { font:600 var(--magi-sys-title-l) var(--magi-ref-display); }
    /* minmax(0, 1fr), not 1fr — the same rule this file states two hundred lines above and then
       undid here. A grid track's automatic minimum is its content, so one thing that cannot break
       — a markdown table, a fenced code block, a path with no spaces — widened the row past the
       page. And the page does not scroll sideways, so it was CLIPPED: measured on a phone, a row
       581px wide in a 358px column with 207px of it unreachable by any means. */
    .row { grid-template-columns:minmax(0, 1fr); gap:var(--magi-sys-space-50); }
    .who { text-align:left; }
    .row.user .txt { font-size:var(--md-sys-typescale-body-large-size); }
    /* The prompt's inner column narrows with the rest of them. Left out, it kept the 1.4rem the
       wide layout gives it and the question sat 6px right of the transcript it is about — which is
       the same misalignment the dock had, one breakpoint down. */
    form, #prompt .inner, #strip:not([hidden]) { padding-left:var(--magi-sys-space-200); padding-right:var(--magi-sys-space-200); }
    /* A phone's rail is a section at the foot of the page rather than a drawer, so there is nothing
       to open and nothing to dim behind. */
    #scrim { display:none; }

  }

  /* ── the two widths ─────────────────────────────────────────────────────
     The breakpoint is 600px, which is M3's own: compact is below 600, medium is 600–839, expanded is
     840 and up, and the guide puts a rail on medium and above. It was 768 — a number taken from the
     handbook — and measuring against the guide showed the cost: a 700px window is medium, the guide
     asks for a rail there, and this page was still drawing tabs.

     LAST in the stylesheet, and that is load-bearing. A media query adds no specificity, so these
     rules only win by coming after the ones they override — and they did not. Written above the
     sections they contradict, the wide rule lost to a later display rule for the strip that used
     to sit above the page, and the page
     offset lost to a later "padding:" shorthand, which resets the padding-left this sets. The
     result was both navigations at once on a desktop and, on a narrow screen, a fixed rail sitting
     on top of the page it was supposed to stand beside. */

  /* Wide: the rail IS the navigation, so the tabs go — two of them for one set of four sections is
     one too many. The page starts to the right of the rail, by exactly its width. */
  @media (min-width:37.5em) {
    /* The rail's room is taken from the BODY, not from the page's own padding. Taken from the
       padding it came out of the CONTENT box, which is why the masthead's rule ran 102px further
       left than the words above it — a border is drawn on the box and padding sits inside it. */
    body { padding-left:var(--magi-comp-rail-w); }
    header, main, #prompt .inner, form, #strip:not([hidden]) { padding-left:var(--magi-sys-space-300); padding-right:var(--magi-sys-space-300); }
    /* ── the medium class is narrow too, and the bar has to fit in it ────────
       At 600 the window grows by one pixel and the rail takes 96 of it, so the masthead has 489px
       where it had 584 — and every rule that made the row fit ended at 599. Measured across the
       medium class: the bar wrapped onto two rows and stood 109px tall at 600, 119px at 610–650,
       and on the shared destination it stayed wrapped to 770. The guide's small app bar does not
       wrap ("⚠ small에서는 줄바꿈 금지") and does not change height with the window.
       So the fitting rules run to the top of the medium class, where the second pane arrives and
       the room comes back. The phone's own bar — the arrow, the headline, the 64dp — stays below
       600; this is only about not wrapping. */
    @media (max-width:52.4375em) {
      header { flex-wrap:nowrap; padding-left:var(--magi-sys-space-200); padding-right:var(--magi-sys-space-200); }
      #state { min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
      /* The trail gives way too. The row does not wrap any more, so the wrapping moved INSIDE it:
         measured on the people and map screens, the crumb took two lines from 600 to 700 and the
         bar stood at 80px where every other screen has 63. */
      #crumbs { flex-wrap:nowrap; min-width:0; overflow:hidden; }
      /* …and each rung stays on its own line. The row does not wrap and the trail does not wrap,
         so the wrapping moved one level further in: measured at 640, the second crumb broke inside
         itself and #crumbs stood 39px tall, which is what made the bar 79. */
      #crumbs > a, #crumbs > span { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; min-width:0; }
      .mark { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
      #whereami { flex:0 1 auto; }
      #palOpen, #prefs { flex:none; }
    }
    #dock { padding-left:var(--magi-comp-rail-w); }
    /* Open, it is a floating panel. Closed, it is part of the furniture. */

    }
  /* Compact (below 600px, M3's own boundary): the tabs navigate, so there is no hamburger — a menu button next to a row of tabs is
     an invitation to look in the wrong place. What is left behind the button is the preferences,
     so on this width it says so and wears a gear. */
  /* Narrow: no drawer at all. The tabs navigate, the theme has its own toggle in the masthead, and
     what is left — language, and which machine this is — is a plain section at the foot of the
     page. A drawer for two selects is a door in front of a cupboard, and on a phone it covered the
     thing the reader had just navigated to. */
  /* ── the phone's first screen ────────────────────────────────────────────
     Measured on a 730px screen: the masthead, the tabs and the filters took 455px before the first
     agent, and a fleet page that shows one and a half rows is a list you scroll to read rather than
     one you glance at. Everything below is that 455 coming down. */
  @media (max-width:37.4375em) {
    /* On a phone a 15rem board lane is 240px and its cards read at 22 characters a line — below the
       floor. The board scrolls sideways, so a lane can afford most of the screen: it nearly fills
       the viewport and snaps, giving the card text a readable measure and one lane to a thumb. */
    .lanes { scroll-snap-type:x mandatory; }
    /* A bounded basis, not just a floor: with flex:0 0 auto + only a min-width the lane grew to its
       widest single-line card (measured 422px on a 390 screen, wider than the viewport, defeating
       the one-lane-to-a-thumb snap). A fixed basis makes the card wrap to the lane instead. */
    .lane { flex:0 0 min(22rem, 85vw); max-width:85vw; scroll-snap-align:start; }
    /* ── the app bar, at the height an app bar has ───────────────────────────
       The guide gives the small top app bar a height and says to keep it: "기본 높이를 그대로 쓰고
       창 전체 폭을 차지한다. 기본보다 낮게 만들지 말 것" — 64dp. This one was 60px of a row whose
       items sat on a shared BASELINE, which is why the report was that the space above the
       companion's page is too tight: the words hung at the top of the bar rather than in the
       middle of it, and the two 40px buttons had 8px above and 12px below.
       Centred, and 64 tall. Nothing else about the bar changes here. */
    /* How tall the two bars are, said once: the strip sticks to the top one, and the page keeps
       its foot clear of the bottom one.
       The navigation bar is its item (48dp of target, 64 with the label under the icon) plus its
       own padding plus the hairline on top — 81px, which is the guide's baseline 80dp bar. It was
       reserved as 56, the height of the OTHER kind of bar, and the 25px difference was covered by
       whatever margin the last card happened to have. Written as the sum of the parts so the two
       cannot drift apart again. */
    :root {
      --magi-comp-appbar-h:calc(64px + env(safe-area-inset-top));
      --magi-comp-navbar-h:calc(64px + 2 * var(--magi-sys-space-100) + 1px);
    }
    header {
      align-items:center;
      padding-top:calc(var(--magi-sys-space-100) + env(safe-area-inset-top)); padding-bottom:var(--magi-sys-space-100);
      /* 16dp, which is the compact layout margin the page's own content uses — so the leading
         control lines up with the first word under it instead of standing 8px further in. */
      padding-inline:var(--magi-sys-space-200);
      gap:var(--magi-sys-space-100);
    }
    /* The brand yields before the row does. At twice the text size on a 320px screen the word
       pushed the preferences button 17px past the edge and the page could be scrolled sideways —
       a logo is the one thing here that can be cut without costing anybody an answer. */
    header .mark {
      font-size:var(--md-sys-typescale-title-large-size);
      min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
    }
    /* ── what the bar does NOT carry on a phone ──────────────────────────────
       Measured on a 390px screen, the bar held seven things: the brand, user@host, two crumbs and
       a separator, a dot, a message, and two buttons. Four of them were ellipsed — "you@s…",
       "Com…", "L…" — and the guide's rule about the one that matters is flat: "헤드라인을 자르지
       말 것". A bar of abbreviations answers nothing; the room has to come from somewhere.

       user@host goes. It answers "which of my three consoles is this", which is a question asked
       at a desk with three windows open, and it is in the preferences in full. Cut to "you@s…" it
       could not answer it anyway. */
    .whereami { display:none; }
    /* And the brand yields to the subject on a companion's page. The guide's app bar has ONE
       headline and it is what the screen is about; here the biggest word was the product's name
       and the companion's own name was the smallest thing on the line, in a crumb at label-small.
       On the top-level screens the brand stays: there the destination is named by the bar at the
       foot, and the mark is the one thing saying which product this is. */
    body[at="agent"] header .mark {
      /* Hidden from the eye, kept in the heading tree. display:none took the page's only h1 out of
         the accessibility tree with it, so a companion's page at compact had no heading at all —
         the opposite of what the h1 was added for. */
      position:absolute; width:1px; height:1px; overflow:hidden;
      clip-path:inset(50%); white-space:nowrap;
    }
    /* The count on the SAME line as the brand. Given its own row it cost 40px of the first screen
       to say something that fits beside a five-letter word. It is allowed to shrink and to clip:
       "5 agents · 2 waiting" is legible at any truncation that keeps the number. */
    /* One line, clipped at the end rather than wrapped. Squeezed between the brand and two icons it
       broke "5 AGENTS ·" across three rows, which is taller than the two-row masthead it replaced. */
    #state {
      font-size:var(--md-sys-typescale-label-small-size); letter-spacing:.08em; margin-left:auto; min-width:0;
      white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
    }
    /* The dot keeps its shape while the words beside it give way.
       Three states are told by three SHAPES here — a small dot with a halo, a bigger solid one, an
       empty ring — and #state was the row's only shrinkable item: measured at 390px on a
       companion's page, where the readout is the dot and nothing else, the 6px circle was drawn
       3.8px wide and the 10px ones 6.2 and 4.6. Squashed into ovals of three similar widths, the
       second indicator the guide asks for was gone and only the colour was left. */
    /* On EVERY screen, not only a companion's. The shared destination writes a permanent 64-
       character summary into the status line, and the two of them are the row's only shrinkable
       items — so there the dot was drawn 1.9px wide at 390 and 1.0px at 320, which leaves a state
       told by colour alone. The dot is six pixels; the sentence beside it can give them up. */
    /* It gives room up, down to the dot. `flex:none` pinned it, and then a "3 agents · 1 waiting
       on you" pushed the preferences button 18px past the edge of a 390px screen and 88 past a
       320 one — taking a navigation destination off the glass with it, since the fixed bar
       stretches to the document. The floor is the dot, the one part of this readout that is not
       words. */
    #state { flex:0 1 auto; min-width:10px; }
    #state::before { flex:none; }
    /* The sentence gives way; the way to the blocked companion does not.
       Both were shrinkable and the button lost: at 320 its label was a single orange "w", and at 2x
       text it was laid out past its clipped container, 24 columns of it owned by the preferences
       button underneath — a 0x0 target sitting on 23 of that button's 48 pixels. It is the only
       control on this bar that goes to somebody who is waiting for an answer, so it is the last
       thing on the bar to be given up, not the first. */
    #state .scount { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
    /* The compact form of the way-to-the-blocked button: a count and a pause glyph, so it cannot
       overflow the icon buttons beside it while the full sentence stays its accessible name. */
    #state .jump { flex:none; }
    #state .jump .jfull { display:none; }
    #state .jump .jshort { display:inline; }
    /* The two icon buttons sit at the end of the same line, not on one of their own. Adding the
       gear pushed the masthead back onto two rows and the first agent from 337px to 393px —
       everything gained on this width, given straight back. */
    header { flex-wrap:nowrap; }
    /* BOTH of them, at the size a finger needs.
       Only the second one was pinned, so the first was the row's shrinkable item: measured at 390px
       on a companion's page it drew 27px wide, at 320px and three crumbs deep 24px — and each
       button carries a 48px press expander centred on its box, so the two expanders OVERLAPPED by
       up to 8px. document.elementFromPoint on the seam returned the later element, which means the
       search button's usable target was 41px and part of it opened the preferences instead.
       48dp of state layer, which is also the target: no expander to bleed past the box, and 8dp of
       clear space between them from the row's own gap. */
    #palOpen, #prefs {
      flex:none;
      --md-icon-button-state-layer-width:48px; --md-icon-button-state-layer-height:48px;
    }
    /* The crumb is hidden only where the tab strip below already says the same thing. On a
       companion's page there are no tabs, and hiding it left a masthead reading "magi" with no
       word anywhere for WHICH companion — the one question that page exists to answer. */
    body:not([at="agent"]) #crumbs { display:none; }
    /* And it must be able to give room back. At 320px the masthead's own children came to 13px
       more than the 16dp margins leave, which the row absorbed by pushing the last button past
       the padding — far enough that its 48dp touch expander crossed the viewport edge and the
       page could be scrolled 1px sideways. The crumb is the one item here made of text that can
       be cut, so it is the one that yields. */
    #crumbs { font-size:var(--md-sys-typescale-label-small-size); min-width:0; overflow:hidden; }
    /* Only when #back is a text crumb. When it is the arrow (paintCrumbs marks the way-out rung
       .up, and on a companion page that rung IS #back), the .up rule below must win — this rule's
       two-ID specificity would otherwise beat .up's display:grid and drop the glyph to the top of
       the box. The display is left to the .up/.leaf rules and to flex-item blockification. */
    #crumbs #back:not(.up) { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
    /* ── the trail, as a phone can carry it: an arrow and a headline ─────────
       A breadcrumb is a wide-screen thing — it spends a word on every level and reads left to
       right. The guide's compact app bar spends the leading slot on ONE control ("선행 버튼:
       메뉴 아이콘 또는 뒤로 화살표") and the rest on the headline, at title large.
       So the trail keeps its two ends and drops its middle: the level ABOVE becomes the arrow, and
       the level you are on becomes the headline. Nothing is lost — the arrow goes exactly where
       the word it replaced went, one press per level, all the way out. Which crumb is which is
       decided in paintCrumbs, where the levels are known, and marked .up and .leaf. */
    #crumbs { flex:1 1 auto; align-items:center; gap:var(--magi-sys-space-100); }
    #crumbs > :not(.up):not(.leaf) { display:none; }
    #crumbs .up {
      /* 48, not 40. It is a hand-built anchor, so the bundle's press expander never reaches it —
         and on a phone this is the only way off a companion's page. */
      flex:none; width:48px; height:48px; display:grid; place-items:center;
      border:0; padding:0; font-size:0; color:var(--magi-ref-fg);
    }
    /* The glyph is drawn, not written, so the crumb's own word stays in the accessibility tree as
       the link's name — "companions" is what a screen reader should say, not "left arrow". */
    #crumbs .up::before {
      /* 24dp, which is the icon size everything else in this bar is drawn at — taken from the
         typescale role that carries that number rather than written as a pixel count, so a reader
         who scales their text scales this with the words beside it. */
      content:"\2190"; font:400 var(--md-sys-typescale-headline-small-size)/1 var(--magi-ref-mono);
      color:var(--magi-ref-fg);
    }
    #crumbs .up:hover::before { color:var(--magi-ref-primary); }
    #crumbs .leaf {
      font:600 var(--md-sys-typescale-title-large-size)/1.25 var(--magi-ref-display);
      letter-spacing:0; color:var(--magi-ref-fg); text-decoration:none; border-bottom:0; padding-bottom:0;
      min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
    }
    /* The filters as one scrolling row rather than three stacked ones. Four chips do not fit across
       390px and never will; a row that scrolls keeps them one line high and keeps the fourth
       reachable, which stacking also did but at three times the cost. */
    #summary {
      flex-wrap:nowrap; overflow-x:auto; scrollbar-width:none;
      margin:var(--magi-sys-space-200) 0 var(--magi-sys-space-50);
      /* Room above and below for what the components put there. A scroller clips in both axes, and
         these chips carry a 48dp press area around 32dp of ink — measured, 8px of it was cut off
         the top and a 48dp target answered as 40. The guide's line for exactly this: "필요하면
         타겟이 보이는 컨테이너 밖으로 뻗어도 된다" — it may stick out, so the box has to let it. */
      padding-top:var(--magi-sys-space-100); padding-bottom:var(--magi-sys-space-150);
      /* …including at the leading edge, where the first control's 48dp expander was cut to 44 by
         the scroller's own clip. Pulled back by the same amount so the row still begins on the
         page's margin. */
      padding-inline:var(--magi-sys-space-100); margin-inline:calc(-1 * var(--magi-sys-space-100));
    }
    #summary::-webkit-scrollbar { display:none; }
    .tile { flex:0 0 auto; }
    /* The ways to the other views come FIRST in the scrolling row.
       Pushed to the end by margin-left:auto they sat at x=519 in a 390px window — not one pixel of
       any of them on screen, in a strip whose scrollbar is hidden and which a vertical wheel does
       not move. They are the only doors to the board and the map anywhere in the console; the
       filters they were sitting behind are the thing you scroll for. */
    .toview { order:-1; }
    .toview.lead { margin-left:0; }
    /* And 8dp of clear space between their press areas, which are 48dp around 40dp of ink. */
    .toview + .toview { margin-inline-start:var(--magi-sys-space-100); }
    /* The ways to the other views keep their size in a row that scrolls.
       Only the chips were pinned, so the three icon buttons were the row's shrinkable items and
       were squeezed from 40px to 24 — with a 48px press expander each, which then OVERLAPPED its
       neighbour by 16px. Measured by hit-testing across the band: the first two answered to 32px
       of the 48 they claim, and the rest of each belonged to the button beside it. */
    .toview { flex:0 0 auto; }
    /* The tab strip is a navigation, not a heading: it does not need the room a heading takes. */
  }
  @media (max-width:37.4375em) {
    /* ── the phone's navigation: a bar at the bottom ─────────────────────────
       The guide is explicit for this width and this count: "Navigation bars provide access to
       three to five destinations", "Compact windows should always use a navigation bar", items
       laid out vertically (icon over label), full width, at the bottom, in a fixed position.
       This console has four destinations.

       What was here instead: the rail hidden outright, with the two top tabs doing navigation —
       so on a phone the MEETING and PEOPLE destinations could not be reached at all. Not a
       styling defect: two of four doors were missing.

       The same markup as the rail, restyled. One navigation to keep true rather than a second
       set of links that has to be told when the first one changes. */
    #rail {
      position:fixed; inset:auto 0 0 0; top:auto; z-index:5; width:auto; transform:none;
      /* The cutout is on a long edge when the phone is sideways, and viewport-fit=cover draws the
         page under it. Only the top and bottom insets were being honoured. */
      padding-inline:calc(var(--magi-sys-space-100) + env(safe-area-inset-left))
                     calc(var(--magi-sys-space-100) + env(safe-area-inset-right));
      flex-direction:row; overflow:visible; gap:0;
      border-right:0; border-top:1px solid var(--magi-ref-outlineVariant);
      background:var(--magi-ref-surface-container);
      padding:var(--magi-sys-space-100) var(--magi-sys-space-100)
              calc(var(--magi-sys-space-100) + env(safe-area-inset-bottom));
    }
    /* The rail's own furniture is a rail's, not a bar's: there is no drawer to widen here. The
       FOOT is not furniture — it holds a destination — so it joins the row instead of going with
       the button. Hidden with the menu, the console had three doors on a phone and the fourth
       (people and permissions) could not be reached at all. */
    #railMenu { display:none; }
    #railNav, #railFoot { display:flex; flex-direction:row; flex:1 1 auto; gap:0;
      border-top:0; padding-top:0; margin-top:0; }
    #railFoot { flex:1 1 0; }
    .raili {
      flex:1 1 0; min-width:0; flex-direction:column; gap:2px;
      align-items:center; justify-content:center; padding:var(--magi-sys-space-50) 0;
      /* 48dp of target, and the label under the icon rather than beside it — the guide's own
         compact item. */
      min-height:48px; text-align:center;
    }
    /* The short word here, the full one everywhere else — the guide's rule is that a bar's label
       is one or two words and is NOT squeezed or cut to fit. Four equal quarters of a 390px screen
       is 97px each; "People and permissions" is 159 of them. */
    .raili .lbl { display:none; }
    /* It wraps rather than being cut. The guide's own words for a bar at twice the text size:
       "the navigation bar should grow vertically … it's okay for scaled text to wrap … ensure the
       full label is always visible on-screen at up to 2x text sizing." Measured at 2x, the
       SELECTED destination's label was the one being ellipsed — "Companions" needed 144px in a
       111px item. Two short lines are the guide's answer; three dots are not. */
    .raili .lblshort { display:block; font-size:var(--md-sys-typescale-label-medium-size); max-width:100%;
      overflow-wrap:anywhere; hyphens:auto; }
    /* The line under each destination is for a widened rail; four of them across a phone is four
       paragraphs where there is room for four words. */
    .raili .sub { display:none !important; }
    /* And the page keeps its foot clear of it. */
    /* 80dp, which is what the bar actually measures — the guide's baseline navigation bar is 80dp
       tall (common 64) and this one draws 81 with its label under its icon. Reserving 56, the M3
       height for the OTHER kind of bar, left a 25px shortfall: nothing was clipped today because
       the last card's own margin covered it, which is the kind of luck that ends with one edit. */
    body { padding-bottom:calc(var(--magi-comp-navbar-h) + env(safe-area-inset-bottom)); }
    /* One level in, the bar goes. A companion's page is the DETAIL half of a list-detail, which on
       compact takes the whole window and offers the way back rather than the whole map — and the
       composer lives at the foot of that screen, which is where the bar would be. The crumb in the
       masthead is the way out; the bar comes back with the list. */
    body[at="agent"] #rail { display:none; }
    body[at="agent"] { padding-bottom:0; }

    /* The dock takes at most half the phone, and the question inside it scrolls.
       A companion blocked on a decision puts its whole question in the prompt bar — the grounds,
       the options, the report — and measured on the demo that was 451px of a 664px screen, fixed
       over everything: the panel behind it could not be read and none of its rows could be tapped.
       The question is still the most important thing on the screen, so it keeps the largest share
       any one thing gets here; what it may not do is take the screen and the controls with it. */

    /* Every panel keeps its foot clear of the dock, not just the conversation.
       The dock is fixed and measured, and only #log reserved its height — so on a phone the last
       rows of the workspace tree and of this panel's list sat under it. Measured on the demo,
       where the companion is blocked and the dock is tall: the second row of the list could not be
       tapped at all, because the prompt bar was over it. */
    body[at="agent"] #side, body[at="agent"] #files {
      padding-bottom:calc(var(--dock, 0px) + var(--magi-sys-space-300));
    }

    /* ── one card, one screen: the panel is the frame ────────────────────────
       A card is a container for grouped content — it says "these things belong together, and this
       is where they stop". Inside a compact panel the card IS the panel: the tab above it has
       already drawn the boundary, and a second outline eight pixels inside the first is a frame
       around a frame, spending width on saying twice what was said once.
       So on a phone the cards in a panel keep their heading and give up their edge. Not the
       COMMIT workbench or an open file, which are objects in a slot rather than sections of a
       panel — those keep their box. */
    body[at="agent"] #side md-outlined-card,
    body[at="agent"] #detail {
      --md-outlined-card-outline-width:0px;
      --md-outlined-card-container-shape:0px;
      --md-outlined-card-container-color:transparent;
      padding-inline:0;
    }
    /* #detail draws its own frame (a plain border, not the card token), so zeroing the token left
       it framed AND unpadded on a phone — the About tab's values jammed against a line they were
       supposed to be inset from. The edge it gives up is the edge it actually draws. */
    body[at="agent"] #detail { border:0; border-radius:0; }
    /* ── the About tab is the disclosure; the fold inside it is a second one ──
       The facts card carries a fold bar, which is right in the card SLOT on a wide screen: there
       the facts share their box with an open file and somebody may want the file to have it. On a
       phone the facts are a tab of their own — the tab is the disclosure — and the fold left the
       tab able to open on a single line reading "What this is ▸" with nothing under it, because
       the folded state is remembered and the desk is where it gets folded.
       So here the facts are simply the panel: no bar, nothing to unfold. The two fields the bar
       summarised (the state and the workspace) are rows in the grid it was hiding. */

    /* The tab strip belongs to the bar above it, not to the content below it.
       Tabs attached to a component move with it and read as one block; floating 16px under the app
       bar, the strip read as the first thing in the page rather than as the bar's own second row —
       and it cost a phone 16px of the little height it has. Under the strip the 16dp margin stays,
       because that IS the page's top margin. */
    body[at="agent"] main { padding-top:0; }
    /* ── the strip travels with the bar it belongs to ────────────────────────
       "⚠ 탭을 앱바 뒤로 스크롤시키지 말 것 — 컴포넌트에 붙은 탭은 한 덩어리로 움직인다."
       Measured before this: the app bar is sticky and the strip was not, so at the bottom of a
       companion's page the four tabs sat 78px above the window — and a companion's page opens
       scrolled to the newest words, so on ARRIVAL there was no way to reach About, Workspace or
       Going on without scrolling back up to look for them.
       Full width, too: the guide's tab container takes the window, and inside the page's 16dp
       margin the strip and its divider stopped short of both edges — which is also 32px this
       width does not have to spare for four labels. */
    body[at="agent"] #ptabs {
      margin-top:0;
      position:sticky; top:var(--magi-comp-appbar-h); z-index:2;
      background:var(--magi-ref-bg);
      margin-inline:calc(-1 * var(--magi-sys-space-200));
    }
    /* The status line stays ON the row, and gives way.
       It had a line of its own here for a few hours, on the reasoning that a message is rare — and
       it is not: the shared destination writes a permanent four-part summary into it, so that
       screen's bar became 133px of three rows with both buttons pushed to a third row at the
       leading edge. Measured at 390 and at 320. A bar whose height depends on what a page happens
       to be saying is not a bar the guide describes. So it shrinks and clips, like the count. */
    /* The message gives way before the headline does. Both can shrink and the headline was going
       first: measured at 320, a companion's name rendered as one letter while the line beside it
       kept 128 of its 246px. The name is what the screen is about. */
    #note { min-width:0; flex:0 3 auto; max-width:45%; }
    body[at="agent"] #files > .filescard {
      border:0; border-radius:0; background:none;
    }
    /* An open file is read by scrolling the PAGE, not by scrolling a box inside it.
       "모바일: 카드 내부는 스크롤하지 않는다 … 스크롤바 두 개가 생기는 것을 막기 위해" — measured
       on a 400-line file at 390px, this box scrolled 7675px through 398 in a page that was also
       scrolling, and sideways as well: a code line ran 359px through a 326px window with the ends
       of the lines reachable only by dragging inside the card. Lines wrap here instead, which is
       what a phone does with a long line of prose and there is no room to do anything else with. */
    /* The READING view only. The editor's box carries both classes, so this took away the one
       scroller it has and `.editstack` then sized itself to the longest line: measured, pressing
       Edit on an ordinary file took the document from 390px to 5149 and put the composer 2586px
       down. A card that scrolls is wrong on a phone; an editor is not that card. */
    #fileview .filebody:not(.editbody) { max-height:none; overflow:visible; }
    #fileview .editbody { overflow:auto; }
    /* And a token with nowhere to break breaks anyway. pre-wrap only wraps at spaces, so one URL
       or one base64 line neither wrapped nor scrolled nor clipped — it widened the DOCUMENT, from
       390 to 636px, and took the fixed composer with it while the bars stayed at 390. */
    #fileview .filecode { white-space:pre-wrap; overflow-wrap:anywhere; }

    /* The add-a-companion field takes the width here. Capped at 16rem it cut its own label —
       "Add a companion (Enter)" needs 222px and the field gave it 188, so the part that says how
       to commit a name is the part that went, at rest, which is the only time a label is read. */
    .scopes md-outlined-text-field { max-width:none; }
    .prefrow.narrowonly { display:flex; }
    /* Two side panes do not fit beside a conversation on a phone, and stacking three columns is
       three screens of scrolling to reach the transcript. The file pane goes under it, opened by
       the same handle — which is why the handle is not hidden here. */
    #agentview { grid-template-columns:minmax(0, 1fr); }
    #filecol { position:static; max-height:none; order:2; }
    /* The aside is four cards of context under a conversation, which is a long way to scroll for
       something you glance at. Tighter, so the transcript keeps the screen. */
    #side { gap:var(--magi-sys-space-150); }
    #side md-outlined-card { padding:var(--magi-sys-space-200) var(--magi-sys-space-200); }
    #agentdetail .hs, .ho { grid-template-columns:4.5rem 1fr; }
    
    /* The dialogs take the whole screen here. The guide gives a dialog two shapes and the reason is
       room: six fields and a select in a basic dialog on a 390px phone is a box with its own
       scrollbar inside a page that also has one. "Full-screen dialogs are for compact breakpoints
       only", and the trigger it names is a dialog holding something that takes keyboard input.
       All four of these do, and only the server dialog was getting it: the palette was a 342px box
       with a field and fifteen rows, scrolling 934px of list through 332 — inside the dialog's own
       scroller — with 48px of unused screen down each side. The palette has a second rule of its
       own, from the search guidance: compact is full-screen, medium and up is docked. */
    #mcpDialog, #palDialog, #askDialog, #fmtDialog {
      max-width:100%; max-height:100%; width:100vw; height:100dvh;
      --md-dialog-container-shape:0;
    }
    /* A full-screen dialog's headline is title-large, not the basic dialog's headline-small: the
       one typographic token that tells the two variants apart, and these had taken the shape of
       one while keeping the type of the other. */
    #mcpDialogK, #palK, #askK, #fmtK {
      font-size:var(--md-sys-typescale-title-large-size);
    }
    /* The list is the dialog now, so it does not keep a scroller of its own inside one. Capped at
       half the window it scrolled 766px through 332 in a 664px box with 100px of empty dialog
       under it — the very shape the full-screen change was made to end. */
    #palList { max-height:none; overflow-y:visible; }
    /* And the strip fills with the bar it is stuck to. `body[scrolled] #ptabs`, written at the top
       of this file, is an exact specificity tie with the compact rule above — one id, one
       attribute and one type each — so source order decided it and the later one won. The strip is
       sticky ONLY here, so the one place the fill was written for is the one place it never
       reached; between 600 and 839 it applied to a strip that does not stick. */
    body[scrolled][at="agent"] #ptabs { background:var(--magi-ref-surface-container-low); }

    /* The close control, pinned to the corner the headline leaves room for. It lives in the
       content slot so the dialog's name stays the title (see closeX); the dialog is the whole
       screen here, so fixed positioning IS its own corner, scroller or no scroller. */
    .dlgclose { display:inline-flex; position:fixed; top:var(--magi-sys-space-150); inset-inline-start:var(--magi-sys-space-100); z-index:1; }
    #mcpDialogK, #palK, #prefsK, #askK, #fmtK {
      display:flex; align-items:center; gap:var(--magi-sys-space-100);
      padding-inline-start:var(--magi-sys-space-500);
    }
  }
