/*
 * The web layer's stylesheet: a hand port of the V4 redesign's globals.css and
 * of the style values its screens set inline, from kirlts/traza-ambiental-mvp
 * at commit e883297.
 *
 * Why hand-written CSS and not Tailwind. The V4 screens were written with
 * Tailwind v4 in a Next.js build, but almost everything they draw comes from
 * `style={{...}}` literals, not from utility classes: the login screen uses
 * Tailwind for hover and focus states and little else. Porting those literals
 * into named classes here reproduces the design exactly without adding a
 * Node toolchain to a repository that has none, and without the Tailwind Play
 * CDN, which compiles styles in the visitor's browser at runtime and is not
 * meant for production. The cost is that a new screen ported from V4 has its
 * inline values turned into classes by hand, in this file.
 *
 * What was deliberately left behind from globals.css:
 *   - the shadcn/ui token set (chart, sidebar, popover, the oklch greys) and
 *     the `.dark` theme: no ported screen uses them yet, and V4 itself shipped
 *     its dark mode commented out;
 *   - the demo-mode animations, which belong to a screen not ported, and the
 *     landing page's two: the typing effect cycled through the six priority
 *     products the knowledge base rules out (templates/landing.html says
 *     why), and the floating logo is motion with nothing to say, kept as a
 *     still glow instead;
 *   - the `!important` war on input colours. V4 needed it to override
 *     Tailwind's preflight; here nothing competes, so plain rules suffice.
 *
 * Fonts: Geist and Geist Mono, self-hosted from static/fonts/ since November
 * 2026 (the @font-face rules below), when the landing page's «1b» redesign
 * asked for them. V4 loaded Geist through next/font, which self-hosts it at
 * build time; until then nothing loaded it here, and the stack fell through
 * to Arial, V4's own declared fallback. The two tokens `--font-sans` and
 * `--font-mono` have always named Geist first, so the faces apply to every
 * page, not the landing alone: a second family name for the landing would
 * have kept the rest of the app on the fallback V4 never meant to show.
 * Loading them from Google Fonts, as the design did, was rejected because
 * every page view would hand the visitor's IP address to a third party, a
 * data-protection exposure this project has not decided to take on. The
 * files are the variable fonts of the `geist` npm package 1.9.2 (one file
 * per family covers every weight the pages use), under the SIL Open Font
 * License, whose text is static/fonts/OFL-Geist.txt beside them.
 */

/* `swap`: the text shows at once in the fallback and changes face when the
   file arrives, rather than staying invisible for up to three seconds. */
