/* ──────────── Colour tokens ────────────

   Every token that can end up behind text is paired with the foreground that goes ON it, named
   `--prestia-on-<token>`. The pairing is a DECLARATION, not something a parser can infer: whether a
   background carries text depends on the markup, and a stylesheet does not contain the markup. What
   the pair does is make the question answerable - `internal/web/view/contrast_test.go` reads these
   names, resolves both themes and measures every pair. A background token with no `-on-` partner is a
   colour nobody claims is behind text (a status dot, a progress track, a border line).

   Ratios below are WCAG 2.1 relative-luminance contrast, light / dark, and every one of them is
   recomputed by that test - if a value here disagrees with the sheet, the test is right.

   | Pair                                        | light | dark  |
   |---------------------------------------------|-------|-------|
   | on-bg on bg (page)                           | 17.40 | 15.67 |
   | on-surface on surface (cards, panels)        | 15.96 | 13.74 |
   | on-border on border (neutral chip, track)    | 13.18 | 10.33 |
   | on-primary on primary (solid button)         | 13.11 |  4.62 |
   | on-primary-hover on primary-hover            |  9.41 |  5.82 |
   | on-accent on accent (primary action)         |  5.67 |  6.02 |
   | on-accent-hover on accent-hover              |  7.66 |  8.20 |
   | on-error on error (danger fill, alert badge) |  7.33 |  5.31 |
   | on-error-hover on error-hover                |  9.86 |  6.64 |
   | on-success on success                        |  4.65 |  6.66 |
   | on-warn on warn                              |  5.27 |  9.05 |
   | on-error-tint on error-tint                  |  5.38 |  4.69 |
   | on-success-tint on success-tint              |  4.71 |  4.67 |
   | on-warn-tint on warn-tint                    |  4.70 |  5.83 |
   | on-sidebar on sidebar                        | 13.11 | 13.11 |
   | on-sidebar-dim on sidebar (nav links)        |  8.08 |  8.08 |
   | on-sidebar-muted on sidebar (section, meta)  |  6.04 |  6.04 |
   | on-sidebar-hover on sidebar-hover            | 10.30 | 10.30 |
   | on-sidebar-active on sidebar-active (current)| 10.54 | 10.54 |
   | on-impersonation on impersonation (the bar)  |  7.72 |  7.72 |

   Two tokens are foregrounds with no surface of their own, because they are only ever ink on the page
   or on a card. Both are measured against both surfaces:

   | Foreground        | on bg          | on surface     |
   |-------------------|----------------|----------------|
   | --prestia-muted   | 6.90 / 6.02    | 6.33 / 5.28    |
   | --prestia-link    | 5.67 / 6.02    | 5.20 / 5.28    |

   WHAT THIS TABLE IS NOT. It says nothing about text drawn on a colour it does not name - and most of
   the product is exactly that, because a rule usually sets `color` and inherits its background from an
   ancestor. That is measured by running axe over the rendered screens in both themes, not here.

   ORNAMENTS STAY LITERAL. A scrollbar thumb, a hairline and a drop shadow are translucent whites and
   blacks over whatever is behind them; they never carry text, so they are written as `rgba()` where
   they are used instead of being promoted to tokens that would then owe a foreground.

   AN ORNAMENT THAT HAS TO CHANGE WITH THE THEME IS A TOKEN, and `--prestia-scroll-shadow` is the one
   that does. It is the edge shadow that says a table has rows past the edge, and a translucent black is
   that cue over a light surface and nothing at all over a dark one: `rgba(0,0,0,.18)` over
   `--prestia-dark-surface` composites to about `#151b22` against a `#1a2129` background, which is a
   difference of three or four units per channel. So it is a token with a value per theme, and like any
   background token that never carries text it owes no `-on-` partner.

   THE SIDEBAR IS A THIRD SURFACE. `bg` and `surface` are the page and what is raised off it, and the
   two of them are what a foreground with no surface of its own is measured against above - but a
   control can also sit on the sidebar, which is navy in both themes. Nothing that carries text spans
   the three (the sidebar has its own ink tokens), and the one thing that does span them all is the
   focus ring, which is why it is built out of two tones instead of being measured against a list.

   THE SIDEBAR IS DARK IN BOTH THEMES. It is chrome, not page, so its tokens have no dark override -
   the ratios are the same in both columns above. Its text used to be white at four different alphas
   over the navy; the section headers came out at 3.45:1, which is why they are named colours now.

   TINTS ARE OPAQUE. The soft tone backgrounds (badge, alert) were `rgba(tone, .16)` composited over
   whatever was behind them, so their real contrast depended on where the element sat and could not be
   computed from the sheet. Each is a solid token per theme instead, which both fixes the measurement
   and keeps a badge reading the same on a card and on the page. */
:root {
  /* Surfaces and ink */
  --prestia-bg: #ffffff;
  --prestia-surface: #f5f5f5;
  --prestia-fg: #1a1a1a;
  --prestia-on-bg: var(--prestia-fg);
  --prestia-on-surface: var(--prestia-fg);
  --prestia-muted: #5a5a5a;
  /* A line, and the neutral fill of a chip or a progress track. */
  --prestia-border: #e0e0e0;
  --prestia-on-border: var(--prestia-fg);
  /* The edge cue of a table with rows past the edge. Never behind text: no -on- partner. */
  --prestia-scroll-shadow: rgba(0,0,0,.18);

  /* Brand */
  --prestia-primary: #1D3341;
  --prestia-on-primary: #ffffff;
  --prestia-primary-hover: #2a4a5c;
  --prestia-on-primary-hover: #ffffff;
  --prestia-accent: #236f89;
  --prestia-on-accent: #ffffff;
  --prestia-accent-hover: #1b5a70;
  --prestia-on-accent-hover: #ffffff;
  --prestia-link: #236f89;

  /* Tones: a solid fill, and the soft tint a badge or an alert is drawn on. */
  --prestia-error: #b00020;
  --prestia-on-error: #ffffff;
  --prestia-error-hover: #8c0019;
  --prestia-on-error-hover: #ffffff;
  --prestia-error-tint: #f2d6db;
  --prestia-on-error-tint: #b00020;
  --prestia-success: #1a863b;
  --prestia-on-success: #ffffff;
  --prestia-success-tint: #dbece0;
  --prestia-on-success-tint: #177534;
  --prestia-warn: #9c5d0a;
  --prestia-on-warn: #ffffff;
  --prestia-warn-tint: #efe5d8;
  --prestia-on-warn-tint: #925709;

  /* Sidebar chrome - the same in both themes, so no dark value below. */
  --prestia-sidebar: #1D3341;
  --prestia-on-sidebar: #ffffff;
  --prestia-on-sidebar-dim: #c6ccd0;
  --prestia-on-sidebar-muted: #9fb3bf;
  --prestia-sidebar-hover: #2f4350;
  --prestia-on-sidebar-hover: #ffffff;
  /* The item the user is on. The fill is what the old translucent highlight composited to over the
     navy, written as the solid colour it drew: an alpha fill is not a declared pair and nothing can
     measure it. The edge has no `-on-` partner because nothing is ever written on a 3px border - it is
     a non-text indicator (1.4.11), so what it has to clear is 3:1 against both of its neighbours. */
  --prestia-sidebar-active: #204354;
  --prestia-on-sidebar-active: #ffffff;
  --prestia-sidebar-active-edge: #4FA8C7;

  /* Impersonation chrome - the strip carried by a session that is viewing the application as somebody
     else. A FOURTH SURFACE, like the sidebar, and for the same reason: it is chrome with its own ink,
     drawn the same in both themes because what it announces does not change with a preference. The
     colour is deliberately not the brand navy - the bar exists to be noticed on a screen the reader has
     already stopped looking at, and a strip in the product's own colour is read as product. */
  --prestia-impersonation: #8c3a00;
  --prestia-on-impersonation: #ffffff;

  /* Focus ring - two tones, the same in both themes, and neither of them ever behind text (no `-on-`
     partner). A ring in ONE colour has to be chosen against the surface the control sits on, and the
     sheet does not contain the markup, so it cannot know that surface: the page is white or near-black,
     a card is neither, and the sidebar is navy in BOTH themes. Drawn in the brand colour, the ring was
     #1D3341 on a #1D3341 sidebar - 1.00:1, which is no indicator at all. Two tones at opposite ends of
     the luminance range remove the question instead of answering it per surface: whichever band a
     surface swallows, the other one is the indicator. The bound is closed-form and needs no list of
     surfaces - see the focus check in `internal/web/view/contrast_test.go`. */
  --prestia-focus-ring: #000000;
  --prestia-focus-halo: #ffffff;

  /* Dark palette values - single source of truth (aligned to the eppsc dark theme: soft,
     slightly blue-tinted, comfortable contrast), referenced by the dark selectors below. A token
     whose foreground flips (white ink on a light-blue fill is unreadable) has its own dark partner. */
  --prestia-dark-bg: #0f1419;
  --prestia-dark-surface: #1a2129;
  --prestia-dark-fg: #e6edf3;
  --prestia-dark-muted: #8b949e;
  --prestia-dark-border: #30363d;
  --prestia-dark-scroll-shadow: rgba(255,255,255,.22);
  --prestia-dark-primary: #4a7a9a;
  --prestia-dark-primary-hover: #3c6a88;
  --prestia-dark-accent: #5b9bb8;
  --prestia-dark-on-accent: #0f1419;
  --prestia-dark-accent-hover: #7fb4cc;
  --prestia-dark-on-accent-hover: #0f1419;
  --prestia-dark-link: #5b9bb8;
  --prestia-dark-error: #ef5350;
  --prestia-dark-on-error: #0f1419;
  --prestia-dark-error-hover: #f4736f;
  --prestia-dark-on-error-hover: #0f1419;
  --prestia-dark-error-tint: #3c292f;
  --prestia-dark-on-error-tint: #f26f6c;
  --prestia-dark-success: #4caf50;
  --prestia-dark-on-success: #0f1419;
  --prestia-dark-success-tint: #22382f;
  --prestia-dark-on-success-tint: #4eb252;
  --prestia-dark-warn: #f0a64d;
  --prestia-dark-on-warn: #0f1419;
  --prestia-dark-warn-tint: #3c362f;
  --prestia-dark-on-warn-tint: #f0a64d;
}

