/* admin.css -- the panel. Loaded after the other four, never instead of them.
 *
 * This file is deliberately small, and the reason is the whole point of the
 * token layer: the admin panel is not a second design system. It is the same
 * components with a few tokens turned down. Production's admin.css carried its
 * own hardcoded #f1c40f palette that had no connection to the accent colour an
 * admin could actually set on the Settings page -- so the site was gold and the
 * panel that configured it was a different gold. Overriding tokens instead of
 * re-declaring components makes that class of drift impossible.
 *
 * Everything below is either (a) a token override scoped to .admin-shell, or
 * (b) a component that only exists in the panel and nowhere else.
 */

/* ══ Token overrides ════════════════════════════════════════════════════
 * Denser than the public site: an admin is reading a table, not being sold
 * to. Tighter type, tighter radii, no display sizes. */
.admin-shell {
  --text-4xl: 1.75rem;
  --text-3xl: 1.5rem;
  --text-2xl: 1.25rem;
  --text-xl: 1.125rem;
  --radius-lg: 10px;
  --measure: 76ch;
}

.admin-shell h1 {
  letter-spacing: -0.01em;
}

/* ══ Sidebar ════════════════════════════════════════════════════════════
 * A grid child of .admin-shell, so its width comes from --sidebar and it needs
 * no position: fixed -- which is what lets it scroll independently and print
 * away cleanly. */
.admin-nav {
  z-index: var(--z-sidebar);
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  padding: var(--space-4);
  background: var(--surface-inverse);
  color: color-mix(in srgb, white 70%, var(--surface-inverse));
}

@media (min-width: 900px) {
  .admin-nav {
    position: sticky;
    top: 0;
    height: 100vh;
    overflow-y: auto;
    padding: var(--space-5) var(--space-4);
  }
}

/* Below 900px the whole nav becomes one horizontally scrollable strip. No
 * drawer, no toggle, no JS -- an admin on a phone can always see where they
 * can go, even if every script on the page fails. */
@media (max-width: 899px) {
  .admin-nav {
    flex-direction: row;
    align-items: center;
    gap: var(--space-4);
    overflow-x: auto;
    padding: var(--space-3) var(--space-4);
  }
  .admin-nav-group {
    flex-direction: row;
    flex: none;
  }
  .admin-nav-heading {
    display: none;
  }
  .admin-nav a {
    white-space: nowrap;
  }
}

.admin-brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: none;
  font-weight: 700;
  color: #fff;
  text-decoration: none;
}

.admin-brand .icon {
  color: var(--brand-accent);
  font-size: 1.5rem;
}

.admin-nav-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.admin-nav-heading {
  padding: 0 var(--space-3);
  margin-bottom: var(--space-1);
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  color: color-mix(in srgb, white 45%, var(--surface-inverse));
}

.admin-nav a {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius);
  font-size: var(--text-sm);
  font-weight: 600;
  color: color-mix(in srgb, white 78%, var(--surface-inverse));
  text-decoration: none;
  transition: background var(--dur-fast) var(--ease),
    color var(--dur-fast) var(--ease);
}

.admin-nav a:hover {
  background: var(--surface-inverse-2);
  color: #fff;
}

.admin-nav a[aria-current='page'] {
  background: var(--brand-accent);
  color: var(--accent-contrast);
}

.admin-nav a[aria-current='page'] .icon {
  color: inherit;
}

.admin-nav .icon {
  color: color-mix(in srgb, white 55%, var(--surface-inverse));
}

/* Pushed to the bottom of the column on wide screens by margin-top:auto --
 * no spacer element, no fixed height. */
.admin-nav-footer {
  margin-top: auto;
  padding-top: var(--space-4);
  border-top: 1px solid var(--surface-inverse-2);
  font-size: var(--text-xs);
  color: color-mix(in srgb, white 55%, var(--surface-inverse));
}

@media (max-width: 899px) {
  .admin-nav-footer {
    margin-top: 0;
    margin-left: auto;
    padding-top: 0;
    border-top: 0;
    border-left: 1px solid var(--surface-inverse-2);
    padding-left: var(--space-4);
    white-space: nowrap;
  }
}

/* Logout is a form, not a link, because a GET logout can be fired by any
 * page on the internet with an <img> tag and has nowhere to carry the CSRF
 * token. A form in a flex column is a block-level interruption, so it gets
 * the same zero margin the nav links have and hands its own sizing to .btn.
 *
 * The button re-states its colours rather than inheriting .btn-ghost's,
 * because .btn-ghost is written for the light surfaces of the public site and
 * would render near-black text on the sidebar's dark ground. This is the only
 * component in the panel that needed that, which is why it is three
 * declarations here instead of a dark variant in components.css. */
