/* App shell. The sticky rule is documented here so it is not rediscovered: no ancestor of the
   preview pane may set overflow: hidden -- that is the single most common way position: sticky
   dies silently. */

.app-header {
  min-height: var(--header-h);
  display: flex;
  align-items: center;
  gap: var(--s-5);
  flex-wrap: wrap;
  padding: var(--s-3) var(--s-6);
  background: var(--c-surface);
  border-bottom: 1px solid var(--c-line);
}

.brand { display: flex; flex-direction: column; }
/* A <p>, not an <h1>: the page owns the h1. */
.brand-name {
  margin: 0;
  font-family: var(--font-serif);
  font-weight: 600;
  font-size: var(--fs-200);
  line-height: var(--lh-tight);
  letter-spacing: -.005em;
}
.brand-tagline {
  margin: 0;
  font-size: var(--fs-50);
  text-transform: uppercase;
  letter-spacing: .09em;
  color: var(--c-ink-3);
}

.header-end {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--s-4);
  flex-wrap: wrap;
}

.app-nav {
  display: flex;
  gap: var(--s-1);
  padding: 0 var(--s-6);
  background: var(--c-surface);
  border-bottom: 1px solid var(--c-line);
  overflow-x: auto;
}
.app-nav a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  padding: var(--s-3) var(--s-4);
  color: var(--c-ink-2);
  text-decoration: none;
  white-space: nowrap;
  border-bottom: 2px solid transparent;
}
.app-nav a[aria-current="page"] {
  color: var(--c-ink);
  border-bottom-color: var(--c-accent);
  font-weight: 600;
}

/* --narrow sets a measure and does NOT centre. The app chrome is full-bleed on a 2rem gutter
   and has to stay that way, because five panes are deliberately full-bleed on that same gutter:
   cases/index, catalog_items/index, admin/audit_logs/index, cases/invoices/show, and the
   pane--form / pane--preview billing split. Constraining .app-header and .app-nav to 52rem
   instead would line them up with this pane and misalign all five. Left-aligning the measure
   puts a page's <h1> on the wordmark's gutter and costs those five nothing. The prototype has
   no centred measure anywhere either (prototype/index.html:62,69).

   --narrower DOES centre and is not dead: sessions/new and both error pages carry it on a
   .card inside .centered-screen. */
.pane { padding: var(--s-5) var(--s-6); }
.pane--narrow { max-width: 52rem; margin-inline: 0; }
.pane--narrower { max-width: 30rem; margin-inline: auto; }


/* Page identity. One <h1> per page, and it is NOT the string in <title>: a dossier heading
   names the deceased, and layouts/application.html.erb keeps that name out of the title
   because a title lands in browser history, the task switcher and every screenshot.
   Carries no money -- include_all_helpers is off and MoneyHelper reaches only the billing
   controllers, so a total here would either raise or force the helper global.

   NO margin-bottom, and .case-nav below carries no margin-top. Both are children of .stack,
   which is a flex column with a gap -- and flex items DO NOT COLLAPSE MARGINS. A margin on
   either one ADDS to that gap instead of overlapping it; the two together put 56px of dead
   space between a dossier's title and its own tabs. Spacing between stacked blocks belongs to
   the stack. */
.page-head h1 {
  font-family: var(--font-serif);
  font-size: var(--fs-400);
  line-height: var(--lh-tight);
  margin: 0;
}
.page-head .sub { margin: var(--s-1) 0 0; color: var(--c-ink-2); font-size: var(--fs-100); }

/* The dossier's own tabs, under its page head. Secondary to .app-nav on purpose: this is
   movement within one dossier, not movement around the app -- but NOT smaller than the tap
   minimum. At --fs-75 the padding and line-height alone leave these at ~37px, and they are the
   tabs a field worker taps on a phone to reach the dossier and the journal, so min-height is
   set explicitly the way .app-nav a has always set it.

   No margin-top: see .page-head above. */
