/*
 * 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: V4 loads Geist through next/font, which self-hosts it at build time.
 * Nothing loads it here, so the stack falls through to Arial, which is V4's
 * own declared fallback. Loading it from Google Fonts 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.
 * Self-hosting the font files is the open alternative.
 */

: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 landing and login gradients and the landing's translucent
     whites, 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 accent bar of the landing page's note: the blue end of the landing
     gradient, which stays literal there as decoration used once. */
  --info-accent: #2563eb;
  /* 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): src/app/page.tsx's inline
   values as classes. Its 3-column grids, 2-column panel and 3-column figures
   collapse to one column below a small tablet's width, where V4 overflowed
   instead. Each card's own colours stay inline in the template, as V4 kept
   them in its data arrays.

   **Every grid track is `minmax(0, 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 first cut used it, and on a phone the
   one-column panel grew to the width of the figures box inside it (the
   three figures side by side, «SINADER» at 24px bold, the 48px «G → T →
   G»), so the page was 393px wide at a 320, 360 and 390px viewport and
   scrolled sideways. With `minmax(0, 1fr)` a track is as wide as the
   container gives it and its content wraps; the phone rules below then
   make what cannot wrap (the chain line, the figures) small enough to fit
   a 320px viewport, measured in headless Chrome at 320, 360 and 390px
   with no horizontal scroll.
   ------------------------------------------------------------------------- */

.landing {
  min-height: 100vh;
  background: linear-gradient(to bottom right, #ecfdf5, #ffffff, #eff6ff);
}

.landing-wrap {
  max-width: 1280px;
  margin: 0 auto;
}

.landing-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: rgba(255, 255, 255, 0.8);
  backdrop-filter: blur(6px);
  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
}

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

.landing-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-weight: 700;
  font-size: 20px;
  color: var(--brand-900);
  letter-spacing: -0.01em;
}

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

.landing-link {
  padding: 8px 24px;
  font-weight: 500;
  color: var(--brand-700);
  text-decoration: none;
  transition: color 150ms;
}

.landing-link:hover {
  color: var(--brand-800);
}

.landing-hero {
  padding: 80px 24px;
  text-align: center;
}

.landing-hero-logo {
  display: flex;
  justify-content: center;
  margin-bottom: 32px;
}

.landing-hero-logo img {
  width: 140px;
  height: 140px;
  object-fit: contain;
  filter: drop-shadow(0 0 48px rgba(52, 211, 153, 0.35));
}

.landing-hero h1 {
  margin: 0 0 24px;
  font-size: clamp(2.25rem, 1rem + 4vw, 3.75rem);
  font-weight: 700;
  line-height: 1.1;
  color: var(--text-strong);
}

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

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

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

.landing-section {
  padding: 80px 24px;
}

.landing-section-tint {
  background: rgba(255, 255, 255, 0.5);
}

.landing-section-muted {
  background: var(--surface-muted);
}

.landing-section-head {
  margin-bottom: 64px;
  text-align: center;
}

.landing-section h2 {
  margin: 0 0 16px;
  font-size: 36px;
  font-weight: 700;
  line-height: 1.2;
  color: var(--text-strong);
}

.landing-section-head p {
  max-width: 800px;
  margin: 0 auto;
  font-size: 20px;
  color: var(--text-muted);
}

.landing-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 32px;
}

.landing-feature,
.landing-product {
  border-radius: 16px;
  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.08);
  transition: box-shadow 200ms;
}

.landing-feature {
  padding: 32px;
  background: var(--surface);
}

.landing-product {
  padding: 24px;
}

.landing-feature:hover,
.landing-product:hover {
  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.12);
}

.landing-feature-icon,
.landing-product-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 12px;
}

.landing-feature-icon {
  width: 56px;
  height: 56px;
  margin-bottom: 24px;
}

.landing-product-icon {
  width: 48px;
  height: 48px;
  margin-bottom: 16px;
}

.landing-feature h3,
.landing-product h3 {
  margin: 0 0 12px;
  font-size: 20px;
  font-weight: 700;
  color: var(--text-strong);
}

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

/* The semáforo card's legend (_status_legend.html): its meanings are a
   <dd>, not a <p>, so they take the card's muted text here rather than
   the body's ink, and read as the rest of the card does. */
.landing-feature .status-legend dd {
  color: var(--text-muted);
}

.landing-product p {
  font-size: 14px;
}

.landing-note {
  margin-top: 48px;
  padding: 24px;
  background: var(--info-bg);
  border-left: 4px solid var(--info-accent);
  border-radius: 8px;
}

.landing-note h3 {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 8px;
  font-size: 18px;
  font-weight: 700;
  color: var(--text-strong);
}

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

.landing-panel {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 48px;
  align-items: center;
  padding: 64px 48px;
  color: #ffffff;
  background: linear-gradient(to bottom right, #059669, #2563eb);
  border-radius: 24px;
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}

.landing-panel h2 {
  margin-bottom: 24px;
  color: #ffffff;
}

.landing-checks {
  display: flex;
  flex-direction: column;
  gap: 16px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.landing-checks li {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  font-size: 18px;
}

.landing-checks svg {
  flex-shrink: 0;
  margin-top: 4px;
}

.landing-panel-box {
  padding: 32px;
  text-align: center;
  background: rgba(255, 255, 255, 0.1);
  backdrop-filter: blur(8px);
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 16px;
}

.landing-chain {
  margin-bottom: 8px;
  font-size: 48px;
  font-weight: 700;
}

.landing-chain-label {
  margin: 0 0 32px;
  font-size: 20px;
}

.landing-figures {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 24px;
  margin: 0;
}

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

.landing-figures dd {
  margin: 0;
  font-size: 14px;
  opacity: 0.9;
}

.landing-closing {
  max-width: 900px;
  text-align: center;
}

.landing-closing > p {
  margin: 0;
  font-size: 20px;
  color: var(--text-muted);
}

.landing-footer {
  padding: 48px 24px;
  color: #ffffff;
  background: var(--text-strong);
}

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

.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;
}

.landing-footer-brand span {
  font-size: 14px;
  color: var(--placeholder);
}

.landing-footer-legal p {
  margin: 0;
  color: var(--placeholder);
}

.landing-footer-legal p + p {
  margin-top: 8px;
  font-size: 14px;
  color: var(--text-subtle);
}

@media (max-width: 720px) {
  .landing-grid,
  .landing-panel,
  .landing-figures {
    grid-template-columns: minmax(0, 1fr);
  }

  .landing-figures {
    gap: 16px;
  }

  .landing-panel-box {
    padding: 24px 16px;
  }

  .landing-chain {
    font-size: 32px;
  }

  .landing-chain-label {
    font-size: 17px;
  }

  .landing-checks li {
    font-size: 16px;
  }

  .landing-hero,
  .landing-section {
    padding: 56px 16px;
  }

  .landing-header-row {
    padding: 12px 16px;
  }

  .landing-link {
    padding: 8px 0;
  }

  .landing-section-head {
    margin-bottom: 40px;
  }

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

  .landing-panel {
    padding: 40px 24px;
  }

  .landing-figures dt {
    font-size: 24px;
  }
}

/* ---------------------------------------------------------------------------
   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), as are the landing page's
   twelve card colours. 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;
  }
}