.admin-nav-logout {
  margin: var(--space-2) 0 0;
}

.admin-nav-logout .btn {
  justify-content: flex-start;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-color: transparent;
  color: color-mix(in srgb, white 78%, var(--surface-inverse));
}

.admin-nav-logout .btn:hover {
  background: var(--surface-inverse-2);
  border-color: var(--surface-inverse-2);
  color: #fff;
}

.admin-nav-logout .icon {
  color: inherit;
}

@media (max-width: 899px) {
  .admin-nav-logout {
    margin: 0 0 0 var(--space-3);
  }
}

/* ══ Page header ════════════════════════════════════════════════════════ */
.admin-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  margin-bottom: var(--space-6);
}

.admin-head p {
  margin-top: var(--space-1);
  color: var(--ink-3);
}

/* ══ Panel ══════════════════════════════════════════════════════════════
 * The admin's card. Same shape as .card, but with a titled header strip --
 * every admin screen is a stack of these, which is what makes the thirteen
 * pages look like one product. */
.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.panel-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--line);
  background: var(--surface-2);
}

.panel-head h2,
.panel-head h3 {
  font-size: var(--text-lg);
}

.panel-body {
  padding: var(--space-5);
}

.panel-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border-top: 1px solid var(--line);
  background: var(--surface-2);
}

/* A footer carrying a result count on the left and pager controls on the
 * right. This is a modifier rather than a .cluster-between next to
 * .panel-foot, because .panel-foot is declared in this file and .cluster-between
 * in components.css -- the later file wins on justify-content regardless of
 * the order the two classes are written in the markup, so the utility would
 * silently lose. */
.panel-foot-split {
  justify-content: space-between;
}

/* Panel headers carry a subtitle on the pages where the heading alone does
 * not say what a table is showing. */
.panel-head p {
  margin-top: var(--space-1);
  font-size: var(--text-sm);
  color: var(--ink-3);
}

/* A panel whose body is a table: the table supplies its own padding, so the
 * body must not add more. */
.panel-flush .panel-body {
  padding: 0;
}

/* ══ Sortable service list ═══════════════════════════════════════
 * Each row of /admin/services is a full editing panel, not a one-line strip,
 * so the list is a .stack of .panel cards and the separators the earlier
 * version drew between rows would land in the middle of a card. Spacing comes
 * from the stack gap instead.
 *
 * The drag states are set by public/js/admin-services.js. Both are optional
 * polish: the up/down buttons in each panel header do the same job without
 * any of this, which is what keeps the feature usable by keyboard and with
 * scripting off. */
.sortable-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.sortable-handle {
  flex: none;
  color: var(--ink-4);
  cursor: grab;
}

.sortable-handle:active {
  cursor: grabbing;
}

/* The card being carried. Left visible rather than hidden, because the
 * browser's own drag image is a snapshot of the element and a hidden source
 * makes the pointer look like it is dragging nothing. */
.service-card.is-dragging {
  opacity: 0.45;
}

/* The gap the card would land in. An outline rather than a border: a border
 * changes the box size and shoves every card below it down by two pixels
 * each time the pointer crosses a row. */
.service-card.is-drop-target {
  outline: 2px dashed var(--brand-accent);
  outline-offset: 3px;
}

/* A textarea holding markup rather than prose. Monospace so tags line up and
 * a stray angle bracket is visible, and tall by default because the details
 * field is several paragraphs, not a sentence. */
.html-input {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: 1.55;
  min-height: 16rem;
  white-space: pre;
  overflow-wrap: normal;
  overflow-x: auto;
}

/* One chevron glyph serves both move buttons; the "up" one is the same
 * artwork turned over. Sprite symbols cannot be flipped with a transform on
 * the <use> element in every browser, so the transform goes on the <svg>. */
.icon-up {
  transform: rotate(180deg);
}

/* ══ Image preview ══════════════════════════════════════════════════════
 * The checkerboard shows through a transparent PNG, so an admin can tell a
 * white logo from a transparent one before they upload it over the header. */
.image-preview {
  display: block;
  max-width: 100%;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background-color: var(--surface-2);
  background-image: linear-gradient(45deg, var(--surface-3) 25%, transparent 25%),
    linear-gradient(-45deg, var(--surface-3) 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, var(--surface-3) 75%),
    linear-gradient(-45deg, transparent 75%, var(--surface-3) 75%);
  background-size: 16px 16px;
  background-position: 0 0, 0 8px, 8px -8px, -8px 0;
}