.case-nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-1);
  border-bottom: 1px solid var(--c-line);
}
.case-nav a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  padding: var(--s-2) var(--s-3);
  font-size: var(--fs-75);
  color: var(--c-ink-2);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  white-space: nowrap;
}
.case-nav a:hover { color: var(--c-ink); }
.case-nav a[aria-current="page"] {
  color: var(--c-accent-dk);
  border-bottom-color: var(--c-accent);
  font-weight: 600;
}

.stack { display: flex; flex-direction: column; gap: var(--s-4); }
/* A .stack stretches its children, which is right for cards and fieldsets and wrong for a
   button: a lone submit was rendering the full width of the form. */
.stack > .btn, .stack > input[type="submit"] { align-self: flex-start; }
.row { display: flex; gap: var(--s-3); align-items: center; flex-wrap: wrap; }
.row-end { margin-left: auto; }

.centered-screen {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: var(--s-5);
}

/* The two-pane billing grid, MOBILE FIRST. One column and ONE scroll is the base state; the sticky
   preview is added above --bp-two-pane and nowhere else.

   Written as min-width rather than a base rule plus a max-width undo, deliberately: the undo form
   shipped the bug this replaces. That block reset `position` and `max-height` and did NOT reset
   `overflow` -- and the leftover was not harmless. It was doing two things nobody asked it to:
   absorbing the invoice's HORIZONTAL overflow at narrow widths (load-bearing, and now the
   .table-scroll wrapper's job -- see cases/billings/_totals.html.erb), and making the pane a
   MONOLITHIC scroll container in paged media, so a multi-sheet invoice printed from this screen
   silently lost its later sheets. Neither failed any spec, because "a declaration is present above
   and absent below" is not a pattern a guard can see. Below, every declaration is additive: there is
   no reset to forget, and a property added to the sticky block cannot reach a phone at all.
   spec/guards/component_layout_spec.rb pins that shape.

   THE SCROLLER IS ON THE PANE, not on #billing_totals. Turbo replaces that node on every row
   autosave (cases/case_items_controller) and every relief toggle (cases/exemptions_controller); the
   <aside> is never replaced, which is the only reason the office's scroll position survives an edit.
   Do not "put the overflow on the thing that overflows".

   align-items: start keeps the preview content-sized rather than stretched to the form's height. Its
   old comment here called it "required for the sticky pane", which was not accurate -- max-height is
   what does that job. It still matters: with NEITHER, a sticky box taller than the viewport pins its
   top at 0 and hides its own tail at every scroll position.

   And still true, still the reason this file opens the way it does: no ancestor of .pane--preview
   may set overflow: hidden -- that is the single most common way position: sticky dies silently. */
.dossier-grid {
  display: grid;
  grid-template-columns: 1fr;
  align-items: start;
}
.dossier-grid .pane--preview { border-block-start: 1px solid var(--c-line); }

/* The form pane is a QUERY CONTAINER, so the billing rows restack on its width rather than the
   window's -- components.css, @container form. They are not the same question above
   --bp-two-pane: the pane is then roughly half the window, so at 1080 a four-column table was
   being asked to fit in ~500px and every label wrapped twice, while a viewport query saw a wide
   screen and left the table alone.

   Two things come free, and both are worth knowing before anyone "tidies" this away:

     * containment makes the pane's min-content ZERO. `grid-template-columns: 1fr` is
       minmax(auto, 1fr), and that auto floor is the item's min-content -- so before this, one
       long unbreakable child could widen the track past the viewport and scroll the whole PAGE
       sideways instead of scrolling itself.
     * it contains absolutely-positioned descendants, which is the same job
       .table-scroll { position: relative } does for the tables: a .u-vh label is out of flow and
       still extends its containing block's scrollable area.

   NOT on .pane--preview. Containment there would be a size-containment question next to a sticky
   scroll container, and spec/guards/component_layout_spec.rb governs that pane specifically. */
.dossier-grid .pane--form { container-type: inline-size; container-name: form; }

