/*
 * layout.css. The five layout primitives from design.md section 4, and nothing else.
 *
 * A screen composes these. A screen file never writes its own layout CSS, and a
 * component never styles anything outside its own root, so this is the only place
 * that decides how the page is divided.
 */

/* l-shell. Sidebar plus main. Collapses to one column under the md breakpoint. */
.l-shell {
  display: grid;
  grid-template-columns: var(--nav-width) minmax(0, 1fr);
  min-height: 100vh;
}

.l-shell__nav {
  grid-column: 1;
  background: var(--surface);
  border-right: var(--border-width) solid var(--line);
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
  z-index: var(--z-nav);
}

.l-shell__main {
  grid-column: 2;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

/* l-bar. The sticky top bar. */
.l-bar {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
  min-height: var(--bar-height);
  display: flex;
  align-items: center;
  /* G12. Without this the title is squeezed rather than wrapped once the tools slot
     holds an environment switcher, a role switcher and a button. */
  flex-wrap: wrap;
  gap: var(--space-5);
  padding: 0 var(--space-7);
  background: var(--surface);
  border-bottom: var(--border-width) solid var(--line);
}

.l-bar__title { flex: 1 1 auto; min-width: 0; }
.l-bar__tools {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex: 0 0 auto;
  /* Wraps, entry [240]: four controls at 360px are wider than the bar, and a group that will
     not wrap scrolls the whole page sideways instead. */
  flex-wrap: wrap;
  max-width: 100%;
}

/* l-stage. Page padding wrapper. Everything a screen renders sits in one of these. */
.l-stage {
  /*
   * G13, and the number is a judgement so here is the arithmetic behind it.
   *
   * On a 2560px monitor a table stretching the full viewport puts the first and last
   * cell of one row so far apart that the eye loses the row between them. The widest
   * table in this panel has 11 columns.
   *
   *   1600 stage
   *   -   48 the two space-7 gutters
   *   = 1552 of table
   *   /   11 columns
   *   =  141px each, which is a readable cell rather than a stretched one.
   *
   * The template caps its own .container at 1280px (app.css 465) and has no wider tier,
   * but that container is a whole page. This is the content column beside a 248px nav,
   * so the page tops out around 1876px including the nav and the gutters, which is the
   * same decision one step along. A table wider than this still scrolls inside its own
   * wrapper rather than pushing the page sideways: table.css sets overflow-x on it,
   * which is house rule 10.
   *
   * One property if the judgement turns out wrong.
   */
  max-width: 1600px;
  margin-inline: auto;
  width: 100%;
  padding: var(--space-7);
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  flex: 1 1 auto;
  min-width: 0;
}

/* l-grid. Column counts as modifiers, all collapsing on small screens. */
.l-grid {
  display: grid;
  gap: var(--space-5);
  min-width: 0;
}

.l-grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }

/*
 * One cell spanning every column. Entry [130]. Named after .l-form__wide below, which is
 * the same one line at form scale, so the pair reads as one idea rather than two.
 *
 * The settings screen is what wanted it. It had a 1.9fr/1fr l-split at the top and five
 * full width sections under it, which is two grids and neither of them chosen: the split's
 * ratio was inherited from a screen that has a main pane and a side pane, and settings has
 * six peers. It is one l-grid--2 now, and the two members that are genuinely wide, the brand
 * block with its two image previews and the databases table with four columns, say so here
 * rather than by sitting outside the grid.
 */
.l-grid__wide { grid-column: 1 / -1; }
.l-grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.l-grid--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }

/* l-split. Two panes with a divider. The wide pane leads at roughly 1.9 to 1. */
.l-split {
  display: grid;
  grid-template-columns: 1.9fr 1fr;
  gap: var(--space-5);
  min-width: 0;
}

.l-split--even { grid-template-columns: repeat(2, minmax(0, 1fr)); }

.l-split > * { min-width: 0; }

/*
 * l-filters. A row of controls above or beside a list. Screens were misusing
 * l-bar__tools, the top bar's primitive, so a filter row read as controls dropped on
 * the page. Arrangement is the template's .card-header, app.css 11300.
 */
.l-filters {
  display: flex;
  /*
   * ONE LINE, entry [063]. It used to wrap, and wrapping is not what a filter bar wants:
   * the last child carries margin-inline-start:auto so it can sit at the far right, and a
   * wrapped last child lands right aligned on a line of its own with nothing beside it,
   * which reads as a control that fell off rather than as a row. Measured on the tickets
   * screen: a 341px pane holding 343px of controls put the submit alone on a second line
   * for the sake of two pixels, when every child already carries min-width: 0 and could
   * have shrunk instead.
   *
   * Below the small breakpoint it wraps again and every child goes full width, which is a
   * stack rather than a spill. That is in the media query at the foot of this block.
   */
  flex-wrap: nowrap;
  align-items: flex-end;
  gap: var(--space-4);
  min-width: 0;
}

