/* SolarConfigAdmin — Polestar-inspired design system.
   No external fonts (CSP: style-src 'self'), no radius, no shadows. The one script on
   the site (app.js) only keeps a reveal from moving the page; nothing here needs it.
   Hairlines and space do the work; the single accent is reserved for
   destructive / high-impact actions and out-of-date status. */

:root {
  --bg: #ffffff;
  --fg: #0f0f0f;
  --muted: #6b6b6b;
  --line: #e3e3e3;
  --accent: #ff7500;
  --font: -apple-system, "Helvetica Neue", Helvetica, Inter, Arial, sans-serif;
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.5;
  overflow-x: hidden;
}

/* ---------- typography ---------- */

h1, h2, h3, h4 { font-weight: 300; margin: 0 0 1rem; letter-spacing: -0.01em; }
h1 { font-size: 2rem; margin-bottom: 2rem; }
h2 { font-size: 1.375rem; margin-top: 3rem; }
h3 { font-size: 1.05rem; font-weight: 400; margin-top: 2rem; }
h4 { font-size: .9rem; font-weight: 500; margin-top: 1.5rem; }

/* Every clickable piece of text without a button frame is underlined: links, the
   nav, the pager, text buttons (Show / Hide / Edit), the Filter disclosure and the
   row More toggle all share this one rule set. The line is a lighter shade of the
   text and turns solid on hover. */
a, .btn-link, .btn-reveal, details.filters > summary, details.folded > summary,
.more-toggle + label, .pager a {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: .25em;
  text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
}
a:hover, .btn-link:hover, .btn-reveal:hover, details.filters > summary:hover,
details.folded > summary:hover,
.more-toggle + label:hover, .pager a:hover { text-decoration-color: currentColor; }
a { display: inline-block; vertical-align: top; color: var(--fg); }
a:focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }

code, .secret { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: .92em; }

p { margin: 0 0 1rem; }
.help { color: var(--muted); font-size: .8rem; margin: .35rem 0 0; }
.count { color: var(--muted); font-size: .8rem; }
/* Row tallies alongside a heading: secondary to the name they qualify. */
.tally { color: var(--muted); font-size: .78rem; letter-spacing: .04em; margin-left: .75rem; }
.error { color: var(--accent); }

/* ---------- header ---------- */

header.top {
  display: flex;
  align-items: center;
  gap: 2.5rem;
  padding: 1.25rem 1.5rem;
  border-bottom: 1px solid var(--line);
}

.brand { text-decoration: none; }
.brand .en {
  font-size: .9rem;
  text-transform: uppercase;
  letter-spacing: .22em;
  font-weight: 500;
}

nav { display: flex; flex-wrap: wrap; gap: 1.75rem; flex: 1; }
nav a { color: var(--muted); }
nav a .en {
  text-transform: uppercase;
  letter-spacing: .12em;
  font-size: .72rem;
  color: var(--muted);
}
nav a:hover .en, nav a:focus-visible .en { color: var(--fg); }

.who {
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: .12em;
  font-size: .72rem;
}

header.top form.inline { gap: 1rem; }

/* ---------- layout ---------- */

main { max-width: 1200px; margin: 0 auto; padding: 2rem 1.5rem; }
main > section, .section { margin-block: 3rem; }

/* ---------- buttons ---------- */

.btn,
button,
input[type="submit"] {
  font: inherit;
  font-size: .72rem;
  text-transform: uppercase;
  letter-spacing: .12em;
  padding: .6rem 1.4rem;
  border-radius: 0;
  cursor: pointer;
  background: transparent;
  color: var(--fg);
  border: 1px solid var(--fg);
  line-height: 1.4;
}
button .en, .btn .en { font-size: 1em; }

.btn:hover, button:hover { border-color: var(--fg); }
.btn:focus-visible, button:focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }

