/* App shell and page blocks (formerly the `@layer components` blocks of tailwind.css): the second half of the component cascade, after
 * components.css and before the unlayered rest in app.css. Order is load-bearing, see Layout.kt STYLESHEETS. */
/* Mobile shell: top/bottom chrome respects iOS safe areas. */
/* All padding is owned here (not via p-* or px-* utilities on .app-main),
   because the utilities layer outranks the components layer: a shorthand
   like `p-3` would clobber this bottom padding and let the fixed bottom
   nav crop the last content. Horizontal lives here too — the gutter must
   grow to the landscape notch (viewport-fit=cover), which utilities
   can't express. */
.app-main {
    /* Top padding reserves the fixed mobile top-bar strip so page headers clear it — the
       STATIC (--mobile-menu-top-strip, not the "live" one) var deliberately: the bar only
       slides via transform (M1 hide-on-scroll, ts/appBar.ts), it never changes size, so this
       padding must never jump with it or the page would reflow on every scroll direction
       change. Bottom reserves the bottom nav bar's strip on shell pages (M2), or just the iOS
       safe area on the bar-less pre-auth login. Scroll anchoring lives on html (the scroll
       container), not here — .app-main never scrolls. */
    padding-top: var(--mobile-menu-top-strip);
    padding-bottom: calc(var(--mobile-chrome-bottom-strip) + 1.5rem);
    padding-inline: max(env(safe-area-inset-left), 0.75rem) max(env(safe-area-inset-right), 0.75rem);
}

/* Back-to-top button (layout.html, shown by ts/backToTop.ts): floats in the
   bottom-right corner, above the M2 bottom nav bar (or the iOS home indicator on
   bar-less pages) and pushed in from the landscape notch. z-20 keeps it under the
   mobile menu overlay (z-30), so an open menu dims it like the rest of the page. */
.back-to-top {
    position: fixed;
    right: max(env(safe-area-inset-right), 1rem);
    bottom: calc(var(--mobile-chrome-bottom-strip) + 1rem);
    z-index: var(--z-topbar);
}

/* List pages (the only pages that show the button) end 4rem lower — the
   button's 3rem plus its 1rem gap — so at the very bottom of the last page
   the last row's actions sit above the button, not under it. Static rather
   than tied to the shown state: growing the page while scrolled is a jump. */
html:has(#list-content) .app-main {
    padding-bottom: calc(var(--mobile-chrome-bottom-strip) + 1.5rem + 4rem);
}

/* Mobile top app bar: pushed clear of the status-bar safe area; M1 height 48px (secondary to
   the M2 bottom nav, which is the phone's primary navigation). Pinned: it carries the page's
   scope in its title (ts/appBar.ts), so it never slides away on scroll. */
.mobile-topbar {
    padding-top: var(--mobile-safe-area-top);
    /* inset-x-0 bar reaches the landscape notch — pad past the cutout
       (0.5rem is the navbar's own gutter). */
    padding-inline: max(env(safe-area-inset-left), 0.5rem) max(env(safe-area-inset-right), 0.5rem);
    /* M1: the bar IS 48px. DaisyUI's navbar gutter (0.5rem) plus the 48px back button
       measured 56 on every child page. */
    padding-block: 0;
    min-height: var(--mobile-menu-top-strip);
}

/* M11: the bar's chrome — page title (or brand on chrome-less pages). The scope rides the title (pinned top bar,
   house rule), so a long "8775M.2/8775.1M.2/8939M · VedaJato" wraps to a second line instead of ending in "… · Ve…"
   (QA walk 2026-10-21): at --t-title two lines of 1.2 are exactly the 48 px bar (M1); the clamp stops a third line
   (titles past ~50 chars at 360 px) from growing it over the content. */
.topbar-title {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
    font-size: var(--t-title);
    font-weight: 600;
    line-height: 1.2;
}

.topbar-back {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: var(--ctl-l);
    min-width: var(--ctl-l);
}

/* Alerts and toasts carry the hue in the FILL like the status pills (TONES.md): -100 tint under
   the dark same-hue text. daisyUI's default is a solid fill with white text, where a btn-outline
   or btn-neutral inside (the orçamento "Atualizar preços" banner) goes dark on dark. Info text is
   blue-800, not `info`: #2563EB on the blue-100 tint measures 4.24:1, under the 4.5:1 floor. */
.alert-success {
    border-color: color-mix(in srgb, var(--c-success) 40%, transparent);
    background-color: var(--c-success-soft);
    color: var(--c-success);
}
.alert-warning {
    border-color: color-mix(in srgb, var(--c-warning) 40%, transparent);
    background-color: var(--c-warning-soft);
    color: var(--c-warning);
}
.alert-error {
    border-color: color-mix(in srgb, var(--c-error) 40%, transparent);
    background-color: var(--c-error-soft);
    color: var(--c-error);
}
.alert-info {
    border-color: color-mix(in srgb, var(--c-info) 40%, transparent);
    background-color: var(--c-info-soft);
    color: var(--c-info-text);
}