@font-face {
  font-family: "Geist";
  src: url("../fonts/Geist-Variable.f208f1ceee9d.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Geist Mono";
  src: url("../fonts/GeistMono-Variable.6aaa5f578bc1.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

:root {
  /* V4's emerald brand scale, as its screens use it inline. */
  --brand-900: #064e3b;
  --brand-700: #047857;
  --brand-600: #059669;
  --brand-500: #10b981;
  --brand-200: #a7f3d0;
  --brand-100: #d1fae5;
  --brand-50: #ecfdf5;

  --text-strong: #111827;
  --text: #374151;
  --text-muted: #4b5563;
  --text-subtle: #6b7280;
  /* Only for an input's placeholder and for text on the dark footer. On a
     white or `--surface-muted` background it measures about 2.5:1, under the
     4.5:1 WCAG 2.1 AA asks of body text, so it is not a colour for anything
     a reader needs: labels, hints, empty states and secondary lines use
     `--text-subtle`, which measures about 4.8:1 on white and 4.6:1 on
     `--surface-muted`. V4 used this grey for those too; the port kept it
     until the UI audit of October 2026 moved every such rule off it. */
  --placeholder: #9ca3af;

  /* The danger button's red: Tailwind's red-700, a red V4's palette already
     carries, 6.5:1 for white text on it; its hover is red-800 (8.3:1). */
  --danger-700: #b91c1c;
  --danger-800: #991b1b;

  /* The flash messages' tones beyond the success green, each a background,
     a border and a text colour that measures at least 6.5:1 on its background
     (warning, the lowest, 6.8:1). */
  --info-bg: #eff6ff;
  --info-border: #bfdbfe;
  --info-text: #1e40af;
  --warning-bg: #fffbeb;
  --warning-border: #fde68a;
  --warning-text: #92400e;
  --error-border: #fca5a5;

  --surface: #ffffff;
  --surface-muted: #f9fafb;
  --border: #e5e7eb;

  /* Named by role, not by value, so a rule says what a colour is for and a
     later change to one role does not drag the others with it. Two pairs
     share a value today on purpose: `--surface-subtle` and `--divider` are
     both V4's gray-100, one as a pill's or a hover's fill and one as the
     hairline between rows; `--brand-800` and `--success-text` are both
     emerald-800, one as brand ink (a link's hover, a code chip) and one as
     the text of a confirmation. Before October 2026 each was a hex literal
     repeated across the file. What stays literal on purpose: the semáforo's
     and the carrier leg's colour sets (`.status-*`, `.leg-*`), one set per
     state, each written once beside the comment that says why it is that
     colour; the login's gradient, decoration used once; the black of a
     shadow at an alpha, which
     is depth rather than a colour of the palette; and white drawn on a fill
     -- the text of a filled button or of the carrier's current leg filter,
     and the light landing button's focus ring on the green gradient.
     config/tests/test_frame.py pins that list: a literal anywhere else
     fails it. */
  --brand-800: #065f46;
  --surface-subtle: #f3f4f6;
  --surface-hover: #f0fdf4;
  --divider: #f3f4f6;
  --border-strong: #d1d5db;
  --success-bg: #eef9f2;
  --success-text: var(--brand-800);

  --error-bg: #fef2f2;
  --error-text: #991b1b;
  /* The refusal notice's darker red, for a paragraph read on the error
     background rather than a single line. */
  --error-strong: #7f1d1d;

  /* The last literals that were neither here nor on the exempt list above,
     named in November 2026. Each is named for the one role it plays, so a
     later change to it does not drag another rule that shares its value
     today: several do, and that is a coincidence of the palette, not a
     link. */
  /* The text typed into a field: V4's slate-900, a shade darker than
     `--text-strong`, which V4's globals.css sets on every input and on the
     browser's autofill. */
  --input-ink: #0f172a;
  /* The Registro de Movimiento's own red: the folio box and the scan card's
     head, Tailwind's red-600. It is the document's mark, not a state, so it
     follows neither the error tones (`--error-*`) nor the danger button
     (`--danger-700`). */
  --doc-red: #dc2626;
  /* The pill of a refused load still waiting for an alternative destination
     (`.rejection-mark-pending`): a fill a step stronger than `--error-bg`,
     so it reads as a badge beside the semáforo and not as a notice. */
  --pending-refusal-bg: #fee2e2;
  /* A scan's confirmation (`.scan-done`), load or unload alike. Lime, the
     same values as the semáforo's «Entregado» today, but a confirmed load
     is not a delivered one, so the two are not one token. */
  --scan-done-text: #4d7c0f;
  --scan-done-bg: #ecfccb;
  /* The «Formato provisional» pill (`.draft-badge`). Amber, the same values
     as the semáforo's «En tránsito» today, but a provisional format is not
     a load on the road. */
  --draft-text: #b45309;
  --draft-bg: #fef3c7;
  /* The landing page's own greens (the «1b» redesign of November 2026),
     beside the brand scale above rather than in it, because no other page
     uses them: the darkest green of its strip, phone and footer; the light
     green of its accents, ring and chosen pill; Category B's green on the
     ruler of rims; and three translucent greens -- the phone's glow, the
     footer's rule and an unchosen pill's edge -- each named for the one
     thing it draws. */
  --brand-950: #022c22;
  --brand-300: #6ee7b7;
  --rim-b: #16a34a;
  --landing-glow: rgba(110, 231, 183, 0.25);
  --landing-rule-dark: rgba(167, 243, 208, 0.2);
  --landing-pill-border: rgba(167, 243, 208, 0.35);
  /* A chosen pill's fill: the light accent at a tenth, so the choice reads
     as a lit outline and the light fill stays the button's alone. */
  --landing-pill-chosen: rgba(110, 231, 183, 0.12);
  /* A field's error on the landing's dark green (`.landing-form
     .field-error`): red-200, over 7:1 on `--brand-900`, where the app's
     `--error-text` would measure under 2:1. */
  --error-on-dark: #fecaca;
  /* The maps' markers: the dark edge every marker is drawn with, and the
     fills of the lettered carrier and plant markers. The edge is read by
     static/js/heat_map.js too, for the waste points, which Leaflet draws as
     SVG paths whose stroke it sets itself, out of a stylesheet rule's reach. */
  --marker-edge: #1f2937;
  --marker-carrier: #1d4ed8;
  --marker-plant: #6d28d9;

  --radius: 0.625rem;
  --font-sans: "Geist", Arial, Helvetica, sans-serif;
  --font-mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
}

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

body {
  margin: 0;
  font-family: var(--font-sans);
  line-height: 1.6;
  color: var(--text-strong);
  background: var(--surface-muted);
  -webkit-font-smoothing: antialiased;
}

/* The links of the content: the navigation's green ink, so a link in a
   card or a note reads as part of the same system as the header's. Until
   November 2026 there was no rule for `a`, and every link without a class
   of its own («Publica una necesidad», the branch names and «Modo
   privado» on the master's home page, «Indicadores de la red» on «Red»)
   came in the browser's blue, and purple once visited, beside a green
   navigation. Underlined, as the browser draws it, so a link inside a
   sentence is told apart by more than its colour (WCAG 1.4.1); the
   navigation, the buttons and the list rows remove the underline with
   classes of their own. One colour whether visited or not: these are an
   application's links, revisited on every working day, so «visited» says
   nothing worth a colour. The hover is wrapped in `:where()` to keep the
   specificity of a bare `a`: a plain `a:hover` would outrank every
   one-class rule and turn a primary button's white label green. */
a {
  color: var(--brand-700);
  text-underline-offset: 2px;
}

a:where(:hover) {
  color: var(--brand-800);
}

/* Form inputs: V4's globals.css guarantees dark, legible input text on every
   input, including browser autofill, which otherwise paints its own colours. */
input,
textarea,
select {
  font: inherit;
  color: var(--input-ink);
}

input::placeholder,
textarea::placeholder {
  color: var(--placeholder);
  opacity: 1;
}

input:-webkit-autofill {
  -webkit-text-fill-color: var(--input-ink);
  -webkit-box-shadow: 0 0 0 1000px var(--surface-muted) inset;
}

/* «Saltar al contenido» (base.html): off the page until a keyboard focuses
   it, then over the header's top-left corner, where the Tab key's first
   stop is expected. Moved rather than `.visually-hidden`, which a focused
   element would keep clipped to one pixel. */
.skip-link {
  position: absolute;
  top: 8px;
  left: 8px;
  z-index: 100;
  padding: 8px 12px;
  font-weight: 600;
  color: var(--brand-700);
  background: var(--surface);
  border-radius: 8px;
  transform: translateY(-200%);
}

.skip-link:focus {
  transform: none;
  outline: 2px solid var(--brand-700);
  outline-offset: 2px;
}

/* `main` is focused only by the skip link (it is `tabindex="-1"`), and the
   region it would ring is the whole page: the link already said where the
   focus went. */
.app-main:focus {
  outline: none;
}

/* The full pattern: the 1px box with no margin, padding or border, so it
   takes no room; `clip-path` beside the deprecated `clip`. Being absolute,
   it is placed against its nearest positioned ancestor and escapes the clip
   of any `overflow` box between the two: see `.table-scroll`. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------------------------------------------------------------------------
   App shell: the header of V4's role sidebars (logo, wordmark, 64px band with
   an emerald hairline), laid out as a top bar. V4's sidebars are fixed 256px
   columns whose content area has a 944px minimum width, which does not fit a
   phone; a top bar does, and the transporter works from a phone. The sidebar's
   navigation is reduced to a row of links, `.app-nav`: up to thirteen of them,
   depending on the branch's company's profiles (templates/base.html says which
   and why), followed by the session's scope and the account's buttons.

   All of that after the brand sits in `.app-menu`, a native <details>. Mobile
   first: up to 1024px it is a «Menú» button whose panel drops below the band
   as one column, so nothing scrolls sideways and no label is cut. From
   1025px, where the browser can style `::details-content`, the summary is
   hidden and the panel is kept shown while the element is closed, laid out
   as a row beside the brand. A browser that cannot style it keeps the menu
   at every width: a click more on a desktop, rather than a row it could not
   show. The cut is the header's own: the other `max-width: 720px` queries
   in this file are for the tables, the landing page and the forms, and
   stay where they are.

   Why 1024px. The cut used to be 720px, the one this file uses for its
   grids, and from 721px the row was shown and wrapped. Measured in headless
   Chrome on 2026-10-04, the band then stood, at 721, 768, 820 and 1024px:
     - a carrier's branch:                     167, 167, 167, 136px;
     - a branch holding every profile:         167, 167, 167, 167px;
     - a master inside such a branch:          211, 211, 211, 167px;
     - a sistema de gestión's branch:          136, 136, 136, 151px;
     - a generator's branch:                   167, 136, 136, 145px.
   On a tablet held upright that is a sixth to a quarter of the screen
   before any content, on every page, and «Cerrar sesión» could end up
   alone on a third line. With the menu up to 1024px the band is 64px at
   every one of those widths, for every one of those sessions, and the
   page still never scrolls sideways. Keeping the cut at 720px was rejected
   for those heights. From 1025px to 1440px the same sessions get a band of
   106 to 167px, two to four lines counting the brand's, which a laptop's
   wider and taller screen absorbs where a tablet's does not.

   The open panel's width. On a phone (up to 720px) it is a column as wide
   as the band. From 721px it is a 360px dropdown hanging from the band's
   right edge, under «Menú», with the same height cap and the same
   scrolling inside it. 360px is the narrowest round width that keeps every
   fixed label on one line: the longest is the scope's «como cuenta
   delegada · Prevencionista de riesgos», 292px at 13px (measured in
   headless Chrome on 2026-10-04), and the panel takes 16px of padding on
   each side and a 1px border, leaving 327px; a browser whose scrollbars
   take room still leaves about 312px. Only a branch name longer than that
   wraps, as it does on a phone; the links and buttons are far narrower
   («Notas operativas», the widest link, is 121px). Measured the same day
   with the menu open at 721x600, 768x1024, 820x1180, 1024x768 and
   1024x1366, for all five sessions above, on «Inicio», «Ubicación» and
   «Mapa»: the panel is 360px wide against the window's right edge, no
   control or scope text is cut, the page never scrolls sideways, and
   every link and button, hit-tested at its centre, is the element there,
   over both maps. A browser that cannot style `::details-content` keeps
   this dropdown above 1024px too, since the rule is not inside the
   desktop row's guard; the row's own rules give the width back.
   Rejected: the phone's full-width column on a tablet as well. At 1024px
   it is a 1,024px sheet holding labels of at most 292px, more than two
   thirds of it empty, laid over the whole page -- on «Mapa», the whole
   map -- for a list a finger reaches just as well at 360px; a dropdown
   from the button that opened it also reads as that button's menu.

   The menu closes only by tapping «Menú» again, or by following one
   of its links, which loads another page. Escape and a tap outside the panel
   leave it open: that is how a native <details> disclosure behaves, and
   making it close on either would take a script, which this frame does not
   load (see the last rejected alternative below). It is the WAI-ARIA
   Authoring Practices' disclosure pattern, which asks for no Escape
   handling, and it meets WCAG: the control is keyboard-operable (Enter and
   Space toggle it), named by its text and exposes its expanded state, and
   the panel is not a modal that traps focus or hides the page from a
   screen reader. This is a choice, not an oversight; adding a script for it
   would be a decision to load JavaScript on every page.

   The open panel is positioned (z-index 10) and lies over the page, which
   on two pages holds a Leaflet map whose panes reach z-index 400 and whose
   controls reach 1000. The maps are isolated (`.leaflet-container`, with the
   heat map, below) so all of Leaflet's layers paint as one layer at the
   page's level, under the panel, rather than the panel's z-index being
   raised to outbid Leaflet's numbers.

   Rejected for the phone:
     - scrolling `.app-nav` sideways inside the band: beside the wordmark
       a phone shows only a few of up to thirteen links, with no cue that
       more exist, and the scope and the buttons still need the band's width;
     - wrapping the row onto several lines: the row needs about 1,500px for
       a carrier's branch, so at phone width it becomes a header of several
       lines on every page, pushing down everything the page is for (wider
       screens do wrap it, below, where it takes two to four lines);
     - a script-driven toggle: the frame loads no script, and <details> is
       already a keyboard-operable button that a screen reader announces with
       its expanded state, with nothing to wire up.

   Above the cut the row wraps instead of overflowing. Measured in headless
   Chrome on 2026-10-04, one line holding the whole wordmark and every label
   untruncated needs about 1,470px for a sistema de gestión's branch, 1,560px
   for a generator's, 1,680px for a carrier's, 2,060px for a branch holding
   every profile and 2,230px for a master administrator inside one (who also
   gets «Salir de la sucursal»); the figure also grows with the branch's
   name. Before the row wrapped it never took that room: the brand was
   allowed to shrink to nothing, so the links ran over the wordmark, the
   scope was cut with an ellipsis, and still the page scrolled sideways --
   to 1,246px for a carrier's branch, 1,578px for every profile and 1,734px
   for the master -- at any window narrower than that, a 768-820px tablet
   included. Now:
     - the brand never shrinks below the logo and the wordmark, so nothing
       paints over it;
     - when the brand and the row do not fit on one line, the row moves to
       a line of its own under the brand, still hugging the right edge as
       the single row does;
     - when the row alone is wider than the window, it wraps again inside,
       the links first and the scope and the buttons after them, and the
       links themselves wrap as a last resort;
     - the band keeps its 64px when everything fits on one line, so a
       header that fits -- a session outside any branch, or a branch on a
       wide enough screen -- looks exactly as before, control for control.
   The cost is a taller band, of two to four lines counting the brand's, on
   most laptops for a session inside a branch: the row did not fit them
   before either, it was only hidden under the wordmark and the ellipsis.
   The account's four buttons -- «Mapa», «Catastro», «Seguridad» and
   «Cerrar sesión» -- are one group (`.app-account`) that does not wrap
   inside, so when the row runs out of room they move to the next line
   together and «Cerrar sesión» is never left alone on a line of its own,
   as it was at 1,280px for a generator's and a sistema de gestión's branch
   and at 1,440px for a carrier's. In the menu's column the group is
   invisible: four more rows.
   Rejected for the desktop:
     - raising the cut past 1024px, so the menu covers more widths: no width
       covers every branch, since the row's length depends on the company's
       profiles, the session's role and the branch's name, so wrapping is
       needed as the fallback anyway; and a cut wide enough for the
       master's 2,230px would hide the navigation behind a click on every
       common desktop, where two lines show all of it;
     - truncating or scrolling the links sideways: the same objections as on
       the phone, a link a user cannot see is a link they do not know of.
   ------------------------------------------------------------------------- */

.app-header {
  /* The menu's panel is positioned against the band, below it. */
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  height: 64px;
  padding: 0 16px;
  background: var(--surface);
  border-bottom: 1px solid var(--brand-100);
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}

.brand {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  flex: 1;
  text-decoration: none;
}

.brand img {
  width: 44px;
  height: 44px;
  object-fit: contain;
  flex-shrink: 0;
}

.brand span {
  font-weight: 700;
  font-size: 17px;
  color: var(--brand-900);
  letter-spacing: -0.01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.app-header form {
  margin: 0;
  flex-shrink: 0;
}

/* The narrow layout, which is also every width's where `::details-content`
   cannot be styled. The summary drops its disclosure triangle: the word
   «Menú» is its label, and `.btn-quiet` already makes it look pressable. */
.app-menu {
  flex-shrink: 0;
}

.app-menu > summary {
  list-style: none;
}

.app-menu > summary::-webkit-details-marker {
  display: none;
}

/* One column below the band, as wide as the band on a phone (from 721px it
   narrows to 360px, below), scrolling down within the viewport when every
   profile's links do not fit the screen's height. Each label wraps rather
   than being cut: the scope's ellipsis is for the desktop row. */
.app-menu-panel {
  position: absolute;
  top: 100%;
  right: 0;
  left: 0;
  /* Over the page's own content. A map stays under it because every
     Leaflet map is isolated (`.leaflet-container`), not because 10 outbids
     Leaflet's z-indices, which it does not. */
  z-index: 10;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 12px;
  max-height: calc(100vh - 64px);
  /* A phone's browser bar shrinks the viewport `vh` ignores; `dvh` follows
     it where known, and the line above stands where it is not. */
  max-height: calc(100dvh - 64px);
  overflow-y: auto;
  padding: 16px;
  background: var(--surface);
  border-bottom: 1px solid var(--brand-100);
  box-shadow: 0 12px 16px -8px rgba(0, 0, 0, 0.12);
}

.app-menu-panel .app-nav {
  flex-direction: column;
  gap: 0;
}

/* 44px rows, a finger's reach, for the links a phone taps. */
.app-menu-panel .app-nav a {
  padding: 11px 0;
  font-size: 15px;
}

.app-menu-panel .session-scope {
  text-align: left;
}

.app-menu-panel .session-scope strong,
.app-menu-panel .session-scope span {
  white-space: normal;
  overflow-wrap: anywhere;
}

.app-menu-panel .btn,
.app-menu-panel form {
  width: 100%;
}

/* The account's four buttons. In the menu's column they are four more rows,
   spaced as the panel spaces its rows; the group only matters on the desktop
   row, below. */
.app-account {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* From 721px the open panel is a 360px dropdown hanging from the band's
   right edge, under «Menú», instead of a column across the whole window
   (the header comment gives the measurements and the rejected
   alternative). Its height cap, its own scrolling and its z-index are the
   phone's, above. The query is not inside the desktop row's guard, so a
   browser that keeps the menu at every width gets the same dropdown on a
   desktop too; where the row is shown, the row's rules below undo the
   width. The left border and the softer, wider shadow mark the panel's
   left edge, which on a phone was the window's. */
@media (min-width: 721px) {
  .app-menu-panel {
    left: auto;
    width: 360px;
    border-left: 1px solid var(--brand-100);
    border-bottom-left-radius: 8px;
    box-shadow: 0 12px 24px -8px rgba(0, 0, 0, 0.16);
  }
}

/* The desktop row. Closed, a <details> renders its content with
   `content-visibility: hidden` on `::details-content`; overriding that is
   what keeps the panel shown, and only a browser that knows the
   pseudo-element can be asked to, hence the `@supports` guard. The wrapping
   rules sit inside the same guard because only there is the row shown; a
   browser without it shows the brand and «Menú», which always fit. */
@media (min-width: 1025px) {
  @supports selector(::details-content) {
    /* Wraps instead of overflowing (the header comment says why). The band
       grows past 64px only when it wraps: 8px above and below plus the
       hairline leave 47px, so a single line is centred exactly where the
       fixed 64px band centred it. */
    .app-header {
      flex-wrap: wrap;
      row-gap: 8px;
      height: auto;
      min-height: 64px;
      padding-top: 8px;
      padding-bottom: 8px;
    }

    /* Never narrower than the logo and the wordmark: the shrinking that let
       the links run over them is what this undoes. It still grows, so on a
       line that fits it pushes the row to the right edge, as before. */
    .brand {
      flex: 1 0 auto;
    }

    /* `margin-left: auto` keeps the row at the right edge when it has moved
       to its own line under the brand; `min-width: 0` lets it shrink to
       that line's width and wrap inside, rather than overflow it. */
    .app-menu {
      flex: 0 1 auto;
      min-width: 0;
      margin-left: auto;
    }

    .app-menu > summary {
      display: none;
    }

    .app-menu::details-content {
      content-visibility: visible;
    }

    /* `width: auto` undoes the tablet dropdown's 360px, which would
       otherwise squeeze the whole row into it; `border: 0` drops its left
       border, and with no border, background or shadow its corner radius
       paints nothing. */
    .app-menu-panel {
      position: static;
      width: auto;
      flex-direction: row;
      flex-wrap: wrap;
      justify-content: flex-end;
      align-items: center;
      gap: 8px 12px;
      max-height: none;
      overflow: visible;
      padding: 0;
      background: none;
      border: 0;
      box-shadow: none;
    }

    .app-menu-panel .app-nav {
      flex-direction: row;
      flex-wrap: wrap;
      justify-content: flex-end;
      gap: 8px 16px;
    }

    .app-menu-panel .app-nav a {
      padding: 0;
      font-size: 14px;
    }

    .app-menu-panel .session-scope {
      text-align: right;
    }

    .app-menu-panel .session-scope strong,
    .app-menu-panel .session-scope span {
      white-space: nowrap;
    }

    .app-menu-panel .btn,
    .app-menu-panel form {
      width: auto;
    }

    /* One unit that never wraps inside, so when the row runs out of room
       the four buttons move to the next line together, and «Cerrar sesión»
       is never left alone on a line of its own. It is 373px wide, far
       less than any window this layout applies to. */
    .app-menu-panel .app-account {
      flex-direction: row;
      align-items: center;
    }
  }
}

.app-main {
  width: 100%;
  max-width: 1024px;
  margin: 0 auto;
  padding: 24px 16px;
}

/* ---------------------------------------------------------------------------
   Card and buttons.
   ------------------------------------------------------------------------- */

.card {
  background: var(--surface);
  border-radius: 16px;
  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.12);
  padding: 32px 40px;
}

/* One size for every filled button: 14px bold. It used to be 16px semibold
   here and 14px bold on `.btn-inline`, which nearly every primary wore, so
   the few that did not (the home page's two «Entrar», «Buscar» on
   «Catastro», «Aplicar» on «Mapa») stood out larger than any other
   button on the platform. The size moved here rather than onto those four
   buttons, so a new button cannot miss it; the sign-in and second-factor
   buttons follow it too. `.btn-quiet` keeps its own, smaller size; the
   carrier's scan button keeps its own, larger one (`.scan-button`, a single
   action at arm's length). No underline: a button that is a link is still
   a button, and the base rule for `a` would otherwise underline it. */
.btn {
  display: inline-flex;
  justify-content: center;
  align-items: center;
  gap: 8px;
  padding: 12px 16px;
  border: 0;
  border-radius: 8px;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  text-decoration: none;
  cursor: pointer;
  transition: background 150ms;
}

/* A button is as wide as its label; `.btn-block` stretches it across its
   container. V4's login made the primary button full width, and the port
   made that the primary's default, so nearly every other primary on the
   platform had to undo it with `.btn-inline`. Of those that did not,
   «Aplicar» on «Mapa» and «Buscar» on «Catastro» took a whole line of
   their wrapping filter row and pushed «Quitar filtros» onto the next
   (measured in headless Chrome at 1280px); the home page's two «Entrar»,
   in the master's branch table and in the list of other branches, now
   take their label's width too, rather than their table cell's or form's.
   Full width is now asked for where the design wants it: the sign-in and
   second-factor forms and the carrier's scan button, each a single action
   at a phone's width. */
.btn-block {
  width: 100%;
}

.btn-primary {
  color: #ffffff;
  background: var(--brand-600);
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}

.btn-primary:hover {
  background: var(--brand-700);
}

/* The header's buttons: the sidebar's secondary action style, small enough
   that the phone's «Menú» shares a 64px band with the wordmark. Its weight
   is its own because `.btn`'s bold is a filled button's: three quiet
   buttons that also wore `.btn-inline` used to come out 14px bold beside
   some forty at 13px. Until November 2026 its `text-decoration` sat in a
   second `.btn-quiet` block a thousand lines further down; `.btn` now
   carries it. */
.btn-quiet {
  padding: 7px 12px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  background: transparent;
  border: 1px solid var(--border-strong);
}

.btn-quiet:hover {
  background: var(--surface-hover);
}

/* The one action that cannot be taken back -- withdrawing a published need,
   cancelling an accepted offer -- on the confirmation page that asks for it.
   Before October 2026 it was `.btn-primary`, the same green as «Guardar», so
   the button that destroys read exactly like the one that saves. Kept to
   irreversible actions on a confirmation page: a refusal or a rejection that
   can still be corrected keeps the primary green, or red would stop meaning
   "this is final". */
.btn-danger {
  color: #ffffff;
  background: var(--danger-700);
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}

.btn-danger:hover {
  background: var(--danger-800);
}

/* Doubled class: `.btn:focus-visible`, further down, has the same
   specificity and would otherwise win with the green ring. */
.btn.btn-danger:focus-visible {
  outline-color: var(--danger-700);
}

/* Every keyboard focus ring in the frame is a 2px `outline` in
   `--brand-700` (5.48:1 on white), 2px clear of the control. It used to be a
   2px `box-shadow` in `--brand-500`, 2.54:1 on white and 1.49:1 against a
   primary button's own green, so a focused primary read as a slightly
   thicker green button. An outline also survives Windows' high-contrast mode
   (`forced-colors`), which drops every `box-shadow`: under the old rings no
   button, login field or document field showed its focus there.
   config/tests/test_frame.py pins that no rule takes an outline away
   without one of these in its place. */
.btn:focus-visible {
  outline: 2px solid var(--brand-700);
  outline-offset: 2px;
}

/* ---------------------------------------------------------------------------
   Login screen: src/app/login/page.tsx without the demo-user panel, the Google
   button and the /register and /demo links, none of which has a flow behind
   it in this repository.
   ------------------------------------------------------------------------- */

.auth-page {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 48px 16px;
  background: linear-gradient(to bottom right, #f0fdf4, #ffffff, #eff6ff);
}

.auth-head {
  width: 100%;
  max-width: 448px;
  text-align: center;
}

.auth-head img {
  width: 72px;
  height: 72px;
  object-fit: contain;
  filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.12));
}

.auth-head .wordmark {
  display: block;
  margin-top: 8px;
  font-weight: 700;
  font-size: 28px;
  color: var(--brand-900);
  letter-spacing: -0.02em;
}

.auth-head h1 {
  margin: 16px 0 0;
  font-size: 30px;
  font-weight: 700;
  color: var(--text-strong);
}

.auth-head p {
  margin: 8px 0 0;
  font-size: 16px;
  font-weight: 500;
  color: var(--text);
}

.auth-body {
  width: 100%;
  max-width: 448px;
  margin-top: 32px;
}

.auth-form {
  display: flex;
  flex-direction: column;
  gap: 24px;
}

.field label {
  display: block;
  margin-bottom: 8px;
  font-size: 14px;
  font-weight: 600;
  color: var(--text-strong);
}

.field-control {
  position: relative;
}

/* The icon draws in `currentColor` (login.html), so its grey is the
   placeholder's token here rather than a `stroke` literal in the markup. */
.field-control svg {
  position: absolute;
  color: var(--placeholder);
  top: 50%;
  left: 12px;
  transform: translateY(-50%);
  pointer-events: none;
}

.field-control input {
  width: 100%;
  padding: 12px;
  font-size: 14px;
  background: var(--surface-muted);
  border: 0;
  border-radius: 8px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}

/* The 40px on the left is the room the icon takes: 12px in, 20px wide and
   8px of air. Only an input after an icon reserves it. Until November 2026
   every `.field-control` input did, and the second factor's code fields and
   the enrollment's password, which have no icon, began their text 40px in
   from an empty gap. */
.field-control svg + input {
  padding-left: 40px;
}

.field-control input:focus {
  outline: 2px solid var(--brand-700);
  outline-offset: 2px;
  background: var(--surface);
}

.alert-error {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 16px;
  border-radius: 8px;
  font-size: 14px;
  font-weight: 500;
  color: var(--error-text);
  background: var(--error-bg);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}

.alert-error svg {
  flex-shrink: 0;
}

/* V4's cards keep 40px of side padding; on a phone that leaves the form too
   narrow, so it tightens below the width of a small tablet. */
@media (max-width: 480px) {
  .card {
    padding: 24px 20px;
  }

  .auth-page {
    padding-top: 32px;
  }

  .auth-head h1 {
    font-size: 26px;
  }
}

/* ---------------------------------------------------------------------------
   Public landing page (templates/landing.html): the «1b» option of the
   November 2026 redesign, its inline values as classes. Deep green above
   and below (the hero, the strip, the closing section and the footer),
   white between them.

   **Every grid track is `minmax(0, 1fr)` or a `minmax(min(100%, …), 1fr)`,
   never a bare `1fr`.** A bare `1fr` is `minmax(auto, 1fr)`: the track may
   not shrink below its content's min-content width. The V4 layout's first
   cut used it, and on a phone its one-column panel grew to the width of the
   figures inside it, so the page was 393px wide at a 320, 360 and 390px
   viewport and scrolled sideways. The `min(100%, …)` minimums are the
   design's own `auto-fit` columns, which fall to one column on a phone
   without a media query; the phone rules at the end then shrink what
   cannot wrap.

   The colours are `:root`'s tokens, the landing's own among them
   (`--brand-950`, `--brand-300`, `--rim-b`, the translucent greens), so no
   rule here writes a literal (config/tests/test_frame.py).
   ------------------------------------------------------------------------- */

.landing {
  min-height: 100vh;
  color: var(--text-strong);
  background: var(--surface);
}

.landing-wrap {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 clamp(16px, 4.5vw, 56px);
}

.landing-mono {
  font-family: var(--font-mono);
}

/* The header sits on the hero's green and scrolls away with it: the design
   draws no sticky bar, and with three section links and no button there is
   nothing a reader needs to keep on screen. */
.landing-header {
  padding-top: 24px;
  color: var(--brand-50);
  background: var(--brand-900);
}

.landing-header-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
}

.landing-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 19px;
  font-weight: 700;
  color: #ffffff;
}

.landing-brand img {
  width: 36px;
  height: 36px;
  object-fit: contain;
}

.landing-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 22px;
  font-size: 14px;
  font-weight: 600;
}

