/* ═══════════════════════════════════════════════════════════════════════════
   Sidebar shell, dashboard widgets, and CSS-only tabs.

   Everything interactive here is HTML and CSS. The panel's Content-Security-
   Policy allows no script at all - not even an external file - so submenus are
   <details> elements and tabs are radio inputs. Both work with a keyboard and
   both work before any script would have loaded, which is a better outcome than
   the JavaScript would have given.
   ═══════════════════════════════════════════════════════════════════════════ */

/* The dark rail is painted HERE, not only on .side.
 *
 * .side is sticky and exactly one viewport tall, so on a page taller than the
 * screen its box stops partway down. Painting the shell means the strip beside
 * the scrolled content is still the sidebar's colour instead of the white gap
 * the old comment below describes. .body paints the page colour back over the
 * rest, and stretches, so a short page has no dark band under it either. */
.shell { display: flex; min-height: 100vh; align-items: stretch; background: var(--side-bg); }

/* ── sidebar ─────────────────────────────────────────────────────────────── */

.side {
  flex: 0 0 15rem;
  background: #1f2733;
  color: #cfd6e0;
  display: flex;
  flex-direction: column;
  /* Stays put while the page scrolls. Without this the menu - and the version
     and copyright pinned under it - scrolled away with the content, so on a
     long page the footer sat hundreds of pixels below the fold and the nav had
     to be scrolled back to. align-self is what makes it stickable: a stretched
     flex item is already the full height of the row and has nothing to stick
     to. A menu taller than the screen scrolls inside the rail, and
     overscroll-behavior stops that scroll running on into the page behind it. */
  align-self: flex-start;
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
  overscroll-behavior: contain;
}
/* Height comes from the flex row stretching it, NOT from 100vh.
   With a fixed 100vh the dark column stopped one screen down and the rest of
   the page had a white strip beside it wherever the content was taller - which
   is most pages. */
.side .brand {
  display: flex;
  align-items: center;
  gap: .55rem;
  padding: 1rem .9rem;
  margin: 0;
  color: #fff;
  text-decoration: none;
  border-bottom: 1px solid rgba(255, 255, 255, .08);
  font-weight: 600;
}
/* An uploaded logo, in place of the letter mark. Bounded so an oversized
   upload cannot push the menu off the screen; the aspect ratio is the
   uploader's business, not ours. */
.brand-logo {
  display: block;
  width: auto;
  height: 1.75rem;
  max-width: 9rem;
  object-fit: contain;
  flex: 0 0 auto;
}
.brand-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: .4rem;
  background: #2563eb;
  color: #fff;
  font-weight: 700;
}
.menu { padding: .5rem 0; flex: 1 1 auto; }
.menu a,
.menu summary {
  display: flex;
  align-items: center;
  gap: .55rem;
  padding: .5rem .9rem;
  color: inherit;
  text-decoration: none;
  font-size: .93rem;
  cursor: pointer;
}
.menu a:hover,
.menu summary:hover { background: rgba(255, 255, 255, .06); color: #fff; }
.menu a.on {
  background: rgba(255, 255, 255, .10);
  color: #fff;
  box-shadow: inset 3px 0 0 #2563eb;
}
.menu-group > summary { list-style: none; font-weight: 500; }
.menu-group > summary::-webkit-details-marker { display: none; }

/* A caret that turns, so a group reads as openable without a script. */
.menu-group > summary::after {
  content: "\203A";
  margin-left: auto;
  opacity: .55;
  transform: rotate(90deg);
  transition: transform .12s ease;
}
.menu-group[open] > summary::after { transform: rotate(-90deg); }

/* Scoped under .menu deliberately - a bare `.menu-sub` scores 0,1,0 and loses
 * BOTH padding-left and font-size to `.menu a` (0,1,1) above, which sets padding
 * as a shorthand. Not theoretical: the sub links measured a 14.4px padding-left
 * and a 14.88px font-size, i.e. exactly their parents', so the submenu was not
 * indented at all on every page of the panel. Only opacity, the one declaration
 * nothing competes for, was surviving. */
.menu .menu-sub { padding-left: 2.5rem; font-size: .88rem; opacity: .92; }

.side-foot {
  display: grid;
  gap: .1rem;
  padding: .7rem .9rem;
  border-top: 1px solid rgba(255, 255, 255, .08);
  font-size: .78rem;
  opacity: .6;
}
/* The version and the copyright read as one line, and wrap to two on a narrow
   rail rather than being clipped. The separator is drawn rather than typed so
   it disappears with the version when the daemon could not be reached. */
.side-foot > span { display: block; }

/* Small monochrome marks. An inline SVG would be crisper but would need either
   a data: URI per icon or a sprite file; a glyph costs nothing and stays
   legible at this size. */
.ico { width: 1rem; text-align: center; opacity: .8; }
.ico-grid::before   { content: "\25A6"; }
.ico-server::before { content: "\25A4"; }
.ico-mail::before   { content: "\2709"; }
.ico-chip::before   { content: "\25C8"; }
.ico-user::before   { content: "\25C9"; }
/* One per menu group. Geometric glyphs rather than an icon font: they are in
   every system face, cost no request, and cannot fail to load and leave a row
   of empty boxes down the side of the panel.

   .ico-users was referenced by the reseller group and had no rule at all, so
   that group has been drawing a blank where its icon should be. */
.ico-users::before  { content: "\25A4"; }
.ico-globe::before  { content: "\25CD"; }
.ico-db::before     { content: "\25A5"; }
.ico-files::before  { content: "\25A7"; }
.ico-shield::before { content: "\25C6"; }

/* ── the page beside it ──────────────────────────────────────────────────── */

.body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; background: var(--bg); }
.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: .6rem 1.2rem;
  border-bottom: 1px solid #e2e6eb;
  background: #fff;
}
.topbar .crumb { font-weight: 600; }
.topbar .me { display: flex; align-items: center; gap: .8rem; }
.topbar .who { display: inline-flex; align-items: center; gap: .4rem; text-decoration: none; }
/* The page fills the space beside the sidebar.
   app.css still carries "main { max-width: 1080px; margin: 0 auto }" from the
   layout that existed before there was a sidebar, when main WAS the page and
   centring it was right. Inside this column flex container those two
   declarations do something quite different: an auto cross-axis margin makes a
   flex item shrink to its own content and centre. So the page was as wide as
   whatever happened to be inside it - narrow on Accounts, wider on the
   Dashboard, and visibly JUMPING when a tab swapped a table for a wider one.
   Reset both here rather than editing app.css, which the signed-out login card
   still relies on. */
.body > main {
  padding: 1.2rem;
  flex: 1 1 auto;
  max-width: none;
  margin: 0;
  width: 100%;
  min-width: 0;
}

/* Long prose stays readable even when the page is very wide. A table wants all
   the width it can get; a paragraph does not. */
.body > main > .page-head .sub,
.card > p.hint { max-width: 46em; }

/* A table wider than the card scrolls inside the card. Without this a single
   wide table makes the WHOLE page scroll sideways, taking the sidebar and the
   topbar with it. */
.card { overflow-x: auto; }

/* ── counter tiles ───────────────────────────────────────────────────────── */