.btn { display: inline-block; text-align: center; text-decoration: none; }
/* No element-qualified rule may set a button's border colour: `a.btn` outranks the
   variant classes below, and an orange button wearing one black edge is what that
   cost last time. Variants own the colour; the base rule above owns the width. */

.btn-primary { background: var(--fg); color: var(--bg); border-color: var(--fg); }
.btn-primary:hover { background: #333; border-color: #333; }

.btn-secondary { background: transparent; color: var(--fg); border: 1px solid var(--fg); }

.btn-danger { background: var(--accent); color: var(--bg); border-color: var(--accent); }
.btn-danger:hover { background: #e66a00; border-color: #e66a00; }

button[disabled], .btn[disabled] { opacity: .35; cursor: not-allowed; }

.btn-link {
  background: none;
  border: 0;
  padding: 0;
  text-transform: none;
  letter-spacing: 0;
  font-size: 1rem;
}

/* Revealing a secret is a real action but a rare one: accent text, not a block
   of accent colour sitting in every row of the table. Hide is the same control in its
   other state and shares the metrics exactly — a taller one would change the row's
   height the moment the value appears, nudging every row below it. Only the colour
   differs: putting a credential on screen is the act worth marking, not taking it off. */
.btn-reveal, .hide-secret {
  text-transform: uppercase;
  letter-spacing: .12em;
  font-size: .7rem;
  /* `a` is top-aligned across the site; here it would ride above the value it belongs to,
     while the Show button it replaces sits on the baseline. */
  vertical-align: baseline;
}
.btn-reveal { color: var(--accent); }

/* Both faces of a secret cell are a row led by their toggle, so Show and Hide sit at
   one x down the whole column: with the toggle trailing, six dots and a 200-character
   token put the same control in two different places. The value takes the rest of the
   line and breaks inside it — `min-width: 0` is what lets it, a flex item refusing to
   shrink below its content otherwise. `hidden` outranks the display, or the mask would
   stay on screen beside the plaintext it was swapped for. */
.plain, .masked { display: flex; flex-wrap: wrap; align-items: baseline; gap: .5rem; }
.plain > code, .masked > .secret { flex: 1 1 auto; min-width: 0; }
.masked[hidden] { display: none; }

/* A row holding a secret in plain sight is marked as a whole: in a 50-row table the
   rule beside the value alone is easy to scroll past and leave up. */
tr.revealed td { background: color-mix(in srgb, var(--accent) 7%, transparent); }
tr.revealed td:first-child { box-shadow: inset 3px 0 0 var(--accent); }

/* A revealed secret sits inline in its cell, not in the message bar: monospace, and it
   wraps rather than blowing out the column width. The tinted row is the marker; a rule
   beside the value too was the same shout twice. */
.secret-plain {
  /* No padding, and the mask's own metrics: a revealed row that is taller than a masked
     one nudges every row under it the moment the value appears. */
  display: inline;
  padding: 0;
  word-break: break-all;
}

/* ---------- publish diff ---------- */

/* Only the words that actually differ carry weight: the surrounding value drops to
   the muted grey so the eye lands on the change, the way a code review reads.
   Two tints rather than red/green — the palette has one accent, and the column
   headings (Before / After) already say which side is which. */
.diff-before, .diff-after { color: var(--muted); }
.diff-before del, .diff-after ins {
  color: var(--fg);
  text-decoration: none;
  border-radius: 2px;
  padding: 0 .15em;
}
.diff-before del { background: color-mix(in srgb, var(--fg) 12%, transparent); }
.diff-after ins { background: color-mix(in srgb, var(--accent) 30%, transparent); }
/* Stretches of untouched lines fold away: a 120-line value with one edited field is
   otherwise a wall to scroll past before reaching the change. */
details.folded > summary {
  cursor: pointer;
  color: var(--muted);
  font-family: var(--font);
  font-size: .72rem;
  text-transform: uppercase;
  letter-spacing: .1em;
  padding: .35rem 0;
  list-style: none;
}
details.folded > summary::-webkit-details-marker { display: none; }
details.folded > summary::before { content: "\2219\2219\2219  "; letter-spacing: .1em; }
details.folded[open] > summary { color: var(--fg); }

/* A multi-line value keeps its own line breaks and indentation. HTML would collapse
   them, which is how a wholesale reformat can look byte-identical to the reader while
   the diff insists it changed. Tall ones scroll rather than pushing the page down. */
.diff-before .block, .diff-after .block {
  white-space: pre-wrap;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .82rem;
  max-height: 24em;
  overflow: auto;
  min-width: 22rem;
}

/* ---------- tables ---------- */

/* The table scrolls inside its own box rather than running the page off the bottom,
   which is what lets the header stay put: `overflow-x` alone already makes this a
   scroll container vertically, but an uncapped one never scrolls, so a sticky header
   in it would have nothing to stick to. The cap leaves room for what sits above and
   below, so the last page-scroll still ends with the header on screen; a table short
   enough not to reach it is laid out exactly as before. */
.table-wrap { overflow: auto; max-height: calc(100vh - 8rem); margin-bottom: 1.5rem; }

table { border-collapse: collapse; width: 100%; }

table th {
  /* Sticks to the top of .table-wrap as the rows scroll under it; opaque, or the rows
     would read straight through the column names. */
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--bg);
  text-align: left;
  text-transform: uppercase;
  letter-spacing: .12em;
  font-size: .72rem;
  font-weight: 400;
  color: var(--muted);
  padding: .8rem .6rem;
  border-bottom: 1px solid var(--fg);
  /* A collapsed border belongs to the table grid, not to the cell, so it stays behind
     while the sticky heading travels. The inset line travels with it. */
  box-shadow: inset 0 -1px 0 var(--fg);
  vertical-align: bottom;
}

table td {
  padding: .8rem .6rem;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
}
table td.num, table th.num { text-align: right; }

td.actions { white-space: nowrap; }
td.actions form.inline, td.actions a { display: inline-flex; vertical-align: middle; }
td.actions > * + * { margin-left: .5rem; }

/* List pages: a long unbroken value (URL, csv list) wraps inside its column instead of
   widening it; short cells keep whole words so "development" never splits. */
table td { overflow-wrap: break-word; }
table td.long { overflow-wrap: anywhere; }
table td.more { white-space: nowrap; width: 1%; }
/* Anchor target for reveal(): keep a little context above the row the browser
   jumps to instead of pinning it flush against the top of the viewport. */
tbody tr[id] { scroll-margin-top: 6rem; }

.abbr { cursor: help; white-space: nowrap; border-bottom: 1px dotted var(--muted); }

/* Per-row More: the checkbox in the last cell shows the following detail row, which
   spans the table so the folded columns and the actions sit right under the data. */
/* The cell is the toggle's containing block. Without it the absolutely positioned
   checkboxes fall back to the initial containing block, escape the scrolling table and
   stretch the document to the table's full height — 1500px of empty scroll under a
   63-row page, and a header that scrolls away however sticky it is. */
td.more { position: relative; }
.more-toggle { position: absolute; opacity: 0; width: 1px; height: 1px; }
.more-toggle + label {
  cursor: pointer;
  text-transform: uppercase;
  letter-spacing: .12em;
  font-size: .72rem;
  color: var(--muted);
}
.more-toggle + label::after { content: " +"; }
.more-toggle:checked + label { color: var(--fg); }
.more-toggle:checked + label::after { content: " \2212"; }
.more-toggle:focus-visible + label { outline: 1px solid var(--fg); outline-offset: 2px; }
tr.row-detail { display: none; }
tr:has(.more-toggle:checked) + tr.row-detail { display: table-row; }
tr:has(.more-toggle:checked) > td { border-bottom: none; }
tr.row-detail > td { padding-top: 0; }
/* Everything a More opens hangs off the right edge: More sits in the last column, so
   that is where the reader is looking when the row unfolds. Only the block's position
   moves — text inside it stays left-aligned and readable. */
tr.row-detail .detail { display: flex; flex-wrap: wrap; align-items: flex-start;
                        justify-content: flex-end; gap: .75rem 2.5rem; }
tr.row-detail dl { display: flex; flex-wrap: wrap; justify-content: flex-end;
                   gap: .5rem 2.5rem; margin: 0; flex: 0 1 auto; }
tr.row-detail dt { font-size: .68rem; color: var(--muted); }
tr.row-detail dt code { text-transform: uppercase; letter-spacing: .08em; }
tr.row-detail dd { margin: 0; }
tr.row-detail .actions { display: flex; gap: .75rem; align-items: center; }
/* A release's diff is a stack of headings and tables, not a folded column: it takes
   the whole width of the detail so the flex row does not lay it out side by side. */
tr.row-detail .detail-diff { flex: 1 1 100%; min-width: 0;
                             display: flex; flex-direction: column; align-items: flex-end; }
tr.row-detail .detail-diff > :first-child { margin-top: 0; }
tr.row-detail .detail-diff h3 { margin: 1.5rem 0 .5rem; }
tr.row-detail .detail-diff h4 { margin: .75rem 0 .5rem; }
/* auto width: a full-width nested table strands KEY at one edge and AFTER at the other. */
tr.row-detail .detail-diff table { margin: 0; width: auto; }
tr.row-detail .actions form.inline { display: inline-flex; }

/* ---------- forms ---------- */

form.inline { display: inline-flex; align-items: center; gap: .5rem; }

label { display: block; }
.form-row { margin-bottom: 1.75rem; max-width: 32rem; }
.form-row label,
form.filters label > code,
.card label,
label > .en {
  font-size: .72rem;
}
.form-row label {
  text-transform: uppercase;
  letter-spacing: .12em;
  color: var(--muted);
  margin-bottom: .35rem;
}

input[type="text"], input[type="password"], input[type="email"], input[type="number"],
input[type="file"], select, textarea {
  font: inherit;
  width: 100%;
  max-width: 32rem;
  padding: .5rem 0;
  color: var(--fg);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
}
input:focus, select:focus, textarea:focus {
  outline: none;
  border-bottom: 2px solid var(--fg);
}
textarea { min-height: 6rem; }
select { padding-block: .45rem; }

/* Django's `{{ form.as_p }}` markup (user create form) */
form p { margin: 0 0 1.75rem; max-width: 32rem; }
form p label {
  text-transform: uppercase;
  letter-spacing: .12em;
  font-size: .72rem;
  color: var(--muted);
  margin-bottom: .35rem;
}
form p .helptext, form ul.errorlist { font-size: .8rem; color: var(--muted); }
form ul.errorlist { color: var(--accent); list-style: none; padding: 0; margin: .35rem 0 0; }

form.filters { display: flex; flex-wrap: wrap; gap: 1.5rem; align-items: flex-end; margin-bottom: 2rem; }
form.filters label { display: flex; flex-direction: column; gap: .2rem; }
form.filters label > code, form.filters label > .en {
  text-transform: uppercase;
  letter-spacing: .12em;
  color: var(--muted);
}
form.filters input { width: 11rem; }

/* Search across every table on the tables page, and within one on its list page. */
form.search { margin: 0 0 1.5rem; flex-wrap: wrap; }
form.search input { width: 28rem; max-width: 100%; }
dl.hit { display: flex; flex-direction: column; gap: .4rem; margin: 0; }
dl.hit dt { font-size: .68rem; color: var(--muted); }
dl.hit dt code { text-transform: uppercase; letter-spacing: .08em; }
dl.hit dd { margin: 0; white-space: pre-wrap; max-height: 12em; overflow: auto; }
dl.hit dd.matched { font-weight: 600; }

/* Collapsible filter panel on the table pages. */
details.filters { border-bottom: 1px solid var(--line); margin-bottom: 2rem; }
details.filters > summary {
  cursor: pointer;
  padding: .6rem 0;
  text-transform: uppercase;
  letter-spacing: .12em;
  font-size: .72rem;
  color: var(--muted);
}
/* Keep the disclosure triangle on the same line as the English label. */
details.filters > summary .en { display: inline-block; }
details.filters[open] > summary { color: var(--fg); }
details.filters > form { padding: .5rem 0 1.5rem; }
.filter-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: .75rem 1.25rem;
  margin-bottom: 1.25rem;
}
.filter-grid label { display: flex; flex-direction: column; gap: .1rem; }
.filter-grid label > code {
  text-transform: uppercase;
  letter-spacing: .08em;
  font-size: .68rem;
  color: var(--muted);
}
.filter-grid input, .filter-grid select { width: 100%; max-width: none; padding: .4rem 0; font-size: .9rem; }
/* Never stretched, not even at the narrow breakpoint. */
details.filters button { width: auto; }