/* Underlined only on hover: these are a menu of sections, set apart as a
   row inside a `<nav>`, not links inside a sentence (WCAG 1.4.1 asks for
   more than colour only there). */
.landing-nav a,
.landing-footer-nav a {
  text-decoration: none;
}

.landing-nav a {
  color: var(--brand-200);
}

.landing-nav a:hover,
.landing-footer-nav a:hover {
  color: #ffffff;
  text-decoration: underline;
}

/* «Ir al inicio», the one link out of the page, drawn apart from the
   section links as the design's light pill would be. */
.landing-nav .landing-nav-home {
  color: var(--brand-950);
  padding: 0 10px;
  background: var(--brand-300);
  border-radius: 9999px;
}

/* On the landing's greens, the light green ring, not the app's dark one:
   `--brand-700` on `--brand-900` measures under 2:1, `--brand-300` over
   7:1. Every control of the closing section sits on that green, the email
   field and the button included: their rings are drawn outside them, on
   the section's ground. The error summary and the thank-you take the focus
   as they load, so they ring on `:focus` (`.error-summary` says why). */
.landing-nav a:focus-visible,
.landing-footer-nav a:focus-visible,
.landing-cta:focus-visible,
.landing-pill:has(input:focus-visible),
.landing-consent input:focus-visible,
.landing-email input:focus-visible,
.landing-submit:focus-visible,
.landing-form .error-summary:focus {
  outline: 2px solid var(--brand-300);
  outline-offset: 2px;
}

/* Focused only to be read, never to be acted on: `main` by the skip link,
   as `.app-main` is, and the thank-you as it loads (templates/landing.html
   says why). A ring round either reads as a field, or as the whole page,
   and tells a sighted reader nothing the scroll did not. */
.landing-main:focus,
.landing-sent:focus {
  outline: none;
}

.landing-hero {
  overflow: hidden;
  color: var(--brand-100);
  background: var(--brand-900);
}

.landing-hero-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
  gap: 48px;
  align-items: end;
  padding-top: 72px;
}

.landing-hero-copy {
  padding-bottom: 80px;
}

/* Side by side, the copy takes the wider share: in two equal halves
   «Trazabilidad de» did not fit one line at 88px, and «de» stood alone
   whatever the wrap balanced. The phone needs only its 300px. */
@media (min-width: 1024px) {
  .landing-hero-grid {
    grid-template-columns: minmax(0, 1.4fr) minmax(min(100%, 300px), 0.6fr);
  }
}

/* Balanced, here and in every landing heading: the copy is fixed, so a word
   stranded on a line of its own («de» at 1280px, «A y B» under the
   categories' title) is fixed by the wrap, never by rewording. Paragraphs
   take `pretty`, which keeps a last line from holding one word. */
.landing h1,
.landing h2,
.landing h3 {
  text-wrap: balance;
}

.landing p,
.landing li,
.landing dd {
  text-wrap: pretty;
}

.landing-hero h1 {
  margin: 0;
  font-size: clamp(44px, 7vw, 88px);
  font-weight: 700;
  line-height: 0.98;
  letter-spacing: -0.04em;
  color: #ffffff;
}

.landing-hero h1 span {
  color: var(--brand-300);
}

.landing-lead {
  max-width: 520px;
  margin: 28px 0 0;
  font-size: 20px;
  color: var(--brand-100);
}

/* The way down to the lead form (templates/landing.html says why its words
   are the button's). A link, underlined and in the light green, not a
   second filled button: the page keeps one button, the form's, and this
   points at it. The arrow is drawn at the label's size and moves with it on
   hover, the one motion on the page. */
.landing-cta {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin-top: 36px;
  font-size: 21px;
  font-weight: 700;
  color: var(--brand-300);
  text-underline-offset: 5px;
}

.landing-cta svg {
  flex-shrink: 0;
  transition: transform 200ms cubic-bezier(0.16, 1, 0.3, 1);
}

.landing-cta:hover {
  color: #ffffff;
}

.landing-cta:hover svg {
  transform: translateX(4px);
}

@media (prefers-reduced-motion: reduce) {
  .landing-cta svg {
    transition: none;
  }
}

/* On the panel's pale green the link takes the panel's own deep ink. */
.landing-cta-panel {
  grid-column: 1 / -1;
  justify-self: start;
  margin-top: 8px;
  color: var(--brand-800);
}

.landing-cta-panel:hover {
  color: var(--brand-900);
}

.landing-cta-panel:focus-visible {
  outline-color: var(--brand-700);
}

/* The phone (aria-hidden; the template says what it pictures). */
.landing-phone {
  justify-self: center;
  width: 300px;
  max-width: 100%;
  height: 400px;
  padding: 12px 12px 0;
  background: var(--brand-950);
  border-radius: 40px 40px 0 0;
  box-shadow: 0 -10px 60px var(--landing-glow);
}

.landing-phone-screen {
  height: 100%;
  padding: 40px 16px 16px;
  font-size: 13px;
  line-height: 1.6;
  color: var(--text-strong);
  background: var(--surface-muted);
  border-radius: 30px 30px 0 0;
}

.landing-phone-card {
  padding: 16px;
  background: var(--surface);
  border-radius: 12px;
  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.08);
}

.landing-phone-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-bottom: 10px;
  border-bottom: 2px solid var(--brand-900);
}

.landing-phone-head .status {
  padding: 2px 8px;
  font-size: 11px;
}

.landing-phone-folio {
  font-family: var(--font-mono);
  font-weight: 700;
  color: var(--doc-red);
}

.landing-phone-fields {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 10px;
  margin-top: 10px;
}

.landing-phone-fields > div > span:first-child {
  display: block;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-subtle);
}

.landing-phone-done {
  margin-top: 12px;
  padding: 12px 14px;
  font-weight: 600;
  color: var(--scan-done-text);
  background: var(--scan-done-bg);
  border-radius: 12px;
}

.landing-phone-done .landing-mono {
  display: block;
  font-weight: 400;
}

/* The scan action, drawn as a tint: on the real screen it is a filled
   Deep Leaf button, but here it is a picture, and filled it read as the
   first screen's call to action while doing nothing. */
.landing-phone-action {
  margin-top: 12px;
  padding: 14px;
  font-size: 16px;
  font-weight: 700;
  text-align: center;
  color: var(--text-muted);
  background: var(--surface-subtle);
  border: 1px dashed var(--border-strong);
  border-radius: 12px;
}

/* In the body's sans, each figure a label over its value: set in 13px mono
   the strip read as terminal output and dressed labels up as figures. The
   chain's «G → T → G» keeps the mono, as the one code-like mark on it. */