/* M2 bottom navigation bar — the phone's primary nav, present on every logged-in shell page.
   Equal-width tabs, icon over always-visible label, safe-area padded. */
.mobile-bottom-nav {
    display: flex;
    align-items: stretch;
    height: calc(var(--mobile-bottom-nav-height) + var(--mobile-safe-area-bottom));
    padding-bottom: var(--mobile-safe-area-bottom);
    padding-inline: max(env(safe-area-inset-left), 0px) max(env(safe-area-inset-right), 0px);
    border-top: 1px solid var(--g-200);
    background-color: var(--g-0);
}

.bottom-nav-tab {
    flex: 1 1 0%;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.125rem;
    min-height: var(--ctl-l);
    padding: 0.25rem 0;
    color: color-mix(in srgb, var(--g-900) calc(0.6 * 100%), transparent); /* 4.96:1 on base-100 — same ratio as .label-text-alt above */
}

/* M3 indicator: a primary-tinted pill behind the icon, plus primary text. */
.bottom-nav-tab[aria-current="page"] {
    color: var(--c-primary);
    font-weight: 600;
}

.bottom-nav-icon {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0.125rem 0.75rem;
    border-radius: 9999px;
}

.bottom-nav-tab[aria-current="page"] .bottom-nav-icon {
    background-color: color-mix(in srgb, var(--c-primary) calc(0.14 * 100%), transparent);
}

.bottom-nav-label {
    font-size: 12px;
    line-height: 1;
}

.bottom-nav-badge {
    position: absolute;
    top: -0.125rem;
    right: 0.125rem;
    min-width: 1rem;
    height: 1rem;
    padding: 0 0.25rem;
    border-radius: 9999px;
    font-size: var(--t-micro);
    line-height: 1rem;
    text-align: center;
    background-color: var(--c-error);
    color: var(--c-error-ink);
}

/* Empty (count 0) means the server rendered no text — hide the dot entirely rather than
   show a blank pill. */
.bottom-nav-badge:empty {
    display: none;
}

/* M6: the task's main action in the thumb zone. `bottom` is the shell's reserved bottom
   strip, not 0 — `.mobile-bottom-nav` is `fixed inset-x-0 bottom-0 z-50 lg:hidden`
   (Layout.kt), and a bottom-0 bar would sit under it on the phone. Static, borderless,
   unpadded from `lg` up, where there is no fixed bottom bar to clear. */
.sticky-action-bar {
    position: sticky;
    z-index: var(--z-sticky);
    background-color: var(--g-0);
    border-top-width: 1px;
    border-color: var(--g-200);
    padding: var(--s3);
    bottom: var(--mobile-chrome-bottom-strip);
}

@media (min-width: 1024px) {
    .sticky-action-bar {
        position: static;
        border-width: 0;
        padding: 0;
        bottom: 0;
    }
}

/* M6: the extended-FAB twin of `.sticky-action-bar` above, for a page whose primary action is
   additive rather than a form save — bottom-right, 16px (1rem) clear of the M2 bottom nav
   (or the safe area on a bar-less page), same shape as `.back-to-top`. the header's FAB (`PageHeader.primary` with `PhoneFace.FAB`) already
   carries `lg:hidden`, so this never needs its own breakpoint. */
.fab {
    position: fixed;
    right: max(env(safe-area-inset-right), 1rem);
    bottom: calc(var(--mobile-chrome-bottom-strip) + 1rem);
    z-index: var(--z-topbar);
}
/* A blocked FAB still floats over the list: the kit's disabled fill is 20 % alpha, so the row under it showed through its
   label (phone /estoque with every unidade in scope, 2026-10-23). Same tint, laid on the page background. */
.fab.btn-disabled { background-color: color-mix(in srgb, var(--c-neutral) 20%, var(--c-bg)); }

/* R14: an empty list carries its create action in the list body (the empty prompt, one declaration), so the FAB
   steps aside. CSS rather than a server flag: it stays right across the fragment swaps that empty or fill the list. */
body:has(#list-content .empty-prompt) .fab {
    display: none;
}

/* A list page shows both after a scroll: the FAB keeps the corner (it is the page's primary
   action), back-to-top stacks above it — the FAB's 3rem `btn` plus a 0.5rem gap. Below lg
   only, where `.fab` is displayed; `body:has()` cannot see `lg:hidden`. */
@media (max-width: 1023.98px) {
    body:has(.fab) .back-to-top {
        bottom: calc(var(--mobile-chrome-bottom-strip) + 1rem + 3.5rem);
    }
}

@media (max-width: 1023.98px) {
    body.sidebar-open {
        overflow: hidden;
    }

    body.sidebar-open .sidebar-shell {
        overscroll-behavior: contain;
    }
}