/*
 * First control takes the slack (the search), last is the submit. Positional so a screen
 * needs no extra class names on controls it did not author.
 *
 * `auto` and NOT `14rem`, entry [063]. A 14rem flex BASIS is not "at least 14rem when there
 * is room", it is the item's base size, and a filter bar mostly sits in a panel's tools
 * slot, which is shrink to fit. So the slot sized itself to the first control's basis and
 * every control after it wrapped: measured on /admin/plans, a 226px form holding a 226px
 * select with the Show button alone on a second line, and on /admin/installations three
 * lines out of four controls. `auto` bases the item on its content and keeps grow: 1, so it
 * still takes the slack where there is slack and stops inventing a width where there is not.
 */
.l-filters > * { flex: 0 1 auto; min-width: 0; }
.l-filters > :first-child { flex: 1 1 auto; }
.l-filters > :last-child { margin-inline-start: auto; }

@media (max-width: 640px) {
  .l-filters { gap: var(--space-3); flex-wrap: wrap; }
  .l-filters > * { flex: 1 1 100%; }
  .l-filters > :last-child { margin-inline-start: 0; }
}

/*
 * A group of controls that act on the thing they sit in. Added 2026-08-24, entry [048].
 *
 * It exists because the plans screen rendered six buttons with nothing governing them: they
 * were concatenated strings flowing inline, and at 1440px they wrapped onto three ragged
 * rows inside one card. docs/ui-patterns.md section 1.4 counts what the template actually
 * does, and the answer is that it never has this problem, because it never puts six controls
 * in one place. The ceiling it keeps to:
 *
 *   three or fewer, all icon shaped   a row of icon buttons
 *   more than three, or any need words  one primary and a kebab
 *   a card header                     never a button, only a kebab
 *   a card body                       at most one button, full width
 *
 * So this primitive is deliberately NOT a wrap-everything-nicely box. It holds a small,
 * bounded row. `--end` pushes it right, which is what a card header and a bar want.
 *
 * Each control may arrive wrapped in its own <form>, because a write is a POST with its own
 * CSRF field. A form is a flex item like anything else, and `display: contents` is NOT used:
 * it would drop the form box out of the layout and take its submit button's accessible
 * relationship with it in some engines.
 */
.l-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  min-width: 0;
}

.l-actions > form { display: flex; margin: 0; }
.l-actions--end { justify-content: flex-end; }

/* A row of icon-shaped controls, up to the three the template never exceeds. Tighter than
   the labelled row, matching advance-table.html's space-x-3 between action buttons. */
.l-actions--icons { gap: var(--space-2); flex-wrap: nowrap; }

/*
 * On a phone a labelled row goes full width and stacks, rather than becoming a horizontal
 * scroll. House rule 10 names the scroll case as a table wrapper only, and a row of controls
 * a thumb has to reach is not that. The icon row does NOT stack: it is at most three 44px
 * targets, 132px plus gaps, which fits 360px with room to spare.
 */
@media (max-width: 640px) {
  .l-actions > *:not(form),
  .l-actions > form { flex: 1 1 100%; }
  .l-actions > form > * { width: 100%; }
  .l-actions--icons > *,
  .l-actions--icons > form { flex: 0 0 auto; }
  .l-actions--icons > form > * { width: auto; }
}