.landing-strip {
  padding: 28px 0;
  font-size: 14px;
  color: var(--brand-200);
  background: var(--brand-950);
}

.landing-strip-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 16px;
}

.landing-chain {
  margin: 0;
  align-self: center;
}

.landing-chain strong {
  font-family: var(--font-mono);
}

.landing-chain strong,
.landing-figures dt {
  color: #ffffff;
}

.landing-figures {
  display: flex;
  flex-wrap: wrap;
  gap: 12px 32px;
  margin: 0;
}

.landing-figures div {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.landing-figures dt {
  font-size: 16px;
  font-weight: 700;
}

.landing-figures dd {
  margin: 0;
}

.landing-section {
  padding: 96px 0;
}

.landing-section-tight {
  padding-top: 0;
}

.landing-section h2 {
  margin: 0;
  max-width: 760px;
  font-size: 44px;
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.03em;
}

.landing-section-lead {
  max-width: 720px;
  margin: 16px 0 0;
  font-size: 18px;
  color: var(--text-muted);
}

/* One column, then the title beside the rows from 1024px. Explicit tracks
   rather than the design's `auto-fit`: between 1024 and about 1140px those
   fitted two tracks, the rows (which spanned two) fell under the title,
   and the title's row held an empty column beside it. */
.landing-features {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 40px 56px;
}

@media (min-width: 1024px) {
  .landing-features {
    grid-template-columns: minmax(0, 1fr) minmax(0, 2.2fr);
  }
}

/* Sticky beside the rows while they scroll, so the section's title holds
   the column that otherwise stood empty for some 1300px. Only where the
   head and the rows sit side by side, from 1024px: stacked, a sticky head
   would ride over the rows under it. */
.landing-features-head {
  max-width: 340px;
}

@media (min-width: 1024px) {
  .landing-features-head {
    position: sticky;
    top: 32px;
    align-self: start;
  }
}

.landing-features-head p {
  margin: 16px 0 0;
  color: var(--text-muted);
}

.landing-rows {
  min-width: 0;
  margin: 0;
  padding: 0;
  list-style: none;
  border-bottom: 1px solid var(--border);
}

.landing-row {
  display: grid;
  grid-template-columns: 64px minmax(0, 1fr);
  gap: 20px;
  padding: 28px 0;
  border-top: 1px solid var(--border);
}

/* `--brand-700`, 5.48:1 on white: `--brand-600` measured 3.76:1, which
   passes as large text at 28px but not at the phone's 20px. */
.landing-row-num {
  font-family: var(--font-mono);
  font-size: 28px;
  font-weight: 500;
  color: var(--brand-700);
}

.landing-row h3 {
  margin: 0 0 8px;
  font-size: 22px;
  font-weight: 700;
}

/* 56ch, about 70 characters of Geist a line: `ch` is the width of a «0»,
   and the first cut's 68ch still let some 86 through at 1440px. A row's
   capability is in the body's own ink; its limit, under it, is quieter and
   hangs from the panel's dashed rule (templates/landing.html says how the
   rows were split). */
.landing-row p {
  max-width: 56ch;
  margin: 0;
  color: var(--text);
}

.landing-row .landing-row-limit {
  margin-top: 8px;
  padding-left: 12px;
  font-size: 15px;
  color: var(--text-muted);
  border-left: 1px dashed var(--border-strong);
}

/* The semáforo row's legend (_status_legend.html): its meanings are a
   <dd>, not a <p>, so they take the row's muted text here rather than the
   body's ink, and read as the rest of the row does. */
/* The legend's disclosure (templates/landing.html says why it starts
   closed): the row's lead-in sentence is the summary, in the row's text
   with the ink's own marker, underlined as the control it is. */
.landing-legend {
  margin: 0;
}

/* Drawn as the control it is: a bordered chip, «Ver los seis estados»,
   with a drawn chevron in place of the browser's 8px triangle, turned when
   open. 40px tall, a comfortable target. */
.landing-legend summary {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 40px;
  margin-top: 12px;
  padding: 6px 14px 6px 10px;
  font-size: 15px;
  font-weight: 600;
  color: var(--brand-800);
  cursor: pointer;
  list-style: none;
  border: 1px solid var(--border-strong);
  border-radius: 9999px;
}

.landing-legend summary::-webkit-details-marker {
  display: none;
}

.landing-legend summary svg {
  flex-shrink: 0;
  color: var(--brand-700);
  transition: transform 200ms cubic-bezier(0.16, 1, 0.3, 1);
}

.landing-legend[open] summary svg {
  transform: rotate(90deg);
}

.landing-legend summary:hover {
  color: var(--brand-900);
  background: var(--surface-hover);
  border-color: var(--brand-200);
}

@media (prefers-reduced-motion: reduce) {
  .landing-legend summary svg {
    transition: none;
  }
}

.landing-legend summary:focus-visible {
  outline: 2px solid var(--brand-700);
  outline-offset: 2px;
}

/* Each state as its badge over its meaning, every row alike: side by side,
   a short meaning («Completo», «Retirada») fitted beside the badge and a
   long one wrapped under it, and the six read in two shapes. */
.landing-row .status-legend {
  margin: 12px 0 0;
}

.landing-row .status-legend-row {
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
}

.landing-row .status-legend dd {
  color: var(--text-muted);
}

/* The ruler of rim diameters (aria-hidden; the template says what it
   draws and why each mark is at its diameter over 63). */
.landing-rim {
  position: relative;
  height: 120px;
  margin: 64px 0 0;
}

.landing-rim-name {
  position: absolute;
  top: 6px;
  font-size: 13px;
  font-weight: 700;
}

.landing-rim-name-a {
  left: 0;
  color: var(--text);
}

.landing-rim-name-b {
  right: 0;
  color: var(--brand-700);
}

.landing-rim-bar {
  position: absolute;
  top: 36px;
  height: 28px;
}

.landing-rim-bar-a {
  left: 0;
  width: 90.48%;
  background: var(--text);
  border-radius: 9999px 0 0 9999px;
}

.landing-rim-bar-b {
  left: 90.48%;
  right: 0;
  background: var(--rim-b);
  border-radius: 0 9999px 9999px 0;
}

/* One inch of B, centred on its diameter (1/63 of the bar), never under
   10px wide, standing a little proud of the bar so the exceptions read
   first. */
.landing-rim-segment {
  position: absolute;
  top: 32px;
  width: max(10px, 1.587%);
  height: 36px;
  background: var(--rim-b);
  border-radius: 4px;
  box-shadow: 0 0 0 2px var(--surface);
  transform: translateX(-50%);
}

.landing-rim-mark {
  position: absolute;
  top: 78px;
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--brand-700);
  transform: translateX(-50%);
}

/* On a phone the marks 45", 49" and 51" are some 11px apart: 49" drops a
   line there (the phone rules at the end) so no two labels touch. On a
   wider screen all five share one line. */

.landing-rim-mark-zero {
  left: 0;
  color: var(--text-subtle);
  transform: none;
}

.landing-rim-at-45 {
  left: 71.43%;
}

.landing-rim-at-49 {
  left: 77.78%;
}

.landing-rim-at-51 {
  left: 80.95%;
}

.landing-rim-at-57 {
  left: 90.48%;
}

.landing-categories {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
  gap: 32px;
  margin-top: 40px;
}

.landing-category {
  padding-top: 16px;
  border-top: 3px solid var(--text);
}

.landing-category h3 {
  margin: 0 0 6px;
  font-size: 20px;
  font-weight: 700;
}

.landing-category p {
  margin: 0;
  color: var(--text-muted);
}

.landing-category-b {
  border-top-color: var(--rim-b);
}

.landing-category-b h3 {
  color: var(--brand-700);
}

.landing-category-none {
  border-top: 3px dashed var(--border-strong);
}

.landing-category-none h3 {
  color: var(--text-muted);
}

.landing-note {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
  gap: 12px 32px;
  align-items: baseline;
  margin-top: 48px;
  padding: 24px 28px;
  background: var(--surface-muted);
  border-radius: 16px;
}

.landing-note h3 {
  margin: 0;
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

.landing-note p {
  grid-column: span 2;
  max-width: 56ch;
  margin: 0;
  color: var(--text);
}

.landing-panel {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
  gap: 32px 56px;
  padding: clamp(28px, 5vw, 56px);
  background: var(--brand-50);
  border-radius: 24px;
}

/* The title takes a row of its own, so the two lists sit side by side
   under it: in the design's three `auto-fit` columns the panel's width
   held only two, and the second list fell under the title, alone. */
.landing-panel h2 {
  grid-column: 1 / -1;
  font-size: 36px;
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--brand-900);
}

.landing-is,
.landing-is-not {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.landing-is li,
.landing-is-not li {
  padding-left: 20px;
}

.landing-is li {
  color: var(--brand-800);
  border-left: 3px solid var(--brand-600);
}

.landing-is-not li {
  color: var(--text);
  border-left: 3px dashed var(--placeholder);
}

.landing-closing {
  padding: 96px 0;
  color: var(--brand-100);
  background: var(--brand-900);
}

/* `start`: aligned to the end, the heading moved down when a refused form
   grew by its errors and summary. */
.landing-closing-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr));
  gap: 56px;
  align-items: start;
}

.landing-closing h2 {
  margin: 0 0 20px;
  font-size: clamp(36px, 5vw, 56px);
  font-weight: 700;
  line-height: 1.02;
  letter-spacing: -0.03em;
  color: #ffffff;
}

.landing-closing-grid > div > p {
  margin: 0;
  font-size: 19px;
}

.landing-form {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* What the form does, above its fields, in the section's body ink; and
   the note of an earlier send in this session, quieter, with the accent's
   rule. */
.landing-form-lead {
  margin: 0;
  font-size: 15px;
  color: var(--brand-100);
}

.landing-form-note {
  margin: 0;
  padding-left: 10px;
  font-size: 14px;
  font-weight: 600;
  color: var(--brand-300);
  border-left: 2px solid var(--brand-300);
}

.landing-actors {
  margin: 0;
  padding: 0;
  border: 0;
}

.landing-actors legend,
.landing-email label {
  margin-bottom: 8px;
  padding: 0;
  font-size: 14px;
  font-weight: 600;
  color: var(--brand-200);
}

.landing-email label {
  display: block;
}

.landing-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* A radio drawn as a pill: the input stays in the page, focusable and
   announced, only out of view; the label is the click target, and `:has`
   draws the choice and the focus on it. Rejected: buttons that set a hidden
   field, as the design's script did -- the page loads no script. */
.landing-pill {
  padding: 10px 18px;
  font-size: 14px;
  font-weight: 600;
  color: var(--brand-100);
  cursor: pointer;
  border: 1px solid var(--landing-pill-border);
  border-radius: 9999px;
}

.landing-pill input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}

/* Chosen: a 2px light outline on a faint tint, and the padding a pixel
   less so the pill does not grow. A filled pill read as a button beside the
   form's one, and outshone it. */
.landing-pill:has(input:checked) {
  padding: 9px 17px;
  color: #ffffff;
  background: var(--landing-pill-chosen);
  border: 2px solid var(--brand-300);
}

/* The address field, white on the section's green. The button left its
   box for the end of the form (templates/landing.html says why). */
.landing-email input {
  display: block;
  width: 100%;
  padding: 14px 16px;
  font-size: 16px;
  background: var(--surface);
  border: 2px solid var(--surface);
  border-radius: 12px;
}

/* A refused field says so by more than the message under it: each pill's
   edge takes the error's light red, and the address field, white on white
   would hide it, takes a ring of it on the section's green -- given way to
   the focus ring while it has the keyboard. */
.landing-actors[aria-describedby] .landing-pill {
  border-color: var(--error-on-dark);
}

.landing-email input[aria-invalid="true"]:not(:focus-visible) {
  outline: 2px solid var(--error-on-dark);
  outline-offset: 2px;
}

/* The light accent's fill and the darkest green's label (9.94:1): the
   section's one filled control, so the eye ends there. It was `--brand-700`
   until the third critique: its white label passed at 5.48:1, but as a
   shape it stood 1.77:1 from the band and sank into it, quieter than the
   chosen pill and the hero's link. (The app's own primary fill,
   `--brand-600`, gives a white 14px label only 3.77:1; DESIGN.md records
   that gap.) */
.landing-submit {
  align-self: flex-start;
  padding: 14px 22px;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  color: var(--brand-950);
  cursor: pointer;
  background: var(--brand-300);
  border: 0;
  border-radius: 8px;
}

.landing-submit:hover {
  background: var(--brand-200);
}

.landing-consent label {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  font-size: 14px;
  color: var(--brand-100);
  cursor: pointer;
}

.landing-consent input[aria-invalid="true"] {
  outline: 2px solid var(--error-on-dark);
  outline-offset: 2px;
}

.landing-consent input {
  flex-shrink: 0;
  width: 18px;
  height: 18px;
  margin: 3px 0 0;
  accent-color: var(--brand-300);
}

/* A field's error on the closing section's green: the app's red ink there
   measures under 2:1, so the error takes a light red that measures over
   7:1 on `--brand-900`. */
.landing-form .field-error {
  font-size: 14px;
  font-weight: 600;
  color: var(--error-on-dark);
}

/* The summary's links at 24px tall, the WCAG 2.2 minimum target (2.5.8):
   at the alert's 14px text they measured 18px. */
.landing-form .error-summary li {
  padding: 3px 0;
}

/* The honeypot (LeadForm.nexo): out of view and, with `tabindex="-1"`
   and `aria-hidden` in the template, out of the keyboard's and a screen
   reader's way. Moved off-screen rather than `display: none`, which some
   bots read as "skip this field". */