/* ══ Login ══════════════════════════════════════════════════════════════
 * The one admin page with no shell: centred card on the dark ground. */
.login-page {
  display: grid;
  place-items: center;
  min-height: 100vh;
  padding: var(--space-5);
  background: var(--surface-inverse);
}

.login-card {
  width: 100%;
  max-width: 26rem;
  padding: var(--space-6);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-lg);
}

.login-card .brand {
  justify-content: center;
  margin-bottom: var(--space-5);
}

/* ══ Message body preview ═══════════════════════════════════════════════
 * A lead's message, rendered as-is inside a bounded box. pre-wrap preserves
 * the visitor's line breaks without any server-side nl2br, and the max-height
 * stops a 5000-character message owning the whole page. */
.message-body {
  max-height: 18rem;
  overflow-y: auto;
  padding: var(--space-4);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-2);
  font-size: var(--text-sm);
  white-space: pre-wrap;
  overflow-wrap: break-word;
}

/* ══ Spam score ═════════════════════════════════════════════════════════
 * A number plus a bar. The obvious way to size the bar is style="width:60%",
 * and that is exactly what the CSP forbids: style-src carries a per-request
 * nonce (server/middleware/security.js), a nonce can only be attached to a
 * <style> element, and a nonce in style-src also nullifies 'unsafe-inline'.
 * So every style="" attribute in this application is dead, width-only or not.
 *
 * Eleven pre-declared steps instead. The template writes
 * `data-score="<clamped 0-10>"` and the stylesheet owns every width, which
 * means the value the page prints into the DOM is an integer attribute rather
 * than a string the browser will parse as CSS. Ten-percent granularity is
 * enough for a bar that exists to say "well under" or "well over" the
 * threshold; the exact number is printed next to it anyway. */
.score-bar {
  height: 6px;
  min-width: 4rem;
  border-radius: var(--radius-pill);
  background: var(--surface-3);
  overflow: hidden;
}

.score-fill {
  height: 100%;
  width: 0;
  border-radius: inherit;
  background: var(--warn);
  transition: width var(--dur) var(--ease);
}

.score-fill[data-score='0'] { width: 0%; }
.score-fill[data-score='1'] { width: 10%; }
.score-fill[data-score='2'] { width: 20%; }
.score-fill[data-score='3'] { width: 30%; }
.score-fill[data-score='4'] { width: 40%; }
.score-fill[data-score='5'] { width: 50%; }
.score-fill[data-score='6'] { width: 60%; }
.score-fill[data-score='7'] { width: 70%; }
.score-fill[data-score='8'] { width: 80%; }
.score-fill[data-score='9'] { width: 90%; }
.score-fill[data-score='10'] { width: 100%; }

.score-fill.is-high {
  background: var(--danger);
}

/* The number and the bar sit on one line and stay together; the bar is
 * decorative (aria-hidden in the template) because the number beside it
 * already says everything the bar says. */
.score-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-1);
}

.score-value {
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--ink-2);
}

/* ══ Leads table ════════════════════════════════════════════════════════
 * Five rules that exist only because a lead row holds five different kinds
 * of value, and a table that treats them all alike is unreadable at any
 * width. Everything here is sizing and wrapping; no colours, so the panel
 * still inherits its whole palette from tokens.css.
 *
 * The widths are `width` on a table cell, which the browser treats as a
 * suggestion rather than a rule -- it is the hint the auto-layout algorithm
 * needs to stop giving eight equal columns to eight very unequal ones. */
.lead-date {
  width: 9rem;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  color: var(--ink-3);
}

/* Identifiers are compared character by character, and a proportional font
 * makes that miserable -- 1 and l and I are the same glyph width apart. The
 * fingerprint is a hash, so it wraps anywhere rather than pushing the row
 * wider than the viewport.
 *
 * min-width, not width: under table-layout: auto a width on a cell is only a
 * suggestion the browser undercuts when the other seven columns want room,
 * and this column was losing that argument and wrapping every address. A
 * min-width is binding. */
.lead-source {
  min-width: 11rem;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--ink-3);
  overflow-wrap: anywhere;
}

/* overflow-wrap: anywhere is set on the cell for the fingerprint's sake and
 * inherits to everything in it, which was splitting addresses across lines
 * mid-octet. An address is read as one token or not at all, so it opts out;
 * a long one widens the column instead, which min-width above allows. */