/*
 * l-form. A form's vertical rhythm, and the rows inside it.
 *
 * ## What was wrong
 *
 * Nothing owned the rhythm. A <form> is a plain block box, .c-field is display:block with
 * no outer margin, .l-grid has no margin either, so every gap between one field group and
 * the next measured ZERO. Driven at /admin/apps at 1440 the register form reported
 * GAP_ABOVE 0px between all five of its blocks: a hint text sat directly on the next
 * field's label, and the submit sat directly on the select above it. The columns were never
 * the problem. The absence of a container was.
 *
 * The right primitive already existed and was private to one screen family: login.css sets
 * .c-login__form to a flex column with gap --space-5, which is why the auth pages have
 * always looked right. This is that, generalised, which is house rule 7.
 *
 * ## What the template does, with line numbers
 *
 * mockup/main-template/input-layout.html
 *   2285  <form class="space-y-4">        vertical form, every field a direct child
 *   2302  <div class="checkbox-area">     the checkbox is a FULL WIDTH sibling, never a cell
 *   2312  <button class="btn ...">        the submit is just the next child in the stack
 *   2377  <form class="space-y-4">        multi column form
 *   2378  <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-7">
 * mockup/main-template/form-validation.html
 *   2244  <div class="grid md:grid-cols-2 gap-6">
 *   2295  <div class="grid md:grid-cols-2 gap-7">
 * mockup/main-template/wizard.html
 *   2333  <div class="mt-6 space-x-3">    the action row, given a LARGER gap than the fields
 *
 * app.css 5414 makes space-y-4 a 1rem margin-top on every child but the first, 5390 makes
 * gap-6 1.5rem and 5405 makes gap-7 1.75rem.
 *
 * So the template puts the rhythm on the FORM and the columns on a grid inside it, and the
 * field itself carries no outer margin. That is what is built here.
 *
 * ## The two numbers
 *
 * Stack gap --space-5, 1rem, is space-y-4 exactly.
 *
 * Column gap --space-7, 1.5rem, is gap-6 at form-validation.html 2244. The template's other
 * two grids use 1.75rem, which is not on our scale (--space-7 is 1.5rem, --space-8 is 2rem),
 * so the scale picks which of the template's own two values to take.
 *
 * ## THE ONE DELIBERATE DEVIATION FROM THE TEMPLATE, ruled 2026-08-27
 *
 * row-gap is the STACK gap and not the column gap. This is the only place in this primitive
 * that does not do what the template does, so it is marked rather than buried.
 *
 * The template sets a single `gap`, which applies to both axes. The consequence is that two
 * rows INSIDE a grid end up further apart (1.75rem) than the grid is from the thing under it
 * (1rem). That is the ragged rhythm this whole file exists to fix, so copying it would be
 * copying the defect along with the design.
 *
 * Splitting the axes takes both of the template's own numbers and puts each on the axis it
 * was measured for: --space-7 across, where a wider channel separates columns, and --space-5
 * down, so one single rhythm runs the length of the form. Neither number is invented. It also
 * means a row collapsed to one column on a phone spaces its fields exactly like every other
 * gap on the page, which a shared `gap` would not.
 */
.l-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  min-width: 0;
}

.l-form__row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--space-7);
  row-gap: var(--space-5);
  align-items: start;
  min-width: 0;
}

.l-form__row--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.l-form__row--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }

.l-form__row > * { min-width: 0; }

/*
 * A field that takes the whole width is a direct child of .l-form, which is what the
 * template does at input-layout.html 2302. This is for the other case: a field that has to
 * stay INSIDE a row because it belongs with it. Without it such a field sits in column one
 * and leaves a hole in column two.
 */
.l-form__wide { grid-column: 1 / -1; }

/*
 * A checkbox beside a text input in the same row.
 *
 * THE TEMPLATE NEVER DOES THIS, so this offset is DERIVED rather than copied, the same way
 * field.css derives the 1rem box from the label line box. Every checkbox-area in the
 * template is a full width sibling of the grid (input-layout.html 2302 and 2354,
 * form-validation.html has none). This panel does put one in a row, so the alignment has to
 * be decided somewhere, and it is decided here rather than on the field, because a field
 * does not know it is in a row. Row alignment is the row's business.
 *
 * The arithmetic. A labelled cell puts its control at label line box plus label margin:
 * --text-sm * --leading-control (1.5rem) + --space-3 (0.5rem) = 2rem. The checkbox row is
 * --touch-min (2.75rem) tall against a --input-h (2.375rem) control, so it is 0.375rem
 * taller and starts half of that higher for the two to share a centre line. 2rem minus
 * 0.1875rem = 1.8125rem, which is 29px, and puts the checkbox text on the same centre line
 * as the text in the input beside it. Measured against the select on /admin/apps: they were
 * 18px out of step before this line.
 */
.l-form__row > .c-field--checkbox {
  padding-top: calc(
    var(--text-sm) * var(--leading-control) + var(--space-3)
    - (var(--touch-min) - var(--input-h)) / 2
  );
}

/*
 * A row of nothing but checkboxes has no label to align against, so the offset above would
 * be a stray 29px of air rather than an alignment. Four rows in the panel are like this:
 * the On sale / Listed pair on both plan forms, the three switches on the plan editor and
 * the last row of the feature matrix.
 *
 * Written as an override rather than folded into the selector above ON PURPOSE. :has() is
 * the only way CSS can ask "does this row hold anything that is not a checkbox", and if an
 * engine does not know it, a rule GUARDED by :has() is dropped and every checkbox loses its
 * alignment, which is the bug. Dropping this rule instead loses only the tightening, and the
 * row still reads correctly, just airier.
 */