.tiles {
  display: grid;
  gap: .8rem;
  margin-bottom: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
}
.tile {
  background: #fff;
  border: 1px solid #e2e6eb;
  border-radius: .5rem;
  padding: .9rem 1rem;
  display: flex;
  flex-direction: column;
  gap: .1rem;
  text-decoration: none;
  color: inherit;
}
a.tile:hover { border-color: #2563eb; }
.tile-n { font-size: 1.7rem; font-weight: 600; line-height: 1.1; }
.tile-l { font-size: .85rem; opacity: .75; }
.tile-note { font-size: .78rem; opacity: .6; }

/* ── widget grid ─────────────────────────────────────────────────────────── */

.widgets {
  display: grid;
  gap: 1rem;
  /* 24rem, not 21. At 21rem a 1366-wide screen fitted three columns of 347px
     and the Disks table needs 361 - so the widest widget scrolled sideways
     inside its own card, 16px short. Two roomy columns beat three cramped
     ones, and on a wider screen auto-fit still adds a third and fourth. */
  grid-template-columns: repeat(auto-fit, minmax(24rem, 1fr));
  /* Stretch, not start. With "start" a short card next to a tall one left a
     band of empty page, which is what made the dashboard look unfinished. */
  align-items: stretch;
}
.widgets > .card { display: flex; flex-direction: column; margin: 0; }
/* The closing note in a widget belongs at the bottom of the card, not floating
   under the last row. */
.widget > p.hint:last-child { margin-top: auto; padding-top: .6rem; }
.widget h2 { font-size: 1rem; margin-top: 0; margin-bottom: .7rem; }
.widgets table.tight td, .widgets table.tight th { padding: .3rem .4rem; }
.tiles + .widgets { margin-top: 0; }

/* ── meters ──────────────────────────────────────────────────────────────── */

.meter-row {
  display: grid;
  /* The BAR takes the spare width, not the label. With "1fr 8rem" the bar was a
     fixed stub and the rest of the card was empty space, which is what made the
     widgets look padded out. */
  grid-template-columns: 13rem 1fr 2.8rem;
  gap: .6rem;
  align-items: center;
  margin-bottom: .6rem;
}
.meter-label { font-size: .9rem; display: flex; flex-direction: column; }
.meter { background: #e2e6eb; border-radius: 999px; height: .55rem; overflow: hidden; }
.meter.sm { height: .4rem; min-width: 4rem; display: inline-block; vertical-align: middle; }
/* The meter column hugs its content so the columns before it get the width. */
td.meter-cell { width: 1%; }
.meter .fill { display: block; height: 100%; background: #2563eb; }
.meter .fill.hot { background: #dc2626; }

/* How full a meter is comes from Format::barWidthClass and the .wNN classes in
 * app.css, which existed for exactly this reason before these bars were written
 * and were simply never adopted by them. A second set of width classes lived
 * here for a while; having two was how the bug it fixed got made in the first
 * place.
 *
 * A bar with no ceiling to be a fraction of. Full, but grey, so it cannot be
 * mistaken for something that has run out. */
.meter .fill.none { background: var(--line); }
.meter-val { font-variant-numeric: tabular-nums; text-align: right; font-size: .85rem; }
.meter-cell { white-space: nowrap; }
/* "13.5 GB" wrapping onto two lines made the disk table twice as tall as it
   needed to be and hard to scan. Numbers never wrap. */
td.num, th.num { white-space: nowrap; }
.meter-cell .meter-val { display: inline-block; width: 2.6rem; }

/* ── tabs, without a line of JavaScript ──────────────────────────────────── */

.tabs > input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }
/* Wraps, because the number of tabs is no longer fixed. The services widget
   takes its groups from the daemon's catalogue, so a two-tab row became five
   and pushed 10px past the edge of its own card - and a row that cannot wrap
   turns the next group added in Go into a horizontal scrollbar on the front
   page. Padding is a little tighter for the same reason. */
.tab-bar { display: flex; flex-wrap: wrap; gap: .3rem; margin-bottom: .7rem; }
.tab-bar label {
  padding: .35rem .65rem;
  border-radius: .4rem;
  border: 1px solid #e2e6eb;
  font-size: .85rem;
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
}
.tab-bar label:hover { border-color: #2563eb; }
.tab-pane { display: none; }

/* Tabs that are LINKS rather than radios.
 *
 * The radio tabs above switch between panes already on the page. These switch
 * between two different queries - a different list, searched and paged - so the
 * tab has to reach the server. Same look, so they read as one idea.
 */
.tab-bar a {
  padding: .35rem .8rem;
  border-radius: .4rem;
  border: 1px solid #e2e6eb;
  font-size: .85rem;
  text-decoration: none;
  color: inherit;
}
.tab-bar a:hover { border-color: var(--accent); }
.tab-bar a.is-on {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.tab-bar a .count { opacity: .75; margin-left: .35rem; }
.tab-bar a.is-on .count { opacity: .85; }

/* The row under a paged table: steps on the left, jump on the right, as asked.
 * They wrap onto separate lines rather than colliding on a narrow screen.
 */
.pager {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: .6rem;
  flex-wrap: wrap;
  margin-top: .8rem;
  font-size: .85rem;
}
.pager .steps, .pager .jump { display: flex; align-items: center; gap: .3rem; }
.pager .steps a, .pager .steps span {
  padding: .25rem .55rem;
  border: 1px solid #e2e6eb;
  border-radius: .35rem;
  text-decoration: none;
  color: inherit;
}
.pager .steps a:hover { border-color: var(--accent); }
/* A step that cannot go anywhere is shown, not hidden: a control that appears
 * and disappears as you page is harder to use than one that greys out. */
.pager .steps span { opacity: .4; }
.pager .jump input { width: 4.5rem; }
.pager .jump button { padding: .25rem .6rem; }
.pager .where { opacity: .7; }

/* Where an address is registered, beside the address.
 *
 * Real artwork from flag-icons (MIT, see assets/flags/LICENSE), served from
 * this origin. Emoji flags were tried first and cannot work: the characters
 * are Unicode regional indicators and WINDOWS HAS NEVER SHIPPED GLYPHS FOR
 * THEM, so half the people using this panel would see "RO" where a flag
 * should be.
 *
 * Individual files rather than a sprite, and the sizes decide it: the median
 * country flag is 0.8 KB and the ones a firewall list actually shows are
 * 0.2-1.1 KB, while 32 coats-of-arms flags carry 1.4 MB of the set between
 * them. A sprite would make every viewer download all of it to render ten
 * small ones.
 *
 * The border matters: several flags are white at the edge (Japan, Poland) and
 * without it they bleed into the page and read as a broken image.
 */
.flag {
  display: inline-block;
  width: 20px;
  height: 15px;
  margin-right: .45rem;
  vertical-align: -2px;
  border-radius: 2px;
  border: 1px solid rgba(0, 0, 0, .18);
  object-fit: cover;
  background: var(--muted-soft, rgba(127, 140, 160, .14));
}
/* The fallback, for a country the set has no artwork for. */
.flag-code {
  width: auto;
  min-width: 20px;
  padding: 0 .25em;
  font-size: .78em;
  line-height: 13px;
  text-align: center;
  border-color: transparent;
}

/* The flag and the address are one fact. Without this a narrow column breaks
   between them and the country code reads as its own column. */
table.tight td.mono { white-space: nowrap; }


/* Each radio lights its own label and reveals its own pane. Written out rather
   than generated: there are two, and an explicit list is easier to check than
   a clever selector. */
#disk-usage:checked  ~ .tab-bar label[for="disk-usage"],
#disk-inodes:checked ~ .tab-bar label[for="disk-inodes"] {
  background: #2563eb;
  border-color: #2563eb;
  color: #fff;
}
#disk-usage:checked  ~ [data-pane="disk-usage"],
#disk-inodes:checked ~ [data-pane="disk-inodes"] { display: block; }

/* ── tabs matched by POSITION rather than by name ──────────────────────────
   The rules above name every id, which is fine for a pair that will never
   change. It was NOT fine for the services widget: its tabs come from the
   daemon's service catalogue, so naming them here made this stylesheet a
   FOURTH copy of a list that already existed in three places and had already
   drifted in two of them. A group added in Go would have rendered as a tab
   that reveals nothing - a dead control, and an invisible one.

   So .tabs.auto pairs the nth radio with the nth pane. Structure it relies on:
   every radio first, then .tab-bar, then one pane per radio in the same order -
   which makes .tab-bar the first div and pane n the (n+1)th. Eight is more
   groups than the catalogue is ever likely to have; extra rules match nothing
   and cost nothing. */
.tabs.auto > input:nth-of-type(1):checked ~ .tab-bar label:nth-of-type(1),
.tabs.auto > input:nth-of-type(2):checked ~ .tab-bar label:nth-of-type(2),
.tabs.auto > input:nth-of-type(3):checked ~ .tab-bar label:nth-of-type(3),
.tabs.auto > input:nth-of-type(4):checked ~ .tab-bar label:nth-of-type(4),
.tabs.auto > input:nth-of-type(5):checked ~ .tab-bar label:nth-of-type(5),
.tabs.auto > input:nth-of-type(6):checked ~ .tab-bar label:nth-of-type(6),
.tabs.auto > input:nth-of-type(7):checked ~ .tab-bar label:nth-of-type(7),
.tabs.auto > input:nth-of-type(8):checked ~ .tab-bar label:nth-of-type(8) {
  background: #2563eb;
  border-color: #2563eb;
  color: #fff;
}
.tabs.auto > input:nth-of-type(1):checked ~ div:nth-of-type(2),
.tabs.auto > input:nth-of-type(2):checked ~ div:nth-of-type(3),
.tabs.auto > input:nth-of-type(3):checked ~ div:nth-of-type(4),
.tabs.auto > input:nth-of-type(4):checked ~ div:nth-of-type(5),
.tabs.auto > input:nth-of-type(5):checked ~ div:nth-of-type(6),
.tabs.auto > input:nth-of-type(6):checked ~ div:nth-of-type(7),
.tabs.auto > input:nth-of-type(7):checked ~ div:nth-of-type(8),
.tabs.auto > input:nth-of-type(8):checked ~ div:nth-of-type(9) { display: block; }

/* A keyboard user must be able to see which tab has focus. */
.tabs > input[type="radio"]:focus-visible ~ .tab-bar label { outline: 2px solid #2563eb; }

/* ── fact lists ──────────────────────────────────────────────────────────── */

dl.facts { display: grid; grid-template-columns: max-content 1fr; gap: .35rem .9rem; margin: 0; }
dl.facts dt { opacity: .7; font-size: .88rem; }
dl.facts dd { margin: 0; font-size: .9rem; }

.pill.bad { background: #fee2e2; color: #991b1b; }

/* ── narrow screens ──────────────────────────────────────────────────────── */

@media (max-width: 52rem) {
  /* A narrow screen gets a strip across the top, not a full-width dark slab
     that pushes the page below the fold. Groups stay collapsed until tapped. */
  .shell { flex-direction: column; }
  /* The rail becomes a strip across the top here, so sticking it to the
     viewport would pin it over the content it is meant to sit above. */
  .side {
    flex: 0 0 auto;
    position: static;
    align-self: auto;
    height: auto;
    overflow: visible;
  }
  .side .brand { border-bottom: none; padding: .7rem .9rem; }
  .menu {
    display: flex;
    flex-wrap: nowrap;
    overflow-x: auto;
    padding: 0 .4rem .4rem;
    gap: .3rem;
  }
  .menu > a, .menu-group { flex: 0 0 auto; }
  .menu-group > summary { white-space: nowrap; border-radius: .4rem; }
  .menu-group > summary::after { display: none; }
  /* An open group becomes a small panel rather than pushing the strip wider. */
  .menu-group[open] {
    position: relative;
  }
  /* <details> reveals the WRAPPER; the indent belongs on the links inside it.
     This was `> .menu-sub` and matched nothing once the class moved onto the
     anchor, so the narrower mobile indent never applied. */
  .menu-group[open] > .menu-sub-wrap { display: block; }
  .menu-group[open] .menu-sub { padding-left: .9rem; }
  .side-foot { display: none; }
  .meter-row { grid-template-columns: 1fr 5rem 2.6rem; }
  .body > main { padding: .8rem; }
}

/* ── backups ─────────────────────────────────────────────────────────────── */

/* The restore form is folded away behind a <details>. Restoring is rare and
   destructive; putting it one deliberate click away means nobody reaches it
   while meaning to read the list. Same reason the button is red. */
details.restore { margin-top: .8rem; border-top: 1px solid #e2e6eb; padding-top: .7rem; }
details.restore > summary {
  cursor: pointer;
  font-size: .9rem;
  color: #991b1b;
  user-select: none;
}
details.restore > summary:hover { text-decoration: underline; }
details.restore fieldset {
  border: 1px solid #e2e6eb;
  border-radius: .4rem;
  padding: .5rem .8rem .7rem;
  margin: .7rem 0;
}
details.restore legend { font-size: .85rem; padding: 0 .3rem; opacity: .75; }
label.check { display: block; font-weight: 400; margin: .2rem 0; }
label.check input { margin-right: .4rem; }

/* A row of controls that reads as one sentence rather than a stacked form. */
/* One line: label, control, label, control, button.
   The controls line up on their bottom edge, so keep explanatory text OUT of
   the labels - a hint under one input makes that label taller and shunts every
   other control in the row out of alignment. Explanations go under the form. */
form.row { display: flex; flex-wrap: wrap; align-items: center; gap: .9rem; }
/* ...but top-aligned when the items are .field stacks.
 *
 * The comment above says to keep explanatory text out of a row because it makes
 * one item taller and shunts the others out of alignment. That is exactly what
 * had happened, and centring is what turns it into a visible defect: a .field
 * carrying a hint is ~154px tall against ~66px for its neighbours, so centring
 * drops their controls to the middle of it. Measured 44px of offset on the
 * customer Databases and SSH-keys forms, 35px on FTP and 188px in the Add a
 * domain row - the input you type first sitting a line and a half above the
 * one beside it.
 *
 * Aligning the tops lines the LABELS up, and therefore the controls under them,
 * and lets each hint hang below its own field where it belongs. Every one of
 * these rows already wraps its button in a .field with a spacer label, so the
 * button lands on the same line as the controls.
 *
 * Scoped with :has() so a row of plain inline `label + control` pairs keeps its
 * centring. If a browser does not know :has() the rule is dropped and the old
 * centring applies - a worse layout, never a broken one. */
form.row:has(> .field) { align-items: flex-start; }
form.row label { display: flex; align-items: center; gap: .4rem; font-size: .9rem; }
form.row label select, form.row label input { width: auto; }
form.row input[type="number"] { width: 4.5rem; }
form.inline { display: inline; }
.btn.danger, .link.danger { color: #991b1b; }
.btn.danger { border-color: #fca5a5; }

/* ── packages ────────────────────────────────────────────────────────────── */

/* A form of many small numeric fields. Three across keeps related values on one
   line without any of them becoming a text box wide enough to lose a "50" in. */
/* Two roughly equal cards side by side, stacking on a narrow screen.
 *
 * For a pair that is read together - allowed against blocked - where putting
 * one under the other makes you scroll to compare two short lists.
 */
.grid-2 {
  display: grid;
  gap: .9rem 1.1rem;
  grid-template-columns: repeat(auto-fit, minmax(22rem, 1fr));
  align-items: start;
}
.grid-2 > .card { margin-bottom: 0; }
/* The cards inside give up their own bottom margin, so the grid keeps it:
   without this whatever came next sat flush against the row (Containers). */
.grid-2 { margin-bottom: 18px; }

/* A card carrying a state the reader must not skim past - a server that is not
 * actually protected yet. The left edge does the work: enough to stop the eye
 * without the shouting of a full-bleed alert, which people learn to ignore.
 */
.card.warn {
  border-left: 3px solid var(--warn);
  background: var(--warn-soft);
}

.grid-3 {
  display: grid;
  gap: .9rem 1.1rem;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
}
/* The cell, whichever element the page used for it.
 *
 * This was `> label` alone, and ssh-access.php, my/php.php and customers.php build
 * their cells from <div> - so those pages got no gap between label, control and
 * hint and a full-size hint, while packages.php and accounts/show.php got both.
 * One grid, two spacings, depending on a tag nobody thought was load-bearing.
 *
 * NOT `> *`. Two of these grids carry a hidden <input> for the action, and
 * display:flex would override its UA display:none and put it on the screen;
 * .span-all wraps the submit button, and a stretch column would blow it out to
 * the full width of the row. Both are direct children. */
.grid-3 > label,
.grid-3 > div:not(.span-all) { display: flex; flex-direction: column; gap: .25rem; font-size: .9rem; }
.grid-3 > label .hint,
.grid-3 > div:not(.span-all) .hint { font-size: .78rem; }
/* A checkbox cell in that grid.
 *
 * The column above is right for label/control/hint, but a checkbox has to sit
 * BESIDE its words, and as a flex item it was put on its own line with the
 * caption stacked under it - 222px tall against a 65px sibling in the same row.
 * Pre-dates the > div addition above; `.grid-3 > label` already outranked
 * label.check (both 0,1,1, this one later), and an A/B against the base commit
 * measured the identical 222px.
 *
 * The second line is not optional. Going back to block makes the hint - a
 * <span> here - inline again, which is the 27px overlap app.css warns about
 * where it guards `.field > input + .hint`; that guard cannot reach this one,
 * because the parent is a label and the hint follows a text node. */
.grid-3 > label.check { display: block; margin: 0 0 5px; }
.grid-3 > label.check > .hint { display: block; }
.grid-3 input[readonly] { background: #f1f3f5; color: #555; }
td.nowrap { white-space: nowrap; }

/* ── account identity ────────────────────────────────────────────────────── */

/* The credentials block. Deliberately loud: it is the only time the password
   exists, and a customer's password lost to a missed screen is a support call. */
.card.credentials { border-color: #2563eb; }
.card.credentials dl.facts.wide { grid-template-columns: max-content 1fr; margin: .8rem 0; }
.card.credentials dd.strong { font-weight: 600; font-size: 1.05rem; }
.copyblock { display: block; margin: .8rem 0; }
.copyblock textarea {
  width: 100%;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .85rem;
  padding: .6rem .7rem;
  border: 1px solid #e2e6eb;
  border-radius: .4rem;
  background: #f8f9fb;
  resize: vertical;
}

/* Optional fields, folded away. Most accounts want the derived username and a
   generated password; showing both fields by default invites somebody to
   invent a weaker password than the one we would have made. */
details.optional { margin-top: .6rem; }
details.optional > summary { cursor: pointer; font-size: .9rem; color: #2563eb; user-select: none; }
details.optional > summary:hover { text-decoration: underline; }

form.reset-pw { margin-top: .9rem; padding-top: .7rem; border-top: 1px solid #e2e6eb; }

/* ── two-factor ──────────────────────────────────────────────────────────── */

.enrol { display: flex; gap: 1.4rem; flex-wrap: wrap; align-items: flex-start; }
.enrol-qr { flex: 0 0 auto; line-height: 0; border: 1px solid #e2e6eb; border-radius: .5rem; padding: .5rem; background: #fff; }
.enrol-qr svg { display: block; width: 190px; height: 190px; }
.enrol-steps { flex: 1 1 18rem; min-width: 0; }
.enrol-steps ol { margin: 0 0 1rem; padding-left: 1.2rem; font-size: .92rem; }
.enrol-steps li { margin-bottom: .45rem; }
code.secret {
  display: inline-block;
  margin-top: .3rem;
  padding: .35rem .55rem;
  background: #f1f3f5;
  border-radius: .3rem;
  letter-spacing: .06em;
  font-size: .92rem;
  word-break: break-all;
}
.card.auth { max-width: 26rem; margin: 8vh auto 0; }

/* ── PHP manager ─────────────────────────────────────────────────────────── */

.exts { display: flex; flex-wrap: wrap; gap: .3rem; }
.exts.pad { margin: .6rem 0; }
.ext {
  display: inline-block;
  padding: .15rem .5rem;
  border-radius: .3rem;
  font-size: .78rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  border: 1px solid transparent;
}
.ext.on  { background: #e7f0ff; color: #1e40af; }
.ext.off { background: #fff; color: #444; border-color: #e2e6eb; cursor: pointer; }
.ext.off:hover { border-color: #2563eb; color: #2563eb; }

/* A job's output. Monospaced and scrolling, because it is a log and pretending
   otherwise makes it unreadable at the moment somebody needs to read it. */
.card.job { border-color: #2563eb; }
pre.joblog {
  /* Symmetrical margins. It was .6rem top and ZERO bottom, so anything placed
     under a log - the Unblock button beside a firewall lookup - sat flush
     against the black box with no gap at all. */
  margin: .6rem 0;
  padding: .7rem .8rem;
  background: #0f172a;
  color: #cbd5e1;
  border-radius: .4rem;
  font-size: .8rem;
  line-height: 1.45;
  max-height: 16rem;
  overflow: auto;
  white-space: pre-wrap;
}

/* ── SSL ─────────────────────────────────────────────────────────────────── */

/* A name the vhost answers to that the certificate does not cover. Red because
   visitors using it get a browser warning, which reads to them as "unsafe". */
.ext.missing { background: #fee2e2; color: #991b1b; text-decoration: line-through; }

form.upload-cert { margin-top: 1rem; border-top: 1px solid #e2e6eb; padding-top: .9rem; }
form.upload-cert label { display: block; margin-bottom: .9rem; font-size: .9rem; }
form.upload-cert textarea {
  display: block;
  width: 100%;
  margin-top: .25rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .78rem;
  padding: .5rem .6rem;
  border: 1px solid #e2e6eb;
  border-radius: .4rem;
  resize: vertical;
}

/* A tile whose value is an address rather than a count. */
.tile-n.sm { font-size: 1.05rem; font-weight: 600; word-break: break-all; }

/* ── firewall ────────────────────────────────────────────────────────────── */

.vendor { border: 1px solid #e2e6eb; border-radius: .5rem; padding: .9rem 1rem; margin-bottom: .8rem; }
.vendor h3 { margin: 0 0 .3rem; font-size: .98rem; }

/* ── ssh keys, addons, service controls ──────────────────────────────────── */

/*
 * A small button used inside a table row. `summary.act` gives a <details>
 * disclosure the same look as a button, which is how a destructive action gets
 * a two-click confirmation on a panel with no JavaScript: the first click opens
 * the panel that says what will happen, the second submits it.
 */
.act {
  display: inline-block;
  padding: .28rem .6rem;
  margin: 0 .15rem .15rem 0;
  font-size: .8rem;
  line-height: 1.3;
  border: 1px solid #d7dce2;
  border-radius: .35rem;
  background: #fff;
  color: #2c3540;
  cursor: pointer;
}
.act:hover      { border-color: #8a95a3; }
.act.primary    { background: #2563eb; border-color: #2563eb; color: #fff; }
.act.danger     { border-color: #c0392b; color: #c0392b; background: #fff; }
.act.danger:hover { background: #c0392b; color: #fff; }
summary.act     { list-style: none; }
summary.act::-webkit-details-marker { display: none; }

td.actions-cell { text-align: right; }
details.inline  { display: inline-block; vertical-align: top; }
details.inline > form,
details.inline > .hint {
  display: block;
  margin-top: .4rem;
  text-align: left;
}
details.danger-detail[open] > form {
  border: 1px solid #f0c7c2;
  background: #fdf6f5;
  border-radius: .4rem;
  padding: .5rem .6rem;
}
details.danger-detail[open] > form .hint { display: block; margin-bottom: .4rem; }

/* ── websites ─────────────────────────────────────────────────────────────── */

/* One site's card: a title line, the path under it, then a bar of actions.
 *
 * The actions used to be four `.field` items in a `.row`, which made each one a
 * flex column of about 140px - so opening any of them wrapped its explanation
 * into a narrow strip beside its neighbours, and the remove panel and the
 * webmail paragraph sat side by side as two ragged columns. Here the closed
 * controls are a plain row of buttons and an OPEN one takes a full-width line
 * of its own, which is the only way a panel of text can read properly next to
 * controls that must stay compact. */
.site-head h2 {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .45rem;
  margin: 0;
}
.site-path { margin: .25rem 0 0; font-size: .82rem; color: var(--dim); }

.site-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem .6rem;
  margin-top: .9rem;
}
.site-actions > * { flex: 0 0 auto; }
/* The line-of-its-own rule. Without it an opened panel is squeezed into the
   width of the button that opened it. */
.site-actions > details[open] { flex: 1 1 100%; }

.site-panel-body {
  margin-top: .55rem;
  padding: .7rem .8rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
}
.site-panel-body > .hint { margin-top: .55rem; }
.site-panel-body > form + .hint { margin-top: .55rem; }

/* Removing a site is the one destructive control here, so its panel is tinted
   the same way the older `details.danger-detail > form` panels are. */
details.danger-detail > .site-panel-body {
  border-color: var(--bad);
  background: var(--bad-soft);
}

/* Shown in place of the remove control on the account's own domain. Takes a
   full line so it reads as a statement rather than a disabled button. */
.site-locked { flex: 1 1 100%; margin: .15rem 0 0; }

/* ── basic setup ──────────────────────────────────────────────────────────── */

/* A native colour well. Given a height so it lines up with the text inputs
   beside it rather than sitting at whatever the browser's default is. */
.colour-input {
  width: 4.5rem;
  height: 38px;
  padding: 2px;
  cursor: pointer;
}
/* The uploaded image, on a chequerboard so a transparent PNG is visibly
   transparent rather than looking like it has a white background. */
.brand-preview {
  margin: 0 0 .5rem;
  padding: .5rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background-color: var(--panel-2);
  background-image:
    linear-gradient(45deg, rgba(128, 128, 128, .18) 25%, transparent 25%),
    linear-gradient(-45deg, rgba(128, 128, 128, .18) 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, rgba(128, 128, 128, .18) 75%),
    linear-gradient(-45deg, transparent 75%, rgba(128, 128, 128, .18) 75%);
  background-size: 14px 14px;
  background-position: 0 0, 0 7px, 7px -7px, -7px 0;
}
.brand-preview img { display: block; max-width: 100%; max-height: 4rem; }

.setup-links { margin: .4rem 0 0; padding-left: 1.1rem; font-size: .9rem; line-height: 1.7; }
.setup-links li { color: var(--dim); }

/* ── nameserver selection ─────────────────────────────────────────────────── */

/* One choice per row: radio, then the name, why you would pick it, and what it
 * means for this server. The whole row is the label, so the text is clickable
 * rather than just the 13px radio. */
.ns-pick { display: grid; gap: .55rem; margin-top: .85rem; }
.ns-opt {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  gap: .65rem;
  margin: 0;
  padding: .75rem .85rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--panel);
  cursor: pointer;
  /* label is 13px and --dim by default; these rows are body copy. */
  font-size: .92rem;
  color: var(--text);
}
.ns-opt:hover { border-color: var(--accent); }
/* Both themes define --accent-soft, so the selected row reads as chosen in dark
   mode too rather than becoming a pale block with unreadable text. */
.ns-opt.on { border-color: var(--accent); background: var(--accent-soft); }
/* And live, before Save is pressed. .on is what the server rendered, so without
   this the highlight would sit on the old choice until the page came back -
   which reads as the click not having registered. No script: the panel serves a
   CSP with no inline script at all, and a selection highlight is not worth an
   external file. Where :has() is unknown the server-rendered .on still shows
   the saved state, which is correct, just not live. */
.ns-opt:has(> input:checked) { border-color: var(--accent); background: var(--accent-soft); }
.ns-opt > input { margin: .3rem 0 0; }
.ns-body { display: grid; gap: .3rem; }
.ns-name { font-weight: 600; display: flex; align-items: center; gap: .45rem; }
.ns-why { line-height: 1.45; }
.ns-note { font-size: .82rem; color: var(--dim); line-height: 1.45; }
.ns-pick > button { justify-self: start; margin-top: .15rem; }

@media (max-width: 560px) {
  .ns-opt { padding: .6rem .65rem; }
}

/* A fingerprint is long and never needs to wrap mid-word in a table. */
.fp { font-size: .72rem; word-break: break-all; }

/* The last row of a .grid-3 form, for the submit button. */
.grid-3 > .span-all { grid-column: 1 / -1; }

/* The one-time private key. Monospace and tall enough to select in one go. */
.card.highlight textarea.mono {
  width: 100%;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .75rem;
  padding: .5rem .6rem;
  border: 1px solid #e2e6eb;
  border-radius: .4rem;
  resize: vertical;
}
.card.highlight label { display: block; margin: .8rem 0 .2rem; font-size: .85rem; font-weight: 600; }

/* A note under a meter, lined up with the label column rather than the bar. */
.meter-note { margin: -.35rem 0 .8rem; max-width: 46rem; }

/* A list that is a list, not a bulleted one. */
ul.plain { list-style: none; margin: .4rem 0; padding: 0; }
ul.plain li { padding: .18rem 0; }

/* A reseller's support line, so their customers write to them and not to us. */
.panel-support {
  margin: 1.5rem 0 0;
  padding: .8rem 1.2rem;
  font-size: .82rem;
  color: #6b7480;
  border-top: 1px solid #e9edf1;
}
.panel-support a { margin-left: .4rem; }

/* ═════════════════════════════════════════════════════════════════════════════
   Theme layer.

   Appended rather than woven through the rules above, on purpose. Those rules
   carry the reasons they exist - which selector OpenLiteSpeed needs, why a tab
   is a radio input, why a caret turns without a script - and rewriting 700
   lines to change colours would put every one of those comments at risk for no
   gain. Everything here is a colour, a radius or a shadow, expressed in the
   tokens app.css defines, so light and dark are one set of rules rather than
   two stylesheets that drift.
   ═══════════════════════════════════════════════════════════════════════════*/

.shell { background: var(--bg); }

/* ── sidebar ──────────────────────────────────────────────────────────────── */

.side {
  background: var(--side-bg);
  border-right: 1px solid rgba(255, 255, 255, .06);
}
.side .brand { color: var(--side-text-on); border-bottom-color: rgba(255, 255, 255, .07); }
.brand-mark {
  background: var(--accent);
  color: #fff;
  border-radius: var(--radius-sm);
  box-shadow: 0 2px 8px rgba(37, 99, 235, .35);
}
.menu summary, .menu a { color: var(--side-text); border-radius: 0; }
.menu summary:hover, .menu a:hover { background: rgba(255, 255, 255, .05); color: var(--side-text-on); }

/* The current page. A left bar rather than a filled row: it marks the item
   without turning a quiet rail into a block of colour. */
.menu a.on {
  background: var(--side-bg-2);
  color: var(--side-text-on);
  box-shadow: inset 3px 0 0 var(--accent);
}
/* `.menu-sub a` matched NOTHING: the class sits on the anchor itself, so these
 * were written for markup that no longer exists - the sub links took their
 * colour from `.menu a { color: inherit }` instead. Scoped under .menu for the
 * same specificity reason as the indent. Only opacity is set on .on, so the
 * active-item background and accent bar from `.menu a.on` still win. */
.menu .menu-sub { color: var(--side-text); opacity: .82; }
.menu .menu-sub:hover, .menu .menu-sub.on { opacity: 1; }
.side-foot { color: var(--side-text); opacity: .5; border-top-color: rgba(255, 255, 255, .07); }

/* ── topbar ───────────────────────────────────────────────────────────────── */

.topbar {
  background: var(--panel);
  border-bottom: 1px solid var(--line);
  color: var(--text);
}
.topbar .crumb { color: var(--text); }
.topbar .who { color: var(--dim); }
.topbar a { color: var(--accent); }

/* ── surfaces ─────────────────────────────────────────────────────────────── */

.card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.card h2 { color: var(--text); }
.card > p.hint, .hint { color: var(--dim); }

/* Counter tiles get the accent edge the rest of the panel does not, because
   they are the first thing read and the only thing read at a glance. */
.tile {
  background: var(--panel);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.tile .n { color: var(--text); }
.tile .l { color: var(--dim); }

/* ── tables ───────────────────────────────────────────────────────────────── */

th { color: var(--dim); border-bottom-color: var(--line); }
td { border-bottom-color: var(--line-soft); color: var(--text); }
tbody tr:hover td { background: var(--panel-2); }
table.tight th, table.tight td { border-bottom-color: var(--line-soft); }

/* ── meters ───────────────────────────────────────────────────────────────── */

.meter { background: var(--panel-2); }
.meter .fill { background: var(--accent); border-radius: 999px; }
.meter .fill.hot { background: var(--bad); }
.meter-val { color: var(--dim); }
.meter-label { color: var(--text); }

/* ── tabs ─────────────────────────────────────────────────────────────────── */

.tab-bar label {
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--dim);
  padding: .3rem .85rem;
  font-size: .85rem;
}
.tab-bar label:hover { border-color: var(--accent); color: var(--text); }

/* ── facts, pills, forms ──────────────────────────────────────────────────── */

dl.facts dt { color: var(--dim); opacity: 1; }
dl.facts dd { color: var(--text); }

.pill.ok      { background: var(--ok-soft);     color: var(--ok); }
.pill.warn    { background: var(--warn-soft);   color: var(--warn); }
.pill.bad     { background: var(--bad-soft);    color: var(--bad); }
.pill.neutral { background: var(--panel-2);     color: var(--dim); }

/* ⚠ Matched by EXCLUSION, not by listing the types.
 *
 * This was a list of input[type=...] selectors, and an <input> with no type
 * attribute matches none of them - the browser treats it as text, the
 * stylesheet does not. Three fields on the SSH keys page were white boxes in
 * the dark for exactly that reason, and every future field written without a
 * type would have joined them silently.
 *
 * Excluding the handful that must keep their native look is the version that
 * cannot be forgotten.
 *
 * This rule sets COLOUR only. The matching SIZING rule is app.css's
 * `input:not([type=checkbox]):where(...)` - it had the very same type-list bug
 * and was fixed later, because fixing it here and not there is exactly how 45
 * fields ended up 175px wide with no padding. Change one list, change both. */
input:not([type=checkbox]):not([type=radio]):not([type=submit]):not([type=button]):not([type=reset]):not([type=file]):not([type=range]):not([type=color]),
select, textarea {
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}
input::placeholder, textarea::placeholder { color: var(--dim); opacity: .75; }
input[readonly], .grid-3 input[readonly] { background: var(--panel-2); color: var(--dim); }

button, .btn, .act {
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}
button:hover, .btn:hover, .act:hover { border-color: var(--accent); color: var(--accent); }
.btn.primary, button.primary {
  background: var(--accent); color: var(--accent-ink); border-color: var(--accent);
}
.btn.primary:hover, button.primary:hover { filter: brightness(1.07); color: var(--accent-ink); }
.btn.danger, .link.danger { color: var(--bad); }
.btn.danger { border-color: var(--bad); }

details.restore { border-top-color: var(--line); }
details.restore fieldset { border-color: var(--line); }
.card.credentials { border-color: var(--accent); }
.copyblock textarea { background: var(--panel-2); color: var(--text); border-color: var(--line); }

/* ── flashes ──────────────────────────────────────────────────────────────── */

.flash { border-radius: var(--radius-sm); }
.flash.ok   { background: var(--ok-soft);   color: var(--ok);   border: 1px solid var(--ok); }
.flash.warn { background: var(--warn-soft); color: var(--warn); border: 1px solid var(--warn); }
.flash.error, .flash.bad {
  background: var(--bad-soft); color: var(--bad); border: 1px solid var(--bad);
}


/* The selected tab, themed. Mirrors the selectors above so the colours live
   with the rest of the theme rather than halfway up the file.
   ⚠ It has to mirror them EXACTLY. This block used to name #svc-server and
   #svc-mail, and when the services widget moved to generated groups the only
   id that still matched was svc-mail - so one tab in five highlighted in the
   brand colour and the other four in the older blue from the rule above. A
   selector list repeated in two places is the same hazard as a data list
   repeated in two places, and it fails the same way: partially, and looking
   like a rendering glitch rather than a stale copy. */
#disk-usage:checked  ~ .tab-bar label[for="disk-usage"],
#disk-inodes:checked ~ .tab-bar label[for="disk-inodes"],
.tabs.auto > input:nth-of-type(1):checked ~ .tab-bar label:nth-of-type(1),
.tabs.auto > input:nth-of-type(2):checked ~ .tab-bar label:nth-of-type(2),
.tabs.auto > input:nth-of-type(3):checked ~ .tab-bar label:nth-of-type(3),
.tabs.auto > input:nth-of-type(4):checked ~ .tab-bar label:nth-of-type(4),
.tabs.auto > input:nth-of-type(5):checked ~ .tab-bar label:nth-of-type(5),
.tabs.auto > input:nth-of-type(6):checked ~ .tab-bar label:nth-of-type(6),
.tabs.auto > input:nth-of-type(7):checked ~ .tab-bar label:nth-of-type(7),
.tabs.auto > input:nth-of-type(8):checked ~ .tab-bar label:nth-of-type(8) {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}

/* ── the theme menu ───────────────────────────────────────────────────────────
   A <details> dropdown, the same element the sidebar groups use. It opens on a
   click and on Enter, it needs no script, and the CSP has no script source to
   give it one. It does not close when you click elsewhere on the page - the
   honest cost of doing this without JavaScript - but choosing an option submits
   and reloads, which closes it.
   ───────────────────────────────────────────────────────────────────────────*/

/* Visible only to a screen reader: the glyph alone is not a label. */
.sr {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

.theme-menu { position: relative; }
.theme-menu > summary {
  list-style: none;
  cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.9rem; height: 1.9rem;
  border-radius: var(--radius-sm);
  color: var(--dim);
  font-size: 1rem;
  user-select: none;
}
.theme-menu > summary::-webkit-details-marker { display: none; }
.theme-menu > summary:hover { background: var(--panel-2); color: var(--accent); }
.theme-menu[open] > summary { background: var(--panel-2); color: var(--accent); }
.theme-menu > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.theme-pop {
  position: absolute; right: 0; top: calc(100% + .4rem); z-index: 40;
  min-width: 10.5rem;
  display: flex; flex-direction: column;
  padding: .3rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lift);
}
.theme-pop button {
  display: flex; align-items: center; gap: .55rem;
  width: 100%; text-align: left;
  padding: .4rem .55rem;
  background: transparent; border: 0; border-radius: var(--radius-sm);
  color: var(--text); font-size: .88rem;
}
.theme-pop button:hover { background: var(--panel-2); color: var(--accent); }
.theme-pop button.on { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.theme-pop .tm-mark { width: 1.1rem; text-align: center; opacity: .85; }

/* ── links do not underline on hover ──────────────────────────────────────────
   The pointer cursor already says a thing is clickable, and in a panel almost
   everything is: a table of accounts underlining a different row every time the
   mouse crosses it is movement with no information in it.

   Keyboard focus is a different question and keeps its underline - a focus ring
   plus an underline is how somebody who cannot see the cursor knows where they
   are.
   ───────────────────────────────────────────────────────────────────────────*/
a:hover,
.tile:hover, a.tile:hover,
.menu a:hover,
.topbar a:hover, .who:hover,
details.restore > summary:hover,
details.optional > summary:hover,
.link:hover, .act:hover, .btn:hover,
.panel-support a:hover {
  text-decoration: none;
}
a:focus-visible, .link:focus-visible { text-decoration: underline; }

/* ── the collapse toggle, and the collapsed sidebar ──────────────────────────
   The topbar's leftmost control. It replaced the page name, which was saying
   the same thing as the <h1> immediately below it.
   ──────────────────────────────────────────────────────────────────────────*/

.side-toggle { display: flex; }
.side-toggle button {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2rem; height: 2rem;
  padding: 0;
  font-size: 1.05rem; line-height: 1;
  background: transparent; border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--dim);
}
.side-toggle button:hover { background: var(--panel-2); color: var(--accent); }
.side-toggle button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Collapsed: a rail of icons.
   Everything that is only a label is hidden, and what remains is centred. The
   width is enough for an icon plus breathing room, not enough for a word. */
.shell.side-mini .side { flex-basis: 3.6rem; }
.shell.side-mini .brand-name,
.shell.side-mini .menu-link span:not(.ico),
.shell.side-mini .menu-group > summary span:not(.ico),
.shell.side-mini .side-foot { display: none; }

.shell.side-mini .brand { justify-content: center; padding-left: 0; padding-right: 0; }
.shell.side-mini .menu-link,
.shell.side-mini .menu-group > summary {
  justify-content: center;
  padding-left: 0; padding-right: 0;
  /* The label is gone, so the text it left behind must not wrap the row open. */
  font-size: 0;
}
.shell.side-mini .menu-link .ico,
.shell.side-mini .menu-group > summary .ico { font-size: 1rem; margin: 0; }

/* The caret would be a second glyph competing with the icon. */
.shell.side-mini .menu-group > summary::after { display: none; }


/* ── the last four ────────────────────────────────────────────────────────────
   Found by auditing the stylesheet for hardcoded colours rather than by opening
   every page in the dark and looking - a colour written as a hex is a
   dark-mode bug whether or not anybody has seen it yet, and the ones nobody has
   seen are on the pages nobody visits often.
   ──────────────────────────────────────────────────────────────────────────*/

/* A secret shown once. It sat on a light grey that turns into a bright slab in
   the dark - and this is the one thing on the page somebody is trying to read
   and copy accurately. */
code.secret {
  background: var(--panel-2);
  color: var(--text);
  border: 1px solid var(--line);
}

/* The pink "you are about to do something irreversible" box. Pink on white
   reads as caution; the same pink on a dark page reads as a rendering fault. */
details.danger-detail[open] > form {
  border-color: var(--bad);
  background: var(--bad-soft);
}

form.upload-cert textarea {
  background: var(--panel-2);
  color: var(--text);
  border: 1px solid var(--line);
}

/* A job log is deliberately a terminal: dark, monospaced, the same in both
   themes. Pinned to its own colours rather than the tokens, so it does not
   become a white box in light mode - it is a console, and a console is dark.
   Stated here so the next person does not "fix" it. */
pre.joblog {
  background: #0f172a;
  color: #cbd5e1;
  border: 1px solid #1e293b;
}

/* ── flyout submenus in the collapsed rail ────────────────────────────────────
   A 3.6rem rail has no room for a label, so a group's pages appear beside it on
   hover. /assets/panel.js forces every group OPEN while the rail is collapsed,
   because <details> does not render a closed group's children at all - they are
   absent rather than hidden, and CSS cannot reveal what is not there.

   Shown on :hover AND :focus-within, so tabbing through the rail opens the same
   panel a mouse does.
   ──────────────────────────────────────────────────────────────────────────*/

/* Expanded: an ordinary indented list, exactly as before. */
.menu-sub-wrap { display: block; }
.menu-flyout-title { display: none; }

.shell.side-mini .menu-group { position: relative; }

.shell.side-mini .menu-sub-wrap {
  position: absolute;
  left: 100%; top: 0;
  z-index: 60;
  min-width: 12rem;
  padding: .35rem;
  margin-left: .25rem;
  background: var(--side-bg-2);
  border: 1px solid rgba(255, 255, 255, .09);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lift);
  /* Hidden by visibility rather than display: a display:none panel cannot be
     transitioned, and more importantly cannot be reached by :focus-within,
     because there is nothing focusable inside it to receive the focus. */
  visibility: hidden;
  opacity: 0;
}
.shell.side-mini .menu-group:hover > .menu-sub-wrap,
.shell.side-mini .menu-group:focus-within > .menu-sub-wrap {
  visibility: visible;
  opacity: 1;
}

/* The rail dropped the label, so the flyout carries it - otherwise a panel of
   four links appears with nothing saying which group it belongs to. */
.shell.side-mini .menu-flyout-title {
  display: block;
  padding: .3rem .6rem .45rem;
  font-size: .72rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--side-text);
  opacity: .55;
}

/* The links inside get their size back - the rail set font-size 0 on the row to
   hide the group's own label, and that inherits. */
.shell.side-mini .menu-sub-wrap .menu-sub {
  display: block;
  font-size: .88rem;
  padding: .4rem .6rem;
  border-radius: var(--radius-sm);
  opacity: .9;
}
.shell.side-mini .menu-sub-wrap .menu-sub:hover { background: rgba(255, 255, 255, .07); opacity: 1; }
.shell.side-mini .menu-sub-wrap .menu-sub.on { background: var(--accent-soft); color: var(--accent); opacity: 1; }

/* A gap between the rail and the panel would close the flyout as the pointer
   crossed it. The padding is on the wrapper and the bridge is a transparent
   strip under the margin. */
.shell.side-mini .menu-sub-wrap::before {
  content: "";
  position: absolute;
  left: -.35rem; top: 0;
  width: .35rem; height: 100%;
}

/* The sidebar cannot clip what hangs out of it. */
.shell.side-mini .side,
.shell.side-mini .menu { overflow: visible; }

/* ── the ones the first audit missed ──────────────────────────────────────────
   The first pass matched selectors by name and believed itself. This list came
   from reading every rule that contains a hex value and checking each one
   against the theme layer, after the live page showed a row of near-white chips
   on the SSL page that the audit had called clean.
   ──────────────────────────────────────────────────────────────────────────*/

/* Extension and hostname chips - the ones that gave it away. */
.ext          { border-color: var(--line); }
.ext.on       { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }
.ext.off      { background: var(--panel-2); color: var(--dim); border-color: var(--line); }
.ext.off:hover{ border-color: var(--accent); color: var(--accent); }
.ext.missing  { background: var(--bad-soft); color: var(--bad); }

/* Rules whose only colour is a divider or an edge. */
details.restore,
form.reset-pw,
form.upload-cert,
.vendor,
.card.highlight textarea.mono { border-color: var(--line); }
.vendor { background: var(--panel); }

details.restore > summary { color: var(--bad); }
details.optional > summary { color: var(--accent); }
.card.job { border-color: var(--accent); }

/* Row actions. .act was themed, its variants were not - so a danger button sat
   on white in the dark, which is the one button that should not look like a
   rendering fault. */
.act.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
.act.danger  { background: transparent; border-color: var(--bad); color: var(--bad); }
.act.danger:hover { background: var(--bad); color: var(--accent-ink); }
.act:hover   { border-color: var(--accent); }

.panel-support { color: var(--dim); border-top-color: var(--line); }

/* Deliberately NOT themed: a QR code is scanned by a camera, and a camera wants
   dark modules on a white field. Inverting it in the dark is the version that
   does not scan. Kept white in both, like the job log is kept dark in both. */
.enrol-qr { background: #fff; border-color: var(--line); }

/* ── acting as another account ────────────────────────────────────────────────
   Deliberately loud. The failure it prevents is changing something in the
   belief that it is your own account, so it sits above the page on every page
   rather than tucked in a corner, and it keeps its colour in both themes -
   this is a state, not decoration.
   ──────────────────────────────────────────────────────────────────────────*/
.acting {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
  padding: .55rem 1.4rem;
  background: var(--warn-soft);
  color: var(--warn);
  border-bottom: 1px solid var(--warn);
  font-size: .88rem;
}
.acting strong { color: var(--warn); }
.acting form { margin: 0; }
.acting .act {
  background: var(--warn); color: #fff; border-color: var(--warn);
  padding: .25rem .7rem; font-size: .82rem;
}
.acting .act:hover { filter: brightness(1.08); color: #fff; border-color: var(--warn); }

/* ── usage cards ──────────────────────────────────────────────────────────────
 *
 * The account's usage as a grid of small cards rather than a stack of full-width
 * bars. Six numbers stacked vertically read as a list to be worked through; the
 * same six as cards can be taken in at a glance, which is what somebody opening
 * their own dashboard actually wants - "am I near anything?" not "what is my
 * inode count".
 *
 * auto-fit rather than a fixed column count, so it is three across on a laptop,
 * two on a tablet and one on a phone without a media query for each.
 */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: .75rem;
  margin-top: .9rem;
}
.stat {
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: .8rem .9rem .9rem;
  display: flex;
  flex-direction: column;
  gap: .35rem;
  min-width: 0;
}
.stat-label {
  font-size: .78rem;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--dim);
}
/* The figure and the ceiling on one line: the number is the answer, the
 * ceiling is what makes it mean anything. */
.stat-fig { display: flex; align-items: baseline; gap: .4rem; flex-wrap: wrap; }
.stat-num { font-size: 1.5rem; font-weight: 600; line-height: 1.1; color: var(--text); }
.stat-of  { font-size: .85rem; color: var(--dim); }
.stat-bar { display: flex; align-items: center; gap: .5rem; }
.stat-bar .meter { flex: 1 1 auto; min-width: 0; }
.stat-pct {
  font-size: .8rem;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
  flex: 0 0 auto;
}
.stat .hint { font-size: .78rem; line-height: 1.35; margin: 0; }
/* Nothing is being consumed, so a bar would be a lie. */
.stat.no-bar .stat-bar { display: none; }

/* ─────────────────────────────────────────────────────────────────────────────
   Density and type.
   
   The palette above changes the colours; this changes the CHARACTER, which is
   most of what made the panel read as generated.
   
   Three things do the work. Section headings become small capitals rather than
   another line of bold body text, so a page has a visible structure instead of
   an even grey texture. Tables get tight rows, ruled column heads and figures
   that line up, because a table is the thing an operator actually reads. And
   every number that came from a machine is set in the monospace face with
   tabular figures, so a column of them can be compared down the page.
   
   Deliberately kept as one appended layer: it can be read as a single idea, and
   removed as one if it turns out to be wrong.
   ───────────────────────────────────────────────────────────────────────────*/

/* ── section headings ─────────────────────────────────────────────────────── */
.card h2,
.card h3 {
  font-size: .72rem;
  font-weight: 600;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--dim);
  margin: 0 0 .55rem;
}
.card h3 { margin-top: 1.15rem; }
/* The page title is the one place that gets to be large and quiet. */
.page-head h1 {
  font-size: 1.55rem;
  font-weight: 500;
  letter-spacing: -.015em;
  margin: 0 0 .15rem;
}
.page-head .sub { font-size: .85rem; }

/* ── surfaces ─────────────────────────────────────────────────────────────── */
.card {
  box-shadow: none;
  padding: .95rem 1.05rem 1.05rem;
}
/* A hairline rule under the heading, so a card reads as a titled section rather
   than a floating tile. */
.card > h2:first-child {
  padding-bottom: .5rem;
  border-bottom: 1px solid var(--line-soft);
}

/* ── tables ───────────────────────────────────────────────────────────────── */
table { font-size: .875rem; }
thead th {
  font-size: .68rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--dim);
  padding: .3rem .55rem .4rem;
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}
tbody td {
  padding: .42rem .55rem;
  border-bottom: 1px solid var(--line-soft);
  vertical-align: middle;
}
tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover td { background: var(--panel-2); }
/* Machine values line up down the column, which is the whole reason to put them
   in a column. */
.mono, td.num, .num, .stat-num, .meter-val, .stat-pct {
  font-variant-numeric: tabular-nums;
}
td.num, th.num { text-align: right; }

/* ── the facts strip ──────────────────────────────────────────────────────────
   What server am I on, what is it doing, who am I. Every operations tool worth
   using answers those without being asked, and answers them in the same place
   on every page.
 *
 * Named .factbar and not .facts: a dozen pages already use <dl class="facts">
 * for their own key/value lists, and an unqualified .facts rule here would put
 * a background, a border and smaller type on every one of them. */
.factbar {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .1rem 1.15rem;
  padding: .4rem 1.15rem;
  background: var(--panel);
  border-bottom: 1px solid var(--line);
  font-size: .74rem;
  color: var(--dim);
  line-height: 1.6;
}
.factbar b {
  font-weight: 600;
  letter-spacing: .075em;
  text-transform: uppercase;
  font-size: .66rem;
  color: var(--dim);
  opacity: .8;
  margin-right: .3rem;
}
.factbar span { color: var(--text); font-variant-numeric: tabular-nums; }
.factbar .sep { opacity: .35; }

/* ── the rail ─────────────────────────────────────────────────────────────── */
/* A wordmark, not a coloured square with a letter in it. */
.brand-mark {
  background: var(--accent);
  color: #fff;
  border-radius: 3px;
  font-weight: 700;
  letter-spacing: -.02em;
}
.brand-name { font-weight: 600; letter-spacing: -.01em; }
.menu-link { font-size: .875rem; }
/* The active item is marked by a bar in the accent, the way a tool does it,
   rather than by a filled rounded pill. */
.menu-link.on {
  background: var(--side-bg-2);
  box-shadow: inset 2px 0 0 var(--accent);
  border-radius: 0;
}

/* ── section heads ────────────────────────────────────────────────────────────
 *
 * A list's title, how many there are, and the one action that makes another -
 * all on one line, the way every tool that manages a list of things does it.
 *
 * The action used to sit below the table, or as a disclosure at the bottom, so
 * "how do I add one" was answered further down the page than the question was
 * asked. Putting it on the heading line also gives the count somewhere to live,
 * and a count is the first thing you want from a list you are responsible for.
 */
.card-head {
  display: flex;
  align-items: baseline;
  gap: .6rem;
  padding-bottom: .5rem;
  margin-bottom: .7rem;
  border-bottom: 1px solid var(--line-soft);
}
.card-head h2,
.card-head h3 {
  margin: 0;
  padding: 0;
  border: 0;
}
/* The count sits with the title, quiet, and pushes the action to the far end. */
.card-head .count {
  font-size: .75rem;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
  margin-right: auto;
}
.card-head .card-act { margin-left: auto; }
.card-head .count + .card-act { margin-left: 0; }
.card-head .btn,
.card-head .act { align-self: center; }

/* A list that is over its allowance should say so where the allowance is. */
.card-head .count.over { color: var(--bad); font-weight: 600; }

/* ── one control per row ──────────────────────────────────────────────────────
 *
 * A row shows what the thing IS; a single "Manage" opens what can be done to
 * it. The alternative - a disclosure per action - put four widgets on every row
 * and forty on a page with ten of them, which is unreadable however tidy each
 * one is on its own.
 */
.rowmenu > summary {
  cursor: pointer;
  list-style: none;
  display: inline-block;
}
.rowmenu > summary::-webkit-details-marker { display: none; }
.rowmenu[open] > summary { color: var(--accent); }
.rowmenu-body {
  display: flex;
  flex-direction: column;
  gap: .55rem;
  margin-top: .55rem;
  /* The actions cell is right-aligned so its buttons sit at the end of the row.
     The panel that drops out of it is a form, and a form reads left to right. */
  text-align: left;
  padding: .65rem .75rem;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  white-space: normal;
  /* Wide enough to hold a form, narrow enough not to shove the table about. */
  min-width: 20rem;
  max-width: 26rem;
}
.rowmenu-item { display: flex; flex-direction: column; gap: .3rem; }
.rowmenu-item + .rowmenu-item { padding-top: .55rem; border-top: 1px solid var(--line-soft); }
.rowmenu-label {
  font-size: .68rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--dim);
}
.rowmenu-item.danger-detail .rowmenu-label { color: var(--bad); }
.rowmenu-item form { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; }
.rowmenu-item .hint { flex: 1 1 100%; margin: 0; }
.rowmenu-item input[type="text"],
.rowmenu-item input[type="password"],
.rowmenu-item input[type="number"],
.rowmenu-item input:not([type]) { flex: 1 1 9rem; min-width: 0; }

/* A create form that a heading button points at, rather than a disclosure the
   reader has to find on their own. */
.sub-form {
  margin-top: 1rem;
  padding-top: .85rem;
  border-top: 1px solid var(--line-soft);
}
.sub-form h4 {
  font-size: .68rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--dim);
  margin: 0 0 .5rem;
}
/* Jumping to it should make it obvious which one you landed on. */
.sub-form:target { box-shadow: inset 2px 0 0 var(--accent); padding-left: .7rem; }

/* An open panel makes its row tall. Without this the cells beside it float in
   the middle of all that height, a long way from the address they belong to. */
tbody tr:has(.rowmenu[open]) td { vertical-align: top; }
tbody tr:has(.rowmenu[open]) td:first-child { padding-top: .6rem; }

/* ── what needs attention ─────────────────────────────────────────────────────
 *
 * The first thing on the server dashboard, because it is the first question
 * anybody opens it to answer. A short list beats a row of warning bars: bars
 * push the page down as they multiply, and three of them start to read as
 * decoration.
 */
.attn { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .1rem; }
.attn li {
  display: flex;
  align-items: baseline;
  gap: .55rem;
  padding: .35rem 0;
  font-size: .9rem;
  border-bottom: 1px solid var(--line-soft);
}
.attn li:last-child { border-bottom: 0; }
.attn-dot {
  flex: 0 0 auto;
  width: .5rem;
  height: .5rem;
  border-radius: 50%;
  background: var(--dim);
}
.attn-bad  .attn-dot { background: var(--bad); }
.attn-warn .attn-dot { background: var(--warn); }
/* Severity is carried by the dot, not by shouting the whole line in red: a page
   of red text is a page nobody reads carefully. */
.attn li a { color: var(--text); }
.attn li a:hover { color: var(--accent); }
.card-head .count.over { color: var(--warn); }

/* ── the inventory strip ──────────────────────────────────────────────────────
 *
 * What the server carries, as a strip rather than five large tiles. These are
 * context for the state above them, not the headline - nobody opens a server
 * dashboard to find out how many databases exist.
 */
.tally { display: flex; flex-wrap: wrap; gap: .35rem 2.25rem; }
.tally-item {
  display: flex;
  align-items: baseline;
  gap: .4rem;
  padding: .15rem 0;
  color: var(--text);
}
a.tally-item:hover .tally-n { color: var(--accent); }
.tally-n { font-size: 1.35rem; font-weight: 600; font-variant-numeric: tabular-nums; }
.tally-l { font-size: .8rem; color: var(--dim); }
.tally-note { font-size: .75rem; color: var(--dim); opacity: .8; }

/* ── the file manager ─────────────────────────────────────────────────────────
 *
 * Two panes: where you are on the left, what is in it on the right. The shape
 * every file manager has had for thirty years, because it answers "where am I"
 * without spending a row of the table on it.
 *
 * The left pane is deliberately not a full recursive tree. A tree has to be
 * built by walking folders the customer may not be able to read, and a spinner
 * over a folder that will never load is worse than a plain list of what is
 * actually here.
 */
.fm { display: grid; grid-template-columns: 15rem 1fr; gap: .75rem; align-items: start; }
@media (max-width: 900px) { .fm { grid-template-columns: 1fr; } }

.fm-side {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: .7rem .8rem .9rem;
  min-width: 0;
}
.fm-side-head {
  font-size: .68rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--dim);
  margin: .5rem 0 .35rem;
}
.fm-side-head:first-child { margin-top: 0; }
.fm-tree { display: flex; flex-direction: column; }
.fm-tree-item {
  display: flex;
  align-items: center;
  gap: .4rem;
  padding: .22rem .3rem;
  border-radius: var(--radius-sm);
  font-size: .85rem;
  color: var(--text);
  min-width: 0;
}
.fm-tree-item:hover { background: var(--panel-2); }
.fm-tree-item.on { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.fm-tree-item, .fm-crumb { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.fm-main { min-width: 0; }
.fm-bar {
  display: flex;
  align-items: baseline;
  gap: .6rem;
  padding-bottom: .5rem;
  margin-bottom: .6rem;
  border-bottom: 1px solid var(--line-soft);
  flex-wrap: wrap;
}
.fm-crumbs { display: flex; align-items: baseline; gap: .25rem; min-width: 0; flex-wrap: wrap; }
.fm-crumb { font-size: .9rem; color: var(--accent); }
.fm-crumb.on { color: var(--text); font-weight: 600; }
.fm-sep { color: var(--dim); opacity: .5; }
.fm-bar .count { margin-left: auto; }

.fm-table { width: 100%; }
.fm-table td:first-child { max-width: 28rem; overflow: hidden; text-overflow: ellipsis; }

/* Type icons. Geometric glyphs for the same reason the menu uses them: no
   request to fail, and no row of empty boxes if one does. */
.fm-ico { display: inline-block; width: 1.1em; text-align: center; opacity: .75; margin-right: .15rem; }
.fm-ico-dir::before     { content: "\25B6"; }
.fm-ico-file::before    { content: "\25AB"; }
.fm-ico-link::before    { content: "\21AA"; }
.fm-ico-special::before { content: "\25C7"; }
.fm-ico-unknown::before { content: "\003F"; }

/* The row of things that make something new, above the listing. */
.fm-actions {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem 1rem;
  padding-bottom: .6rem;
  margin-bottom: .6rem;
  border-bottom: 1px solid var(--line-soft);
}
.fm-actions > details { min-width: 0; }
.fm-actions form { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; margin-top: .4rem; }
.fm-actions .hint { flex: 1 1 100%; margin: 0; }

/* ── the file manager's action menu and popup ────────────────────────────── */
.fm-menu { list-style: none; margin: 0; padding: 0; min-width: 11rem; }
.fm-menu > li + li { border-top: 1px solid var(--line-soft); }
.fm-menu-item {
  display: block;
  padding: .38rem .55rem;
  font-size: .875rem;
  color: var(--text);
  cursor: pointer;
  list-style: none;
}
.fm-menu-item::-webkit-details-marker { display: none; }
.fm-menu-item:hover { background: var(--panel-2); color: var(--accent); }
.fm-menu-item.danger { color: var(--bad); }
/* Without script an action expands here instead of in a popup. */
.fm-act[open] > .fm-menu-item { background: var(--panel-2); font-weight: 600; }
.fm-act form {
  display: flex;
  flex-direction: column;
  gap: .45rem;
  padding: .5rem .55rem .65rem;
  background: var(--panel-2);
}
.fm-act label { display: flex; flex-direction: column; gap: .25rem; font-size: .85rem; }

.fm-dialog {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0;
  background: var(--panel);
  color: var(--text);
  min-width: min(24rem, 92vw);
  max-width: 92vw;
}
.fm-dialog::backdrop { background: rgba(8, 10, 14, .45); }
.fm-dialog-head {
  display: flex;
  align-items: baseline;
  gap: 1rem;
  padding: .6rem .8rem;
  border-bottom: 1px solid var(--line);
}
.fm-dialog-title { font-size: .95rem; }
.fm-dialog-close { margin-left: auto; }
.fm-dialog-body { padding: .8rem; }
/* Inside the dialog the form is the whole content, so it loses the tucked-in
   look it has when it opens inline under a menu item. */
.fm-dialog-body form { background: none; padding: 0; gap: .6rem; }

/* ── the editor ───────────────────────────────────────────────────────────────
 *
 * Tall enough to be worth opening. A three-line box is what a textarea defaults
 * to and it is useless for a config file, let alone a template - the whole
 * reason somebody opened the editor instead of downloading the file is to see
 * it in context.
 *
 * A viewport-relative height rather than a row count so it fills whatever screen
 * it is on, with a floor for very short windows.
 */
.editor-area {
  width: 100%;
  min-height: 24rem;
  height: 62vh;
  resize: vertical;
  font-size: .85rem;
  line-height: 1.5;
  tab-size: 4;
  padding: .6rem .7rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
  color: var(--text);
  /* Long lines scroll rather than wrap: a wrapped line makes the line numbers
     in an error message stop matching what is on screen. */
  white-space: pre;
  overflow: auto;
}
.editor-area:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
.editor .row { margin-top: .7rem; gap: .5rem; }

/* ── the row menu floats, so a row never grows to hold it ──────────────────────
 *
 * In flow, opening a menu of eight actions made the row eight items tall and
 * shoved the rest of the listing down the page - the same complaint as the forms
 * before it, one level up.
 *
 * Floating it needs the card to stop clipping. .card carries overflow-x: auto so
 * a wide table can scroll inside it, and an absolutely positioned child of a
 * scroll container is cut off at its edge - the trap that makes this look like a
 * z-index problem when it is not one. So this card opts out, and the columns
 * that would have made it too wide are dropped on a narrow screen instead, which
 * is a better answer on a phone than sideways scrolling anyway.
 */
.fm-main { overflow: visible; }
.rowmenu { position: relative; }
.rowmenu[open] > .rowmenu-body {
  position: absolute;
  top: calc(100% + .25rem);
  right: 0;
  z-index: 40;
  margin-top: 0;
  box-shadow: 0 6px 24px rgba(8, 10, 14, .18);
}
/* The open row sits above its neighbours so the menu is not covered by the row
   below it. */
tbody tr:has(.rowmenu[open]) { position: relative; z-index: 41; }
/* And the row no longer needs to align its cells to the top, because it is no
   longer stretched by anything. */
tbody tr:has(.rowmenu[open]) td { vertical-align: middle; }

@media (max-width: 820px) {
  /* Owner and permissions are the two columns somebody is least often looking
     for, and dropping them keeps the table inside the screen. */
  .fm-table th:nth-child(4), .fm-table td:nth-child(4),
  .fm-table th:nth-child(5), .fm-table td:nth-child(5) { display: none; }
}

/* ── search, and acting on several at once ────────────────────────────────── */
.fm-search { display: flex; gap: .4rem; align-items: center; margin-bottom: .6rem; }
.fm-search input[name="q"] { flex: 1 1 18rem; min-width: 0; }

.fm-bulk {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .4rem .8rem;
  padding: .5rem .6rem;
  margin-bottom: .6rem;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}
.fm-bulk-label { font-size: .8rem; color: var(--dim); }
.fm-bulk-group { display: flex; gap: .3rem; align-items: center; }
.fm-bulk input { max-width: 14rem; }

th.fm-check, td.fm-check { width: 1.6rem; padding-right: 0; }

/* Any card holding a row menu stops clipping, not just the file manager's.
 *
 * .card carries overflow-x: auto so a wide table can scroll inside it, and an
 * absolutely positioned child of a scroll container is cut off at its edge. The
 * file manager opted out by name and the trash page did not, so the same menu
 * was fine on one page and sliced in half on the other. Keyed on what the card
 * CONTAINS, so the next page to use a row menu does not have to remember. */
.card:has(.rowmenu) { overflow: visible; }

/* Opening upward when there is no room below.
 *
 * Added by script after measuring, because CSS cannot ask how much space is
 * left. Without script the menu opens downward and the page scrolls to it,
 * which is the ordinary behaviour and not broken - just less pleasant. */
.rowmenu[open].up > .rowmenu-body {
  top: auto;
  bottom: calc(100% + .25rem);
}

/* ── the editor's colour layer ─────────────────────────────────────────────────
 *
 * A coloured <pre> behind a transparent textarea. Every property that affects
 * where a glyph lands must be IDENTICAL on both, or the colours drift away from
 * the letters as you scroll down - font, size, line height, letter spacing,
 * padding, border width, tab size, wrapping. They are set together here for
 * that reason, and changing one without the other is the bug.
 *
 * The textarea keeps its own caret and selection; only the text is made
 * transparent. Everything about editing is still the browser's.
 */
.editor-wrap { position: relative; }
.editor-wrap > .editor-hl,
.editor-wrap > .editor-area {
  font: inherit;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .85rem;
  line-height: 1.5;
  letter-spacing: normal;
  tab-size: 4;
  padding: .6rem .7rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  white-space: pre;
  overflow: auto;
  margin: 0;
}
.editor-hl {
  position: absolute;
  inset: 0;
  background: var(--panel-2);
  color: var(--text);
  pointer-events: none;
  overflow: hidden;
}
.editor-wrap > .editor-area.editor-live {
  position: relative;
  background: transparent;
  /* The text itself is invisible; the caret is not. */
  color: transparent;
  caret-color: var(--text);
}
.editor-wrap > .editor-area.editor-live::selection { background: rgba(13, 110, 253, .28); }

/* Restrained on purpose. A page of eight colours is harder to read than plain
   text - the job is to separate what is code from what is prose about it. */
.tok-com { color: var(--dim); font-style: italic; }
.tok-str { color: #0a7d43; }
.tok-kw  { color: #9021a6; font-weight: 600; }
.tok-var { color: #a0410d; }
.tok-num { color: #0b6ea8; }
.tok-tag { color: #0948b3; font-weight: 600; }

:root:not([data-theme="light"]) {
  color-scheme: light dark;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --tok-str: #7ee2a8;
  }
  :root:not([data-theme="light"]) .tok-str { color: #6fd39b; }
  :root:not([data-theme="light"]) .tok-kw  { color: #d9a2f2; }
  :root:not([data-theme="light"]) .tok-var { color: #f0a56b; }
  :root:not([data-theme="light"]) .tok-num { color: #7fc4f0; }
  :root:not([data-theme="light"]) .tok-tag { color: #8ab4ff; }
}
:root[data-theme="dark"] .tok-str { color: #6fd39b; }
:root[data-theme="dark"] .tok-kw  { color: #d9a2f2; }
:root[data-theme="dark"] .tok-var { color: #f0a56b; }
:root[data-theme="dark"] .tok-num { color: #7fc4f0; }
:root[data-theme="dark"] .tok-tag { color: #8ab4ff; }

/* What the in-place save says after it has saved. */
.editor-note { font-size: .85rem; color: var(--ok); align-self: center; }
.editor-note.bad { color: var(--bad); }
.editor-revert {
  margin-top: .8rem;
  padding-top: .7rem;
  border-top: 1px solid var(--line-soft);
}
.editor-revert form { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }
.editor-revert .hint { flex: 1 1 22rem; margin: 0; }

/* ── dropping files onto the list ────────────────────────────────────────── */
.fm-main { position: relative; }
.fm-main.fm-dragging {
  outline: 2px dashed var(--accent);
  outline-offset: -4px;
  background: var(--accent-soft);
}
.fm-drop-note {
  margin-bottom: .6rem;
  padding: .45rem .6rem;
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
  color: var(--text);
  font-size: .85rem;
}
.fm-drop-note.bad { background: var(--bad-soft); color: var(--bad); }
.fm-drop-hint { align-self: center; }
/* The select-all box the script adds to the header. */
th.fm-check input { margin: 0; }

/* ── find and replace ─────────────────────────────────────────────────────────
 *
 * Sits directly above the editor and only exists once the script has put it
 * there, so nothing here renders for somebody without JavaScript.
 *
 * It wraps rather than scrolls: on a narrow screen the replace half drops to a
 * second line, which is a better answer than a bar you have to scroll sideways
 * to reach the button on.
 */
.editor-find {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .35rem;
  margin-bottom: .5rem;
  padding: .4rem .45rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
}
/* The script toggles .hidden, and the display above would otherwise win. */
.editor-find[hidden] { display: none; }

.editor-find input[type="search"],
.editor-find input[type="text"] {
  flex: 1 1 11rem;
  min-width: 7rem;
  font-size: .85rem;
  padding: .3rem .45rem;
}

/* Fixed width so the bar does not jump about as the wording changes between
   "No matches", "3 of 12" and "Replaced 8 matches". */
.editor-find-count {
  flex: 0 0 auto;
  min-width: 7.5rem;
  font-size: .78rem;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
}

.editor-find-case {
  display: inline-flex;
  align-items: center;
  gap: .25rem;
  font-size: .78rem;
  color: var(--dim);
  white-space: nowrap;
  cursor: pointer;
}
.editor-find-case input { margin: 0; }

/* The two arrows are square so they read as a pair rather than as two words. */
.editor-find button[data-act="prev"],
.editor-find button[data-act="next"],
.editor-find-close {
  min-width: 1.9rem;
  text-align: center;
}
/* Pushed to the end of the row, so closing is always in the same place
   whether or not the bar has wrapped onto two lines. */
.editor-find-close { margin-left: auto; }

@media (max-width: 640px) {
  /* Room for the inputs is worth more than the count on a phone; the position
     in the file is still shown by the selection itself. */
  .editor-find-count { min-width: 0; flex-basis: 100%; }
}