.lead-ip {
  overflow-wrap: normal;
}

.lead-fingerprint {
  color: var(--ink-4);
}

/* The campaign tag is the one line in this cell that gets read for a reason
 * other than blocking someone, so it is the only one at full contrast. */
.lead-campaign {
  color: var(--ink);
  font-weight: 600;
}

/* The message is the column the page exists for, so it gets the width and
 * the only preserved line breaks in the table.
 *
 * The scrolling lives on an inner div, not on the cell. A table cell
 * ignores max-height -- the table layout algorithm sizes rows from their
 * content and browsers treat the property as a suggestion it is free to
 * discard -- so a rule written on the td would appear to work until the
 * first thousand-word message arrived and pushed every other row off the
 * screen. A block-level child inside the cell honours it normally.
 *
 * 14rem is deliberately generous: a scrollbar inside a cell is a cost
 * worth paying only for genuinely long text. */
.lead-message {
  min-width: 20rem;
}

.lead-message > div {
  max-height: 14rem;
  overflow-y: auto;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.lead-notes {
  min-width: 12rem;
}

.lead-notes textarea {
  width: 100%;
  min-height: 3.5rem;
  resize: vertical;
}

/* ══ Users ═════════════════════════════════════════════════════════════════
 *
 * What each role may do used to be a paragraph of muted text above the
 * table, which is the shape a caption takes when nobody decides what it is.
 * It is a definition list now, so the role name and its description are
 * associated in the markup and not only by being adjacent.
 *
 * The grid collapses to one column on a narrow screen rather than letting
 * the description column shrink to two words a line. */
.role-legend {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: var(--space-3) var(--space-4);
  align-items: baseline;
  margin: 0;
}

.role-legend dt {
  margin: 0;
}

.role-legend dd {
  margin: 0;
  color: var(--ink-2);
}

@media (max-width: 40rem) {
  .role-legend {
    grid-template-columns: 1fr;
    gap: var(--space-1) var(--space-4);
  }

  .role-legend dd {
    margin-bottom: var(--space-3);
  }
}

/* The role cell holds a badge, a select and a button stacked. Without a
 * width the select sizes to its longest option and drags the column wider
 * than the four other columns put together. */
.role-form {
  min-width: 11rem;
}

.role-form .badge {
  margin-bottom: var(--space-2);
}

/* A field that should not stretch to the width of the panel: a role select
 * is three short words, and a full-width control implies a full-width
 * answer. */
.field-narrow {
  max-width: 22rem;
}

/* The add-user form is a details element wearing the panel chrome, so the
 * summary has to look like a panel head and behave like a button. The
 * marker is removed on both engines -- ::marker for standards mode,
 * ::-webkit-details-marker for Safari -- because the icon in the summary is
 * the affordance. */
.add-user > summary {
  cursor: pointer;
  list-style: none;
  user-select: none;
}

.add-user > summary::marker,
.add-user > summary::-webkit-details-marker {
  display: none;
  content: '';
}

.add-user > summary:hover {
  background: var(--surface-2);
}

.add-user > summary:focus-visible {
  outline: 2px solid var(--brand-accent);
  outline-offset: -2px;
}

.add-user > summary .icon {
  transition: transform var(--dur-fast) var(--ease);
}

.add-user[open] > summary .icon {
  transform: rotate(45deg);
}

/* ══ Content sub-pages ══════════════════════════════════════════════════
 * Four pages hang off the /admin/content hub, and each needs a way back to
 * it. The admin nav marks Content as the active item on all five, so without
 * this link the only route back is the browser's Back button. */
.subpage-back {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
  color: var(--ink-3);
  font-size: var(--text-sm);
  text-decoration: none;
}

.subpage-back:hover,
.subpage-back:focus-visible {
  color: var(--ink);
  text-decoration: underline;
}

/* The sprite has one horizontal arrow. Turning it over is cheaper than a
 * second symbol, and it goes on the <svg> rather than the <use> because a
 * transform on <use> is not honoured everywhere. */
.subpage-back .icon {
  transform: rotate(180deg);
}

/* The colour input is a swatch, not a text field: browsers draw it as a
 * filled rectangle, so the generic .field input sizing would stretch a block
 * of raw colour across the form. */
.color-input {
  width: 4.5rem;
  height: 2.5rem;
  padding: var(--space-1);
  cursor: pointer;
}

/* Blur is set with a slider whose value is printed beside it. The <output>
 * is what public/js/range-output.js writes into; it is rendered server-side
 * with the stored value first, so the number is correct with scripting off
 * even though it then stops tracking the thumb. */
.range-row {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
}

.range-row input[type='range'] {
  flex: 1 1 14rem;
  min-width: 10rem;
  accent-color: var(--brand-accent);
}

.range-value {
  min-width: 3.5rem;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

/* ══ Rich text editor ═══════════════════════════════════════════════════
 * The chrome for public/js/rich-text.js. None of it is loaded until that
 * file builds the markup, so with scripting off these rules match nothing
 * and the .html-input textarea above is what the admin sees.
 *
 * The wrapper carries the border and the focus ring so the toolbar and the
 * editing surface read as one control rather than a row of buttons that
 * happens to sit above a box. */
.rich-text {
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface);
  transition: border-color var(--dur-fast) var(--ease),
    box-shadow var(--dur-fast) var(--ease);
}

/* :focus-within rather than :focus -- the element that actually takes focus
 * is the contenteditable div inside, and the ring belongs on the control. */
.rich-text:focus-within {
  border-color: var(--brand-accent);
  box-shadow: 0 0 0 4px var(--accent-ring);
}

/* An explicit rule because the [hidden] attribute is only a UA default, and
 * a display value from any other selector beats it. Both the textarea and
 * the surface are hidden this way when the HTML view is toggled. */
.rich-text [hidden] {
  display: none;
}

.rich-text__bar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  padding: var(--space-2);
  border-bottom: 1px solid var(--line);
  background: var(--surface-2);
  border-radius: calc(var(--radius) - 1px) calc(var(--radius) - 1px) 0 0;
}