.l-form__row:not(:has(> :not(.c-field--checkbox))) > .c-field--checkbox {
  padding-top: 0;
}

/*
 * The submit, separated from the fields it acts on.
 *
 * The template's own form pages do NOT rule this off: input-layout.html 2312 and
 * form-validation.html 2329 leave the button as the next child in the 1rem stack, and only
 * wizard.html 2333 marks it at all, with a larger gap. But the template does define the
 * shape for an action separated from the body it belongs to, and it is .card-footer at
 * app.css 11322: border-top 1px slate-200 (--line, tokens.css 244) over 1.5rem of padding.
 * That is taken whole rather than mixed with the card header's proportions. The 1rem above
 * the rule is the stack gap, so the action sits 1rem below the last field, then the rule,
 * then 1.5rem.
 *
 * RULED 2026-08-27, keep the rule. A form on a full width panel in a control plane is not a
 * template demo card: the action writes, and a visible separation before it is worth having.
 * .card-footer is the right source to take that treatment from, so it is taken whole and not
 * mixed with the card header's proportions.
 *
 * It is a block, not a flex row: one button needs no row, and a form with several controls
 * composes .l-actions on the same element rather than this file growing a second copy of it.
 */
.l-form__foot {
  padding-top: var(--space-7);
  border-top: var(--border-width) solid var(--line);
}

/* A horizontally scrolling strip. Used by the channel rack on the overview screen. */
.l-rail {
  display: flex;
  gap: var(--space-4);
  overflow-x: auto;
  padding-bottom: var(--space-3);
  scroll-snap-type: x proximity;
}

.l-rail > * { scroll-snap-align: start; flex: 0 0 auto; }

@media (max-width: 1180px) {
  .l-grid--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .l-grid--3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .l-form__row--4,
  .l-form__row--3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .l-split,
  .l-split--even { grid-template-columns: minmax(0, 1fr); }
}

/*
 * THE SIDEBAR STACKS ABOVE THE PAGE UNDER --bp-md. Entry [240], the owner's ruling on the
 * approved Sitemapify file: "a drawer is a different interaction, not a different layout, and it
 * was never approved. In a Wix dashboard iframe on a phone it hides the nav behind a control the
 * merchant did not ask for." The G11 drawer, off canvas with a scrim and a toggle from entry
 * [044], is gone: below 900px the shell is one column, the nav sits at the top at full width
 * with every row and the plan box in view, and the top bar and the screen follow. The nav.js
 * drawer module has nothing to open and the toggle is not drawn.
 */
@media (max-width: 900px) {
  /* Two rows, and the PAGE takes the slack: the shell is min-height 100vh and a grid hands spare
     height to every auto row alike, which on a tall viewport put white under the stacked nav's
     plan box. Entry [243], seen in a 3600px shot. */
  .l-shell { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }

  .l-shell__nav {
    position: static;
    height: auto;
    overflow: visible;
    border-right: 0;
    border-bottom: var(--border-width) solid var(--line);
    z-index: auto;
  }

  .l-shell__main { grid-column: 1; }
}

/* The footer strip, gap G8. */
.l-foot {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-5);
  padding: var(--space-4) var(--space-7);
  border-top: var(--border-width) solid var(--line);
  color: var(--ink-3);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
}

/*
 * Entry [239]: --bp-md moved from 820 to 768 to meet the approved Sitemapify file. At exactly
 * 768 the file still draws its tiles two across (its own collapse is at 600), so a four column
 * grid keeps two columns here and goes to one at --bp-sm; two and three column grids collapse
 * here as before, because a card split in two at 768 is what the file draws for them.
 */
@media (max-width: 900px) {

  .l-grid--2,
  .l-grid--3 { grid-template-columns: minmax(0, 1fr); }

  .l-form__row,
  .l-form__row--3,
  .l-form__row--4 { grid-template-columns: minmax(0, 1fr); }

  /* One column, so there is no labelled cell beside the checkbox to align it against and
     the offset would read as a stray gap above the row. */
  .l-form__row > .c-field--checkbox { padding-top: 0; }

  .l-stage { padding: var(--space-5); }
  /* Entry [241]: one row at 360. The approved file draws the bar at gap 9 and padding 13
     under 900; on the scale that is 8 and 12. Six 44px controls at the suite's 20px gap would
     not fit a 360px bar, and the ruling is one row. */
  .l-bar { padding: 0 var(--space-3); gap: var(--space-2); }
  .l-bar__tools { gap: var(--space-2); }
}

/* After the 1180 block, so the cascade lands: four columns, two under 1180, one under 640. */
@media (max-width: 640px) {
  .l-grid--4 { grid-template-columns: minmax(0, 1fr); }
}