/* Login / TOTP forms: labels stack above the field */
.card label { margin-bottom: 1.75rem; }
.card label > .en {
  text-transform: uppercase;
  letter-spacing: .12em;
  color: var(--muted);
}

/* ---------- page header ---------- */

/* The way back out of a page sits above its title, at reading size: a breadcrumb
   shrunk to a caption reads as a footnote to the heading rather than a way out. */
.crumb { margin: 0 0 .75rem; }
/* What writes to the table on the left, what takes a copy out of it on the right:
   the two are opposite directions and should not read as one list of four links. */
.actions-bar {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem 2rem;
  flex-wrap: wrap;
  margin: 0 0 2rem;
}
.actions-bar p { margin: 0; }

/* ---------- import preview ---------- */

.tally-bar { color: var(--muted); font-size: .8rem; letter-spacing: .04em; margin: 0 0 1.5rem; }
.tally-bar strong { color: var(--fg); font-weight: 400; }
.status { text-transform: uppercase; letter-spacing: .1em; font-size: .7rem; white-space: nowrap; }
.status-unchanged { color: var(--muted); }
/* The accent marks the rows that will actually change something, and the ones
   that stop the import — the same reservation the rest of the site keeps for it. */
.status-update::before { content: ""; display: inline-block; width: 6px; height: 6px;
  background: var(--accent); margin-right: .5rem; vertical-align: middle; }