@media (min-width: 1024px) {
    .app-main {
        padding-top: 1.25rem;
        padding-bottom: 1.25rem;
        padding-inline: 1.25rem;
    }

    /* Same 4rem back-to-top reservation as the mobile rule above. */
    html:has(#list-content) .app-main {
        padding-bottom: calc(1.25rem + 4rem);
    }
}
/* ── Global card / form overflow protection ── */
/* Prevent long text (emails, URLs, addresses) from blowing out card widths
   without clipping form overlays, focus rings, or sticky action bars. */
.card,
.card-body,
#content,
form,
fieldset,
.form-control {
    min-width: 0;
}

/* hx-boost scrolls #content into view after each swap (htmx
   scrollIntoViewOnBoost defaults true). The fixed .mobile-topbar is out of
   flow, so without a scroll margin the swapped page lands behind the bar.
   scroll-margin-top is the property scrollIntoView honors on the target. */
#content {
    scroll-margin-top: var(--mobile-topbar-live-strip);
}

@media (min-width: 1024px) {
    /* No fixed bar on desktop — align to the top edge. */
    #content {
        scroll-margin-top: 1.25rem;
    }
}

.card {
    overflow: visible;
}

@media (max-width: 1023.98px) {
    /* M22: list rows follow M3 list items — 56px for a one-line row. Nothing else about the
       shell changes, so this stays off the `listItemShell` call sites. */
    .entity-list-item {
        min-height: var(--row);
    }
}

/* Just-saved row. Lists re-render in their own sort order, so the record the operator just
   created usually lands off-screen and the success toast was the only evidence it existed.
   Save redirects carry the row's DOM id as a URL fragment (UserController, CatalogoController);
   the browser does the scrolling natively and this is only the "here it is" marker. Scroll
   margin clears the fixed mobile top bar, same value #content uses. */
.entity-list-item:target,
[data-categoria-card]:target {
    box-shadow: var(--focus);
    scroll-margin-top: var(--mobile-topbar-live-strip);
}

@media (min-width: 1024px) {
    .entity-list-item:target,
    [data-categoria-card]:target {
        scroll-margin-top: 1.25rem;
    }
}

/* Jumped-to field error. The validation summary links each entry to its inline message
   (fragments/error.html), and without the margin the browser parks it under the fixed mobile
   top bar — the operator lands on the error and cannot see it. No ring here: the message is
   already red, and the control beside it already carries input-error. */
[id$="-error"]:target {
    scroll-margin-top: calc(var(--mobile-topbar-live-strip) + 2rem);
    scroll-margin-bottom: 5rem;
}

@media (min-width: 1024px) {
    [id$="-error"]:target {
        scroll-margin-top: 3.25rem;
        scroll-margin-bottom: 0;
    }
}

.card-body :where(p, span, dd, dt, li, h1, h2, h3, h4, h5, h6) {
    overflow-wrap: anywhere;
}

:where(.input, .select, .textarea, .file-input) {
    max-width: 100%;
}

/* Legend primitives shared by all calendar/list legends (server fragments in
   search-filters.html + user-dot.html, client builder in calendarLegend.ts).
   Swatch classes are STANDALONE: an element carries exactly one of them, never
   base+modifier combined — dot vs stripe conflict on width/border-radius, so an
   additive modifier would resolve by source order. */
.legend-strip {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s3);
    --gap: var(--s3);
    margin-bottom: var(--s4);
    font-size: var(--t-caption);
    line-height: 1rem;
}

.legend-item {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
}

/* Names the dimension the swatches encode ("Criado por:"). Without it the strip is a row of
   bare names beside coloured stripes and reads as stray data, not as a legend. */
.legend-title {
    display: inline-flex;
    align-items: center;
    font-weight: 600;
    color: color-mix(in srgb, var(--g-900) 60%, transparent);
}

@media (min-width: 1024px) {
    .legend-title {
        text-transform: uppercase;
        letter-spacing: 0.025em;
    }
}

/* bg-base-300 is a deliberate fallback: marker swatches are always painted over
   via data-swatch-color; it only shows for creator/user dots missing a color. */
/* --dot: the same token the agenda card's attribution dot reads (tokens.css) */
.legend-swatch {
    border-radius: 9999px;
    flex-shrink: 0;
    background-color: var(--g-200);
    width: var(--dot);
    height: var(--dot);
}

.legend-swatch--stripe {
    width: var(--s1);
    border-radius: 0.125rem;
    flex-shrink: 0;
    background-color: var(--g-200);
    height: var(--dot);
}