/* Dark theme token mapping. app.js resolves the light/dark/system preference and writes the
   effective value to data-theme, so this drives explicit dark *and* system->dark. The @media
   block only takes effect before app.js runs (or with JS disabled), giving the right palette
   from the OS preference with no flash. Only the mapping is restated; values live once above.
   A token defined as `var(--prestia-fg)` needs no line here: var() resolves at use time, so it
   follows whatever the token it points at is bound to in this block. */
:root[data-theme="dark"] {
  --prestia-bg: var(--prestia-dark-bg);
  --prestia-surface: var(--prestia-dark-surface);
  --prestia-fg: var(--prestia-dark-fg);
  --prestia-muted: var(--prestia-dark-muted);
  --prestia-border: var(--prestia-dark-border);
  --prestia-scroll-shadow: var(--prestia-dark-scroll-shadow);
  --prestia-primary: var(--prestia-dark-primary);
  --prestia-primary-hover: var(--prestia-dark-primary-hover);
  --prestia-accent: var(--prestia-dark-accent);
  --prestia-on-accent: var(--prestia-dark-on-accent);
  --prestia-accent-hover: var(--prestia-dark-accent-hover);
  --prestia-on-accent-hover: var(--prestia-dark-on-accent-hover);
  --prestia-link: var(--prestia-dark-link);
  --prestia-error: var(--prestia-dark-error);
  --prestia-on-error: var(--prestia-dark-on-error);
  --prestia-error-hover: var(--prestia-dark-error-hover);
  --prestia-on-error-hover: var(--prestia-dark-on-error-hover);
  --prestia-error-tint: var(--prestia-dark-error-tint);
  --prestia-on-error-tint: var(--prestia-dark-on-error-tint);
  --prestia-success: var(--prestia-dark-success);
  --prestia-on-success: var(--prestia-dark-on-success);
  --prestia-success-tint: var(--prestia-dark-success-tint);
  --prestia-on-success-tint: var(--prestia-dark-on-success-tint);
  --prestia-warn: var(--prestia-dark-warn);
  --prestia-on-warn: var(--prestia-dark-on-warn);
  --prestia-warn-tint: var(--prestia-dark-warn-tint);
  --prestia-on-warn-tint: var(--prestia-dark-on-warn-tint);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    --prestia-bg: var(--prestia-dark-bg);
    --prestia-surface: var(--prestia-dark-surface);
    --prestia-fg: var(--prestia-dark-fg);
    --prestia-muted: var(--prestia-dark-muted);
    --prestia-border: var(--prestia-dark-border);
  --prestia-scroll-shadow: var(--prestia-dark-scroll-shadow);
    --prestia-primary: var(--prestia-dark-primary);
    --prestia-primary-hover: var(--prestia-dark-primary-hover);
    --prestia-accent: var(--prestia-dark-accent);
    --prestia-on-accent: var(--prestia-dark-on-accent);
    --prestia-accent-hover: var(--prestia-dark-accent-hover);
    --prestia-on-accent-hover: var(--prestia-dark-on-accent-hover);
    --prestia-link: var(--prestia-dark-link);
    --prestia-error: var(--prestia-dark-error);
    --prestia-on-error: var(--prestia-dark-on-error);
    --prestia-error-hover: var(--prestia-dark-error-hover);
    --prestia-on-error-hover: var(--prestia-dark-on-error-hover);
    --prestia-error-tint: var(--prestia-dark-error-tint);
    --prestia-on-error-tint: var(--prestia-dark-on-error-tint);
    --prestia-success: var(--prestia-dark-success);
    --prestia-on-success: var(--prestia-dark-on-success);
    --prestia-success-tint: var(--prestia-dark-success-tint);
    --prestia-on-success-tint: var(--prestia-dark-on-success-tint);
    --prestia-warn: var(--prestia-dark-warn);
    --prestia-on-warn: var(--prestia-dark-on-warn);
    --prestia-warn-tint: var(--prestia-dark-warn-tint);
    --prestia-on-warn-tint: var(--prestia-dark-on-warn-tint);
  }
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  color: var(--prestia-fg);
  background: var(--prestia-bg);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  /* NO overflow-x HERE. `overflow-x: hidden` on the page is WCAG failure F102: it does not make wide
     content fit, it CLIPS it - the part past the viewport is unreachable by scrolling, by dragging and
     by the keyboard, so at 320px the right-hand columns of a card simply stop existing and nothing on
     screen says so. Reflow is a property of the content, and every element that cannot reflow is given
     its own scroll region where it lives (a table in its card, the health tables in a .table-scroll);
     with that done the page has nothing left to clip and needs no backstop. A backstop here would only
     hide the next element that forgets. */
}

.container {
  max-width: 64rem;
  margin: 0 auto;
  padding: 2rem 1.25rem;
}

/* Consistent headings so content never reads as loose text floating on the page. */
h1 { font-size: 1.5rem; font-weight: 700; color: var(--prestia-fg); margin: 0 0 1.25rem; line-height: 1.2; }
h2 { font-size: 1.15rem; font-weight: 600; color: var(--prestia-fg); margin: 1.75rem 0 .75rem; line-height: 1.25; }
h3 { font-size: 1rem; font-weight: 600; color: var(--prestia-fg); margin: 1.25rem 0 .5rem; }
/* Standalone explanatory paragraphs read as muted intro text under the heading, not body noise. */
.page-content > p { color: var(--prestia-muted); max-width: 70ch; }

a { color: var(--prestia-link); }

/* Secondary text: a note that has to read as subordinate to what it sits under. It existed only as
   `.toolbar .muted` and `.tasks-section > .muted`, so the three places that put it on a standalone
   paragraph - the offline counter among them - got no styling at all. */
.muted { color: var(--prestia-muted); font-size: .9rem; }

/* Adjacent buttons never touch. Containers that already use flex gap opt out. */
.btn + .btn { margin-left: .5rem; }
:where(.row-actions, .page-header__actions, .pager__nav, .topbar-actions) .btn + .btn { margin-left: 0; }
td form + form, td form + .btn, td .btn + form, td a.btn + form, td form + a.btn { margin-left: .35rem; }
/* A form that sits in a row of other things: a filter bar, the controls in a table cell. It lays its
   parts out in a line and aligns them on their BOTTOM edge, because its parts are no longer all the
   same height - a labelled field is a label stacked over a control, and a button aligned to its top
   would sit level with the label instead of with the control it submits. */