.status-error { color: var(--accent); }
.row-changes, .row-errors { margin: 0; padding-left: 1rem; }
.row-changes li, .row-errors li { margin-bottom: .25rem; }
.row-changes { list-style: none; padding-left: 0; }
.row-changes .arrow { color: var(--muted); }
/* TOTP enrolment: the QR is the path everyone takes, so the manual key folds
   away behind a summary instead of dumping a wrapped otpauth:// URI on the page. */
.totp-enrol img { display: block; margin-bottom: 1.5rem; }
.totp-manual { margin-bottom: 1.75rem; }
.totp-manual summary { cursor: pointer; font-size: .82rem; color: var(--muted); }
.totp-manual summary:hover { color: var(--fg); }
.totp-manual .help { margin: .75rem 0 .35rem; }
.totp-manual .secret { margin: 0; word-break: break-all; }
.card label input.code-input { letter-spacing: .35em; }

/* ---------- modal (CSS-only, shown via :target) ---------- */

.modal {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(15, 15, 15, .45);
  align-items: center;
  justify-content: center;
  z-index: 10;
}
.modal:target { display: flex; }

.modal-card {
  background: var(--bg);
  border: 1px solid var(--line);
  max-width: 620px;
  width: 92vw;
  padding: 2rem;
  max-height: 90vh;
  overflow: auto;
}