.landing-honeypot {
  position: absolute;
  left: -10000px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* A status, not a field: a drawn check beside the words on the darkest
   green, the address the visitor sent in the light accent. */
.landing-sent {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  margin: 0;
  padding: 20px 22px;
  font-size: 18px;
  font-weight: 600;
  color: var(--brand-100);
  background: var(--brand-950);
  border-radius: 12px;
}

.landing-sent svg {
  flex-shrink: 0;
  margin-top: 2px;
  color: var(--brand-300);
}

.landing-sent strong {
  font-weight: 700;
  color: var(--brand-300);
  overflow-wrap: anywhere;
}

.landing-footer {
  padding: 48px 0 32px;
  color: var(--brand-200);
  background: var(--brand-950);
}

.landing-footer-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 40px;
}

.landing-footer-brand {
  display: flex;
  align-items: center;
  gap: 12px;
}

.landing-footer-brand img {
  width: 40px;
  height: 40px;
  object-fit: contain;
}

.landing-footer-brand strong {
  display: block;
  font-size: 18px;
  color: #ffffff;
}

.landing-footer-brand span {
  font-size: 14px;
}

.landing-footer-nav {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 200px;
  font-size: 14px;
}

.landing-footer-nav strong {
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--brand-300);
}

.landing-footer-nav a {
  color: #ffffff;
}

.landing-footer-legal {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 8px;
  margin-top: 40px;
  padding-top: 20px;
  font-size: 13px;
  border-top: 1px solid var(--landing-rule-dark);
}

.landing-footer-legal p {
  margin: 0;
}

/* A phone: the ruler's labels and the section titles come down a size, and
   the feature rows' 64px number column narrows. The rows' `span 2` is
   undone so the one-column grid does not make an implicit second track. */
@media (max-width: 720px) {
  .landing-note p {
    grid-column: auto;
  }

  /* The phone at its content's height: the page's first real section
     started a screen and a half down behind it. */
  .landing-phone {
    width: 270px;
    height: auto;
  }

  .landing-phone-screen {
    height: auto;
    padding: 28px 14px 18px;
  }

  .landing-row {
    grid-template-columns: 40px minmax(0, 1fr);
    gap: 12px;
  }

  .landing-row-num {
    font-size: 20px;
  }

  .landing-section,
  .landing-closing {
    padding: 64px 0;
  }

  .landing-section h2 {
    font-size: 32px;
  }

  .landing-panel h2 {
    font-size: 28px;
  }

  .landing-hero-grid {
    padding-top: 48px;
  }

  .landing-hero-copy {
    padding-bottom: 0;
  }

  .landing-rim-name,
  .landing-rim-mark {
    font-size: 11px;
  }

  .landing-rim-mark.landing-rim-at-49 {
    top: 96px;
  }

  .landing-submit {
    align-self: stretch;
  }
}

/* ---------------------------------------------------------------------------
   Home page.
   ------------------------------------------------------------------------- */

.page-title {
  margin: 0 0 16px;
  font-size: 24px;
  font-weight: 700;
  color: var(--text-strong);
}

.session-identity {
  margin: 0;
  font-size: 16px;
  color: var(--text);
}

.session-identity strong {
  color: var(--text-strong);
}

/* A secondary line under a title or in a card: a company's RUT, its legal
   name and branch, a line of status. Until November 2026 these were
   `.session-identity`, the home page's «Sesión iniciada como» line, in
   twenty places on ten pages. The same look as that line today, written
   apart so either can change alone. */
.meta-line {
  margin: 0;
  font-size: 16px;
  color: var(--text);
}

.meta-line strong {
  color: var(--text-strong);
}

.session-email {
  margin: 4px 0 0;
  font-family: var(--font-mono);
  font-size: 14px;
  color: var(--text-subtle);
  overflow-wrap: anywhere;
}

/* ---------------------------------------------------------------------------
   Branch scope: which branch the session acts for, in the header, and the
   list a user enters a branch from on the home page. In the desktop row the
   header text keeps to one line per part, and is truncated only in the
   unlikely case that the scope alone is wider than a wrapped line of the
   row; on a phone or a tablet it sits in the menu's panel, where
   `.app-menu-panel` lets it wrap instead.
   ------------------------------------------------------------------------- */

.session-scope {
  display: flex;
  flex-direction: column;
  min-width: 0;
  margin: 0;
  font-size: 13px;
  line-height: 1.2;
  text-align: right;
  color: var(--text-subtle);
}

.session-scope strong,
.session-scope span {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.session-scope strong {
  color: var(--text-strong);
}

.card + .card {
  margin-top: 16px;
}

.section-title {
  margin: 0 0 12px;
  font-size: 18px;
  font-weight: 700;
  color: var(--text-strong);
}

.branch-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.branch-list li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 0;
  border-top: 1px solid var(--border);
}

.branch-list small {
  display: block;
  color: var(--text-subtle);
}

.branch-list form {
  margin: 0;
  flex-shrink: 0;
}

/* ---------------------------------------------------------------------------
   The master administrator's corporate view: the consolidated inventory, its
   breakdown per branch, and one branch's lots. V4's «vista maestra» was not
   ported, so these are plain tables in the frame's colours. A real <table>
   rather than the movement list's grid rows because these are figures read
   across columns, and a screen reader needs the header cells to say which.
   The wrapper scrolls sideways on a phone instead of squeezing the numbers.
   ------------------------------------------------------------------------- */

.subsection-title {
  margin: 20px 0 8px;
  font-size: 15px;
  font-weight: 700;
  color: var(--text-strong);
}

/* `position: relative` makes the wrapper the containing block of what is
   absolutely positioned inside it, so its scroll clips that too. Without it
   a `.visually-hidden` header cell (the «Acciones» of the master's
   «Desglose por sucursal», templates/home.html) was placed against the
   document, at its static position near the far end of the 596px table,
   outside the wrapper's clip: the master's home page was 718px wide at a
   320px viewport and scrolled sideways over blank space (measured in
   headless Chrome; hiding the wrapper brought it back to 320px). */
.table-scroll {
  position: relative;
  overflow-x: auto;
}

/* Every wrapper is `tabindex="0"` with `role="region"` and a name: a table
   holds nothing focusable, so without it a keyboard could not scroll to the
   columns the wrapper cuts off (WCAG 2.1.1; axe's
   `scrollable-region-focusable`). Where a heading right above already says
   what the table holds, the wrapper is named by it (`aria-labelledby`), so
   the two cannot drift apart; otherwise by an `aria-label` of its own. The
   ring is drawn inside, where the card's own edges cannot cut it.
   config/tests/test_frame.py pins the attributes on every wrapper in the
   templates, and that each `aria-labelledby` points at an id there. */
.table-scroll:focus-visible {
  outline: 2px solid var(--brand-700);
  outline-offset: -2px;
}

.inventory-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
  color: var(--text-strong);
}

.inventory-table th,
.inventory-table td {
  padding: 10px 12px;
  text-align: left;
  border-top: 1px solid var(--border);
  white-space: nowrap;
}

.inventory-table thead th {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-subtle);
  background: var(--surface-muted);
  border-top: 0;
}

.inventory-table .num {
  text-align: right;
}

.inventory-table tfoot th,
.inventory-table tfoot td {
  font-weight: 700;
  border-top: 2px solid var(--border);
}

.inventory-table form {
  margin: 0;
}

/* ---------------------------------------------------------------------------
   Navigation and flash messages.
   ------------------------------------------------------------------------- */

.app-nav {
  display: flex;
  gap: 16px;
}

.app-nav a {
  font-size: 14px;
  font-weight: 600;
  color: var(--brand-700);
  text-decoration: none;
}

.app-nav a:hover {
  color: var(--brand-900);
}

/* The open page's link (`current_page`, apps/organizations/templatetags/
   frame.py): the darker ink and an underline, not colour alone, so it reads
   as current to someone who cannot tell the two greens apart. The account's
   buttons, bordered pills, carry the same underline on their label. The
   first cut gave them only a tint, border and ink -- colour alone, and the
   very tint `.btn-quiet:hover` paints, so a current button looked like a
   hovered one. */
.app-nav a[aria-current="page"],
.app-account .btn[aria-current="page"],
.page-subnav .btn[aria-current="page"] {
  color: var(--brand-900);
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 4px;
}

.app-account .btn[aria-current="page"],
.page-subnav .btn[aria-current="page"] {
  border-color: var(--brand-200);
}

/* A page's own navigation between sibling pages, in its head (a network
   generator's «Movimientos», «Inventario» and «Red»). It wore `.filter-bar`
   until November 2026, and with it a filter bar's box; the box is kept,
   without the filter bar's 24px bottom margin, which inside the head's
   centred row pushed the links up off the centre. */
.page-subnav {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
  padding: 16px 20px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
}

/* The messages framework's flashes, one tone per level. `base.html` renders
   each as `flash flash-<tags>`; until October 2026 only `.flash` was styled,
   so the 18 `messages.error` calls in the views -- a refused offer, a scope
   that no longer allows the action -- painted in the success green and read
   as confirmations. The green stays the default, for `success` and for any
   level a later view adds without a rule here. */
.flash-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 0 0 16px;
}

.flash {
  margin: 0;
  padding: 12px 16px;
  border-radius: 10px;
  font-size: 14px;
  color: var(--success-text);
  background: var(--success-bg);
  border: 1px solid var(--brand-200);
}

.flash-info {
  color: var(--info-text);
  background: var(--info-bg);
  border-color: var(--info-border);
}

.flash-warning {
  color: var(--warning-text);
  background: var(--warning-bg);
  border-color: var(--warning-border);
}

.flash-error {
  color: var(--error-text);
  background: var(--error-bg);
  border-color: var(--error-border);
}

/* Notices that stay until someone acts on them, as a list: today a
   convenio's notices (_contract_notice.html), each dismissed with its own
   «Entendido». They wore `.flash-list` and `.flash flash-info` until
   November 2026, though they are not the messages framework's one-time
   flashes, and `.flash-list`, written for the frame's `<div>`, never reset
   the `<ul>`'s 40px left padding. The look is a flash's info tone, written
   apart so a change to the flashes leaves them be. */
.notice-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 0 0 16px;
  padding: 0;
  list-style: none;
}

.notice-info {
  margin: 0;
  padding: 12px 16px;
  border-radius: 10px;
  font-size: 14px;
  color: var(--info-text);
  background: var(--info-bg);
  border: 1px solid var(--info-border);
}

/* ---------------------------------------------------------------------------
   Movements: V4's generator «Movimientos» page, its «Registrar Movimiento»
   and «Ficha» modals, and the carrier's mobile scan screen. Values are the
   ones those screens set inline (src/app/dashboard/generador/movimientos,
   src/components/generador-v3/modales, src/components/transportista-v3).
   ------------------------------------------------------------------------- */

.mono {
  font-family: var(--font-mono);
}

.num {
  text-align: right;
}

.muted,
.empty {
  color: var(--text-subtle);
}

.empty {
  padding: 48px 20px;
  margin: 0;
  text-align: center;
  font-size: 14px;
}

/* 12px, not V4's 11px: a hint is read, not glanced at -- it says what a
   field accepts and why a held field cannot change. */
.hint {
  display: block;
  margin: 6px 0 16px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-subtle);
}

/* A page action's button keeps its label on one line. It used to also set
   `width: auto`, to undo the full-width primary, and later a 14px bold that
   only the buttons wearing it had; with the primary at its label's width
   (see `.btn-block`) and that size on every `.btn`, neither had anything
   left to do here. Below 481px it may wrap: the query beside
   `.draft-badge` says why. */
.btn-inline {
  white-space: nowrap;
}

.page-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 24px;
  padding: 20px 24px;
  border-radius: 14px;
  background: linear-gradient(to right, var(--brand-50), var(--brand-100));
  border: 1px solid var(--brand-200);
}

.page-head .page-title {
  margin: 0;
  color: var(--brand-900);
}

.page-lede {
  margin: 2px 0 0;
  font-size: 14px;
  font-weight: 500;
  color: var(--brand-700);
}

.filter-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
  margin-bottom: 24px;
  padding: 16px 20px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
}

/* A document's own fields: those of a `.rm-field` (_field.html and the
   fields laid out by hand like it) and of a form's lines (`.rm-lines-form`).
   Until November 2026 the selectors were `.rm-doc input`, `select` and
   `textarea`, which reached every control a page put inside a document --
   «Catastro»'s map filters, the accreditation's treatment checkboxes, a
   consent card's box -- and each then had to be undone where it was not
   wanted. Opting fields in rather than other components out means a new
   component inside a document starts from its own rules. A checkbox or a
   radio is never stretched: it keeps the browser's own size. */
.filter-bar input,
.rm-doc .rm-field input:not([type="checkbox"]):not([type="radio"]),
.rm-doc .rm-field select,
.rm-doc .rm-field textarea,
.rm-doc .rm-lines-form input:not([type="checkbox"]):not([type="radio"]),
.rm-doc .rm-lines-form select,
.rm-doc .rm-lines-form textarea {
  width: 100%;
  padding: 9px 11px;
  font-size: 14px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 8px;
}

.filter-bar input {
  flex: 1;
  min-width: 220px;
  width: auto;
}

/* The textarea is listed: the base rule used to take every field's outline
   away and this one gave the focus back only to inputs and selects, so the
   five free-text fields (a refusal's reason, an offer's and a
   counter-offer's note, the carrier's route, the operational notes) showed
   no focus at all. The ring is an outline, not the border's colour alone,
   because a native checkbox paints no border: the accreditation's
   treatments and a line's «Quitar» showed no focus either. */