.inline-form { display: inline-flex; align-items: flex-end; gap: .5rem; margin: 0; }

/* Text that exists for the accessibility tree and not for the eye. Not `display: none` and not
   `visibility: hidden`, which take it out of both. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* THE FIRST TAB STOP OF EVERY PAGE. Forty navigation links stand between the top of the document and
   what the page is about, and going through them on every page is what WCAG 2.4.1 exists to stop. Off
   screen until it takes focus, then it comes down over the top bar; it is not `visually-hidden`,
   because it has to become VISIBLE, which a clipped element cannot do. */
.skip-link {
  position: absolute;
  top: -4rem;
  left: .75rem;
  z-index: 700;
  padding: .5rem .9rem;
  border-radius: 6px;
  background: var(--prestia-bg);
  color: var(--prestia-fg);
  border: 1px solid var(--prestia-border);
  text-decoration: none;
  transition: top .15s ease;
}
.skip-link:focus { top: .75rem; }

/* THE ONLY FOCUS INDICATOR IN THE PRODUCT, so it has to be visible on every surface a control can sit
   on. Two bands: the dark one is the outline, the light one is a box-shadow spread past the outline's
   outer edge, so it shows on BOTH sides of it - the 2px offset gap and the 2px beyond - because an
   outline paints over a box-shadow. Whichever band the surface swallows, the other is the ring.
   Worst case over any surface whatsoever: 4.58:1, measured by `internal/web/view/contrast_test.go`. */
:focus-visible {
  outline: 2px solid var(--prestia-focus-ring);
  outline-offset: 2px;
  box-shadow: 0 0 0 6px var(--prestia-focus-halo);
}

/* ──────────── Layout: Sidebar + Topbar ──────────── */
.layout-wrapper {
  display: flex;
  min-height: 100vh;
}

/* Sidebar - fixed 250px */
.sidebar {
  position: fixed;
  top: 0;
  left: 0;
  width: 250px;
  height: 100vh;
  background: var(--prestia-sidebar);
  color: var(--prestia-on-sidebar);
  display: flex;
  flex-direction: column;
  z-index: 500;
  overflow: hidden;
}

.sidebar-brand {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: 1rem 1.25rem;
  font-size: 1.1rem;
  font-weight: 700;
  color: var(--prestia-on-sidebar);
  text-decoration: none;
  border-bottom: 1px solid rgba(255,255,255,.08);
  flex-shrink: 0;
}
.sidebar-brand:hover { color: var(--prestia-on-sidebar); }

/* Only exists where the drawer is a drawer; the media query below turns it on. */
.sidebar-close {
  display: none;
  position: absolute;
  top: .75rem;
  right: .75rem;
  padding: .25rem .4rem;
  color: var(--prestia-on-sidebar);
  text-decoration: none;
  font-size: 1.2rem;
  line-height: 1;
}
.sidebar-brand-logo { height: 52px; width: auto; max-width: 100%; display: block; }

.sidebar-nav {
  flex: 1;
  overflow-y: auto;
  padding: .5rem 0;
}
.sidebar-nav::-webkit-scrollbar { width: 4px; }
.sidebar-nav::-webkit-scrollbar-thumb { background: rgba(255,255,255,.2); border-radius: 2px; }

.sidebar-section {
  padding: .75rem 1.25rem .25rem;
  font-size: .7rem;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--prestia-on-sidebar-muted);
  font-weight: 600;
}

.sidebar-nav-link {
  display: flex;
  align-items: center;
  gap: .65rem;
  padding: .55rem 1.25rem;
  color: var(--prestia-on-sidebar-dim);
  text-decoration: none;
  font-size: .875rem;
  border-left: 3px solid transparent;
  transition: background .15s, color .15s;
}
.sidebar-nav-link:hover, .sidebar-nav-link:focus-visible {
  background: var(--prestia-sidebar-hover);
  color: var(--prestia-on-sidebar-hover);
}

/* THE ITEM THE USER IS ON, READ FROM THE DOCUMENT AND NOT FROM A SECOND SWITCH. What marks it is
   `aria-current="page"` - the same attribute a screen reader announces - so the eye and the
   accessibility tree are told by one fact, and there is no state a class could disagree with. It is
   also why the rule is written on the attribute: a modifier class would have to be composed from a
   boolean into the class attribute, and a class written that way stops being visible to the guards that
   read the emitted markup.
   Three signals and not one of them colour alone (1.4.1): a lighter fill, white ink where the resting
   link is dim, and the 3px edge the resting link already reserves. The edge clears 3:1 against BOTH of
   its neighbours - 4.84:1 on the sidebar and 3.89:1 on the fill it borders. The colour it recovers,
   #2B84A2, cleared the first (3.07:1) and not the second (2.47:1). It comes last so that the current
   item stays marked while the pointer is over it. */
.sidebar-nav-link[aria-current="page"] {
  background: var(--prestia-sidebar-active);
  color: var(--prestia-on-sidebar-active);
  border-left-color: var(--prestia-sidebar-active-edge);
  font-weight: 600;
}

/* A ROW THAT FILLS ITS COLUMN HAS NO ROOM OUTSIDE ITSELF FOR A RING. The sidebar clips its overflow and
   its links run edge to edge, so the ring the rule above draws outwards is cut off on both sides and
   only its top and bottom survive. These draw the same three bands inwards instead - halo, ring, halo -
   out of the same two tokens, so the same bound holds. */
.sidebar-brand:focus-visible, .sidebar-nav-link:focus-visible {
  outline-offset: -2px;
  box-shadow: inset 0 0 0 6px var(--prestia-focus-halo);
}

.sidebar-nav-link i { font-size: 1.1rem; width: 1.25rem; text-align: center; }