.modal-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1.5rem;
  margin-bottom: 1.5rem;
}
.modal-header h2 { margin: 0; }

/* A modal row owns the full card width: the field stretches so a long value
   (an email address) stays readable instead of being clipped mid-domain. */
.modal .form-row { max-width: none; }
.modal .form-row form.inline { display: flex; width: 100%; }
.modal .form-row form.inline input,
.modal .form-row form.inline select { flex: 1 1 auto; min-width: 0; max-width: none; }
.modal .form-row form.inline button { flex: 0 0 auto; }

/* ---------- cards / panels ---------- */

.card { border: 1px solid var(--line); padding: 2rem; max-width: 32rem; }

.panel { border: 1px solid var(--line); padding: 2rem; margin-bottom: 2rem; }

/* one-time token / high-impact notice */
.warn {
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  padding: 2rem;
  margin-bottom: 2rem;
}
p.warn { padding: 1rem 1.5rem; }
/* same notice, no alarm: a statement of fact rather than something to act on */
.warn-plain { border-left-color: var(--fg); }

/* ---------- status badges ---------- */

.badge {
  display: inline-block;
  text-transform: uppercase;
  letter-spacing: .12em;
  font-size: .68rem;
  padding: .25rem .6rem;
  border: 1px solid var(--fg);
}
.badge-accent { border-color: var(--accent); color: var(--accent); }