.filter-bar input:focus,
.rm-doc .rm-field input:focus,
.rm-doc .rm-field select:focus,
.rm-doc .rm-field textarea:focus,
.rm-doc .rm-lines-form input:focus,
.rm-doc .rm-lines-form select:focus,
.rm-doc .rm-lines-form textarea:focus {
  border-color: var(--brand-600);
  outline: 2px solid var(--brand-700);
  outline-offset: 2px;
}

/* The rule above is `:focus`, which a text field needs (a click into it is
   where typing goes), but a checkbox or radio a mouse clicks takes the focus
   too in Chrome and Firefox, and would keep the dark ring after every click.
   Only a keyboard's focus (`:focus-visible`) rings the box. Inside an
   `.option-card` the box never rings: the card does
   (`.option-card:has(input:focus-visible)`, below), and a second ring
   around the dot inside the first was noise. */
.rm-doc .rm-field input[type="checkbox"]:focus:not(:focus-visible),
.rm-doc .rm-field input[type="radio"]:focus:not(:focus-visible),
.rm-doc .rm-lines-form input[type="checkbox"]:focus:not(:focus-visible),
.rm-doc .rm-lines-form input[type="radio"]:focus:not(:focus-visible),
.option-card input:focus {
  outline: none;
}

/* Positioned so that each cell's visually-hidden label (`.cell-label`, an
   absolute 1px box) is placed against the table and clipped by it, as
   `.table-scroll` does for its own: from 721px a row may overflow its bare
   `fr` tracks, and a label left to the document's containing block at the
   start of a clipped cell would scroll the page sideways.

   The table is the grid and each row a subgrid of it (`.movement-row`,
   below), so the columns are sized once, from every row's cells together,
   and line up from row to row. Until November 2026 each row was its own
   grid: a bare `fr` track's minimum is its content's, so each row sized
   its columns from its own cells, and on «Movimientos» at 1440px «Fecha»
   started anywhere between x 391 and x 401 and «Destino» between x 817 and
   x 856, more the more a row's content differed (a two-line refusal mark
   pushed every column after it). `minmax(0, …fr)` on the row's tracks
   would also line them up, by giving every row the same zero minimum, and
   was rejected because it was tried and reverted: the semáforo pill
   (`.status`, `white-space: nowrap`) is wider than its 1.1fr share, and
   instead of its column growing to fit it, «Entregado sin certificado» and
   «Pagado, certificado retenido» were cut at this box's `overflow: hidden`
   (headless Chrome, 1440px). A subgrid keeps the bare `fr`, whose minimum
   is now the widest pill of the whole list, so the column grows to the
   pill instead. Wrapping the pill was the other way out, rejected because
   a two-line capsule is the very blot the network's refusal mark was.

   The tracks live in `--list-columns`, which a list with other columns
   overrides (`.movement-table-refusals`) and the row's fallback reads too.
   Nothing here scrolls the page sideways: from 721px the table clips its
   own overflow, and below 720px it stops being a grid and the rows stack
   into the one `minmax(0, 1fr)` track of the phone query. */
.movement-table {
  --list-columns: 1.1fr 0.8fr 1.5fr 0.9fr 1.4fr 1.1fr;
  position: relative;
  overflow: hidden;
  display: grid;
  grid-template-columns: var(--list-columns);
  column-gap: 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
}

/* Every child takes the table's full width: a row, to be a subgrid of all
   its tracks, and the empty state (`.empty`), which is one sentence and not
   a row of cells. Without it the empty state would fill the first track
   only. */
.movement-table > * {
  grid-column: 1 / -1;
}

/* «Red»'s refusals waiting for an alternative destination
   (network_index.html): four cells, not the movement lists' six. They used
   to sit in the six tracks, which left the content two thirds of the card
   and the refusal mark in a narrow fourth track, broken over four lines
   inside a capsule. The mark's track is `fit-content(40%)`, as a document
   line's last column is (`.rm-line` says why not `auto`): it takes the
   mark's width on one line while the card has room for it, and never more
   than 40% of the row, though only the pending mark («espera destino
   alternativo», no company name in it) is listed here today. */
.movement-table-refusals {
  --list-columns: 1.2fr 1fr 2fr fit-content(40%);
}

/* `subgrid` comes second so that a browser without it (before Chrome 117,
   Safari 16, Firefox 71) drops that line and keeps the row's own tracks:
   misaligned from row to row, as every row was before November 2026, but
   still in columns. A subgrid may set its own column gap, and `gap` here
   is the table's 12px on purpose: a different one would shift the cells
   off the table's track lines by the difference. It is also the gap of
   the fallback's columns and of the stacked cells on a phone. */
.movement-row {
  display: grid;
  grid-template-columns: var(--list-columns);
  grid-template-columns: subgrid;
  gap: 12px;
  align-items: center;
  padding: 14px 20px;
  font-size: 13px;
  color: var(--text-strong);
  text-decoration: none;
  border-bottom: 1px solid var(--divider);
  transition: background 150ms;
}

a.movement-row:hover {
  background: var(--surface-muted);
}

/* A row that is a link shows a keyboard's focus as a ring inside its own
   edges: `.movement-table` clips its overflow to round its corners, so the
   browser's default ring, drawn outside the row, lost its sides at the
   table's edges and kept only thin lines between rows. */
a.movement-row:focus-visible {
  outline: 2px solid var(--brand-700);
  outline-offset: -2px;
  background: var(--surface-muted);
}

.movement-row small {
  display: block;
  font-size: 11px;
  color: var(--text-subtle);
}

.movement-row .kind {
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--brand-700);
}

.movement-row-head {
  padding: 12px 20px;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-subtle);
  background: var(--surface-muted);
}

.pager {
  display: flex;
  gap: 12px;
  align-items: center;
  justify-content: center;
  margin-top: 16px;
  font-size: 13px;
  color: var(--text-subtle);
}

.note {
  margin: 24px 0 0;
  padding: 14px 18px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--success-text);
  background: var(--success-bg);
  border: 1px solid var(--brand-200);
  border-radius: 10px;
}

/* The semáforo: V4's ESTADOS table, one colour set per MovementStatus. */
.status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
  border-radius: 9999px;
}

.status-dot {
  width: 8px;
  height: 8px;
  border-radius: 9999px;
  background: currentColor;
}