.sidebar-footer {
  padding: .75rem 1.25rem;
  border-top: 1px solid rgba(255,255,255,.08);
  font-size: .8rem;
  flex-shrink: 0;
}
.sidebar-user { display: flex; align-items: center; gap: .6rem; }
.sidebar-user-avatar { font-size: 1.6rem; color: var(--prestia-on-sidebar-muted); line-height: 1; flex-shrink: 0; }
.sidebar-user-id { min-width: 0; }
.sidebar-user-name { font-weight: 600; color: var(--prestia-on-sidebar); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sidebar-user-role { color: var(--prestia-on-sidebar-muted); font-size: .72rem; text-transform: capitalize; }

.sidebar-footer-meta { color: var(--prestia-on-sidebar-muted); font-size: .7rem; margin-top: .5rem; font-variant-numeric: tabular-nums; }
/* THE VERSION LINE SETS NO INK OF ITS OWN, and both halves of that are the finding. It carried
   `opacity: .7`, which composites the text toward whatever is behind it, so the rendered contrast
   stopped being the contrast of any declared pair and no reading of the sheet could compute it; at .7
   this line failed AA on every screen of the product, in both themes, while every pair the sheet
   declares measured clean. Replacing the opacity with `--prestia-on-sidebar-muted` made it WORSE: that
   token is #9fb3bf, ink for the navy sidebar, and this span renders inside `.backoffice-footer` in the
   main column, whose backdrop is `--prestia-bg` - 2.17:1 in the light theme. So it takes the footer's
   own `--prestia-muted` by inheritance (6.90:1 light, 6.02:1 dark, and the footer is the only place this class
   exists), and a check now fails on any rule that paints text in a surface-scoped ink somewhere the
   sheet cannot show that surface behind it. */
.footer-version { font-variant-numeric: tabular-nums; }

/* Main content area - offset by sidebar */
.main-wrapper {
  margin-left: 250px;
  flex: 1;
  /* min-width:0 lets this flex item shrink below its content's intrinsic width, so a wide table
     scrolls inside its card instead of pushing the whole page sideways. */
  min-width: 0;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

/* Topbar - sticky white bar */
.topbar {
  position: sticky;
  top: 0;
  z-index: 400;
  display: flex;
  align-items: center;
  gap: .75rem;
  padding: .6rem 1.5rem;
  background: var(--prestia-bg);
  border-bottom: 1px solid var(--prestia-border);
  min-height: 52px;
}
[data-theme="dark"] .topbar { background: var(--prestia-surface); }

/* On desktop the body already carries the eyebrow + <h1> and the sidebar highlights the active
   item, so the topbar title is pure duplication - hide it. It returns on mobile (≤768px), where
   the sidebar is collapsed behind the hamburger and the title is the only on-screen context. */
.topbar-title {
  font-size: 1rem;
  font-weight: 600;
  color: var(--prestia-fg);
  display: none;
  /* It only shows at the width where the sidebar is a drawer, which is also the width where the top bar
     has the least room. A flex item is floored by its own min-content unless it is told otherwise, so
     without these two a long screen name pushes the whole bar - and with it the page - wider than the
     viewport. */
  min-width: 0;
  overflow-wrap: anywhere;
}

.topbar-actions {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex-shrink: 0;
  margin-left: auto;
}

.topbar-actions .btn--ghost { color: var(--prestia-fg); }

/* User menu dropdown (no-JS <details>; app.js closes it on outside click). */
.user-menu { position: relative; }
.user-menu__btn { list-style: none; display: inline-flex; align-items: center; gap: .5rem; cursor: pointer; padding: .25rem .5rem; border-radius: 8px; color: var(--prestia-fg); }
.user-menu__btn::-webkit-details-marker { display: none; }
.user-menu__btn:hover { background: var(--prestia-surface); }
.user-menu__avatar { font-size: 1.4rem; line-height: 1; color: var(--prestia-muted); display: flex; }
.user-menu__id { display: flex; flex-direction: column; line-height: 1.15; text-align: left; }
.user-menu__name { font-size: .88rem; font-weight: 600; max-width: 12rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.user-menu__role { font-size: .72rem; color: var(--prestia-muted); }
.user-menu__caret { font-size: .7rem; color: var(--prestia-muted); transition: transform .15s; }
.user-menu[open] .user-menu__caret { transform: rotate(180deg); }
.user-menu__pop { position: absolute; right: 0; top: calc(100% + .4rem); min-width: 14rem; display: flex; flex-direction: column; background: var(--prestia-bg); border: 1px solid var(--prestia-border); border-radius: 10px; box-shadow: 0 6px 24px rgba(0,0,0,.18); padding: .35rem; z-index: 600; }
.user-menu__pop a, .user-menu__pop button { display: flex; align-items: center; gap: .6rem; padding: .55rem .65rem; border-radius: 6px; background: none; border: none; color: var(--prestia-fg); text-decoration: none; font: inherit; width: 100%; text-align: left; cursor: pointer; }
.user-menu__pop a:hover, .user-menu__pop button:hover { background: var(--prestia-surface); }
.user-menu__pop form { margin: 0; }
.user-menu__pop hr { border: none; border-top: 1px solid var(--prestia-border); margin: .3rem .2rem; }

/* A switchable account row: the switch form (a full-width submit button) flexes to fill, and the ×
   forget-button sits at the end, showing its surface only on hover/focus so the menu stays clean. */

/* THE NAME IS CLIPPED AT A NARROW WIDTH, NOT REMOVED. It is the only text inside the account
   disclosure - the avatar and the caret are both aria-hidden - so `display: none` left the <summary>
   with no accessible name at all on every screen below 640px. Clipped, it is out of the layout and
   still in the accessibility tree, which is what names the control. */
@media (max-width: 640px) {
  .user-menu__id {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
}

/* Hamburger - hidden on desktop. A link to the drawer's fragment, so it is drawn as the control it
   looks like and not as the link it is. */
.hamburger {
  display: none;
  background: none;
  border: none;
  color: var(--prestia-fg);
  text-decoration: none;
  font-size: 1.3rem;
  padding: .25rem;
  cursor: pointer;
}

/* Page content */
.page-content {
  flex: 1;
  min-width: 0;
  padding: 1.75rem 2rem;
}
/* A wide table (many columns) scrolls inside its own card instead of widening the page. */
.card { overflow-x: auto; }

/* Frontend variant (agent-facing, pladecof feel): readable max width. Both surfaces share the
   base page background; cards/panels use the elevated --prestia-surface so they always stand out. */
.layout-wrapper--front .page-content > .container { max-width: 1100px; margin-inline: auto; }

/* Backoffice footer */
.backoffice-footer {
  padding: .75rem 1.5rem;
  font-size: .8rem;
  color: var(--prestia-muted);
  border-top: 1px solid var(--prestia-border);
  text-align: center;
}

/* Impersonation bar. It sits above the top bar, on every page of the session, and it is not
   dismissible: what it prevents is an admin who forgot they were somebody else, and a notice that can be
   closed is a notice that will be. The control that ends it is drawn as an outline on the bar's own
   colour rather than as a .btn, because a .btn here would take the page's surface tokens onto a surface
   the page does not have - which is the drift the surface-scoped ink check reads for. */
.impersonation-bar {
  display: flex; align-items: center; flex-wrap: wrap; gap: .5rem;
  padding: .55rem 1.5rem; font-size: .88rem; line-height: 1.4;
  color: var(--prestia-on-impersonation); background: var(--prestia-impersonation);
}
.impersonation-bar__stop {
  margin-left: auto;
  border: 1px solid var(--prestia-on-impersonation);
  background: var(--prestia-impersonation); color: var(--prestia-on-impersonation);
  cursor: pointer; font-size: .82rem; font-weight: 600;
  padding: .25rem .7rem; border-radius: 6px;
}
/* The hover cue is the border and not a fill: a second fill would be a second surface, and a surface
   that carries text owes a declared ink pair of its own. */
.impersonation-bar__stop:hover { text-decoration: underline; }
@media (max-width: 640px) {
  .impersonation-bar { padding: .5rem 1rem; font-size: .82rem; }
  .impersonation-bar__stop { margin-left: 0; }
}

/* Sidebar overlay on mobile */
.sidebar-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,.4);
  z-index: 499;
}

/* Responsive */
@media (max-width: 768px) {
  .topbar-title { display: block; }
  /* Off canvas is HIDDEN, not merely moved. A drawer that is only translated off screen is still in
     the tab order and still in the accessibility tree: forty links a keyboard reaches and nobody can
     see. `visibility` removes it from both at once, it is the stylesheet saying it because this is
     where the viewport is known, and it needs no script - which is what the attribute that used to say
     it did need. It is transitioned as a STEP with a delay and not over a duration - see the rules
     below, where the timing is a keyboard question rather than a cosmetic one.

     WHAT OPENS IT IS `:target`, AND THAT IS THE ONLY THING THAT OPENS IT. The hamburger is a link to
     `#sidebar`, the overlay and the close control are links back to `#main`, and the open state is
     therefore the URL - which the browser maintains with no script at all. A class set by app.js used
     to be the switch, so with scripting off the menu could not be reached on a phone by any means. Two
     switches would be worse than one: the scripted path and the unscripted one would disagree, and the
     unscripted one is the one nobody is looking at. AND ONLY A NAVIGATION MOVES `:target`:
     `history.pushState` and `history.replaceState` write the URL without navigating, so a drawer
     "opened" that way reads `location.hash === "#sidebar"` and stays `visibility: hidden`. Whatever
     moves this state from script has to be a navigation - see the sidebar section of app.js. */
  .sidebar {
    transform: translateX(-100%);
    visibility: hidden;
    /* WHEN visibility FLIPS IS A KEYBOARD QUESTION, NOT A COSMETIC ONE. Transitioned as a duration,
       `visibility` reads `hidden` at progress 0 - the exact instant `hashchange` fires - and `focus()`
       on a hidden element is a silent no-op, so the drawer opened with the keyboard left on <body> and
       a screen reader on nothing while the rest of the page went inert. It is a step, not a fade: here
       it flips at the END of the slide, so the drawer stays on screen while it slides out. */
    transition: transform .25s ease, visibility 0s linear .25s;
  }
  .sidebar:target {
    transform: translateX(0);
    visibility: visible;
    /* and at the START of the slide in, so the drawer is focusable the moment it is open. */
    transition: transform .25s ease, visibility 0s linear 0s;
  }
  .sidebar:target ~ .sidebar-overlay {
    display: block;
  }
  .sidebar-close {
    display: inline-flex;
  }
  .main-wrapper {
    margin-left: 0;
  }
  .hamburger {
    display: inline-flex;
  }
}

/* Flat data tables: clean rows, uppercase header, subtle row hover. Not a card. */
table { border-collapse: collapse; width: 100%; margin: 1rem 0; font-size: .9rem; }
th, td { text-align: left; padding: 0.55rem 0.8rem; border-bottom: 1px solid var(--prestia-border); vertical-align: middle; }
th { color: var(--prestia-muted); font-size: 0.78rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; border-bottom: 2px solid var(--prestia-border); }
tbody tr:hover { background: var(--prestia-surface); }
/* Action column: a tight, right-aligned cell of icon buttons (the .row-actions wrapper). */
td:has(.row-actions) { width: 1%; white-space: nowrap; text-align: right; }

/* A table is not reflowed at a narrow width, it SCROLLS INSIDE ITS OWN REGION. Laying the cells out as
   stacked blocks would take `display: table-*` off them, and with it the header association that
   `scope` and `<caption>` exist to give - the reflowed table looks fine and stops being a table for
   anyone reading it through the accessibility tree. WCAG 1.4.10 exempts content that needs two
   dimensions to be understood, and a data table is the example the criterion gives; what it does not
   exempt is being able to find and reach the part that is off screen, so the region is in the tab order
   (2.1.1), is named by the table's own caption, and shows a shadow on whichever edge still has content
   behind it. The shadow is the pure-CSS pair of a solid `local` cover over a fixed `scroll` gradient:
   the cover rides with the content and uncovers the shadow only while there is more to scroll to. */
.table-scroll {
  overflow-x: auto;
  background-color: var(--prestia-surface);
  border-radius: 8px;
  background-image:
    linear-gradient(to right, var(--prestia-surface), rgba(0,0,0,0)),
    linear-gradient(to left, var(--prestia-surface), rgba(0,0,0,0)),
    linear-gradient(to right, var(--prestia-scroll-shadow), rgba(0,0,0,0)),
    linear-gradient(to left, var(--prestia-scroll-shadow), rgba(0,0,0,0));
  background-position: left center, right center, left center, right center;
  background-size: 2.5rem 100%, 2.5rem 100%, .75rem 100%, .75rem 100%;
  background-repeat: no-repeat;
  background-attachment: local, local, scroll, scroll;
}
.table-scroll > table { margin-top: 0; }

/* A RECORD'S FIELDS ARE A DESCRIPTION LIST AND NOT A TABLE. Written as a two-column <table> they had
   no column headers to declare - the whole table is a column of names and a column of values - and
   inside a card that scrolls at a narrow width they left the part past the edge reachable by mouse and
   by nothing else. A <dl> reflows, associates each value with its own term without any wiring, and
   needs no scroll region at all. Each pair is wrapped in a <div>, which HTML allows and which keeps the
   list semantics that laying the <dl> itself out as a grid would take away. */
.data-list { margin: 0; }
.data-list > div { display: flex; gap: .9rem; padding: .4rem 0; border-bottom: 1px solid var(--prestia-border); }
.data-list > div:last-child { border-bottom: none; }
.data-list dt { font-weight: 600; flex: 0 0 9rem; }
.data-list dd { margin: 0; min-width: 0; flex: 1 1 auto; }
/* The caption is the table's heading and its accessible name; it is read, not decoration. */
caption { text-align: left; font-size: .85rem; font-weight: 600; color: var(--prestia-fg); padding: .5rem .8rem; }
caption .muted { font-weight: 400; }

form { margin: 1rem 0; }
label { display: block; margin: 0.85rem 0 0.3rem; font-size: .9rem; font-weight: 500; }
input, select, textarea { padding: 0.5rem 0.65rem; font: inherit; color: var(--prestia-fg); background: var(--prestia-bg); border: 1px solid var(--prestia-border); border-radius: 6px; }
/* A CONTROL DOES NOT TURN OFF ITS OWN FOCUS RING. `input:focus` outranks `:focus-visible` on
   specificity, so `outline: none` here used to delete the indicator for every control on the product
   and leave a 15%-alpha glow as the whole of it. The border change is the extra cue; the ring is the
   two-tone one above, and a check now fails on any rule that turns it off. */
input:focus, select:focus, textarea:focus { border-color: var(--prestia-accent); }
button {
  padding: 0.5rem 0.9rem;
  font: inherit;
  font-weight: 500;
  color: var(--prestia-on-primary);
  background: var(--prestia-primary);
  border: 0;
  border-radius: 6px;
  cursor: pointer;
}
button:hover { background: var(--prestia-primary-hover); color: var(--prestia-on-primary-hover); }
button + button { margin-left: .5rem; }

/* List controls: search filters, bulk-action bar, export bar, pager - all consistent toolbars. */

.pager { display: flex; align-items: center; justify-content: space-between; gap: .75rem; margin: 1rem 0; flex-wrap: wrap; }
/* The pager's own controls (components.Pagination): a range and the two ends, because a keyset page is
   named by a boundary row and there is no page number to link to. The range reads as a note, on tabular
   figures so the row does not jiggle as the totals widen. A disabled end is not a link: it goes
   nowhere, so it is not focusable - a control that answers nothing is a dead click (STANDARDS.md
   section 17). */
.pager__range { color: var(--prestia-muted); font-size: .85rem; }
.pager__nav { display: flex; gap: .35rem; align-items: center; flex-wrap: wrap; }
.pager__btn {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 2rem; height: 2rem; padding: 0 .5rem;
  border: 1px solid var(--prestia-border); border-radius: 6px;
  background: var(--prestia-surface); color: var(--prestia-fg);
  font-size: .85rem; text-decoration: none; line-height: 1;
  font-variant-numeric: tabular-nums; transition: background .12s, border-color .12s, color .12s;
}
/* The hover ink is the link token and not --prestia-primary: in the dark theme that token is a mid
   blue that lands near 3.4:1 on the surface it is drawn on, while --prestia-link is measured as ink
   on both surfaces in both themes. */
.pager__btn:hover { border-color: var(--prestia-accent); color: var(--prestia-link); }
.pager__btn.is-disabled { opacity: .4; pointer-events: none; }

/* Empty state: what a section says when it read successfully and there is nothing there. Distinct from
   the unread notice (.alert--warning), which is what it says when the read did not answer at all. */
.empty-state { color: var(--prestia-muted); font-style: italic; padding: .75rem 0; margin: 0; }
.btn[aria-disabled="true"], .btn:disabled { opacity: .45; pointer-events: none; cursor: default; }

/* Theme toggle button */
.theme-toggle {
  background: none;
  border: 1px solid var(--prestia-border);
  color: var(--prestia-fg);
  padding: 0.3rem 0.6rem;
  font-size: 0.85rem;
  border-radius: 4px;
  cursor: pointer;
}
.theme-toggle:hover { background: var(--prestia-surface); color: var(--prestia-fg); }

/* Buttons - a complete style so anchors and <button>s look identical (an .btn anchor must
   read as a button, never a bare link). Variants override colour. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
  padding: .5rem .9rem; font: inherit; font-size: .9rem; font-weight: 500; line-height: 1.2;
  color: var(--prestia-on-primary); background: var(--prestia-primary); border: 1px solid transparent; border-radius: 6px;
  cursor: pointer; text-decoration: none; white-space: nowrap;
}
.btn:hover { background: var(--prestia-primary-hover); color: var(--prestia-on-primary-hover); }
.btn--primary { background: var(--prestia-accent); border-color: var(--prestia-accent); color: var(--prestia-on-accent); }
.btn--primary:hover { background: var(--prestia-accent-hover); border-color: var(--prestia-accent-hover); color: var(--prestia-on-accent-hover); }
.btn--ghost { background: none; color: var(--prestia-fg); border: 1px solid var(--prestia-border); }
.btn--ghost:hover { background: var(--prestia-surface); color: var(--prestia-fg); }
.btn--sm { padding: .3rem .6rem; font-size: .82rem; }

.btn--secondary { background: var(--prestia-accent); border-color: var(--prestia-accent); color: var(--prestia-on-accent); }
.btn--secondary:hover { background: var(--prestia-accent-hover); border-color: var(--prestia-accent-hover); color: var(--prestia-on-accent-hover); }
.btn--danger { background: var(--prestia-error); border-color: var(--prestia-error); color: var(--prestia-on-error); }
.btn--danger:hover { background: var(--prestia-error-hover); border-color: var(--prestia-error-hover); color: var(--prestia-on-error-hover); }

/* Card */
.card {
  background: var(--prestia-surface);
  border: 1px solid var(--prestia-border);
  border-radius: 10px;
  padding: 1.25rem 1.5rem;
  margin-bottom: 1rem;
  box-shadow: 0 1px 3px rgba(0,0,0,.06);
}

/* The [hidden] attribute must win over component display rules (e.g. .form-field{display:flex}). */
[hidden] { display: none !important; }

/* Pre-auth pages (login, password recovery/change, MFA, constancia verify): a single centered
   card on an otherwise empty viewport. Reuses the .card surface tokens so it tracks light/dark. */
.auth { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 2rem 1rem; background: var(--prestia-bg); }
.auth__card { width: 100%; max-width: 400px; background: var(--prestia-surface); border: 1px solid var(--prestia-border); border-radius: 10px; box-shadow: 0 1px 3px rgba(0,0,0,.06); padding: 2rem; }
.auth__logo { display: block; height: 3rem; width: auto; margin: 0 auto 1.25rem; }
.auth__title { margin: 0 0 .25rem; text-align: center; font-size: 1.35rem; font-weight: 600; color: var(--prestia-fg); }
.auth__subtitle { margin: 0 0 1.25rem; text-align: center; font-size: .9rem; color: var(--prestia-muted); }
.auth__form { display: flex; flex-direction: column; gap: 1rem; margin: 0; }
.auth__form .field { display: flex; flex-direction: column; gap: .35rem; }
.auth__form .field__label { font-weight: 600; font-size: .85rem; color: var(--prestia-fg); }
.auth__form .field__input { width: 100%; }
.auth__form .field--check { flex-direction: row; align-items: center; gap: .5rem; font-size: .9rem; }
.auth__form .field--check input[type="checkbox"] { width: auto; }
.auth__form .btn--block { margin-top: .25rem; }
.auth__divider { border: none; border-top: 1px solid var(--prestia-border); margin: 1.5rem 0; }
.auth__foot, .auth__alt { margin: 1.25rem 0 0; text-align: center; font-size: .85rem; }

/* Tablero list card - color accent from CSS variable set via style attr */
/* THE FLOOR IS min(Npx, 100%) AND NOT Npx, in this grid and in the four below it. `minmax(240px, 1fr)`
   says a column is never narrower than 240px, which is a promise the grid keeps by overflowing its
   container when the container is narrower than that - at 320px the content column is about 256px wide
   and a 260px or 280px floor is simply wider than the page. Wrapping the floor in `min()` keeps it as
   the breakpoint that decides how many columns fit and drops it to the full width when one column no
   longer fits, which is the reflow the fixed value refused. */
.card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(240px, 100%), 1fr)); gap: 1rem; margin-bottom: 1rem; }
.card-grid .card { border-top: 4px solid var(--card-color, var(--prestia-accent)); }
/* Dashboard quick-access tile: an icon beside a title and description, the whole tile a link. */
.dash-card { display: flex; gap: .75rem; align-items: flex-start; text-decoration: none; color: inherit; margin-bottom: 0; transition: border-color .12s, transform .12s, box-shadow .12s; }
.dash-card:hover { border-color: var(--prestia-accent, var(--prestia-link)); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,.08); }
.dash-card__icon { font-size: 1.5rem; line-height: 1; color: var(--prestia-accent, var(--prestia-link)); flex: none; }
.dash-card strong { display: block; }
.dash-card p { margin: .15rem 0 0; }