.legend-swatch--ring {
    border-radius: 9999px;
    flex-shrink: 0;
    border-width: 2px;
    background-color: transparent;
    width: var(--dot);
    height: var(--dot);
}
/* ── Design system overrides ── */
/* Form labels: small tracked labels (design system spec), upper-cased from `lg` up only.
   `legend.label-text` is in the selector because a fieldset's group label is a field label too
   — it just has no `.label` wrapper to be nested in. Without it every checkbox/radio group
   ("Tipos de impermeabilização", the Sim/Não groups) printed one case beside another in the
   same section.

   M12: no ALL-CAPS labels on the phone — capitals hide the shape of the word, and our readers
   go word by word. Size, weight and colour are unchanged; only the transform is desktop-only. */
.label .label-text,
legend.label-text {
    font-size: 0.75rem;
    font-weight: 600;
    /* solid premixed ink (base-content at 85% over white) — predictable over tinted rows */
    color: #353b47;
}

@media (min-width: 1024px) {
    .label .label-text,
    legend.label-text {
        text-transform: uppercase;
        letter-spacing: 0.05em;
    }
}

/* Hints under a field are prose, not a second label — uppercasing them made every
   help line shout as loud as the field it explains. Status alts keep their color. */
.label .label-text-alt:not(.ink-error):not(.ink-success) {
    font-size: 0.75rem;
    color: color-mix(in srgb, var(--g-900) calc(0.6 * 100%), transparent);
}

/* A field label and its hint share one row (.label is space-between). Long pt-BR hints
   ran into the label; let the hint drop to its own line instead of colliding.

   padding-inline: 0 drops DaisyUI's 0.25rem side padding. It put every field label 4px
   right of its own input box, while a fieldset's group label — `legend.label-text`, which
   the rule above styles to look identical but which has no `.label` wrapper to inherit the
   padding — sat flush at 0. Two headings that read as the same thing, stacked down one
   column on two different left edges (122 labels across the form pages). Flush lines the
   label up with its control, the legend and the section head.

   Vertical padding stays. Hints that used to carry `ml-1` to match the old inset lost it
   in the same change. */
.label {
    flex-wrap: wrap;
    column-gap: 0.5rem;
    padding-inline: 0;
}

/* Clickable choices are values, not field labels. Keep them readable on mobile. */
.label:is(.cursor-pointer, .label-pick) .label-text {
    color: var(--g-900);
    font-size: 0.875rem;
    font-weight: 500;
    letter-spacing: 0;
    text-transform: none;
}

.label:is(.cursor-pointer, .label-pick) :where(.radio, .checkbox) {
    flex-shrink: 0;
}

/* Buttons: keep icon + text on one line; narrow mobile headers can opt into icon-only labels. */
.btn {
    white-space: nowrap;
}

/* Below ~400px a long pt-BR label can outgrow the row and be silently
   clipped by .app-main's overflow-x-hidden — let it wrap instead.
   height: auto lets the button grow; DaisyUI min-heights keep the size. */
@media (max-width: 399.98px) {
    .btn {
        white-space: normal;
        height: auto;
    }
}

.btn > svg {
    flex-shrink: 0;
}

/* Cards: border only, no shadow (calm / flat design) */
.card:not(.dropdown-content) {
    border: 1px solid var(--g-200);
    box-shadow: none;
}
/* ── Numbered form sections + side rail ── */
/* What globalHandlers.ts swaps in for an upload whose file is gone: it inherits the image's
   size class (`media-thumb-img`, 160 px square) and reads as a muted box, not loose text. `!inline-flex` because an inherited
   `block` would otherwise win on display. */
.image-unavailable {
    display: inline-flex !important;
    align-items: center;
    justify-content: center;
    background-color: var(--g-100);
    padding: var(--s2);
    text-align: center;
    font-size: var(--t-caption);
    line-height: 1rem;
    color: color-mix(in srgb, var(--g-900) 60%, transparent);
}
/* Shared form language: a stacked run of numbered section cards in a centered
   column, with a sticky rail on the left that jumps to (and highlights) them.
   Rolled out on orcamento first; any form adopts it by wrapping field groups in
   .form-section-card + .form-section-grid. */
.form-section-card {
    border-radius: var(--radius);
    border-width: 1px;
    border-color: var(--g-200);
    background-color: var(--g-0);
    padding: var(--s4);
}

/* Two tiers: the roomier padding starts at 1024, not at the 640 step it used to ride. */
@media (min-width: 1024px) {
    .form-section-card {
        padding: 1.25rem var(--s5);
    }
}

/* flex-wrap, because a head carrying `extra` badges (the detail page's
   Infiltrações section shows a count and a total) overran 360px in one
   unwrappable row and pushed its own collapse chevron past the viewport —
   the affordance vanished on the section that most needed it. */
.form-section-head {
    margin-bottom: var(--s4);
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--s2);
    --gap: var(--s2);
    border-bottom-width: 1px;
    border-color: var(--g-200);
    padding-bottom: var(--s2);
}

.form-section-num {
    font-size: var(--t-label);
    line-height: 1.25rem;
    font-weight: 600;
    letter-spacing: 0.1em;
    color: color-mix(in srgb, var(--c-primary) 80%, transparent);
}

