/* ═══════════════════════════════════════════════════════════════════════════
   BASE — geometry only

   The frame the application stands in and the mechanics of the view grammar.
   Where things sit, how they collapse, what scrolls, how a grid divides.

   There is not one colour, type family, rule weight, shadow or radius value in
   this file, and there must never be. If you need one, you are writing a theme.
   The whole reskin depends on this line holding.

   What IS here is the full measurement contract — every gutter, rhythm, row
   height and column width the interface uses is a custom property declared in
   one block below. A skin retunes the *feel* by reassigning them: Chicago is
   dense and structural, Swiss is airy and quiet, and neither had to touch a
   rule in this file to be either. A skin that only changes colour is not a
   skin, it is a palette.

   Spec: ../GRAMMAR.md
   ═══════════════════════════════════════════════════════════════════════════ */

@layer base {

  /* ── the measurement contract ──────────────────────────────────────────
     Sizes, not looks. Every one of these is fair game for a skin. */
  :root {
    /* The spacing scale. One ratio, six steps — spacing that comes off a
       scale reads as intentional; spacing typed in by eye reads as an
       accident, and the eye can tell the difference even when it cannot say
       why. */
    --space-1: .25rem;
    --space-2: .5rem;
    --space-3: .75rem;
    --space-4: 1.25rem;
    --space-5: 2rem;
    --space-6: 3.25rem;

    /* Named for their job, assigned from the scale, so a skin can say "looser"
       in one place without arithmetic. */
    --gap-s: var(--space-1);
    --gap-m: var(--space-3);
    --gap-l: var(--space-5);

    --pad: var(--space-4);          /* inline gutter inside a pane        */
    --pad-block: var(--space-4);    /* block padding at a pane's ends     */
    /* The strip gets its own gutter. It is the one place in the application
       where horizontal room genuinely runs out, so widening the panes for air
       must not squeeze the header until the last state chip falls off it. */
    --chrome-pad: var(--space-4);
    --section-gap: var(--space-5);  /* rhythm between sections in a pane  */
    --field-gap: var(--space-4);    /* rhythm inside a form               */

    --row-h: 3rem;                  /* a record in the master list        */
    --cell-pad-block: var(--space-3);
    --cell-pad-inline: var(--space-3);
    --card-pad: var(--space-4);

    --rail: 14rem;                  /* the module rail                    */
    --master: 22rem;                /* the record list                    */
    --inspector: 21rem;             /* the properties rail                */
    --chrome: 4rem;                 /* the header, open                   */
    --chrome-shut: 2.75rem;         /* the header, collapsed to a strip   */

    --tap: 2.75rem;                 /* minimum touch target               */
    --measure: 68ch;                /* the longest a line of prose may be */
    --radius: 0;                    /* square unless a skin says otherwise */

    --ease: cubic-bezier(.2, .7, .3, 1);
    --quick: .16s;
    --calm: .28s;
  }

  html, body { block-size: 100%; }

  /* The application root is a box the shell is rendered *into*, not a box in
     the layout. `display: contents` dissolves it so the header and the shell
     become the body grid's own children.

     Without this the whole frame silently fails: body's `auto 1fr` rows apply
     to the wrapper instead, which is a block sized to its content, so the rows
     resolve to `2697px 0px`. Every pane is then as tall as the longest list
     rather than as tall as the window — nothing scrolls, the overflow is
     clipped by the viewport instead, and when the content is short the panes
     stop wherever it stops and the page shows through underneath them.
     design.html never had the bug because it has no wrapper. */
  [data-app] { display: contents; }

  body {
    display: grid;
    grid-template-rows: auto 1fr;
    /* Pinned to the viewport. Without the explicit `minmax(0, 1fr)` the single
       implicit column is sized to the widest item's *intrinsic* width, so one
       long toolbar in the header widens the whole document and pushes the
       inspector off the right-hand edge — a bay that is simply gone, with no
       scrollbar to say so. */
    grid-template-columns: minmax(0, 1fr);
    /* The application owns the viewport; the panes scroll, the page does not.
       A back-office tool that scrolls as a whole loses its toolbar exactly
       when somebody reaches for it. */
    block-size: 100dvh;
    overflow: hidden;
  }

  /* ── the header ─────────────────────────────────────────────────────────
     Collapses to a strip rather than disappearing: the brand and the toggle
     that brings it back have to survive, or the control is unreachable. */
  [data-chrome] {
    display: flex;
    align-items: center;
    /* One step tighter than a pane. The strip is the one place in the
       application where horizontal room actually runs out. */
    gap: var(--space-3);
    block-size: var(--chrome);
    padding-inline: var(--chrome-pad);
    overflow: hidden;
    transition: block-size var(--calm) var(--ease);
  }
  [data-chrome][data-collapsed="true"] { block-size: var(--chrome-shut); }
  [data-chrome] > [data-chrome-body] {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    flex: 1;
    min-inline-size: 0;
  }
  [data-chrome][data-collapsed="true"] > [data-chrome-body] > :not([data-chrome-keep]) {
    display: none;
  }

  /* A property list in the header runs along it rather than down it. The
     default two-column stack is right in an inspector and four rows too tall
     for a strip that is one row high — and the strip clips, so the overflow
     would be silent. */
  [data-chrome] [data-fields] {
    grid-template-columns: none;
    grid-auto-flow: column;
    grid-auto-columns: max-content;
    align-items: baseline;
    gap: 0 var(--space-2);
    white-space: nowrap;
  }
  [data-chrome] [data-fields] dd { margin-inline-end: var(--space-4); }

  /* The strip is one row high and clips. A toolbar that wraps inside it does
     not get taller, it gets cut in half — so in here it does not wrap, and
     when there is no room left it scrolls instead. */
  [data-chrome] [data-toolbar] {
    flex-wrap: nowrap;
    min-inline-size: 0;
    overflow-x: auto;
    scrollbar-width: none;
  }
  [data-chrome] [data-toolbar]::-webkit-scrollbar { display: none; }

  /* Its scrollbar is hidden, so the last item is faded out instead: a word cut
     off at a hard edge reads as a broken layout, the same word fading reads as
     "there is more this way". Not a colour — it is the scroll affordance, and
     every skin needs it.

     Gated on `data-overflow`, which app.js sets only when the bar really is
     wider than its box. An unconditional fade dims the last state on a screen
     with room to spare — it hides information to solve a problem that is not
     there, which is worse than the hard edge it was meant to fix. */
  [data-chrome] [data-rack-states][data-overflow="true"] {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 2rem), transparent);
    mask-image: linear-gradient(to right, #000 calc(100% - 2rem), transparent);
  }

  [data-chrome] [data-toolbar] button { min-block-size: auto; white-space: nowrap; }

  /* A labelled control in the strip lays out along it, not down it, and keeps
     its natural width — a <select> cannot ellipsis, so squeezing one hides the
     end of the name it is showing. The label stays in the DOM for the screen
     reader and comes off the screen, because the options say what they are. */
  [data-chrome] [data-field] {
    flex: 0 0 auto;
    flex-direction: row;
    align-items: center;
  }
  [data-chrome] [data-field] label {
    position: absolute;
    inline-size: 1px; block-size: 1px;
    margin: -1px; padding: 0; border: 0;
    overflow: hidden; white-space: nowrap;
    clip-path: inset(50%);
  }
  [data-chrome] [data-field] :is(select, input) {
    inline-size: auto;
    min-block-size: auto;
    max-inline-size: 18rem;
  }

  /* ── the four bays ──────────────────────────────────────────────────────
     Named columns, so a bay that is absent from a view leaves no hole and the
     grid does not have to be recounted. */
  [data-shell] {
    display: grid;
    grid-template-columns:
      [rail] var(--rail)
      [master] var(--master)
      [detail] minmax(0, 1fr)
      [inspector] var(--inspector);
    min-block-size: 0;
    overflow: hidden;
  }

  [data-pane] {
    min-inline-size: 0;
    min-block-size: 0;
    overflow-y: auto;
    overflow-x: hidden;
    overscroll-behavior: contain;
    scrollbar-gutter: stable;
    /* Room to breathe at the end of a scroll. A list whose last row sits hard
       against the bottom edge always looks truncated, whether it is or not. */
    padding-block-end: var(--space-6);
    padding-block-start: 0;
  }

  [data-pane="rail"]      { grid-column: rail; }
  [data-pane="master"]    { grid-column: master; }
  [data-pane="detail"]    { grid-column: detail; }
  [data-pane="inspector"] { grid-column: inspector; }

  /* The inspector collapses to nothing and gives its width back to the detail
     pane. The toggle that reopens it lives in the header, not in here, so
     there is nothing left to keep visible. */
  [data-shell]:has([data-pane="inspector"][data-collapsed="true"]) {
    grid-template-columns:
      [rail] var(--rail)
      [master] var(--master)
      [detail] minmax(0, 1fr)
      [inspector] 0;
  }
  [data-pane="inspector"][data-collapsed="true"] {
    overflow: hidden;
    visibility: hidden;
  }

  /* ── rhythm inside a pane ──────────────────────────────────────────────
     Every direct child takes the gutter; the space *between* them is one
     rhythm value rather than two paddings meeting, which is what makes a
     column of sections read as evenly set rather than as a stack of boxes
     that happen to be adjacent. */
  [data-pane] > * { padding-inline: var(--pad); }
  [data-pane] > * + * { margin-block-start: var(--section-gap); }
  [data-pane] > [data-bleed] { padding-inline: 0; }

  /* A pane header stays put while its content scrolls under it. */
  [data-pane] > header {
    position: sticky;
    inset-block-start: 0;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    min-block-size: var(--chrome);
    padding-block: var(--pad-block);
    margin-block-start: 0;
  }

  /* Prose is set to a measure. A description that runs the full width of a
     wide detail pane is a description nobody finishes. */
  [data-pane] p { max-inline-size: var(--measure); }

  /* ── layout grammar ────────────────────────────────────────────────────── */
  [data-cols] { display: grid; gap: var(--gap-m); }
  [data-cols="auto"] { grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); }
  [data-cols="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  [data-cols="3"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  [data-cols="4"] { grid-template-columns: repeat(4, minmax(0, 1fr)); }

  [data-gap="s"] { gap: var(--gap-s); }
  [data-gap="m"] { gap: var(--gap-m); }
  [data-gap="l"] { gap: var(--gap-l); }

  [data-align="start"]  { text-align: start; justify-items: start; }
  [data-align="center"] { text-align: center; justify-items: center; }
  [data-align="end"]    { text-align: end; justify-items: end; }

  [data-bleed] { margin-inline: calc(var(--pad) * -1); }

  /* A card. Padding is a token so a skin can make the same markup feel
     tight and industrial or open and expensive. */
  [data-tone] { padding: var(--card-pad); border-radius: var(--radius); }

  /* ── the record list ───────────────────────────────────────────────────── */
  [data-list] { display: flex; flex-direction: column; }
  [data-list] [data-row] {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--space-3);
    min-block-size: var(--row-h);
    padding-inline: var(--pad);
    cursor: pointer;
    transition: background-color var(--quick) var(--ease),
                color var(--quick) var(--ease);
  }
  [data-list] [data-row] > span {
    min-inline-size: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* ── the data table ────────────────────────────────────────────────────
     Cells do not wrap. A rate that breaks across two lines stops being a
     number you can scan down a column, and a bed description that wraps
     doubles the height of every row around it. Wide tables scroll. */
  [data-grid] { inline-size: 100%; }
  [data-grid] th { text-align: start; }
  [data-grid] [data-num] {
    text-align: end;
    font-variant-numeric: tabular-nums;
  }
  [data-grid] tbody tr {
    cursor: pointer;
    transition: background-color var(--quick) var(--ease);
  }
  [data-grid] th, [data-grid] td {
    padding-block: var(--cell-pad-block);
    padding-inline: var(--cell-pad-inline);
    white-space: nowrap;
  }
  [data-grid] th:first-child, [data-grid] td:first-child { padding-inline-start: 0; }
  [data-grid] th:last-child, [data-grid] td:last-child { padding-inline-end: 0; }
  [data-grid] th[aria-sort] { cursor: pointer; user-select: none; }
  [data-grid-scroll] { overflow-x: auto; }

  /* ── fields ────────────────────────────────────────────────────────────── */
  [data-fields] {
    display: grid;
    grid-template-columns: minmax(5.5rem, auto) minmax(0, 1fr);
    gap: var(--space-3) var(--space-4);
    align-items: baseline;
  }
  [data-fields] dd {
    min-inline-size: 0;
    overflow-wrap: anywhere;
  }

  [data-form] {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--field-gap);
  }
  [data-field] { display: flex; flex-direction: column; gap: var(--space-2); min-inline-size: 0; }
  [data-field][data-span="full"] { grid-column: 1 / -1; }
  [data-field][data-span="half"] { grid-column: span 1; }
  [data-field] :is(input, select, textarea) {
    inline-size: 100%;
    min-block-size: var(--tap);
    border-radius: var(--radius);
    transition: border-color var(--quick) var(--ease),
                box-shadow var(--quick) var(--ease);
  }
  [data-field] textarea { min-block-size: 6rem; resize: vertical; }

  /* ── toolbar ───────────────────────────────────────────────────────────── */
  [data-toolbar] {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
  }
  [data-toolbar] button { min-block-size: var(--tap); }
  button {
    border-radius: var(--radius);
    transition: background-color var(--quick) var(--ease),
                color var(--quick) var(--ease),
                border-color var(--quick) var(--ease),
                opacity var(--quick) var(--ease);
  }

  /* ── inline emphasis ───────────────────────────────────────────────────── */
  [data-state] { display: inline-block; white-space: nowrap; }
  [data-key] { font-variant-numeric: tabular-nums; }
  /* A headline number never wraps. `L 4,500.00` broken across two lines stops
     being a quantity and becomes two fragments, and the reader has to
     reassemble it — which is exactly the work the big type was meant to save.
     If it does not fit, the skin's type scale is wrong, and the overflow
     assertion in tools/browse.js says so out loud. */
  [data-metric] {
    display: block;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
  }

  /* ── the gate ──────────────────────────────────────────────────────────
     Sign-in is the one view that is not the shell: it centres in the viewport
     and owns the whole page. */
  [data-gate] {
    display: grid;
    place-items: center;
    min-block-size: 100dvh;
    padding: var(--space-4);
  }
  [data-gate] > * {
    inline-size: min(27rem, 100%);
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
  }
  body:has([data-gate]) { grid-template-rows: 1fr; overflow: auto; }

  /* ── narrow ────────────────────────────────────────────────────────────
     One bay at a time on a phone. The rail becomes a horizontal strip, the
     master and detail swap on selection (the view sets data-focus), and the
     inspector arrives over the top rather than beside. */
  @media (max-width: 60rem) {
    [data-shell] { grid-template-columns: minmax(0, 1fr); grid-auto-rows: minmax(0, 1fr); }
    [data-pane] { grid-column: 1; grid-row: 1; }

    [data-pane="rail"] {
      grid-row: auto;
      display: flex;
      overflow-x: auto;
      overflow-y: hidden;
    }
    [data-shell] { grid-template-rows: auto minmax(0, 1fr); }
    [data-pane="rail"] { grid-row: 1; }
    [data-pane="master"], [data-pane="detail"], [data-pane="inspector"] { grid-row: 2; }

    [data-shell][data-focus="master"] [data-pane="detail"],
    [data-shell][data-focus="detail"] [data-pane="master"] { display: none; }

    [data-pane="inspector"] {
      position: fixed;
      inset: auto 0 0 0;
      z-index: 3;
      max-block-size: 70dvh;
    }
    [data-shell]:has([data-pane="inspector"][data-collapsed="true"]) [data-pane="inspector"],
    [data-pane="inspector"][data-collapsed="true"] { display: none; }

    [data-form] { grid-template-columns: minmax(0, 1fr); }
    [data-field][data-span="half"] { grid-column: 1 / -1; }
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   STATE

   Interaction state, expressed as geometry. What selection *looks* like is a
   theme's business; that a selected row must not shift the rows around it is
   this file's.
   ═══════════════════════════════════════════════════════════════════════════ */

@layer state {

  [aria-current], [aria-selected="true"] { position: relative; }

  :is(button, a, [data-row])[aria-disabled="true"],
  :is(button, input, select, textarea):disabled {
    cursor: not-allowed;
    opacity: .45;
  }

  [data-busy="true"] { cursor: progress; }

  /* The empty state occupies the pane rather than sitting at the top of it,
     so an empty list does not read as a list that failed to load. */
  [data-empty] {
    display: grid;
    place-content: center;
    gap: var(--space-2);
    text-align: center;
    min-block-size: 14rem;
    padding: var(--space-6) var(--space-4);
  }
}