/* Breadcrumb trail (detail/form pages reached from a list). Clickable text + chevron separators. The
   list markup is the component's: a trail is an ordered list of places, and an <ol> is what says so to
   anything that is not looking at the screen. */
.breadcrumb { display: flex; align-items: center; flex-wrap: wrap; gap: .35rem; margin-bottom: .85rem; font-size: .85rem; list-style: none; padding-left: 0; }
.breadcrumb li { display: inline-flex; align-items: center; gap: .35rem; }
.breadcrumb__item { color: var(--prestia-link); text-decoration: none; }
.breadcrumb__item:hover { text-decoration: underline; }
.breadcrumb__item--current { color: var(--prestia-muted); font-weight: 500; }
.breadcrumb__sep { font-size: .7rem; color: var(--prestia-muted); }

/* List/detail page header: title on the left, primary action(s) pushed right (eppsc parity). */
.page-header { display: flex; align-items: center; justify-content: space-between; gap: .75rem; margin-bottom: 1rem; flex-wrap: wrap; }
.page-header h1 { margin: 0; font-size: 1.3rem; }
/* THE ACTIONS WRAP, AND THAT IS WHAT KEEPS THE PAGE FROM SCROLLING SIDEWAYS AT 320px. A flex row that
   does not wrap contributes the SUM of its items' min-content widths to its parent, so it cannot be
   squeezed below that sum by anything: `.page-header` wrapping this block onto a line of its own does
   not help, because the line is still only as wide as the page. Two header buttons measured 377px
   inside a 288px page column and pushed the document to 393px against a 320px viewport - the last
   page-level horizontal scroll left after `body { overflow-x: hidden }` was removed, and one that
   clipping had been hiding rather than fixing (WCAG 1.4.10, failure F102). Wrapping drops the
   contribution to the LARGEST single item instead of the sum, which is a button, and buttons fit.
   This is shell markup, so it holds for every screen that has header actions and not only for the one
   that happened to overflow first. */