.form-section-title {
    flex: 1 1 0%;
    font-size: var(--t-body);
    line-height: 1.5rem;
    font-weight: 700;
    color: var(--c-primary);
}

@media (min-width: 1024px) {
    .form-section-title {
        text-transform: uppercase;
        letter-spacing: 0.025em;
    }
}

/* Collapsible section: <details class="form-section-card"> whose <summary> IS the
   .form-section-head (fragments/form-sections :: section-summary). The native marker is
   hidden in favour of the trailing chevron, which flips while open; a closed card drops
   the head's bottom rule so it reads as a single bar. Same markup on the orçamento form,
   the infiltrações page and the orçamento detail page. */
.form-section-card > summary.form-section-head {
    cursor: pointer;
    -webkit-user-select: none;
    user-select: none;
    list-style-type: none;
    align-items: center;
}

.form-section-card > summary.form-section-head::-webkit-details-marker {
    display: none;
}

.form-section-card:not([open]) > summary.form-section-head {
    margin-bottom: 0;
    border-bottom-width: 0;
    padding-bottom: 0;
}

.form-section-chevron,
.detail-comments-chevron,
.disclosure-chevron {
    height: var(--s4);
    width: var(--s4);
    flex-shrink: 0;
    color: color-mix(in srgb, var(--g-900) 60%, transparent);
    transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}

.form-section-chevron,
.detail-comments-chevron {
    margin-left: auto;
}

.form-section-card[open] > summary .form-section-chevron {
    transform: rotate(180deg);
}

/* M21: a phone FORM is one flat page. The card border/padding/background and the collapse
   affordance go, so each section reads as a plain heading over its fields — the section
   numbers already hide below lg (`form-section-num hidden lg:inline`). CSS cannot open a
   closed <details>, so phoneForm.ts sets `open` on every card while this query matches;
   the server's own open/errors-force-open decision still owns desktop.
   Scoped to `form .form-section-card`: a detail/dashboard page or the carro checklist's
   categoria accordion (outside any <form>, its inputs reach the form via `form=` attribute)
   reuses the same card class for its own collapse look and must keep its own open/closed
   state and tap affordance on a phone (L3-1). */
@media (max-width: 1023.98px) {
    form .form-section-card {
        border-radius: 0;
    border-width: 0;
    background-color: transparent;
    padding: 0;
    }

    form .form-section-card > summary.form-section-head {
        pointer-events: none;
    }

    form .form-section-card .form-section-chevron {
        display: none;
    }
}

/* The compact client-comments disclosure cannot use DaisyUI's collapse-arrow: that arrow
   has a fixed top offset sized for the default (taller) collapse title. Keep this chevron in
   normal flex flow so it remains vertically centered with the compact title. */
.detail-comments-collapse > summary.collapse-title {
    display: flex;
    align-items: center;
    gap: var(--s2);
    --gap: var(--s2);
}

.detail-comments-collapse[open] .detail-comments-chevron {
    transform: rotate(180deg);
}

/* Generic disclosure marker for a plain <details> that is not a DaisyUI collapse. The UA
   ::marker triangle ignores the palette, sizes itself off the font, and cannot animate, so
   every disclosure outside the collapse components looked foreign next to the SVG chevrons. */
.disclosure-summary {
    display: flex;
    list-style-type: none;
    align-items: center;
    gap: var(--s2);
    --gap: var(--s2);
}

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

details[open] > .disclosure-summary .disclosure-chevron {
    transform: rotate(180deg);
}

/* Field grid: one column below 1024 (the phone layout at every width), two from 1024 up.
   Flex-wrap rather than grid so a field left alone on its row — odd field
   count, or a neighbour that spans the row — grows into the empty half
   instead of stopping mid-card. */
.form-section-grid {
    display: flex;
    flex-wrap: wrap;
    /* one full spacing step between field rows (was 14/18 px): at 14 px a stacked pair of
       labelled fields read as one block (user 2026-10-14 "more spacing on forms"). From lg the
       two-column rhythm below tightens the rows to --s4. */
    gap: var(--s5);
}

/* `.form-cells` / `.telefone-dual` wrappers (#orcamento-contact-fields, telefoneInputDual) dissolve into the
   grid, so their children ARE the flex items — without this arm they'd fall back to
   `flex: 0 1 auto` and shrink to content width instead of filling their cell. */
.form-cells {
    display: contents;
}

.form-section-grid > *,
.form-section-grid > :is(.form-cells, .telefone-dual) > * {
    flex: 1 1 100%;
    min-width: 0;
}

