  :root {
    color-scheme: light dark;
    --bg: #eceef1; --panel: #fff; --head: #f6f8fa; --fg: #1a1a1a;
    --muted: #6b6b6b; --line: #dfe1e4; --sel: #eaeef2;
    --ok: #1a7f37; --bad: #cf222e; --warn: #9a6700; --accent: #0969da;
    /* A router of the drawing, which stands on the panel rather than
       against it. */
    --box: #e4e9ef; --box-line: #ccd2da;
    /* A link of a topology that is stopped, which has to stand out from the
       panel as much as a router does, unlike the lines between panels. */
    --wire: #8c959f;
    /* A traced route, which violet keeps apart from the green of the links
       it is drawn on. */
    --trace: #8250df;
    /* A forwarding loop, in orange, apart from the trace and from the green
       and red of the links. */
    --loop: #bc4c00;
    --track: #eceef1; --fill: #0969da;
    --hover: color-mix(in srgb, var(--fg) 5%, transparent);
    /* The edge of a chip in a head, which the lines between the parts of
       the page are too faint for on a dark one. */
    --edge: color-mix(in srgb, var(--fg) 22%, transparent);
    --tint: color-mix(in srgb, var(--fill) 14%, transparent);
    --side: 13rem; --top: 33vh; --rail: 28px; --gutter: 4px;
  }
  /* The page is dark when it is told to be, and when it has been told
     nothing and the system asks for it. */
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
      --bg: #090a0c; --panel: #14161a; --head: #1b1f24; --fg: #e6e6e6;
      --muted: #9aa0a6; --line: #2a2e35; --sel: #22262d;
      --ok: #3fb950; --bad: #f85149; --warn: #d29922; --accent: #58a6ff;
      --box: #1b1f24; --box-line: #2a2e35;
      --wire: #59616b;
      --trace: #a371f7;
      --loop: #db6d28;
      --track: #0e1013; --fill: #3b7ace;
    }
  }
  :root[data-theme="dark"] {
    color-scheme: dark;
    --bg: #090a0c; --panel: #14161a; --head: #1b1f24; --fg: #e6e6e6;
    --muted: #9aa0a6; --line: #2a2e35; --sel: #22262d;
    --ok: #3fb950; --bad: #f85149; --warn: #d29922; --accent: #58a6ff;
    --box: #1b1f24; --box-line: #2a2e35;
    --wire: #59616b;
    --trace: #a371f7;
    --loop: #db6d28;
    --track: #0e1013; --fill: #3b7ace;
  }
  :root[data-theme="light"] { color-scheme: light; }
  * { box-sizing: border-box; }
  html, body { height: 100%; }
  body {
    margin: 0; overflow: hidden; background: var(--bg); color: var(--fg);
    font: 13px/1.45 system-ui, sans-serif;
  }

  /* ===== panels ===== */

  /* Two gutters, so the side panel takes width from the rest and the
     topology takes height from the tabs under it. The page itself never
     scrolls: every panel keeps what it cannot show to itself. */
  .app {
    height: 100dvh; display: grid;
    grid-template-columns: var(--side) var(--gutter) minmax(0, 1fr);
  }
  .right {
    display: grid; min-height: 0;
    grid-template-rows: var(--top) var(--gutter) minmax(0, 1fr);
  }
  /* A panel collapsed leaves its room to the drawing, the side panel a
     strip of its buttons and the one under the drawing its tabs, and has
     no size for its gutter to drag, which only parts it from the drawing. */
  .app:has(> #side-panel.collapsed) {
    grid-template-columns: var(--rail) var(--gutter) minmax(0, 1fr);
  }
  .right:has(> #bottom-panel.collapsed) {
    grid-template-rows: minmax(0, 1fr) var(--gutter) auto;
  }
  .app:has(> #side-panel.collapsed) > #gutter-side,
  .right:has(> #bottom-panel.collapsed) > #gutter-top { pointer-events: none; }
  .panel {
    display: flex; flex-direction: column; overflow: hidden;
    min-width: 0; min-height: 0; background: var(--panel);
  }
  .head {
    display: flex; align-items: center; gap: .4rem; flex: none; height: 26px;
    padding: 0 .4rem; background: var(--head);
    border-bottom: 1px solid var(--line); font-size: 11px; color: var(--muted);
    white-space: nowrap;
  }
  .head .label {
    font-weight: 600; letter-spacing: .05em; text-transform: uppercase;
  }
  /* What is about the topology is on the left of the head of the drawing,
     and what is about the page on the right, with the state of the topology
     and what the page has to say beside it: a topology starting, what went
     wrong, and that the worker has to spin.
     A head too narrow for all of it says the label of the toggle shorter,
     then leaves out the variation of the topology, the state, the toggle
     and the labels of the buttons that run the topology, and cuts short the
     name of the topology and the trace before any button. */
  #topology-head { container: topology-head / inline-size; }
  .spacer { flex: 1 0 0; }
  #status { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
  #status:empty { display: none; }
  #topology-summary { flex: none; }
  #topology-summary:empty { display: none; }
  #areas-short { display: none; }
  @container topology-head (max-width: 820px) {
    #areas-label { display: none; }
    #areas-short { display: inline; }
  }
  @container topology-head (max-width: 680px) {
    #topology-title .variation { display: none; }
  }
  @container topology-head (max-width: 560px) {
    #topology-summary { display: none; }
  }
  @container topology-head (max-width: 520px) {
    #areas-toggle { display: none; }
  }
  /* Start, Stop and Pause say what they do in their icons alone, their
     labels left to be read out rather than shown. */
  @container topology-head (max-width: 500px) {
    #run, #pause { padding: .25rem .4rem; }
    #run-label, #pause-label {
      position: absolute; width: 1px; height: 1px; overflow: hidden;
      clip-path: inset(50%); white-space: nowrap;
    }
  }
  #repo { display: inline-flex; flex: none; color: var(--muted); }
  #repo:hover { color: var(--fg); }
  .body { flex: 1; min-height: 0; position: relative; }

  .gutter { background: var(--bg); touch-action: none; }
  body.dragging { user-select: none; }
  .gutter:hover, .gutter.dragging { background: var(--accent); }
  .gutter.vertical { cursor: col-resize; }
  .gutter.horizontal { cursor: row-resize; }
  /* What collapses the side panel and the one under the drawing, at the
     end of its head, pointing the way the panel goes, and the other way
     once it is collapsed. */
  button.fold {
    display: inline-flex; align-items: center; flex: none; padding: .15rem;
    border: 0; background: transparent; color: var(--muted);
  }
  button.fold:hover { color: var(--fg); }
  button.fold svg { transition: transform .15s; }
  .collapsed button.fold svg { transform: rotate(180deg); }
  #side-fold { margin-left: auto; }
  #bottom-fold { margin-right: .3rem; }
  #bottom-panel.collapsed #views { display: none; }

  /* ===== side panel ===== */

  /* The name of the lab heads the side panel, in the colours of the logo
     beside it, on a bar as tall as the heads so it lines up with the one
     across the gutter. */
  .brand {
    display: flex; align-items: center; gap: .4rem; flex: none; height: 26px;
    padding: 0 .4rem; background: var(--head);
    border-bottom: 1px solid var(--line); white-space: nowrap; overflow: hidden;
  }
  /* The logo and the name are a link to the page as it opens, which looks
     no different from them. */
  .brand .home {
    display: flex; align-items: center; gap: .4rem; min-width: 0;
    color: inherit; text-decoration: none;
  }
  .brand img { width: 18px; height: 18px; flex: none; }
  .brand .name {
    min-width: 0; overflow: hidden; text-overflow: ellipsis;
    font-size: 14px; font-weight: 600;
  }
  .brand .holo {
    background: linear-gradient(90deg, #f109fd, #8a5cf0 55%, #17c3df);
    -webkit-background-clip: text; background-clip: text; color: transparent;
  }

  .choices {
    flex: 1; min-height: 0; overflow: auto; padding: .4rem;
  }
  /* Says something from the first paint, until the topologies are in. */
  .choices:empty::before {
    content: "Loading topologies…"; display: block;
    padding: .4rem .45rem; color: var(--muted); font-size: 12.5px;
  }
  /* One folder per protocol, each remembering whether it is open. The
     protocols head their topologies as plain headings rather than on bars
     of their own, so that the list reads as one surface. Their names keep
     their case, which says OSPFv2 from OSPFV2. */
  .folder + .folder { margin-top: .15rem; }
  .folder-head {
    display: flex; align-items: center; gap: .4rem; width: 100%;
    padding: .4rem .45rem; border: 0; border-radius: 6px;
    background: transparent; color: var(--fg); cursor: pointer;
    font-size: 12.5px; font-weight: 600; letter-spacing: .02em;
  }
  .folder-head:hover { background: var(--hover); }
  .folder-head .chevron {
    display: inline-flex; color: var(--muted); transition: transform .15s;
  }
  .folder-head[aria-expanded="true"] .chevron { transform: rotate(90deg); }
  .folder-head .count {
    margin-left: auto; padding-right: .1rem; color: var(--muted);
    font-size: 11px; font-weight: 400; letter-spacing: 0;
  }
  .folder-head[aria-expanded="false"] + .folder-body { display: none; }
  .folder-body {
    display: flex; flex-direction: column; gap: .6rem; padding: .3rem 0 .5rem;
  }

  /* A physical topology, with the variations configured on it listed under
     it. */
  .group { display: flex; flex-direction: column; gap: .15rem; }
  .group-head {
    display: flex; align-items: baseline; gap: .4rem; padding: .1rem .55rem;
  }
  .group-head .title { font-weight: 600; font-size: 12.5px; }
  .group-head .size { margin-left: auto; font-size: 11px; color: var(--muted); }
  .group-body {
    display: flex; flex-direction: column; gap: .15rem;
    margin-left: .6rem; padding-left: .3rem; border-left: 1px solid var(--line);
  }

  .choice {
    display: flex; flex-wrap: wrap; align-items: baseline; gap: .3rem .4rem;
    text-align: left;
    padding: .4rem .55rem; border-radius: 8px; border: 0;
    background: transparent; color: var(--fg); cursor: pointer;
    transition: background .12s;
  }
  .choice:hover { background: var(--hover); }
  /* The topology chosen is tinted with the colour the controls are filled
     with, its name in the accent. */
  .choice[aria-pressed="true"] { background: var(--tint); }
  .choice[aria-pressed="true"] .title { color: var(--accent); font-weight: 600; }
  .choice .title { font-size: 12.5px; }
  /* A topology with a single variation is chosen by its own name, which
     stands for the variation as well. */
  .choice.single .title { font-weight: 600; }
  .choice .size { margin-left: auto; font-size: 11px; color: var(--muted); }
  /* The tags take a line of their own under the name, as plain words,
     wrapping onto as many more as they need without breaking any. */
  .choice .tags {
    width: 100%; font-size: 11.5px; line-height: 1.5; color: var(--muted);
  }
  .choice .tag { white-space: nowrap; }
  .choice .tag:not(:last-child) { margin-right: .45rem; }
  /* Collapsed, the side panel is a strip with the button that expands it
     where the head of the drawing lines up with it, and what it holds
     written down it, which expands it as well, as anywhere on it does. */
  #side-panel.collapsed { cursor: pointer; }
  #side-panel.collapsed .brand { justify-content: center; padding: 0; }
  #side-panel.collapsed .brand .home,
  #side-panel.collapsed .choices { display: none; }
  #side-panel.collapsed #side-fold { margin: 0; }
  .rail-label {
    align-self: center; padding: .7rem 0; writing-mode: vertical-rl;
    font-size: 11px; font-weight: 600; letter-spacing: .05em;
    text-transform: uppercase; color: var(--muted);
  }
  #side-panel:not(.collapsed) .rail-label { display: none; }
  #side-panel.collapsed:hover .rail-label { color: var(--fg); }

  button {
    font: inherit; font-size: 12px; padding: .25rem .6rem; border-radius: 5px;
    border: 1px solid transparent; background: var(--accent); color: #fff;
    cursor: pointer;
  }
  /* The button that starts and stops the topology, beside its name in the
     head of the drawing, filled in as the view showing is, with what it
     does drawn as well as said. */
  #run {
    display: inline-flex; align-items: center; gap: .3rem; flex: none;
    margin-left: .3rem; padding: .1rem .55rem .1rem .45rem;
    background: var(--fill); font-size: 11px; font-weight: 700;
  }
  #run svg, #pause svg { width: 10px; height: 10px; }
  #run .stop, #run.running .play { display: none; }
  #run.running .stop { display: block; }
  /* Pause, beside Stop while the topology runs, drawn as a control of less
     weight, and in the accent while the topology is paused, for a topology
     that stands still not to be taken for one that has settled. */
  #pause {
    display: inline-flex; align-items: center; gap: .3rem; flex: none;
    padding: .1rem .55rem .1rem .45rem; border-color: var(--line);
    background: transparent; color: var(--fg); font-size: 11px;
    font-weight: 600;
  }
  #pause:hover { background: var(--sel); }
  #pause .play, #pause.paused .pause { display: none; }
  #pause.paused .play { display: block; }
  #pause.paused {
    border-color: var(--accent); background: var(--tint);
    color: var(--accent);
  }
  button.ghost {
    background: transparent; border-color: var(--line); color: var(--fg);
  }
  button.ghost:hover:not(:disabled) { background: var(--sel); }
  button:disabled { opacity: .45; cursor: default; }
  button.icon {
    display: inline-flex; align-items: center; padding: .25rem .3rem;
    color: var(--muted);
  }
  button.icon:hover:not(:disabled) { color: var(--fg); }
  /* The buttons that set up the page, at the end of the head of the drawing
     with the link to Holo, drawn as it is. */
  #theme, #settings-open {
    flex: none; padding: .15rem; border: 0; background: transparent;
  }
  /* The toggle shows what it would switch to. */
  #theme .sun { display: none; }
  :root[data-theme="dark"] #theme .sun { display: initial; }
  :root[data-theme="dark"] #theme .moon { display: none; }

  /* ===== topology ===== */

  #topology {
    display: flex; align-items: center; justify-content: center; padding: 4px;
  }
  #topology svg { width: 100%; height: 100%; }
  /* Zoomed in, the drawing is dragged around by the ground it is on. */
  #topology.zoomed { cursor: grab; touch-action: none; }
  #topology.panning { cursor: grabbing; }
  #topology.zoomed .router, #topology.zoomed [data-ifaces] { cursor: pointer; }
  .panel:has(#topology) { position: relative; }
  #zoom {
    position: absolute; right: 8px; bottom: 8px; display: flex; gap: 2px;
    opacity: .75;
  }
  #zoom:hover { opacity: 1; }
  #zoom button {
    padding: .1rem .45rem; background: var(--panel); font-size: 11px;
    min-width: 1.8rem; font-variant-numeric: tabular-nums;
  }
  #zoom[hidden] { display: none; }
  .box { fill: var(--box); stroke: var(--box-line); stroke-width: 1; }
  .lan { fill: var(--panel); stroke: var(--line); stroke-width: 1; }
  text.name {
    fill: var(--fg); font: 600 14px system-ui, sans-serif; text-anchor: middle;
  }
  .addr {
    fill: var(--muted); font: 10.5px ui-monospace, Menlo, monospace;
    text-anchor: middle;
  }
  .badge { fill: var(--muted); font: 9.5px system-ui, sans-serif; }
  .area .ground {
    fill: currentColor; fill-opacity: .07; stroke: currentColor;
    stroke-opacity: .4; stroke-width: 1; stroke-dasharray: 5 4;
  }
  .area .area-name {
    fill: currentColor; font: 600 9.5px system-ui, sans-serif;
    fill-opacity: .8;
  }
  .area0 { color: #0969da; } .area1 { color: #8250df; }
  .area2 { color: #1a7f37; } .area3 { color: #bc4c00; }
  :root[data-theme="dark"] .area0 { color: #58a6ff; }
  :root[data-theme="dark"] .area1 { color: #a371f7; }
  :root[data-theme="dark"] .area2 { color: #3fb950; }
  :root[data-theme="dark"] .area3 { color: #db6d28; }
  .lan-name {
    fill: var(--muted); font: 10px ui-monospace, Menlo, monospace;
    text-anchor: middle;
  }
  .link { stroke: var(--wire); stroke-width: 2; }
  .link.up { stroke: var(--ok); }
  .link.idle { stroke: var(--wire); stroke-dasharray: 4 4; }
  .link.down { stroke: var(--bad); }
  /* A link carrying the IPv6 unicast topology of IS-IS alone. */
  .link.mt-ipv6 { stroke-dasharray: 10 3 2 3; }
  /* An OSPF virtual link, which crosses an area rather than a link: long
     dashes, so that it is told apart from a link whether the topology runs
     or not. */
  .vlink { stroke: var(--muted); stroke-width: 2; stroke-dasharray: 12 6; }
  /* A link being captured on, with a halo under its line. */
  .capture-halo {
    stroke: var(--accent); stroke-opacity: .3; stroke-width: 9;
    stroke-linecap: round;
  }
  /* A line is thin, so what takes the click is a wider one that is not
     drawn. */
  .hit { stroke: transparent; stroke-width: 12; }
  .running .hit, .running .router { cursor: pointer; }
  .router.stopped { opacity: .4; }
  /* A packet crossing a link, from the router that sent it: blue when it
     carries routes, link state or labels, and green, as the link is, when
     it carries anything else, unless its type was given a color of its
     own. */
  .pulse {
    --pulse: var(--ok);
    fill: var(--pulse); stroke: var(--pulse); stroke-opacity: .3;
    stroke-width: 3;
  }
  .pulse.routes { --pulse: var(--accent); }
  .dot { fill: var(--muted); opacity: .35; }
  /* A traced route: the links the packet crosses are drawn in the color of
     the trace and a little thicker, the equal-cost ones alike. Nothing of it
     moves, since an animated path repaints the drawing on every frame. */
  #trace { pointer-events: none; }
  .link.traced { stroke: var(--trace); stroke-width: 3.5; }
  /* The router a trace leaves and the one it reaches, outlined in its
     color. */
  .box.traced { stroke: var(--trace); stroke-width: 2.5; }
  /* The arrowhead on each line the trace reaches its router by. */
  .trace-arrow { fill: var(--trace); }
  /* A cross takes the pointer, unlike the rest of the trace, so that the
     tip can say why the packet is dropped there. */
  .trace-drop { pointer-events: auto; }
  .trace-drop circle { fill: var(--panel); stroke: var(--bad); stroke-width: 2; }
  .trace-drop path { stroke: var(--bad); stroke-width: 2; stroke-linecap: round; }
  /* The trace in the head of the panel, with the color it is drawn in. */
  .chip {
    display: inline-flex; align-items: center; gap: .4rem; min-width: 0;
    margin-left: .6rem; padding: 0 0 0 .45rem; border: 1px solid var(--edge);
    border-radius: 5px; color: var(--fg); overflow: hidden;
  }
  .chip .swatch {
    flex: none; width: 10px; height: 4px; border-radius: 2px;
    background: var(--trace);
  }
  .chip button { padding: 0 .35rem; font-size: 14px; line-height: 1; }
  /* A forwarding loop whose row is hovered or clicked: each link it crosses
     is a line beside the link, with an arrowhead the way it goes. */
  #loops { pointer-events: none; }
  .loop line { stroke: var(--loop); stroke-width: 2.5; stroke-linecap: round; }
  .loop-arrow { fill: var(--loop); }
  /* Where to trace a route to: a router, by the loopback it has in the
     family picked, or any address. Each fills in the other. */
  .trace-form {
    display: grid; grid-template-columns: auto minmax(0, 1fr);
    align-items: center; gap: .35rem .5rem; padding: .3rem .35rem .25rem;
  }
  .trace-form label { color: var(--muted); font-size: 11px; }
  .trace-pick { display: flex; gap: .4rem; }
  .trace-form select, .trace-form input {
    min-width: 0; padding: .2rem .4rem;
    border: 1px solid var(--line); border-radius: 5px;
    background: var(--panel); color: var(--fg); font-size: 12px;
  }
  .trace-form select { flex: 1; }
  .trace-form input {
    min-width: 13rem;
    font: 12px ui-monospace, SFMono-Regular, Menlo, monospace;
  }
  .trace-form select:focus, .trace-form input:focus {
    outline: none; border-color: var(--accent);
  }
  .trace-form .seg { margin: 0; }
  .trace-form button[type="submit"] { grid-column: 1 / -1; justify-self: end; }
  .trace-error { grid-column: 1 / -1; color: var(--bad); font-size: 11px; }
  .trace-error:empty { display: none; }
  .trace-hint { grid-column: 1 / -1; color: var(--muted); font-size: 11px; }
  /* While the form is open, a router in the drawing is picked by a click. */
  #topology.picking .router { cursor: crosshair; }
  .dot.up { fill: var(--ok); opacity: 1; }

  /* The name of the topology, with what it is on hover. */
  /* The title is as wide as what it says, and a head too narrow for it cuts
     it short to no less than 8rem, or than what it says if that is less: the
     least a flex item shrinks to is the smaller of its width and of its
     content, while its content alone sets how wide it is to begin with. */
  #topology-title {
    display: flex; align-items: center; gap: .35rem;
    flex: 0 1 content; width: 8rem; cursor: help;
  }
  #topology-title .name, #topology-title .variation {
    min-width: 0; overflow: hidden; text-overflow: ellipsis;
  }
  #topology-title .proto, #topology-title .hint { flex: none; }
  #topology-title .proto { font-weight: 600; letter-spacing: .04em; }
  #topology-title .name { font-size: 14px; font-weight: 600; color: var(--fg); }
  #topology-title .variation { color: var(--fg); }
  #topology-title .hint {
    display: inline-flex; align-items: center; justify-content: center;
    width: 13px; height: 13px; border-radius: 50%; font-size: 9px;
    border: 1px solid var(--line); color: var(--muted);
  }
  #topology-title:hover .hint { border-color: var(--accent); color: var(--accent); }
  .toggle {
    display: flex; align-items: center; gap: .3rem; flex: none;
    margin-left: .6rem; cursor: pointer; accent-color: var(--accent);
  }
  .toggle input { flex: none; margin: 0; }
  .intro {
    max-width: 34rem; padding: 0 1rem; text-align: left; color: var(--fg);
  }
  .intro p, .intro ul { margin: 0 0 .5rem; }
  .intro p:last-child { margin-bottom: 0; color: var(--muted); }
  .intro ul, #about-text ul { padding-left: 1.1rem; }
  .intro li + li, #about-text li + li { margin-top: .2rem; }
  .intro a, #about-text a { color: var(--accent); }
  .about-logo { display: block; width: 72px; height: 72px; margin: 0 auto .6rem; }
  #loops-open, #download, #about, #yang-open {
    flex: none; padding: .1rem .3rem; border: 0; background: transparent;
    color: var(--fg); font-size: 11px; cursor: pointer;
  }
  #loops-open:hover, #loops-open[aria-expanded="true"],
  #download:hover, #download[aria-expanded="true"],
  #about:hover, #about[aria-expanded="true"],
  #yang-open:hover, #yang-open[aria-expanded="true"] { color: var(--accent); }

  /* The YANG modules the lab knows, listed on the left, and the one picked
     printed on the right, as YANG or as the tree of its nodes. */
  #yang-backdrop {
    position: fixed; inset: 0; z-index: 20; background: rgb(0 0 0 / .35);
  }
  #yang-box {
    position: fixed; z-index: 21; inset: 5vh 5vw; display: grid;
    grid-template-rows: auto minmax(0, 1fr); overflow: hidden;
    border: 1px solid var(--line); border-radius: 8px; background: var(--panel);
    box-shadow: 0 8px 24px rgb(0 0 0 / .22);
  }
  #yang-box .head { height: 34px; gap: .5rem; }
  #yang-filter {
    width: 14rem; padding: .2rem .4rem; border: 1px solid var(--line);
    border-radius: 5px; background: var(--panel); color: var(--fg);
    font: inherit; font-size: 12px;
  }
  #yang-filter:focus { outline: none; border-color: var(--accent); }
  #yang-box .seg { margin: 0; }
  #yang-close { margin-left: auto; font-size: 16px; line-height: 1; }
  .yang-body {
    display: grid; grid-template-columns: minmax(12rem, 18rem) minmax(0, 1fr);
    min-height: 0;
  }
  #yang-list {
    overflow: auto; padding: .25rem; border-right: 1px solid var(--line);
  }
  #yang-list button {
    display: block; width: 100%; padding: .2rem .45rem; border: 0;
    border-radius: 5px; background: transparent; color: var(--fg);
    font-size: 12px; text-align: left; white-space: nowrap; overflow: hidden;
    text-overflow: ellipsis;
  }
  #yang-list button:hover { background: var(--sel); }
  #yang-list button[aria-pressed="true"] {
    background: var(--sel); box-shadow: inset 2px 0 0 var(--accent);
  }
  #yang-list .revision { color: var(--muted); font-size: 11px; }
  #yang-list .group-label {
    display: flex; margin: .5rem .45rem .2rem; font-size: 11px;
    font-weight: 600; letter-spacing: .05em; text-transform: uppercase;
    color: var(--muted);
  }
  #yang-list .group-label:first-child { margin-top: .2rem; }
  #yang-list .group-label .count {
    margin-left: auto; font-weight: 400; letter-spacing: 0;
  }
  /* A module only imported for what it defines, rather than implemented. */
  #yang-list .imported { color: var(--muted); }
  #yang-text {
    margin: 0; padding: .5rem .75rem; overflow: auto; color: var(--fg);
    font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
  }
  #yang-text .kw { color: var(--accent); }
  #yang-text .str { color: var(--ok); }
  #yang-text .note { color: var(--muted); font-family: system-ui, sans-serif; }
  #menu {
    position: fixed; z-index: 10; min-width: 11rem; padding: .25rem;
    border: 1px solid var(--line); border-radius: 7px; background: var(--panel);
    box-shadow: 0 8px 24px rgb(0 0 0 / .22);
  }
  #menu .what {
    display: flex; align-items: center; gap: .4rem;
    padding: .25rem .45rem; font-size: 11px; color: var(--muted);
    border-bottom: 1px solid var(--line); margin-bottom: .2rem;
  }
  #menu .what button {
    margin: -.2rem -.3rem -.2rem auto; font-size: 14px; line-height: 1;
  }
  #menu > button {
    display: block; width: 100%; text-align: left; padding: .3rem .45rem;
    border: 0; border-radius: 5px; background: transparent; color: var(--fg);
    font-size: 12px; cursor: pointer;
  }
  #menu > button:hover { background: var(--sel); }
  /* A window is moved by its head, which says so to the pointer. */
  #about-box > .head, #settings-box > .head, #download-box > .head,
  #loops-box > .head, #packets-box > .head, #bridge-box > .head,
  #menu > .what { cursor: move; user-select: none; touch-action: none; }
  #about-box {
    position: fixed; z-index: 10; left: 50%; top: 50%;
    transform: translate(-50%, -50%); max-width: min(34rem, 86vw);
    max-height: 90vh; display: flex; flex-direction: column;
    border: 1px solid var(--line); border-radius: 8px;
    background: var(--panel); color: var(--fg); font-size: 12px;
    line-height: 1.6; box-shadow: 0 8px 24px rgb(0 0 0 / .22);
  }
  #about-text { overflow: auto; padding: .9rem 1rem; }
  #about-text p, #about-text ul { margin: 0 0 .6rem; }
  #about-text p:last-child { margin-bottom: 0; color: var(--muted); }
  /* The settings of the page, one to a row: its name and what it does on
     the left, and the control that sets it on the right. */
  #settings-box {
    position: fixed; z-index: 10; left: 50%; top: 50%;
    transform: translate(-50%, -50%); width: min(24rem, 86vw);
    border: 1px solid var(--line); border-radius: 8px; background: var(--panel);
    color: var(--fg); font-size: 12px; box-shadow: 0 8px 24px rgb(0 0 0 / .22);
  }
  /* What sets how the packets are drawn, under whether they are, which it
     only does while they are. */
  #packets-open {
    display: block; margin-top: .45rem; padding: .1rem .55rem;
    font-size: 11px;
  }
  /* The types of packet drawn, a window of their own opened from the
     settings, over them. */
  #packets-box {
    position: fixed; z-index: 10; left: 50%; top: 50%;
    transform: translate(-50%, -50%); width: min(22rem, 86vw);
    max-height: 90vh; display: flex; flex-direction: column;
    border: 1px solid var(--line); border-radius: 8px; background: var(--panel);
    color: var(--fg); font-size: 12px; box-shadow: 0 8px 24px rgb(0 0 0 / .22);
  }
  #packet-types-reset { margin-left: auto; padding: 0 .5rem; font-size: 11px; }
  #packets-close { font-size: 16px; line-height: 1; }
  .packets-body {
    overflow: auto; padding: .6rem .8rem .7rem; accent-color: var(--accent);
  }
  .packets-body > p {
    margin: 0 0 .5rem; color: var(--muted); line-height: 1.5;
  }
  #settings-close, #about-close {
    margin-left: auto; font-size: 16px; line-height: 1;
  }
  /* What to download of the topology and its routers, and in which format.
     Several files go out as one zip, with a folder per router. */
  #download-box {
    position: fixed; z-index: 10; left: 50%; top: 50%;
    transform: translate(-50%, -50%); width: min(26rem, 90vw);
    max-height: 90vh; display: flex; flex-direction: column;
    border: 1px solid var(--line); border-radius: 8px; background: var(--panel);
    color: var(--fg); font-size: 12px; box-shadow: 0 8px 24px rgb(0 0 0 / .22);
  }
  #download-close { margin-left: auto; font-size: 16px; line-height: 1; }
  .download-body { overflow: auto; padding: .3rem .8rem; }
  #download-box fieldset {
    margin: 0; padding: .5rem 0; border: 0; accent-color: var(--accent);
  }
  #download-box fieldset + fieldset { border-top: 1px solid var(--line); }
  #download-box legend {
    display: contents; font-weight: 600;
  }
  #download-box fieldset > legend + * { margin-top: .35rem; }
  #download-box label {
    display: flex; align-items: center; gap: .4rem; padding: .1rem 0;
    cursor: pointer;
  }
  #download-box label:has(input:disabled) { color: var(--muted); cursor: default; }
  #download-box .note { color: var(--muted); margin-top: .2rem; }
  #download-box .note:empty { display: none; }
  #download-format { margin: .35rem 0 0; width: fit-content; }
  #download-box .pick { float: right; font-weight: 400; }
  button.link {
    padding: 0 .2rem; border: 0; background: transparent; color: var(--accent);
    font-size: 11px;
  }
  button.link:hover { text-decoration: underline; }
  #download-nodes {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(6.5rem, 1fr));
    column-gap: .6rem; clear: both;
  }
  .download-foot {
    display: flex; align-items: center; gap: .6rem; padding: .5rem .8rem;
    border-top: 1px solid var(--line);
  }
  #download-status { flex: 1; color: var(--muted); }
  /* How to get a live capture going, which opens when one could not: what
     went wrong, then the steps, each command on a line of its own and
     picked whole by a click, to be copied. */
  #bridge-box {
    position: fixed; z-index: 10; left: 50%; top: 50%;
    transform: translate(-50%, -50%); width: min(30rem, 90vw);
    max-height: 90vh; display: flex; flex-direction: column;
    border: 1px solid var(--line); border-radius: 8px; background: var(--panel);
    color: var(--fg); font-size: 12px; line-height: 1.6;
    box-shadow: 0 8px 24px rgb(0 0 0 / .22);
  }
  #bridge-close { margin-left: auto; font-size: 16px; line-height: 1; }
  .bridge-body { overflow: auto; padding: .8rem 1rem .2rem; }
  .bridge-body p, .bridge-body ol { margin: 0 0 .6rem; }
  .bridge-body ol { padding-left: 1.2rem; }
  .bridge-body li + li { margin-top: .4rem; }
  .bridge-body a { color: var(--accent); }
  #bridge-why { font-weight: 600; }
  .bridge-body code {
    font: 11px ui-monospace, SFMono-Regular, Menlo, monospace;
  }
  .bridge-body code.command {
    display: block; width: fit-content; max-width: 100%; margin-top: .25rem;
    padding: .1rem .45rem; border-radius: 4px; background: var(--sel);
    overflow-wrap: anywhere; user-select: all;
  }
  .bridge-foot {
    display: flex; justify-content: flex-end; padding: .5rem .8rem;
    border-top: 1px solid var(--line);
  }
  /* The loops, in a window that stays open while the topology is changed
     under it, each under what was done before it. It opens over the tabs
     rather than the drawing, which shows the loop hovered, as large as what
     it lists until it is resized, and as large as it was resized to after. */
  #loops-box {
    position: fixed; z-index: 10; right: 1rem; bottom: 1rem;
    width: min(32rem, 92vw); max-height: min(28rem, 70vh);
    display: flex; flex-direction: column;
    border: 1px solid var(--line); border-radius: 8px; background: var(--panel);
    color: var(--fg); font-size: 12px; box-shadow: 0 8px 24px rgb(0 0 0 / .22);
  }
  #loops-box.sized { max-width: calc(100vw - 32px); max-height: calc(100vh - 32px); }
  #loops-clear { margin-left: auto; padding: 0 .5rem; font-size: 11px; }
  #loops-close { font-size: 16px; line-height: 1; }
  #loops-list { flex: 1 1 auto; min-height: 0; overflow: auto; padding: .2rem 0; }
  /* What was done, with how many loops formed after it and when it was
     done on the right. */
  .loops-mark {
    display: flex; gap: .6rem; padding: .45rem .8rem .2rem; font-weight: 600;
  }
  .loops-mark .what { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
  .loops-mark .count {
    flex: none; margin-left: auto; font-weight: 400;
    font-variant-numeric: tabular-nums;
  }
  .loops-mark .count.none, .loops-mark .when {
    color: var(--muted); font-weight: 400;
  }
  .loops-mark .when { flex: none; font-variant-numeric: tabular-nums; }
  /* A way round the loops after something was done went, with all of them
     in its row, which lists them under it once it is clicked. */
  .loops-group { cursor: pointer; }
  .loops-group:hover { background: var(--sel); }
  .loops-group.pinned { box-shadow: inset 2px 0 0 var(--loop); background: var(--sel); }
  .loops-row {
    display: grid; grid-template-columns: 4.2rem minmax(0, 9rem) minmax(0, 1fr) 3.6rem;
    align-items: start; gap: .6rem; padding: .15rem .8rem;
    font: 11.5px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
  }
  .loops-row.one { color: var(--muted); }
  .loops-row.one .prefix { color: var(--fg); }
  .loops-group .caret {
    display: inline-block; margin-right: .4rem; vertical-align: 1px;
    border-left: 4px solid currentColor; border-block: 3.5px solid transparent;
    transition: transform .1s;
  }
  .loops-group.pinned .caret { transform: rotate(90deg); }
  /* A row of one loop, and each loop a row lists, has its prefix where the
     others have theirs, past the room of a caret. */
  .loops-group:not(.several) .caret, .loops-row.one .caret { visibility: hidden; }
  .loops-row > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .loops-row .after { color: var(--muted); }
  .loops-row .lasted { text-align: right; }
  .loops-row .lasted.open { color: var(--loop); font-weight: 600; }
  .loops-none { padding: .6rem .8rem; color: var(--muted); line-height: 1.5; }
  .loops-foot {
    flex: none; padding: .45rem .8rem; border-top: 1px solid var(--line);
    color: var(--muted); line-height: 1.5;
  }
  /* The edges and corners a window is resized by, a little either side of
     its border, the corner at the bottom right marked as the one to take. */
  .grip { position: absolute; z-index: 1; touch-action: none; }
  .grip.n, .grip.s { left: 8px; right: 8px; height: 6px; cursor: ns-resize; }
  .grip.e, .grip.w { top: 8px; bottom: 8px; width: 6px; cursor: ew-resize; }
  .grip.n { top: -3px; } .grip.s { bottom: -3px; }
  .grip.e { right: -3px; } .grip.w { left: -3px; }
  .grip.ne, .grip.se, .grip.sw, .grip.nw { width: 14px; height: 14px; }
  .grip.ne { top: -4px; right: -4px; cursor: nesw-resize; }
  .grip.sw { bottom: -4px; left: -4px; cursor: nesw-resize; }
  .grip.nw { top: -4px; left: -4px; cursor: nwse-resize; }
  .grip.se { bottom: -4px; right: -4px; cursor: nwse-resize; }
  .grip.se::after {
    content: ""; position: absolute; right: 7px; bottom: 7px;
    width: 6px; height: 6px; border-right: 1.5px solid var(--muted);
    border-bottom: 1.5px solid var(--muted); border-bottom-right-radius: 2px;
    opacity: .7;
  }
  .setting {
    display: flex; align-items: flex-start; gap: 1rem; padding: .7rem .8rem;
    cursor: pointer; accent-color: var(--accent);
  }
  .setting + .setting { border-top: 1px solid var(--line); }
  .setting > span { flex: 1; }
  .setting .name { display: block; font-weight: 600; }
  .setting .what {
    display: block; margin-top: .15rem; color: var(--muted); line-height: 1.5;
  }
  .setting input { flex: none; margin: .15rem 0 0; }
  .setting select {
    flex: none; padding: .2rem .4rem; border: 1px solid var(--line);
    border-radius: 5px; background: var(--panel); color: var(--fg);
    font-size: 12px; cursor: pointer;
  }
  .setting select:focus { outline: none; border-color: var(--accent); }
  /* A fold for each protocol, with a box that turns all of its types on and
     off and the colors they are drawn in, and once unfolded a row for each
     type: whether it is drawn, its color, and a way back to the color of
     its kind once it has another. */
  #packet-protocols summary {
    display: flex; align-items: center; gap: .4rem; padding: .2rem .25rem;
    border-radius: 5px; cursor: pointer; list-style: none;
  }
  #packet-protocols summary::-webkit-details-marker { display: none; }
  #packet-protocols summary:hover { background: var(--hover); }
  #packet-protocols .chevron {
    display: inline-flex; color: var(--muted); transition: transform .15s;
  }
  #packet-protocols details[open] .chevron { transform: rotate(90deg); }
  #packet-protocols input { margin: 0; }
  #packet-protocols .protocol { font-weight: 600; }
  #packet-protocols .swatches { display: flex; gap: 3px; margin-left: auto; }
  #packet-protocols .swatches i {
    width: 7px; height: 7px; border-radius: 50%;
  }
  #packet-protocols .swatches i.off { opacity: .2; }
  .packet-type {
    display: flex; align-items: center; gap: .3rem;
    padding: .1rem .25rem .1rem 1.55rem;
  }
  .packet-type label {
    flex: 1; display: flex; align-items: center; gap: .4rem; cursor: pointer;
  }
  .packet-type .swatch {
    width: 1.6rem; height: 1rem; padding: 0; border: 1px solid var(--line);
    border-radius: 4px;
  }
  /* The palette a type's color is picked from, over the list under the
     color it opens from: a dot for each color of the page, ringed when it
     is the one the type is drawn in and marked when it is the default, and
     then any other color. */
  #palette {
    position: absolute; z-index: 1; padding: .45rem;
    border: 1px solid var(--line); border-radius: 6px; background: var(--panel);
    box-shadow: 0 6px 18px rgb(0 0 0 / .2);
  }
  #palette-shades { display: flex; gap: .3rem; }
  #palette-shades button {
    position: relative; width: 18px; height: 18px; padding: 0;
    border: 2px solid var(--panel); border-radius: 50%;
    box-shadow: 0 0 0 1px var(--line);
  }
  #palette-shades button[aria-pressed="true"] {
    box-shadow: 0 0 0 2px var(--fg);
  }
  #palette-shades button.default::after {
    content: ""; position: absolute; left: 50%; bottom: -7px;
    width: 4px; height: 4px; margin-left: -2px; border-radius: 50%;
    background: var(--muted);
  }
  #palette .other {
    display: flex; align-items: center; gap: .4rem; margin-top: .55rem;
    cursor: pointer; color: var(--fg);
  }
  #palette-other {
    width: 1.6rem; height: 1rem; padding: 0; border: 1px solid var(--line);
    border-radius: 4px; background: none; cursor: pointer;
  }
  #palette-other::-moz-color-swatch { border: 0; border-radius: 3px; }
  #palette-other::-webkit-color-swatch-wrapper { padding: 0; }
  #palette-other::-webkit-color-swatch { border: 0; border-radius: 3px; }
  /* What a variation in the side panel is, shown beside the panel while it
     is hovered: a drawing of its routers and links in miniature, and the
     description of it. It is never taller than the window, and a description
     that does not fit is scrolled down and back up slowly under the drawing,
     which stays. */
  #peek {
    position: fixed; z-index: 10; max-width: min(24rem, 60vw);
    max-height: calc(100dvh - 12px);
    padding: .5rem .6rem; border: 1px solid var(--line); border-radius: 6px;
    background: var(--panel); color: var(--fg); font-size: 12px;
    line-height: 1.5; box-shadow: 0 8px 24px rgb(0 0 0 / .22);
  }
  #peek:not([hidden]) { display: flex; flex-direction: column; }
  #peek .thumb { flex: none; }
  #peek .help { min-height: 0; margin-top: .5rem; overflow: hidden; }
  #peek .help > div { will-change: transform; }
  /* A help text is a lead and a list of points under it. */
  #peek p, #topology-help p { margin: 0; }
  #peek ul, #topology-help ul { margin: .3rem 0 0; padding-left: 1.1rem; }
  #peek li + li, #topology-help li + li { margin-top: .15rem; }
  #peek code, #topology-help code {
    font: 11px ui-monospace, SFMono-Regular, Menlo, monospace;
  }
  /* What a router or a link is carrying, fetched when it is hovered. */
  #tip {
    position: fixed; z-index: 10; pointer-events: none;
    max-width: min(32rem, 70vw); padding: .4rem .55rem;
    border: 1px solid var(--line); border-radius: 6px;
    background: var(--panel); color: var(--fg); white-space: pre;
    font: 11.5px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
    box-shadow: 0 8px 24px rgb(0 0 0 / .22);
  }
  #tip .hint { color: var(--muted); font-family: system-ui, sans-serif; }
  .thumb { display: block; max-width: 100%; height: auto; }
  .thumb line { stroke: var(--muted); stroke-width: .5; }
  .thumb rect { fill: var(--box); stroke: var(--muted); stroke-width: .4; }
  #topology-help {
    position: fixed; z-index: 10; max-width: min(38rem, 70vw);
    padding: .5rem .6rem; border: 1px solid var(--line); border-radius: 6px;
    background: var(--panel); color: var(--fg); font-size: 12px;
    line-height: 1.5; white-space: normal;
    box-shadow: 0 8px 24px rgb(0 0 0 / .22);
  }
  [hidden] { display: none !important; }

  /* ===== tabs ===== */

  .tabs { padding: 0; gap: 0; }
  /* The tabs scroll sideways within a strip of their own rather than pushing
     the view control out of the panel, and a topology with more routers than
     the strip shows gets a button either side of it to step through them. */
  .strip {
    position: relative; display: flex; height: 100%; min-width: 0;
    flex: 0 1 auto; overflow-x: auto; overflow-y: hidden;
    scrollbar-width: none;
  }
  .strip::-webkit-scrollbar { display: none; }
  .step {
    flex: none; height: 100%; padding: 0 .5rem; border: 0; border-radius: 0;
    background: transparent; color: var(--muted); font-size: 14px;
    line-height: 1;
  }
  #tab-prev { border-right: 1px solid var(--line); }
  #tab-next { border-left: 1px solid var(--line); }
  .step:hover:not(:disabled) { background: var(--sel); color: var(--fg); }
  .tab {
    flex: none;
    display: flex; align-items: center; gap: .35rem; height: 100%;
    padding: 0 .6rem; border: 0; border-right: 1px solid var(--line);
    border-radius: 0; background: transparent; color: var(--muted);
    font-size: 12px;
  }
  .tab:hover { background: var(--sel); }
  .tab[aria-pressed="true"] {
    background: var(--panel); color: var(--fg); font-weight: 600;
    box-shadow: inset 0 -2px 0 var(--accent);
  }
  .tab .pip {
    width: 6px; height: 6px; border-radius: 50%; background: var(--muted);
    opacity: .35;
  }
  .tab .pip.up { background: var(--ok); opacity: 1; }

  .seg {
    flex: none; display: flex; margin: 0 .1rem 0 auto; border: 1px solid var(--line);
    border-radius: 5px; overflow: hidden;
  }
  .seg button {
    padding: .1rem .5rem; border: 0; border-radius: 0; font-size: 11px;
    background: transparent; color: var(--muted);
  }
  .seg button[aria-pressed="true"] { background: var(--sel); color: var(--fg); }
  /* The switch between the views of a router is a control rather than one
     more tab, so it sits in a track of its own with the view showing filled
     in, the way the buttons that act on the lab are. */
  #seg {
    gap: 2px; margin-right: .3rem; padding: 2px; border: 0; border-radius: 6px;
    background: var(--track);
  }
  #seg button { padding: .15rem .6rem; border-radius: 4px; }
  #seg button:hover:not([aria-pressed="true"]) { color: var(--fg); }
  #seg button[aria-pressed="true"] {
    background: var(--fill); color: #fff; font-weight: 600;
  }

  /* One node at a time, and of that node either its CLI, its log or its
     notifications, so that none has to make room for the others. */
  .view { position: absolute; inset: 0; display: none; }
  .view.active { display: block; }
  /* Centred while it fits, and scrolled from its top when the panel is
     too short for it, rather than spilling over the head above. */
  .empty {
    position: absolute; inset: 0; display: flex; align-items: safe center;
    justify-content: safe center; overflow: auto; padding: .5rem 0;
    color: var(--muted); font-size: 12px;
  }
  .term, .log, .notes { position: absolute; inset: 0; display: none; }
  #views[data-view="cli"] .view.active .term { display: block; }
  #views[data-view="log"] .view.active .log { display: block; }
  #views[data-view="notifications"] .view.active .notes { display: block; }
  .term { padding: 2px 0 2px 4px; overflow: hidden; }
  /* The off half of the cursor's blink, which xterm would draw with a CSS
     animation: see blinkCursor(). xterm colors a steady cursor with
     !important rules of its own, which this outranks by being more
     specific. */
  .term.blink-off .xterm .xterm-cursor.xterm-cursor-block:not(.xterm-cursor-blink) {
    background-color: inherit !important; color: var(--accent) !important;
  }
  .log, .notes {
    margin: 0; padding: 3px 6px; overflow: auto; white-space: pre-wrap;
    word-break: break-word; color: var(--fg);
    font: 11.5px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
  }
  /* Each notification under the time it arrived and its path. */
  .notification + .notification { margin-top: .6rem; }
  .notification .stamp { color: var(--muted); }

  .ok { color: var(--ok); } .bad { color: var(--bad); } .warn { color: var(--warn); }