.page-header__actions { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.page-header__titles { display: flex; flex-direction: column; gap: .15rem; }
.page-header__eyebrow { font-size: .68rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--prestia-muted); }

/* Badges and tags */
.badge {
  display: inline-block;
  padding: .15em .55em;
  font-size: .8rem;
  font-weight: 600;
  border-radius: 10px;
  white-space: nowrap;
}
/* Each tone is a solid tint token with its own foreground, so the pair is measurable and a badge
   reads the same on a card as on the page. */
.badge--ok { background: var(--prestia-success-tint); color: var(--prestia-on-success-tint); }
.badge--warn { background: var(--prestia-warn-tint); color: var(--prestia-on-warn-tint); }
.badge--bad { background: var(--prestia-error-tint); color: var(--prestia-on-error-tint); }
.badge--alert { background: var(--prestia-error); color: var(--prestia-on-error); }
.badge--muted { background: var(--prestia-border); color: var(--prestia-on-border); }
/* Inline remove control inside a badge (e.g. an Área chip on the user detail page). */
.badge .inline-form { display: inline; }

/* Checkbox set (e.g. a Unidad's Áreas). */

.check-item { display: flex; align-items: center; gap: .45rem; font-size: .9rem; }

/* Alerts: a bordered, tinted message block (success / error / warning / info). Used for flash
   messages and inline banners. The left accent + soft tint read on both light and dark themes.
   Each variant states the ink it always inherited, so the pair tint/ink is declared and measured;
   the tone itself is carried by the icon, which is what makes the block scannable. */
.alert {
  display: flex; align-items: flex-start; gap: .55rem;
  border: 1px solid var(--prestia-border); border-left: 3px solid var(--prestia-muted);
  border-radius: 8px; padding: .75rem 1rem; margin-bottom: 1.25rem;
  font-size: .9rem; line-height: 1.45; background: var(--prestia-surface);
}
.alert i { margin-top: .1rem; flex-shrink: 0; }
.alert--success { border-left-color: var(--prestia-success); background: var(--prestia-success-tint); color: var(--prestia-fg); }
.alert--success i { color: var(--prestia-on-success-tint); }
.alert--error { border-left-color: var(--prestia-error); background: var(--prestia-error-tint); color: var(--prestia-fg); }
.alert--error i { color: var(--prestia-on-error-tint); }
.alert--warning { border-left-color: var(--prestia-warn); background: var(--prestia-warn-tint); color: var(--prestia-fg); }
.alert--warning i { color: var(--prestia-on-warn-tint); }

/* Busy state. The counter's Admit button wears it while its admission is in flight: HTMX puts
   .htmx-request on the form it is submitting, and that is the one control in the product where being
   able to press it twice authorizes the same visit twice (STANDARDS.md §17). It is the same spinner
   .is-busy draws, declared once for both. */