@media (min-width: 1024px) {
    /* Half the row minus half the column gap (--s5/2); flex-grow reclaims the rest
       when nothing wraps up beside it. */
    .form-section-grid > *,
    .form-section-grid > :is(.form-cells, .telefone-dual) > * {
        flex-basis: calc(50% - 0.75rem);
    }

    /* Two-column rhythm (user 2026-10-21, mockup S2): --s4 between rows, --s5 between columns. A help or error line
       sits flush under its control (the .label's 0.5rem pads made it a 36px row). Desktop only: the phone's single
       column has no neighbour to align with and keeps its spacing.
       2026-10-23 (user, endereço mockups): the caption line every hint-less cell used to reserve (an ::after spacer,
       so a hint never pushed its neighbour's row) is gone — an empty line under every field cost more than the
       alignment it bought. Accepted: a hint or error grows its own row, and the neighbour no longer lines up with it. */
    .form-section-grid {
        gap: var(--s4) var(--s5);
    }

    .form-section-grid .form-control > .label:has(.label-text-alt):not(:has(.label-text)) {
        padding-block: 0;
    }
}

/* Blocks that own an internal layout (option grids in a fieldset, warning
   paragraphs, conditional reveals) read as full-width rows rather than as a
   half column. Anything else takes one cell; add `col-span-full` by hand for
   the exceptions. */
.form-section-grid > fieldset,
.form-section-grid > p,
.form-section-grid > .conditional-field,
.form-section-grid > :is(.col-span-full, .span-full),
.form-section-grid > :is(.form-cells, .telefone-dual) > fieldset,
.form-section-grid > :is(.form-cells, .telefone-dual) > p,
.form-section-grid > :is(.form-cells, .telefone-dual) > .conditional-field,
.form-section-grid > :is(.form-cells, .telefone-dual) > :is(.col-span-full, .span-full) {
    flex-basis: 100%;
}

/* A choice list on desktop (user 2026-10-23, endereço mockups — Estado: 27 radios behind a filter): a stacked
   list (`.is-stack`, Forms.kt choicesClasses: every radio group, a checkbox group over CHOICE_STACKED_WHEN options) is a
   block, not a half cell. It spans the row and its chips flow into columns — 10rem is the widest chip ("Rio Grande do
   Norte" + its radio), so the card's width gives 3 columns at 1024 and 4 at the form's full width, and ten options take
   ~3 rows instead of ten. Still radios, never a select. Option cards (.choice-body, G3/G5) keep one per line: an info
   line needs the width. Every radio group of three or more chips takes its own full row (user 2026-10-27: every
   choice looks the same; this reverses "short lists keep flowing"); a two-chip group (`.is-pair`, Sim/Não) keeps its half
   cell, and a `flow` group (cliente Origem) still flows. Chips align to the top of their cell (`align-items: start`): a
   one-line chip never stretches to a taller neighbour. Phone: a stack is one chip per line, a pair two side by side. */
@media (min-width: 1024px) {
    .form-section-grid > .form-control:has(.choices.is-stack):not(:has(.choice-body)),
    .form-section-grid > :is(.form-cells, .telefone-dual) > .form-control:has(.choices.is-stack):not(:has(.choice-body)) {
        flex-basis: 100%;
    }

    .form-section-grid .choices.is-stack:not(:has(.choice-body)) {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
        align-items: start;
    }
}

/* A conditional reveal that is one plain field (orçamento entrada / parcelas) opts back into a half
   cell, so a pair revealed together shares a row; alone, flex-grow still fills the row. */
@media (min-width: 1024px) {
    .form-section-grid > .conditional-field.is-cell {
        flex-basis: calc(50% - 0.75rem);
    }
}

/* Section rail wrapper: the sticky side rail column next to the form, desktop only.
   M21 — a phone form has no rail. It used to be a horizontal chip strip pinned under the
   top bar; on a 384px screen that was a second row of chrome over a form that already
   scrolls, so it goes rather than shrinks. The scroll-spy in declarativeHandlers.ts still
   runs over a display:none nav and simply lights nothing. */
.form-rail {
    display: none;
}

@media (min-width: 1024px) {
    .form-rail {
        position: sticky;
        top: 1.25rem;
        display: block;
        /* 12rem: the longest label ("Configurações regionais") fits on two lines
           without the rail width varying from form to form. */
        width: 12rem;
        flex-shrink: 0;
        align-self: flex-start;
    }
}

.form-rail-item {
    display: flex;
    align-items: center;
    gap: var(--s2);
    --gap: var(--s2);
    border-left-width: 3px;
    border-color: var(--g-200);
    padding-top: var(--s1);
    padding-bottom: var(--s1);
    padding-left: var(--s3);
    font-size: var(--t-caption);
    line-height: 1rem;
    color: color-mix(in srgb, var(--g-900) 70%, transparent);
}

.form-rail-item:hover {
    border-color: color-mix(in srgb, var(--c-primary) 40%, transparent);
    color: var(--g-900);
}

.form-rail-item-active {
    border-color: var(--c-accent);
    color: var(--c-primary);
}