.status-published { color: #1e40af; background: #dbeafe; }
.status-published .status-dot { background: #3b82f6; }
.status-in_transit { color: #b45309; background: #fef3c7; }
.status-in_transit .status-dot { background: #d97706; }
.status-delivered { color: #4d7c0f; background: #ecfccb; }
.status-delivered .status-dot { background: #84cc16; }
.status-delivered_settled { color: #b91c1c; background: #fee2e2; }
.status-delivered_settled .status-dot { background: #dc2626; }
.status-complete { color: #15803d; background: #dcfce7; }
.status-complete .status-dot { background: #16a34a; }
/* A withdrawn need (Movement.withdraw()): grey, because V4's ESTADOS table
   has no such state to borrow a colour from -- the sixth state is a
   provisional decision -- and a withdrawn need asks nobody for anything. */
.status-withdrawn { color: #475569; background: #f1f5f9; }
.status-withdrawn .status-dot { background: #94a3b8; }

/* The semáforo's legend (_status_legend.html), shared by the movement list's
   note and the landing's card: each state's badge, then what it means. The
   meaning wraps under the badge on a narrow screen rather than squeezing it,
   because the badge does not wrap (`.status` is nowrap). It sets no colour:
   inside the note it takes the note's; the landing card mutes it with a rule
   of its own, beside `.landing-feature p`. */
.status-legend {
  display: grid;
  gap: 8px;
  margin: 10px 0;
}

.status-legend-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 10px;
}

.status-legend dt,
.status-legend dd {
  margin: 0;
}

/* The movement list's semáforo note holds a lead-in, the legend and a
   closing line: its paragraphs drop the browser's 1em margin, so the
   legend's own margin is the gap between them and the note's padding the
   only one at its edges. Scoped to this note by its own class, because the
   other notes with several paragraphs (the public profile's outdated
   consent) rely on that margin to separate them. */
.note-legend > p {
  margin: 0;
}

/* The carrier's own leg (CarrierLeg), in place of the semáforo on its pages:
   the same badge, four colour sets. Amber for a load on the road, as the
   semáforo's «En tránsito»; green once delivered, with no further shade,
   because whether a CDF followed is not the carrier's to read; grey for a
   load refused at destination -- the leg is over, and nothing about it is
   the carrier's to act on (the follow-up is the origin's), so it is not the
   red the origin's refusal mark uses while someone must act. */
.leg-pending_load { color: #1e40af; background: #dbeafe; }
.leg-pending_load .status-dot { background: #3b82f6; }
.leg-in_transit { color: #b45309; background: #fef3c7; }
.leg-in_transit .status-dot { background: #d97706; }
.leg-delivered { color: #15803d; background: #dcfce7; }
.leg-delivered .status-dot { background: #16a34a; }
.leg-rejected { color: #374151; background: #e5e7eb; }
.leg-rejected .status-dot { background: #6b7280; }

/* The carrier's leg filters: links, so the list works without JavaScript.
   The current one is white on the dark green, the white of text on a fill
   that `:root` lists as literal on purpose. It was `var(--surface)` until
   November 2026: the same white, but a surface is a background, and a later
   change to the page's surfaces would have recoloured this text with it. */
.leg-filters a[aria-current="page"] {
  color: #ffffff;
  background: var(--brand-700);
}

/* A refused load, beside the semáforo rather than inside it (see
   _rejection_mark.html). Red while it waits for an alternative destination,
   because that is the one state someone must act on; neutral once answered. */
.rejection-mark {
  display: inline-flex;
  margin: 4px 0 0 6px;
  padding: 4px 10px;
  font-size: 12px;
  font-weight: 600;
  color: var(--text);
  background: var(--surface-subtle);
  border-radius: 9999px;
}

.rejection-mark-pending {
  color: var(--error-text);
  background: var(--pending-refusal-bg);
}

/* The refusal on the ficha: the notice the generator receives. */
.rejection-notice {
  margin: 16px 0;
  padding: 14px 18px;
  color: var(--error-strong);
  background: var(--error-bg);
  border: 1px solid var(--error-border);
  border-radius: 10px;
}

.rejection-reason {
  margin: 4px 0 12px;
  font-weight: 600;
}

.kind-badge {
  padding: 4px 12px;
  font-size: 12px;
  font-weight: 600;
  color: var(--text);
  background: var(--surface-subtle);
  border-radius: 9999px;
}

/* The document itself: header with the issuer and the red folio box, the
   lines in a dark-banded table, the stamp at the foot. */
.rm-doc {
  max-width: 720px;
  margin: 0 auto;
  padding: 24px 32px 28px;
  background: var(--surface);
  border-radius: 14px;
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.18);
}

.rm-eyebrow,
.rm-label,
.rm-doc .rm-field label,
.rm-doc .rm-lines-form label,
.rm-facts dt,
.rm-line-head {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* On the pages laid out as a document (the ficha, its forms, the two
   confirmations) the eyebrow is the page's `<h1>`: it names the page and
   nothing else on it does, so a screen reader moving by headings had nowhere
   to land. It keeps the eyebrow's look -- the folio box is what the eye
   reads first -- and the font-size above overrides a heading's 2em. Pages
   with a `.page-title` keep the eyebrow as a `<p>` above it. */
.rm-eyebrow {
  margin: 0 0 18px;
  color: var(--brand-700);
}

/* A choice drawn as a card: the radios of a need's and an offer's service,
   and the five single checkboxes -- three consents (the convenio's, the
   catastro's, the public ficha's) and two declarations that are not consents
   (the private mode's switch, a month with no hazardous waste). Named
   `.rm-kind` and `.rm-kind-option` until November 2026, after the movement
   form's kind choice that was the first of them; the checkboxes had borrowed
   the name since. It no longer leans on `.rm-doc label` for its
   type and resets it: that rule now reaches only a field's label, so the
   card's text is its `<strong>` and `<small>`, each styled below. */
.option-cards {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin: 0 0 22px;
  padding: 0;
  border: 0;
}

/* The group's error, on a line of its own under the cards, as a field's
   error sits under its input (_field.html). Every page draws it inside the
   group -- the fieldset of _option_cards.html, each checkbox's `<div>` --
   so it was one more item of the row, beside the last card: «Este campo es
   obligatorio.» to the right of «Ambos» on «Publicar necesidad» at 1280px.
   The full width, with the row allowed to wrap above, is what puts it under.
   Moving it out of the group was rejected: the fieldset is what names it in
   its aria-describedby and turns red with `.has-error`, and the partial and
   the five checkbox pages would each need the same edit. The cards keep one
   line: `flex: 1` sizes them from zero, so they wrap only below their
   words' width, and below 720px they stack anyway. */
.option-cards > .field-errors {
  width: 100%;
}

.option-card {
  flex: 1;
  display: block;
  padding: 12px 14px;
  border: 2px solid var(--border);
  border-radius: 10px;
  cursor: pointer;
}

.option-card:has(input:checked) {
  border-color: var(--brand-600);
  background: var(--brand-50);
}

/* The radio inside the card is small and the card is what reads as the
   control, so a keyboard's focus rings the card, not only the dot. */
.option-card:has(input:focus-visible) {
  outline: 2px solid var(--brand-700);
  outline-offset: 2px;
}

.option-card input {
  margin: 0 6px 0 0;
}

.option-card strong {
  font-size: 13px;
  color: var(--text-strong);
}

.option-card small {
  display: block;
  font-size: 11px;
  font-weight: 400;
  line-height: 1.35;
  color: var(--text-subtle);
}

.rm-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: 20px;
  padding-bottom: 18px;
  border-bottom: 2px solid var(--brand-900);
}

.rm-issuer {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
}

.rm-issuer strong,
.rm-issuer span,
.rm-issuer small {
  display: block;
}

.rm-issuer strong {
  font-size: 15px;
}

.rm-issuer span {
  font-size: 12px;
  color: var(--text-subtle);
}

.rm-issuer small {
  font-size: 11px;
  color: var(--text-subtle);
}

.rm-folio {
  padding: 10px 16px;
  text-align: center;
  color: var(--doc-red);
  border: 2px solid var(--doc-red);
  border-radius: 8px;
}

.rm-folio small,
.rm-folio span {
  display: block;
  font-size: 9px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.rm-folio span {
  margin: 6px 0 4px;
  font-size: 11px;
}

.rm-folio strong {
  font-size: 18px;
  font-weight: 800;
}

/* `minmax(0, 1fr)`, not `1fr`, here and in every grid of the document
   layout below: a `<select>`'s min-content is its longest option, a
   company's legal name in «Gestor / destino» or «Destinatario», and a bare
   `fr` track would grow to it and push the page sideways on a phone. */
.rm-grid,
.rm-facts {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 14px 20px;
  margin: 18px 0;
}

.rm-field {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.rm-doc .rm-field label,
.rm-doc .rm-lines-form label,
.rm-facts dt {
  color: var(--text-subtle);
}

.rm-facts dd {
  margin: 2px 0 0;
  font-size: 13px;
  color: var(--text);
}

.field-error {
  font-size: 12px;
  color: var(--error-text);
}

/* A field's errors in one element, so the widget's aria-describedby can name
   them by a single id (_field.html); stacked as the separate lines they were
   when each was a flex item of `.rm-field` on its own. */
.field-errors {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.has-error input,
.has-error select,
.has-error textarea {
  border-color: var(--error-border);
}

.alert-error ul {
  margin: 0;
  padding-left: 18px;
}

/* The summary of a refused form (_form_errors.html): a heading line, then
   one link per error. Its text is a column, so the alert's centring is
   undone. It takes the focus as the page loads, by `autofocus` and not by a
   click or a key, so the ring is drawn on `:focus`: `:focus-visible` would
   leave it to each browser's guess, and the ring is how a sighted keyboard
   user learns where Tab goes next. The links keep the alert's colour,
   underlined, as every link in running text is. */
.error-summary {
  align-items: flex-start;
  margin: 0 0 16px;
}

.error-summary:focus {
  outline: 2px solid var(--error-text);
  outline-offset: 2px;
}

.error-summary-title {
  margin: 0 0 4px;
  font-weight: 700;
}

.error-summary a {
  color: inherit;
  text-decoration: underline;
}

/* The asterisk of a required field (_field.html), and the one in the note
   that states the convention (_required_note.html), in the error tone so
   the two are recognisably the same mark. */
.required-mark {
  color: var(--error-text);
  font-weight: 700;
}

/* Positioned for the same reason as `.movement-table`: the visually-hidden
   label in each cell of a line, and the spoken word beside a «→», is an
   absolute 1px box, placed against this box and clipped by it rather than
   left to the document. */
.rm-lines {
  position: relative;
  overflow: hidden;
  margin: 18px 0 8px;
  border: 1px solid var(--border);
  border-radius: 10px;
}

/* The third track is `fit-content(40%)`, not `auto`. It holds a quantity,
   a «Quitar» checkbox or the CDF's «Destinatario» select, and an `auto`
   track grows to its content's max-content before the `fr` tracks share
   what is left: a select's max-content is its longest option, so with an
   80-character legal name «Destinatario» took the whole row and left
   «Tratamiento aplicado» and «Peso» a few pixels each, at 1440px as at
   320px (headless Chrome). `fit-content(40%)` is as narrow as a checkbox
   needs and no wider than 40% of the row for a select.

   Known gap, left open on purpose: the tracks are fixed at three, and the
   movement form's line has four cells when it offers by-products and edits
   a saved movement, so on a desktop its «Quitar» drops to a second line.
   It does not overflow; below 720px the form's lines stack anyway. */
.rm-line {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) fit-content(40%);
  gap: 10px;
  align-items: center;
  padding: 12px 14px;
  font-size: 13px;
  border-top: 1px solid var(--divider);
}

.rm-line-head {
  padding: 9px 14px;
  font-size: 10px;
  color: var(--brand-100);
  background: var(--brand-900);
  border-top: 0;
}

.rm-line-foot {
  font-size: 12px;
  color: var(--text-subtle);
  background: var(--surface-muted);
}

.rm-line-errors {
  grid-column: 1 / -1;
  margin: 0;
  padding-left: 18px;
  font-size: 12px;
  color: var(--error-text);
}

/* A need's photographs: a row of thumbnails of one fixed height, each keeping
   its proportions (the img's width and height attributes give the browser the
   ratio) and cropped to a square-ish box at most. Used on the ficha, the offer
   page and each row of the market's list. */
.need-photos {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 8px 0 0;
  padding: 0;
  list-style: none;
}

.need-photos img {
  display: block;
  width: auto;
  height: 72px;
  max-width: 120px;
  object-fit: cover;
  border-radius: 6px;
}

.rm-photos-section .rm-label,
.rm-photos-section .empty,
.rm-photos-section .hint,
.rm-photos-section form,
.rm-photos-section .need-photos {
  padding: 12px 14px 0;
}

.rm-photos-section form {
  padding-bottom: 12px;
}

/* The offers received on a need's ficha: who offers what on the left, the
   answer (or the two buttons that give it) on the right. */
.rm-offers .rm-label,
.rm-offers .hint {
  padding: 12px 14px 0;
}

.rm-offer {
  grid-template-columns: minmax(0, 1fr) auto;
}

.rm-offer small {
  display: block;
  margin-top: 4px;
  color: var(--text-subtle);
}

.inline-form {
  display: inline-block;
  margin: 0 0 0 6px;
}

/* A link button in a row of `.inline-form` buttons, spaced as they are.
   The links wore `.inline-form` until November 2026, which also turned
   their `.btn` flex box into an inline block. */
.inline-action {
  margin: 0 0 0 6px;
}

.offer-answer {
  display: block;
  font-weight: 600;
}

.rm-badges {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin: 18px 0;
}

.rm-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 20px;
  margin-top: 18px;
  padding-top: 16px;
  border-top: 1px dashed var(--border-strong);
}

.rm-label {
  margin: 0 0 8px;
  color: var(--text-subtle);
}

.scan-list {
  margin: 0;
  padding: 0;
  font-size: 12px;
  list-style: none;
}

.rm-stamp {
  max-width: 180px;
  text-align: center;
}

.rm-stamp svg {
  display: block;
  margin: 0 auto;
  border: 1px solid var(--border);
  border-radius: 6px;
}

.rm-stamp small {
  display: block;
  margin-top: 4px;
  font-size: 10px;
  line-height: 1.3;
  color: var(--text-subtle);
}

/* Wraps: a row of three buttons, or one with a long label, is wider than
   the ~248px a phone's card leaves for content. */
.rm-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 12px;
  margin-top: 20px;
  padding-top: 16px;
  border-top: 1px solid var(--divider);
}

/* The carrier's phone screen. */
.scan-screen {
  max-width: 420px;
  margin: 0 auto;
}

.scan-card {
  overflow: hidden;
  margin-bottom: 16px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
}

.scan-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 12px 14px;
  font-size: 12px;
  color: var(--doc-red);
  border-bottom: 2px solid var(--brand-900);
}

/* The folio is the screen's heading: the carrier's page has no other, and a
   screen reader navigating by headings lands on it. Sized as the bold line it
   replaced, not as a heading. */
.scan-folio {
  margin: 0;
  font-size: inherit;
  font-weight: 700;
}

.scan-card-body {
  padding: 14px;
}

.scan-waste {
  margin: 0;
  font-size: 14px;
  font-weight: 700;
}

.scan-route {
  margin: 6px 0 0;
  font-size: 12px;
  color: var(--text-subtle);
}

.scan-button {
  padding: 16px;
  font-size: 16px;
  border-radius: 12px;
}

.scan-unload {
  background: var(--brand-900);
}

.scan-done {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 16px;
  text-align: center;
  color: var(--scan-done-text);
  background: var(--scan-done-bg);
  border-radius: 12px;
}

.scan-done small {
  font-size: 12px;
}

/* ---------------------------------------------------------------------------
   The summary card: a bordered box of label and value rows, ported from
   V4's compact «Autorizaciones» card on the gestor dashboard
   (src/components/gestor-v3/secciones/dashboard.tsx) for the gestor plant's
   ficha de acreditación, and since used for what a company page has on
   record (its location, notes, private mode, publication and authorization
   states, its public ficha and indices). Named `.acc-summary`, `.acc-list`,
   `.acc-row` and `.acc-value` until November 2026, after the accreditation:
   eight pages wore the accreditation's names, so a change meant for one of
   them restyled all. The certified codes' emerald chips and the treatment
   checkboxes below stay `acc-*`: only the accreditation draws them. V4 has
   no screen that edits the accreditation; its form below the card reuses
   the movement form's fields.
   ------------------------------------------------------------------------- */

.summary-card {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin: 18px 0;
  padding: 20px;
  border: 1px solid var(--border);
  border-radius: 12px;
}

/* The rows are a description list, each label a `<dt>` and its value a
   `<dd>`, so a screen reader announces the pairs as pairs. Until October
   2026 they were two `<span>`s, read as one run of text in which the
   reader had to guess where a label ended and its value began. The list
   carries the column's gap itself: a summary's other content (the
   operational notes' text) is not in one. */
.summary-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin: 0;
}

.summary-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.summary-row > dt {
  font-size: 12px;
  color: var(--text-subtle);
}

.summary-row > dd {
  margin: 0;
}

.summary-value {
  font-size: 13px;
  font-weight: 700;
  color: var(--text);
}

.acc-chips {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 4px;
}

.acc-chip {
  padding: 1px 7px;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 700;
  color: var(--brand-800);
  background: var(--brand-100);
  border-radius: 6px;
}

.acc-treatments {
  margin: 18px 0 0;
  padding: 0;
  border: 0;
}

.acc-treatments legend {
  margin-bottom: 8px;
  font-size: 14px;
  font-weight: 700;
  color: var(--text);
}

.acc-options {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 6px 20px;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* The treatments are checkboxes in a fieldset, not document fields, so
   they take the frame's ring here: the document's field rule no longer
   reaches them, and until November 2026 it was also what made their
   labels 11px bold capitals. */
.acc-treatments input:focus-visible {
  outline: 2px solid var(--brand-700);
  outline-offset: 2px;
}

.acc-treatments label {
  display: flex;
  gap: 8px;
  align-items: flex-start;
  font-size: 13px;
  color: var(--text);
}

/* The desktop grids stack on a phone rather than scroll sideways. */
@media (max-width: 720px) {
  .movement-row,
  .rm-grid,
  .rm-facts,
  .acc-options {
    grid-template-columns: minmax(0, 1fr);
  }

  .movement-row-head {
    display: none;
  }

  /* The table stops being a grid, so no row is laid against the desktop's
     tracks at a phone's width: each row is a block of its own, stacked into
     the one track above. */
  .movement-table {
    display: block;
  }

  /* With the header row hidden, each stacked cell shows its own label, the
     header's own wording, so «5200,00 → 5150,00» still says it is the scale
     weights. Every cell carries that label as an element (`.cell-label`,
     visually hidden from 721px), because the header row is `aria-hidden`
     at every width: it is a row of spans, not table headers, and read
     aloud it would be one more strip of words. Until October 2026 the
     label was generated here from a `data-label` attribute, so a screen
     reader on a desktop heard each row as a strip of loose values with no
     column named at all; a label written once, in the markup, is heard at
     every width and never twice. Here every piece of `.visually-hidden` is
     undone, by one rule shared with a form's field labels (described
     below); a `.visually-hidden` nested in the label (the spoken «de
     origen a destino» of «O → D») stays hidden. A
     figure aligns left here, under its label, rather than to the right
     edge of a column that no longer exists. */
  .movement-row .cell-label,
  .rm-lines-form .visually-hidden {
    position: static;
    width: auto;
    height: auto;
    margin: 0;
    overflow: visible;
    clip: auto;
    clip-path: none;
    white-space: normal;
  }

  .movement-row .cell-label {
    display: block;
    /* A figure's cell is `.mono`; its label is not. */
    font-family: var(--font-sans);
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-subtle);
  }

  .movement-row > .num {
    text-align: left;
  }

  /* A form's lines (`.rm-lines-form`: the movement's waste lines, the
     CDF's outputs, a month's consumption areas) stack too: three fields
     side by side left a select and a number input a few pixels each at a
     320px viewport. With the header row hidden, each field shows the label
     it already has for screen readers, so the stacked line still says
     which select is «Destinatario» and which is «Tratamiento aplicado».
     The real label is shown, undone from `.visually-hidden` by the rule
     a list's `.cell-label` shares above, so a screen reader hears each name
     once, from the one element that is both seen and read. Read-only lines
     (the ficha's) stay in columns: their cells are text and wrap. */
  .rm-lines-form > .rm-line {
    grid-template-columns: minmax(0, 1fr);
  }

  .rm-lines-form > .rm-line-head {
    display: none;
  }

  /* Type and colour come from `.rm-doc .rm-lines-form label`, as for every
     field label of a document form; only the stacking is set here. */
  .rm-lines-form .rm-line > span > .visually-hidden {
    display: block;
    margin-bottom: 4px;
  }

  .rm-lines-form .num {
    text-align: left;
  }

  /* The movement form's «Quitar» cell is a checkbox inside its label: once
     its text shows, the two sit on one row. The checkbox keeps its own
     width at every size (the document's field rule never stretches a
     checkbox), where the old `.rm-doc input`'s 100% had centred it on a
     line of its own above the text. */
  .rm-lines-form .rm-line > span > label:not(.visually-hidden) {
    display: inline-flex;
    align-items: center;
    gap: 8px;
  }

  .rm-doc {
    padding: 20px 16px;
  }

  .option-cards {
    flex-direction: column;
  }
}

/* ---------------------------------------------------------------------------
   Second factor: the enrollment QR and the sheet of recovery codes.
   ------------------------------------------------------------------------- */

.totp-qr {
  margin: 12px 0 4px;
}

.recovery-codes {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px 24px;
  margin: 0 0 24px;
  padding: 0;
  list-style: none;
  font-size: 16px;
}

/* ---------------------------------------------------------------------------
   Declarations: V4's generator «Declaraciones» page
   (src/app/dashboard/generador/declaraciones/page.tsx) -- the amber draft
   pill in the header, the amber "you download it, you file it" notice, and
   the card's eyebrow. The pending list reuses the inventory table.
   ------------------------------------------------------------------------- */

.draft-badge {
  padding: 6px 12px;
  font-size: 12px;
  font-weight: 600;
  color: var(--draft-text);
  white-space: nowrap;
  background: var(--draft-bg);
  border: 1px solid var(--warning-border);
  border-radius: 9999px;
}

/* Below a small tablet's width the draft pill and the compact buttons may
   break onto a second line. Both keep their label on one line elsewhere,
   but at a 320px viewport a card leaves ~248px for content and several
   labels run to ~370px: the pill's «Formato por confirmar con la
   especialista ambiental» reached x 365px and the network's «Descargar
   matrices de septiembre de 2026 (.zip)» x 389px (headless Chrome), and
   the monthly and annual downloads are as long. The pill's radius drops so
   two lines read as a tag rather than a capsule. */
@media (max-width: 480px) {
  .draft-badge {
    white-space: normal;
    border-radius: 10px;
  }

  .btn-inline {
    white-space: normal;
    text-align: center;
  }
}

/* A standing warning in the page, as opposed to a flash (`.flash-warning`)
   that arrives once after a redirect: the declarations' «you download it,
   you file it», and the notices on the reports, the heat map and the
   catastro publication that something is private, absent or suspended.
   Named `.declaration-notice` until November 2026, after the declarations
   page it was ported for; six pages wore that name. */
.notice-warning {
  margin: 0 0 24px;
  padding: 14px 18px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--warning-text);
  background: var(--warning-bg);
  border: 1px solid var(--warning-border);
  border-radius: 10px;
}

.card-eyebrow {
  margin: 0 0 6px;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-subtle);
}

.pending-reasons {
  margin: 0;
  padding-left: 18px;
  white-space: normal;
}

/* A counter-offer under its row in the list of offers sent: the generator's
   terms, and the sender's two answers to them. Spans the whole row, below
   the six columns. */
.offer-counter {
  grid-column: 1 / -1;
  padding-top: 8px;
}

.offer-counter small {
  display: block;
  margin-bottom: 8px;
}

.offer-counter .inline-form:first-of-type {
  margin-left: 0;
}

/* ---------------------------------------------------------------------------
   The heat map («Mapa de residuos»): the map itself, its legend and the
   swatches the legend and the table below it share. The map's height is
   fixed rather than a share of the viewport so the legend and the table stay
   reachable on a phone without scrolling past a full-screen map; full screen
   is a mode of its own, below. The legend's text is the page's normal size
   rather than `.hint`'s, because it says what a red point does and does not
   mean.
   ------------------------------------------------------------------------- */

/* Every Leaflet map -- this one and «Ubicación»'s `.location-map` -- is its
   own stacking context. Leaflet gives its panes z-index 400 to 700 and its
   controls 1000, and without isolation those numbers compete with the rest
   of the page: the open header menu (`.app-menu-panel`, z-index 10) was
   painted under the map, and its links under the zoom buttons took the taps
   meant for them (measured at 390x844 on «Ubicación», 2026-10-04).
   Isolated, the whole map paints as one layer at the page's own level, under
   anything the page positions above it, while Leaflet's layers keep their
   order inside it.

   Rejected: raising the menu's z-index above 1000. It would have to outbid
   numbers Leaflet chooses, and any version or plugin that raises them again
   brings the bug back, and it would have to stay below full screen's 2000
   (`.heat-stage.is-full-screen`, below) or the header would sit over a
   full-screen map. Isolation does not disturb full screen: the map is
   inside the fixed block whose 2000 lifts it, map and all, over the header.

   The selector is Leaflet's own class rather than this file's map classes,
   so a map added on another page is isolated without anyone remembering
   to; before the script runs there is no class and nothing to isolate. */
.leaflet-container {
  isolation: isolate;
}

.heat-map {
  height: 420px;
  margin-bottom: 16px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.heat-legend p {
  margin: 8px 0 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text);
}

.heat-scale {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 16px;
  margin: 0;
  padding: 0;
  font-size: 13px;
  list-style: none;
}

.heat-swatch {
  display: inline-block;
  width: 12px;
  height: 12px;
  margin-right: 6px;
  vertical-align: -1px;
  border: 1px solid rgba(0, 0, 0, 0.35);
  border-radius: 50%;
}

/* A table whose figures carry their breakdown: each breakdown on its own
   line, and the long folios allowed to wrap at their hyphens instead of
   widening the table. The heat map's tables and «Catastro»'s list; named
   `.heat-points` until November 2026. `.heat-swatch` keeps the map's name
   on «Catastro» on purpose: there it shows the colour the branch takes on
   the map, so a change to the map's swatch must change it too.

   A known gap, left open on purpose: the swatch's colour is an inline
   `style` (heat_map.html and catastro.html); the landing page's twelve
   card colours were inline too, until its November 2026 redesign drew
   every colour from a class. Nothing breaks today, since the site sends no
   Content-Security-Policy; one without `'unsafe-inline'` for styles would
   blank them. The fix then is a class per intensity step
   (`[data-intensity=…]`), whose colours are a closed set the server
   computes. */
.breakdown-table small {
  display: block;
}

.breakdown-table a,
.breakdown-table [data-need] {
  overflow-wrap: anywhere;
}

/* The heat map's popup (static/js/heat_map.js): its paragraphs and its
   list, side by side. Until November 2026 the list's rule sat two hundred
   lines further down, after the full-screen rules. */
.heat-popup p {
  margin: 0 0 4px;
}

.heat-popup ul {
  margin: 0 0 4px;
  padding-left: 16px;
}

/* The filter panel: the heat map's filters and layer switches, which sit
   with the map in one block (`.heat-stage`) so full screen keeps them at
   hand, and «Catastro»'s search, which filters the same inventory by the
   same categories. Named `.heat-filters`, `.heat-region`,
   `.heat-filter-actions` and `.heat-filter-note` until November 2026, when
   «Catastro» wore the map's names: its «Nombre o R.U.T.» search was a
   `.heat-region`. `.filter-panel-field` is a labelled control of the
   panel, its label's text above it. */
.filter-panel {
  display: flex;
  flex-wrap: wrap;
  gap: 12px 24px;
  align-items: flex-start;
  margin-bottom: 12px;
  padding: 12px 16px;
  font-size: 13px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.filter-panel fieldset {
  margin: 0;
  padding: 0;
  border: 0;
}

.filter-panel legend,
.filter-panel-field {
  font-weight: 600;
  color: var(--text-strong);
}

/* Each checkbox row at least 24px tall, its box centred in it: a 13px box
   in a 20.8px row, the rows stacked with no gap, had each box's 24px circle
   overlapping the next one's, so it met neither of WCAG 2.5.8's two ways
   through (a 24x24 target, or one spaced so its 24px circle touches no
   other target); axe counted 324 such boxes on the heat map. Spaced
   rather than enlarged: the platform's checkboxes are the browser's own
   size everywhere else, and a 24px box here would be the only one. The
   label stays the click target for the whole row, as before. The same
   panel serves «Catastro»'s category filter. */
.filter-panel fieldset label {
  display: flex;
  align-items: center;
  gap: 4px;
  min-height: 24px;
  font-weight: 400;
}

/* The input is «Catastro»'s «Nombre o R.U.T.» search, which the heat map
   does not have; it is drawn as the select beside it. Until November 2026
   it took `.rm-doc input`'s full-width document field instead, and the
   panel's other controls on that page took the document's type. */
.filter-panel-field input,
.filter-panel-field select {
  display: block;
  margin-top: 2px;
  padding: 6px 8px;
  font-weight: 400;
}

/* The panel's controls ring with the frame's focus colour. They used to
   on «Catastro» only, through the document rule that reached them there,
   and drew the browser's own ring on the heat map. */
.filter-panel input:focus-visible,
.filter-panel select:focus-visible {
  outline: 2px solid var(--brand-700);
  outline-offset: 2px;
}

.filter-panel-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-self: center;
}

.filter-panel-note {
  flex-basis: 100%;
  margin: 0;
}

/* Carriers and plants: a lettered marker rather than a coloured one, so the
   two layers are told apart from the waste points, and from each other,
   without telling colours apart. */
.heat-place {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  margin-right: 6px;
  font-size: 11px;
  font-weight: 700;
  line-height: 1;
  color: #ffffff;
  border: 1px solid var(--marker-edge);
  border-radius: 3px;
}

.heat-place--carrier {
  background: var(--marker-carrier);
}

.heat-place--plant {
  background: var(--marker-plant);
}

.heat-places {
  margin-top: 8px;
}

/* Full screen: the block of filters and map laid over the whole window. The
   same rule serves the browser's own full screen and the fallback without it
   (or without JavaScript), so both look alike. Above Leaflet's panes and
   controls, whose z-index reaches 1000 -- isolated inside the map now (see
   `.leaflet-container`), so 2000 is what lifts the block over the header,
   its open menu included. */
.heat-stage.is-full-screen {
  position: fixed;
  inset: 0;
  z-index: 2000;
  display: flex;
  flex-direction: column;
  padding: 12px;
  overflow: auto;
  background: var(--surface-muted);
}

.heat-stage.is-full-screen .heat-map {
  flex: 1 1 auto;
  min-height: 320px;
  height: auto;
  margin-bottom: 0;
}

body:has(.heat-stage.is-full-screen) {
  overflow: hidden;
}

/* ---------------------------------------------------------------------------
   «Ubicación»: the small map where a branch places its point. Small on
   purpose -- it locates one yard, not a region -- and fixed in height so the
   fields under it stay in reach on a phone.
   ------------------------------------------------------------------------- */

.location-map {
  height: 280px;
  margin: 18px 0 8px;
  background: var(--surface-muted);
  border: 1px solid var(--border);
  border-radius: 12px;
}

/* The private-mode page: what the mode hides and what it does not, as two
   short lists, so neither reads as a footnote of the other. */
.private-mode-scope {
  margin: 8px 0 0;
  padding-left: 18px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text);
}

/* A generator's operational notes, on its own page and on the sistema de
   gestión's network screens: free text typed by the company, so a long word
   or a pasted URL wraps instead of widening the page past a phone's screen. */
.operational-notes {
  font-size: 14px;
  line-height: 1.5;
  overflow-wrap: anywhere;
}

/* The printable ficha (templates/traceability/movement_print.html): the
   bar above the document, and the route as a numbered list of legs, each
   leg's facts one per line. A refused leg is marked as the ficha's notice
   is, in red, so the refusal stands out on paper as it does on screen. */
.rm-print-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
  max-width: 720px;
  margin: 0 auto 16px;
}

.rm-route ol {
  margin: 0;
  padding-left: 22px;
  font-size: 13px;
}

.rm-route li {
  margin-bottom: 12px;
}

.rm-route li > strong,
.rm-route li > span {
  display: block;
}

.rm-route-rejection {
  color: var(--error-strong);
}

/* ---------------------------------------------------------------------------
   Printing. The monthly bulletin (templates/reporting/bulletin.html) is
   printed by the browser -- its «Imprimir» is how it becomes a PDF, since no
   PDF is generated on the server -- so a printed page drops what only works
   on screen: the app's header with its navigation, and anything marked
   ``no-print`` (the month selector, the download link). Cards lose their
   shadow and padding so the tables use the paper's width, and a scrolling
   table is printed whole rather than clipped. Written for every page rather
   than for the bulletin's alone: no printed page needs the navigation. The
   movement's printable ficha (templates/traceability/movement_print.html)
   is printed the same way.
   ------------------------------------------------------------------------- */

@media print {
  body {
    background: var(--surface);
  }

  /* No `!important`: this query is the last rule for both selectors, and
     every element marked `no-print` (bulletin.html) is styled by one class
     at most, so the later rule of equal or higher specificity wins without
     it. A `no-print` on something a more specific rule displays would need
     it back, and test_frame.py fails if the marked elements change. */
  .app-header,
  .no-print {
    display: none;
  }

  .card {
    box-shadow: none;
    padding: 0;
    break-inside: avoid;
  }

  .card + .card {
    margin-top: 24px;
  }

  .table-scroll {
    overflow: visible;
  }

  .bulletin-table tr {
    break-inside: avoid;
  }

  /* The printable ficha uses the paper's width, and no leg of its route or
     line of its CDF is split across two pages. */
  .rm-print {
    max-width: none;
    padding: 0;
    box-shadow: none;
  }

  .rm-route li,
  .rm-line {
    break-inside: avoid;
  }
}