.is-busy, .mostrador-form.htmx-request .btn[type="submit"] { position: relative; pointer-events: none; }
.is-busy::after, .mostrador-form.htmx-request .btn[type="submit"]::after {
  content: "";
  display: inline-block;
  width: .85rem;
  height: .85rem;
  /* currentColor, not white: the spinner is drawn in the ink of the control it sits on, and that ink
     is a different colour on a dark-theme accent button. */
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: spin .6s linear infinite;
  margin-left: .4rem;
  vertical-align: middle;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* Neutralise the browser's yellow autofill so it doesn't clash with the white card. */

.checkbox-label { display: flex; align-items: center; gap: .4rem; font-size: .85rem; font-weight: 400; margin: 0; }
.checkbox-label input[type="checkbox"] { width: auto; }


/* Iconized row actions (view / edit / delete) - uniform across every list */
.row-actions { display: inline-flex; flex-wrap: wrap; align-items: center; gap: .5rem; }
.row-actions .inline-form > .form-field { flex: 1 1 100%; min-width: 0; }
/* An iconized control (components.ConfirmButton in icon form). Two of these are a third of the width of
   two labelled buttons, which is what keeps a row's actions on screen on a phone. The icon inside is
   decorative - the control's name is on the control - so the ink only has to read as a shape, but the
   hover ink is still the measured link token rather than --prestia-primary, for the reason above. */
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2rem; height: 2rem; padding: 0;
  border: 1px solid var(--prestia-border); border-radius: 6px;
  background: var(--prestia-bg); color: var(--prestia-fg);
  cursor: pointer; text-decoration: none; font-size: 1rem; line-height: 1;
  transition: background .15s, color .15s, border-color .15s;
}
.icon-btn:hover { background: var(--prestia-surface); border-color: var(--prestia-accent); color: var(--prestia-link); }
.icon-btn--primary { border-color: var(--prestia-accent); color: var(--prestia-link); }
.icon-btn--primary:hover { background: var(--prestia-accent); border-color: var(--prestia-accent); color: var(--prestia-on-accent); }
/* Destructive before it is hovered, not only while: colour is not the only cue - the control carries a
   name and a confirmation - but a delete that looks like every other button is a trap. */
.icon-btn--danger { color: var(--prestia-error); }
.icon-btn--danger:hover { background: var(--prestia-error); border-color: var(--prestia-error); color: var(--prestia-on-error); }

/* Explicit form fields: label above its control, comfortable vertical rhythm (netspo style). */
.form-field { display: flex; flex-direction: column; gap: .35rem; margin-bottom: 1rem; }
.form-field > label { font-weight: 600; font-size: .85rem; color: var(--prestia-fg); }
.form-field > input:not([type=checkbox]):not([type=radio]):not([type=color]),
.form-field > select, .form-field > textarea { width: 100%; }
/* A FIELD LAID OUT BY A CONTAINER TAKES THE CONTAINER'S RHYTHM AND NOT ITS OWN. The margin below a
   field is what separates two fields STACKED in a form; inside a flex row it is added to the row's own
   gap, so a `.form-row` that wraps drew 1rem of gap between its lines and 2rem under them, and an
   `.inline-form` had its bar pushed off the line it shares with its button. Both containers already say
   how much space they put between their children. */
.inline-form .form-field, .form-row > .form-field { margin-bottom: 0; }
.form-field__hint { font-size: .8rem; color: var(--prestia-muted); }
/* The error of a field, and the control it belongs to. Both are drawn by components.FormField, which
   also wires them to the control with aria-describedby and aria-invalid: the red border is the cue for
   whoever sees the field, the wiring is the same cue for whoever hears it. The invalid state is written
   for every control the component renders, not only <input>, because a select and a textarea fail
   validation exactly as often. */
.form-field__error { font-size: .8rem; color: var(--prestia-error); }
.form-field > input.is-invalid, .form-field > select.is-invalid, .form-field > textarea.is-invalid { border-color: var(--prestia-error); }
.required-star { color: var(--prestia-error); margin-left: .15rem; font-weight: 700; }

/* Rendered (user-built) form fields: help text, choice groups and decorative blocks. */

.form-checkbox-group { display: flex; flex-direction: column; gap: .35rem; }

/* In real (non-inline) forms, fields stack under their label - cleaner than label-beside-field,
   and fixes textareas that rendered next to their label. Inline filter/search forms keep rows. */

textarea { min-height: 7rem; resize: vertical; width: 100%; max-width: 460px; }

/* The page's status region (view/action.templ): always in the document, empty until an action has
   something to say, so that a swap can fill it and a screen reader can announce the arrival. Empty it
   takes no room at all: the region is hidden while it has nothing to say, the same way the health
   dashboard hides its own, and the alert inside supplies the spacing when there is a sentence. */
.action-status .alert { margin-bottom: 1rem; }
.action-status .form-field__hint { display: block; margin-top: .25rem; }

/* A list of alerts (coding observations): the items carry .alert, so the list only stacks them. */
.alert-list { list-style: none; margin: .5rem 0 0; padding: 0; display: flex; flex-direction: column; gap: .5rem; }
.alert-list .alert { margin-bottom: 0; }

/* A numeric column: right-aligned, on tabular figures so the digits line up down the column. */
th.table__num, td.table__num { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }

/* An audit case that has just been decided: the row stays where it was, tinted as settled. */
tr.caso--resuelto { background: var(--prestia-success-tint); color: var(--prestia-fg); }

/* One entry of an internment's evolution log: a dated note, separated from the next. */
.evolucion-entry { padding: .6rem 0; border-bottom: 1px solid var(--prestia-border); }
.evolucion-entry:last-child { border-bottom: none; }

/* Raw text kept as written (an ingest report, a stack of counts) without escaping the card's width. */
.pre-wrap { white-space: pre-wrap; word-break: break-word; margin: 0; font-size: .82rem; }

/* The scroll region around each of the health page's two tables, spaced off the heading that names it.
   These are the only tables in the product that are not inside a card, so they are the only ones that
   carry their own region (view/health.templ). */

/* Branded error / not-found page: centered card with the status code. */
.error-page { min-height: 70vh; display: flex; align-items: center; justify-content: center; padding: 2rem; text-align: center; }
.error-card { max-width: 460px; }
.error-card__code { font-size: 4rem; font-weight: 800; color: var(--prestia-accent); line-height: 1; margin-bottom: .25rem; }
.error-card h1 { font-size: 1.5rem; margin: 0 0 .5rem; }
.error-card p { color: var(--prestia-muted); margin: .35rem 0; }
.error-card .btn { margin-top: 1.25rem; }

/* Card head/foot and dense tables (user management, audit and the health cards use them). */

/* Section heading row inside a card: title on the left, action on the right (e.g. "Ver salud en vivo"). */
/* Wraps, because the row is a heading plus a button and the button carries `white-space: nowrap`:
   inside a card at 320px the two do not fit side by side, and a flex row that cannot wrap widens its
   container instead. */
.card__head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; padding-bottom: .6rem; margin-bottom: .9rem; border-bottom: 1px solid var(--prestia-border); }
.card__head h2 { margin: 0; font-size: .95rem; }

/* A table that shows many short rows fits more of them in the same card. */
.table--compact th, .table--compact td { padding: .3rem .5rem; font-size: .82rem; }

/* Utilities - no inline styles */

.form-row { display: flex; gap: 1rem; flex-wrap: wrap; }
.form-row > .field, .form-row > .form-field { flex: 1 1 12rem; min-width: 0; }
/* On a narrow viewport multi-column form rows and inline field groups stack to one column so nothing
   pushes the page wider than the screen, and the chrome gives back the room it takes on a wide one:
   `.page-content` plus `.card` spend 7rem of horizontal padding, which is a third of a 320px viewport
   spent on nothing. Reflow (WCAG 1.4.10) is measured at that width, so the padding is part of it. */