/* Compact controls, desktop only. This MUST stay inside the min-width query:
   the (pointer: coarse) 16px floor above is what stops iOS zooming on focus,
   and a smaller font here would out-specify it on a touch laptop. */
@media (min-width: 1024px) {
    .form-section-card .input,
    .form-section-card .select {
        height: 2.5rem;
        min-height: 2.5rem;
        font-size: 0.875rem;
    }

    .form-section-card .textarea {
        font-size: 0.875rem;
    }

    /* A button sitting on the same row as an input (CEP/postal lookup) has to
       follow the input down to 2.5rem, or it stands a half-rem taller. */
    .form-section-card .btn.address-lookup-btn {
        height: 2.5rem;
        min-height: 2.5rem;
    }
}
/* ── Sidebar navigation ── */
.sidebar-shell .sidebar-submenu > ul {
    gap: 0.25rem;
    margin-top: 0.375rem;
    margin-bottom: 0.25rem;
    /* DaisyUI insets child <ul> (margin/padding-inline-start + a ::before
       connector line), which shrinks the whole child row — including its
       active background — to ~half width. Zero it so children span the
       full sidebar width like top-level items; hierarchy comes from the
       link's text indent below, not from a shrunken container. */
    margin-inline-start: 0;
    padding-inline-start: 0;
}

.sidebar-shell .sidebar-submenu .sidebar-submenu-link {
    line-height: 1.2;
    white-space: normal;
    /* Children carry the same-size icon + gap as the parent row. The inline-start indent clears the tree guide above
       and nests the child icon just past the parent icon; the row and active
       highlight still fill the whole width for a consistent tap target. */
    padding-inline-start: 1.5rem;
}

/* Fold control for submenu groups. Hidden everywhere except expanded desktop
   (shown by the media rule below): the collapsed rail is icon-only and the
   mobile dropdown renders flat, so neither folds. Pushed to the row end and
   rotates to point up when its <details> is open. */
.sidebar-shell .submenu-chevron {
    display: none;
    margin-inline-start: auto;
    flex-shrink: 0;
    transition: transform 0.2s ease;
}
.sidebar-shell .sidebar-submenu[open] > summary .submenu-chevron {
    transform: rotate(180deg);
}

/* DaisyUI's .menu focus rule paints `color: base-content` (near-black) over a
   10%-ink pill, 1.5:1 on the dark `bg-primary` rail: the entry reads as "disabled"
   (WCAG 1.4.3) and there is no ring (2.4.7). :focus-visible only covered the
   keyboard; an entry clicked for an in-app swap keeps :focus, so the item you
   just picked was the unreadable one (2026-10-14). Restore a real ring in the
   rail's own palette and keep the light text. Wins on specificity over
   DaisyUI's :where(). */
.sidebar-shell .menu :is(a, summary):is(:focus, :focus-visible),
.sidebar-shell .sidebar-logout:focus-visible,
.sidebar-shell .global-search-collapsed-trigger:focus-visible {
    outline: 2px solid var(--fallback-pc, var(--c-primary-ink));
    outline-offset: -2px;
    /* no --fallback-pc here: an opaque primary-content turns this pill into
       white-on-white in the floor browsers that define it */
    background-color: color-mix(in srgb, var(--c-primary-ink) calc(0.1 * 100%), transparent);
    color: var(--fallback-pc, var(--c-primary-ink));
}
/* ── Shared display vocabulary (Thymeleaf + kotlinx.html) ── */
/* Both the Thymeleaf templates and the kotlinx.html views under
   src/main/kotlin/veda/view/ render these same field-label, page-title and
   status-badge shapes. They live here, not as a Kotlin `const val`, because a
   template has no way to read a Kotlin constant — CSS is the only vocabulary
   both renderers can share. text-base-content/60 measures 4.76:1 on
   base-200 (see tokens.css) — do not lighten it. */
.field-label {
    display: block;
    margin-bottom: var(--s1);
    font-size: var(--t-caption);
    line-height: 1rem;
    color: color-mix(in srgb, var(--g-900) 60%, transparent);
}

@media (min-width: 1024px) {
    .field-label {
        text-transform: uppercase;
    }
}

.field-label-lg {
    display: block;
    margin-bottom: var(--s1);
    font-size: var(--t-label);
    font-weight: 600;
    line-height: 1.375;
    color: color-mix(in srgb, var(--g-900) 60%, transparent);
}

.page-title {
    font-size: var(--t-title);
    line-height: 1.75rem;
    font-weight: 700;
    color: var(--c-primary);
}

.empty-note {
    color: color-mix(in srgb, var(--g-900) 60%, transparent);
}

/* An empty section's create-CTA is the one thing to do on that section, but the shared action
   row is `justify-end` — under a centred empty state it read as an unrelated corner button.
   Centre it when it directly follows an empty state; populated sections keep the end alignment.
   (0,2,0) beats Tailwind's `.justify-end` (0,1,0), so no utility ordering games. */