/* The literal is --bp-two-pane, recorded in tokens.css: a media query cannot read a custom property,
   so the value is written here and the token is documentation only -- keep the two in step by hand.
   This is the repo's ONLY min-width query; every other @media is max-width. A fractional viewport
   width just below the breakpoint matches neither branch and falls to one column, the safe side.
   1081 until 2026-08-28, when it moved to 1024 so the 10.2" iPad in landscape -- 1080 CSS px, and
   short of the old value by one pixel -- gets the side-by-side preview. */
@media (min-width: 1024px) {
  .dossier-grid { grid-template-columns: var(--grid-form-preview); }
  .dossier-grid .pane--form { border-inline-end: 1px solid var(--c-line); }
  .dossier-grid .pane--preview {
    border-block-start: 0;
    position: sticky;
    /* 0, and NOT --app-chrome-h. The prototype offsets its `top` because ITS header is sticky
       (prototype/index.html:34); this app's chrome is static and scrolls away, so 0 is correct -- an
       offset here would leave a permanent gap above the pane once scrolled. That divergence from the
       prototype is recorded in docs/design-system.md, "departures from the prototype". */
    top: 0;
    /* THE PORTING ERROR, and the whole bug. The prototype sizes this to the chrome above it
       (prototype/index.html:71, `calc(100vh - 79px)`); the port kept a bare 100vh, so the pane's
       last --app-chrome-h sat below the fold and the bottom of the invoice could not be reached
       until the page had scrolled -- and the page's entire scroll range IS that same height whenever
       the form pane is shorter than the cap.
       dvh, not vh: every iPad in landscape is above this breakpoint and its toolbar moves, and on
       iOS 100vh is the LARGE viewport -- the same off-by-a-toolbar bug in miniature. dvh rather than
       svh because svh under-sizes the scrollport by the toolbar's height whenever the toolbar is
       hidden, while a changing dvh only changes how much of the invoice is visible and shifts no
       content. (.centered-screen's 100vh wants the opposite answer for the opposite reason: its box
       is centred, so a moving dvh would move the card.)
       MEASURED 2026-08-27, and it is why the dvh line is gated by @supports below rather than
       written as a second declaration: the usual two-declaration fallback relies on the unknown
       unit being invalid AT PARSE TIME, and a declaration containing var() is never checked
       against the property grammar then -- it is a pending-substitution value, validated after
       the cascade has already picked a winner. So on an engine without dvh both declarations
       parse, the dvh one WINS, substitution fails, and the property is invalid at computed-value
       time: for a non-inherited property that computes to its INITIAL value, max-height: none.
       The pane would have no cap at all, which is worse than the bug being fixed. */
    max-height: calc(100vh - var(--app-chrome-h));
    overflow: auto;
    /* NO overscroll-behavior here, and that is a REMOVAL rather than an omission. It was
       `contain`, it is nowhere in prototype/index.html, no document ever recorded it, and TOUCH is
       the justification for dropping it: with `contain`, an iOS swipe starting anywhere in this pane
       can never move the page, at any scroll position, and this pane is roughly half an iPad
       landscape screen.
       The cost is real and accepted -- one over-flick over the invoice on a trackpad launches the
       page down the 33-row CTQ form, and the sticky pane keeping the invoice on screen is what makes
       that tolerable. If chaining ever proves annoying, that is a new decision with a recorded
       reason, and its form is overscroll-behavior-y INSIDE this block, never at phone widths. The
       guard permits exactly that and nothing else. */
  }

  /* @supports, not a second declaration -- see the note above. dvh over vh because on iOS 100vh is
     the LARGE viewport, the height with any retractable browser UI hidden, so a bare 100vh
     over-sizes the scrollport by that UI's height wherever it is persistent. Whether it moves at
     all on an iPad is UNVERIFIED from here; what is certain is that dvh is never larger than the
     visible viewport and vh can be, which is the direction that matters.
     (.centered-screen's 100vh wants svh for the opposite reason: its box is centred, so a value
     that changes would move the card. Not bundled here.) */
  @supports (max-height: 1dvh) {
    .dossier-grid .pane--preview { max-height: calc(100dvh - var(--app-chrome-h)); }
  }
}