@media (max-width: 640px) {
  .form-row { flex-direction: column; gap: .75rem; }
  .data-list > div { flex-direction: column; gap: .1rem; }
  .data-list dt { flex: 0 0 auto; }
  .form-row > .field { flex: 1 1 auto; }
  .inline-form { flex-wrap: wrap; }
  .inline-form .field__input { flex: 1 1 100%; min-width: 0; }
  .inline-form .form-field { flex: 1 1 100%; min-width: 0; }
  .page-content { padding: 1.25rem 1rem; }
  .card { padding: 1rem; }
  .health-card { padding: 1rem; }
  .topbar { padding: .6rem 1rem; }
  .container { padding: 1.5rem 1rem; }
  .docs { padding: 1.25rem 1rem 3rem; }
}

/* System health dashboard (Administración -> Salud del sistema). Live-refreshed via SSE. */
.health-timestamp { color: var(--prestia-muted); font-size: .85rem; }
.health-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(260px, 100%), 1fr)); gap: 1rem; margin-top: 1rem; }
.health-card { background: var(--prestia-surface); border: 1px solid var(--prestia-border); border-radius: 8px; padding: 1.1rem 1.25rem; box-shadow: 0 1px 3px rgba(0,0,0,.08); display: flex; flex-direction: column; min-width: 0; }
.health-card--wide { grid-column: 1 / -1; }
/* The two tables of this page are the only ones not inside a card, so the space a card would have put
   between the heading and the table is put here instead. */
.health-card--wide .table-scroll { margin-top: .75rem; }
.health-card h2 { font-size: .9rem; text-transform: uppercase; letter-spacing: .04em; color: var(--prestia-muted); margin: 0 0 .75rem; }
/* An `auto` track and an `fr` track are both floored by the min-content of what they hold, and what
   these hold is machine text - a version, a path, an identifier - which has no place to break and so
   sets a floor as wide as the longest one. `minmax(0, ...)` takes that floor off both, and the value
   is then allowed to break anywhere rather than push the card past the screen. */
.health-card dl { display: grid; grid-template-columns: minmax(0, auto) minmax(0, 1fr); gap: .3rem .75rem; margin: 0; }
.health-card dt { color: var(--prestia-muted); font-size: .85rem; overflow-wrap: anywhere; }
.health-card dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; font-weight: 500; overflow-wrap: anywhere; }
/* Per-core CPU bars + generic progress bar */
.cpu-cores { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(220px, 100%), 1fr)); gap: .35rem .9rem; margin-top: .75rem; }
.cpu-core { display: flex; align-items: center; gap: .5rem; font-size: .8rem; font-variant-numeric: tabular-nums; }
.cpu-core__label { color: var(--prestia-muted); min-width: 3.2em; }
.cpu-core__track, .health-bar { flex: 1; height: 6px; background: var(--prestia-border); border-radius: 3px; overflow: hidden; }
.cpu-core__fill, .health-bar__fill { display: block; height: 100%; background: var(--prestia-primary); transition: width .4s ease; }
.cpu-core__val { min-width: 3em; text-align: right; color: var(--prestia-muted); }
.health-bar { display: block; margin-top: .6rem; }
.health-deps__title { display: inline; }
.health-services { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(min(280px, 100%), 1fr)); gap: .5rem .75rem; }
.health-service { display: flex; align-items: center; gap: .5rem; }
.health-service-name { flex: 1; }
.health-service-state { font-weight: 600; font-size: .85rem; }
.health-service-detail { color: var(--prestia-muted); font-size: .8rem; }
.status-dot { width: .65rem; height: .65rem; border-radius: 50%; flex-shrink: 0; display: inline-block; background: var(--prestia-muted); }
.status-dot--ok { background: var(--prestia-success); }
.status-dot--bad { background: var(--prestia-error); }

/* Usage-analytics dashboard (Administración -> Analítica). */
.stat-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(180px, 100%), 1fr)); gap: 1rem; margin: 1rem 0; }
.stat-tile { background: var(--prestia-surface); border: 1px solid var(--prestia-border); border-radius: 8px; padding: 1.1rem 1.25rem; }
.stat-tile__value { font-size: 1.8rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.stat-tile__label { color: var(--prestia-muted); font-size: .85rem; margin-top: .25rem; }
.abars { display: flex; flex-direction: column; gap: .4rem; }
.abar { display: grid; grid-template-columns: minmax(0, 16rem) 1fr auto; align-items: center; gap: .75rem; font-size: .85rem; }
.abar__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.abar__track { height: 8px; background: var(--prestia-border); border-radius: 4px; overflow: hidden; }
.abar__fill { display: block; height: 100%; background: var(--prestia-primary); }
.abar__val { font-variant-numeric: tabular-nums; color: var(--prestia-muted); min-width: 3em; text-align: right; }

/* User manual (/docs): a public long-form page - table of contents beside the section being read.
   Public chrome (no sidebar), so it carries its own header and its own reading column. */
.docs { max-width: 1080px; margin: 0 auto; padding: 1.5rem 1.25rem 4rem; }
.docs__head { display: flex; align-items: center; gap: 1rem; padding-bottom: 1rem; border-bottom: 1px solid var(--prestia-border); }
.docs__logo { height: 40px; width: auto; }
.docs__headings { flex: 1; min-width: 0; }
.docs__eyebrow { margin: 0; font-size: .78rem; text-transform: uppercase; letter-spacing: .06em; color: var(--prestia-muted); }
.docs__title { margin: .15rem 0 0; font-size: 1.5rem; font-weight: 700; }
.docs__exit { font-size: .9rem; white-space: nowrap; }
.docs__body { display: grid; grid-template-columns: minmax(0, 14rem) minmax(0, 1fr); gap: 2rem; margin-top: 1.5rem; }
.docs__toc { align-self: start; position: sticky; top: 1.5rem; background: var(--prestia-surface); border: 1px solid var(--prestia-border); border-radius: 8px; padding: 1rem 1.1rem; }
.docs__toc-title { margin: 0 0 .5rem; font-weight: 600; font-size: .85rem; text-transform: uppercase; letter-spacing: .04em; color: var(--prestia-muted); }
.docs__toc ol { margin: 0; padding-left: 1.2rem; display: flex; flex-direction: column; gap: .35rem; font-size: .9rem; }
.docs__toc a { text-decoration: none; }
.docs__toc a:hover { text-decoration: underline; }
.docs__toc a[aria-current="page"] { font-weight: 600; color: var(--prestia-fg); }
.docs__content { line-height: 1.65; max-width: 44rem; }
.docs__content h2 { font-size: 1.1rem; font-weight: 700; margin: 2rem 0 .5rem; }
.docs__content p { margin: 0 0 1rem; }
.docs__content ul, .docs__content ol { margin: 0 0 1rem; padding-left: 1.4rem; display: flex; flex-direction: column; gap: .5rem; }
.docs__lead { font-size: 1.05rem; color: var(--prestia-muted); }
/* A numbered procedure in the manual: one step per line, breathing more than a plain list. */
.docs__steps { display: flex; flex-direction: column; gap: .6rem; margin: 0 0 1rem; padding-left: 1.4rem; }
/* A screenshot fills the width of its figure; the light/dark pair is swapped by the rules below. */
.docs__shot { width: 100%; height: auto; display: block; }
.docs__figure { margin: 0 0 1.5rem; }
.docs__figure img { max-width: 100%; height: auto; border: 1px solid var(--prestia-border); border-radius: 8px; display: block; }
.docs__figure figcaption { margin-top: .4rem; font-size: .85rem; color: var(--prestia-muted); }
/* A figure carries both themes and only the matching one is shown, with no script involved. The
   selectors mirror the token mapping above: data-theme drives the explicit choice (app.js writes the
   effective value there), and the media query covers the system preference before app.js runs and
   when JS is off - which is exactly when :root has no data-theme yet. Every rule below is scoped to
   .docs__figure so that it outranks the `.docs__figure img` box rule above: a bare .docs__shot--dark
   loses to it on specificity, and the light theme would then stack both screenshots. */
.docs__figure .docs__shot--dark { display: none; }
:root[data-theme="dark"] .docs__figure .docs__shot--light { display: none; }
:root[data-theme="dark"] .docs__figure .docs__shot--dark { display: block; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .docs__figure .docs__shot--light { display: none; }
  :root:not([data-theme]) .docs__figure .docs__shot--dark { display: block; }
}
@media (max-width: 800px) {
  .docs__body { grid-template-columns: 1fr; }
  .docs__toc { position: static; }
}