/* ---------- messages ---------- */

.messages { list-style: none; padding: 0; margin: 0 0 2rem; }
.messages li {
  border: 1px solid var(--line);
  border-left: 3px solid var(--fg);
  padding: 1rem 1.25rem;
  margin-bottom: .5rem;
}
.messages li.error { border-left-color: var(--accent); color: var(--accent); }
.messages li.success { border-left-color: var(--fg); }

/* ---------- misc ---------- */

/* Under the table: Previous · "N rows · page X of Y" · Next, centred. The side
   that cannot be followed is still rendered so the block never shifts between pages. */
.pager { display: flex; justify-content: center; align-items: baseline; gap: 2rem; margin: 1.5rem 0; }
.pager > span { color: var(--muted); font-size: .8rem; }
.pager a, .pager .disabled {
  text-transform: uppercase;
  letter-spacing: .12em;
  font-size: .72rem;
}
.pager .disabled { color: var(--line); }


/* ---------- responsive ---------- */

@media (max-width: 900px) {
  header.top {
    flex-wrap: wrap;
    gap: 1rem 1.5rem;
    padding: 1rem;
  }
  .brand { flex: 0 0 100%; }
  nav { flex: 0 0 100%; gap: 1rem 1.25rem; }
  header.top form.inline { flex: 0 0 100%; justify-content: space-between; }
  h1 { font-size: 1.5rem; margin-bottom: 1.25rem; }
  h2 { font-size: 1.2rem; margin-top: 2rem; }
  main { padding: 1.5rem 1rem; }
  .card, .warn, .panel { padding: 1.25rem; max-width: none; }
  .form-row, form p, .card label { max-width: none; }
  form.filters { gap: 1rem; }
  form.filters input { width: 100%; }
  form.filters label { flex: 1 1 12rem; }
  table th, table td { padding: .6rem .4rem; }
  main form .btn,
  main form > button,
  main .card button { width: 100%; }
  td.actions form.inline, td.actions button { width: auto; }
  /* Inline row editors (rename, email, role) must not become full-width blocks. */
  td form.inline button, td form.inline input, td form.inline select { width: auto; }
  /* The filter panel keeps its own compact grid and a normal-width submit. */
  .filter-grid { grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr)); }
  .filter-grid input, .filter-grid select { width: 100%; }
  details.filters button { width: auto; }
  tr.row-detail .actions form.inline button { width: auto; }
}

@media (max-width: 600px) {
  header.top { padding: 1rem .75rem; }
  main { padding: 1rem; }
  .modal-card { width: 100%; max-width: none; padding: 1rem; }
}