.rich-text__button {
  padding: 0.35em 0.7em;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink-2);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: 1.2;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease),
    color var(--dur-fast) var(--ease);
}

.rich-text__button:hover:not(:disabled) {
  background: var(--surface-3);
  color: var(--ink);
}

.rich-text__button:focus-visible {
  outline: 2px solid var(--brand-accent);
  outline-offset: 1px;
}

/* is-active and aria-pressed are set together by the script. The attribute
 * is what assistive technology reads; the class is what this paints. */
.rich-text__button.is-active {
  background: var(--surface);
  border-color: var(--line-strong);
  color: var(--ink);
}

.rich-text__button:disabled {
  opacity: 0.4;
  cursor: default;
}

/* The B and I buttons say what they do by how they are drawn. */
.rich-text__button[aria-label^='Bold'] {
  font-weight: 800;
}

.rich-text__button[aria-label^='Italic'] {
  font-style: italic;
}

/* The editing surface. Typography here is deliberately close to the public
 * .prose rules rather than to the admin form font, because the point of a
 * WYSIWYG field is that the admin sees roughly the page they are writing. */
.rich-text__surface {
  padding: 0.65em 0.85em;
  min-height: 9rem;
  max-height: 32rem;
  overflow-y: auto;
  color: var(--ink);
  line-height: 1.65;
}

.rich-text__surface:focus {
  outline: none;
}

.rich-text__surface > :first-child {
  margin-top: 0;
}

.rich-text__surface > :last-child {
  margin-bottom: 0;
}

.rich-text__surface p,
.rich-text__surface ul,
.rich-text__surface ol,
.rich-text__surface blockquote {
  margin: 0 0 var(--space-4);
}

.rich-text__surface h2,
.rich-text__surface h3,
.rich-text__surface h4 {
  margin: var(--space-5) 0 var(--space-3);
  line-height: 1.25;
}

.rich-text__surface h2 {
  font-size: var(--text-xl);
}

.rich-text__surface h3 {
  font-size: var(--text-lg);
}

.rich-text__surface h4 {
  font-size: var(--text-base);
}

.rich-text__surface ul,
.rich-text__surface ol {
  padding-left: var(--space-6);
}

.rich-text__surface li {
  margin-bottom: var(--space-2);
}

.rich-text__surface blockquote {
  padding-left: var(--space-4);
  border-left: 3px solid var(--brand-accent);
  color: var(--ink-2);
}

.rich-text__surface a {
  color: var(--brand-accent);
  text-decoration: underline;
}

/* The textarea keeps its own border while it is the visible control in the
 * HTML view, but the wrapper already draws one, so it loses it here. */
.rich-text__source {
  border: 0;
  border-radius: 0 0 calc(var(--radius) - 1px) calc(var(--radius) - 1px);
}

.rich-text__source:focus {
  box-shadow: none;
}