.empty-note + .card-actions {
    justify-content: center;
}

/* Shape only, and `badge` is deliberately NOT applied here: these classes are emitted
   after DaisyUI's in the components layer, so a re-applied `badge` base would re-set
   background/border/colour and beat the `badge-success`/-warning/-error modifier the
   caller pairs them with (same specificity, later rule wins) — every status badge
   rendered white. The call site emits `badge` itself (Chrome.kt statusBadge).
   M24: one badge component, one height — status, tipo and "Automático" used to render at
   three sizes (badge-sm 20px / badge-lg 32px + the ad-hoc outline spans). `statusBadgeLg` is
   now a Kotlin alias of `statusBadge` (kept so its callers don't churn), and `badge-status-lg`
   is gone from the CSS: nothing emits it any more.
   No `nowrap` at any width: a badge is `justify-center` with white-on-colour text, so a label
   wider than its column overflowed both edges onto the white row and vanished ("m preparaçã"
   in the contrato status track). It wraps inside `max-w-full` instead. */
/* DS-B: no component class re-applied here. `badge-sm` left only its inline padding here;
   the outline variants' callers emit `badge badge-outline [badge-sm]` themselves, as statusBadge emits `badge`. */
.badge-status {
    padding-inline: 0.438rem;
    min-height: 1.75rem;
    font-size: var(--t-label);
}

.badge-status-outline,
.badge-status-outline-sm {
    min-height: 1.25rem;
}

.badge-status,
.badge-status-outline,
.badge-status-outline-sm {
    height: auto;
    max-width: 100%;
    white-space: normal;
    overflow-wrap: break-word;
    padding-top: var(--s1);
    padding-bottom: var(--s1);
    text-align: center;
    line-height: var(--lh-tight);
}

/* Report vocabulary (ListPage.kt's statTile / kpiStrip / totalsRow). Home, financeiro and
   ponto spelled the headline figure four ways and the aggregate row five; these are the one
   spelling each, and the only reason they are CSS rather than a Kotlin constant is that a
   vocabulary class is what the guardrail can check. */
/* flex-wrap: a tile too narrow for label + value ("Valor fechado" beside R$ 2.233.821,38 at
   1024, four tiles beside the sidebar) drops the value under the label instead of overflowing. */
.stat-tile {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--s1) var(--s3);
    border: 1px solid var(--c-line); background: var(--c-bg); padding: var(--s3);
}

.stat-tile-label { min-width: 0; font-size: var(--t-label); line-height: 1.25rem; color: var(--c-ink); }

.stat-tile-value { flex-shrink: 0; text-align: right; font-size: 1.125rem; line-height: 1.75rem; font-weight: 700; font-variant-numeric: tabular-nums; }

/* Deliberately heavier than a row: the total is the line a reader jumps to. */
.totals-row {
    border: 2px solid color-mix(in srgb, var(--c-primary) 30%, transparent);
    background: color-mix(in srgb, var(--c-bg-2) 50%, transparent); padding: var(--s3);
}
/* DS-A size scale (int-specs/design-system/lab/02, lab.css "size lives on the row"): S 32 · M 40 · L 48.
   `actionRow` emits `.actions.size-*`; every .btn inside takes the row's height. A standalone kit control
   (header buttons, the kebab) carries the size class itself. Below lg every size is L.
   Every row selector is inside :where(), so it weighs one class like a Tailwind utility and a utility on the
   same element wins (h-24 tile, flex-none arrow, px-4 FAB). `@layer` here is Tailwind's bucket, not a CSS
   cascade layer: without :where `.actions .btn` (0,2,0) beat `h-24` (0,1,0) and shrank the picker tiles. */
.size-s { --ctl: var(--ctl-s); }
.size-m { --ctl: var(--ctl-m); }
.size-l { --ctl: var(--ctl-l); }
.actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.actions.is-end { justify-content: flex-end; }
:where(.actions) .btn, .btn:where(.size-s, .size-m, .size-l) { height: var(--ctl); min-height: var(--ctl); }
:where(.actions) .btn-square, .btn-square:where(.size-s, .size-m, .size-l) { width: var(--ctl); }
:where(.size-s) .btn, .btn:where(.size-s) { padding-inline: 0.75rem; }
@media (max-width: 1023.98px) {
    .size-s, .size-m { --ctl: var(--ctl-l); }
    .actions { flex-wrap: nowrap; } /* plain: it must outrank the base rule above */
    :where(.actions) > .btn, :where(.actions) > form { flex: 1 1 0; min-width: 0; }
    :where(.actions) > form > .btn { width: 100%; }
}
:where(.actions.is-fill) > .btn, :where(.actions.is-fill) > form { flex: 1 1 0; min-width: 0; }
:where(.actions.is-fill) > form > .btn { width: 100%; }
