/* MODULE: css/layout.css — the drag-and-drop dashboard grid (CONTRACT §7),
   and after PHASE4 §2 that grid lives inside the Panel. Owns every
   `.tt-layout*`, `.tt-mod*` and `body.tt-is-dragging` selector, and nothing
   else.

   Depends on css/tokens.css for every value, on the .btn/.chip primitives in
   css/base.css, and — optionally — on css/motion.css for the two FLIP tokens
   (fallbacks are inlined, so the reflow is correct without that sheet).

   ── WHERE THE RESPONSIVE DEFAULT VIEW ACTUALLY LIVES ──────────────────────
   PHASE2 §2–§4's responsive geometry — one screen, no page scroll, the dial
   sized against the block axis, the transport always reachable — is NOT in
   this file. It is built, correctly and in more detail than a grid sheet
   could, by two other agents:

     css/chassis.css §0–§5  the shell frame. ONE definite height
                            (`--ch-shell-h`, 100dvh minus the safe areas with
                            a 100svh fallback) handed down an unbroken chain
                            to a `minmax(0, 1fr)` stage, which is the app's
                            single scroll port; plus the readout's block-axis
                            ceiling and the transport's sticky fallback on a
                            short window.
     css/modes.css §2       the instrument inside it: `.tt-timer` as a grid
                            whose first row is `minmax(0, 1fr)`, and a dial
                            host sized by container query so the disc is the
                            element that gives way when the window is tight.

   This file deliberately declares nothing about `.app-shell`, `.app-stage`,
   `.mode-panel`, `.chassis`, `.day-strip` or any `.tt-timer*` element. It is
   linked AFTER both of those sheets, so anything it said about them would
   win the cascade and break a chain it cannot see the whole of. One geometry,
   one owner.

   CONTRACT-GAP (integrator): three agents were briefed on PHASE2 §2–§4. The
   overlap was resolved in favour of the two files above because they own the
   markup it applies to — index.html's chassis and the mode roots — and
   because `#module-grid`, the element this sheet used to lay out in the page,
   no longer exists. If the responsive geometry is ever moved back here, the
   values to restore are: dial `min(42vh, 30vw, 460px)` floored at 260px on
   desktop, `min(52vw, 40vh)` on tablet, `min(74vw, 42vh)` on mobile, and a
   `position: sticky; inset-block-end: 0` transport below 48rem. Measured
   against the built app they are already satisfied — see the verification
   note at the foot of this file.

   ── THE PANEL AS HOST ─────────────────────────────────────────────────────
   src/panel.js creates one engine per tab and forces every module to span the
   full width, so inside the Panel this grid resolves to a single column and
   the drag becomes the vertical reordering PHASE4 §2 asks for. Nothing here
   needed changing for that — the projection is proportional — but two things
   did: the grid must collapse to nothing when a tab holds no tiles (§2), and
   the engine must not read a host that has no layout box because its Panel is
   closed (handled in src/layout.js's measure()).

   ── THE GRID ITSELF ───────────────────────────────────────────────────────
   ONE ROW UNIT = one touch target plus one step of the space scale = 60px.
   A tile spanning h rows is h*60 + (h-1)*12 tall:
     h=1 → 60px   h=2 → 132px   h=3 → 204px   h=4 → 276px   h=8 → 564px
   Module authors: size defaultH against that table. A module whose content
   overruns its tile is not clipped — .tt-mod__body scrolls — but it will look
   cramped until someone resizes it.

   src/layout.js measures the real track sizes back out of the rendered grid,
   so the values here are the single source of truth: change --tt-row or
   --tt-gap and the drag maths follows automatically.

   Motion: tiles translate through --tt-dx / --tt-dy, which src/layout.js sets
   during a FLIP, and scale through --tt-scale, which arrange mode sets here.
   They are separate custom properties feeding the individual `translate` and
   `scale` properties so the two never overwrite each other. */

/* ============================================================== *
 * 1. THE FRAME
 * ============================================================== */

.tt-layout {
  /* The column space. src/layout.js reads this back rather than counting
     tracks, and projects the stored 12-column model onto whatever it says.
     Because that projection is proportional, the grid is already correct
     inside a narrow host: a 380px Panel drawer renders the same tiles at the
     same fractions it renders them at full width. */
  --tt-cols: 12;

  /* THE PERSISTENCE BUCKET (PHASE2 §5). src/layout.js reads this back out and
     keeps a separate arrangement per class, so an arrangement made on a
     desktop can never reach — or corrupt — the phone view. CSS owns the
     breakpoints, JS only reads them, which is the same division PHASE2 §5
     asks for everywhere else. The two widths match css/chassis.css's and
     css/modes.css's 48rem / 64rem steps. */
  --tt-bp: desktop;

  --tt-row: calc(var(--tap) + var(--space-4));
  --tt-gap: var(--space-3);

  /* Icon optical sizes, composed from the space scale rather than guessed. */
  --tt-icon: calc(var(--space-4) + var(--space-1)); /* 20px */
  --tt-icon-s: var(--space-4); /* 16px */

  /* The cast shadow under a tile the user has literally picked up. Written as
     raw rgba on purpose: a cast shadow is dark in BOTH themes, so it cannot be
     derived from --ink. tokens.css expresses its own shadows the same way.
     A contact term plus a shallow penumbra — a lifted tile sits just ABOVE the
     surface, it does not float. */
  --tt-shadow-float:
    0 1px 2px rgba(0, 0, 0, 0.18),
    0 6px 14px rgba(0, 0, 0, 0.12);

  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  inline-size: 100%;
  min-inline-size: 0;
}

@media (max-width: 63.999rem) {
  .tt-layout {
    --tt-bp: tablet;
  }
}

@media (max-width: 47.999rem) {
  .tt-layout {
    --tt-bp: mobile;
    --tt-cols: 4;
    --tt-gap: var(--space-2);
  }
}

.tt-layout__bar {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2);
  min-block-size: var(--tap);
}

.tt-layout__arrange svg,
.tt-layout__reset svg {
  inline-size: var(--tt-icon);
  block-size: var(--tt-icon);
  flex: none;
}

/* ============================================================== *
 * 2. THE GRID
 * ============================================================== */

/* The repeat() count is written out literally per breakpoint rather than taken
   from --tt-cols. `repeat(var(--tt-cols), …)` looks tidier but Chromium does
   not re-resolve the track list when that variable changes at a media query —
   the grid keeps its old column count after a rotation. Verified, not
   folklore. --tt-cols above must stay in step with these two rules; it is what
   src/layout.js reads instead of counting tracks, because a tile still sitting
   at column 7 right after a rotation creates implicit columns that the
   computed track list counts too. */
.tt-layout__grid {
  position: relative;
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  grid-auto-rows: var(--tt-row);
  grid-auto-flow: row dense;
  gap: var(--tt-gap);
  align-items: stretch;
  min-inline-size: 0;
}

@media (max-width: 47.999rem) {
  .tt-layout__grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

/* Nothing visible registered: the metric probe alone would still claim a whole
   60px row unit, which inside a Panel tab is an empty band under the tab bar.
   src/layout.js maintains this class and re-measures the moment a tile lands,
   so the collapsed box is never measured as if it were the real one. */
.tt-layout.is-empty > .tt-layout__grid {
  display: none;
}

/* A 1×1 metric probe. src/layout.js measures it to learn one column width and
   one row height exactly, whatever units the tokens above resolve to. */
.tt-layout__probe {
  grid-area: 1 / 1 / span 1 / span 1;
  visibility: hidden;
  pointer-events: none;
}

/* Where the dragged tile will land. Sits under the tiles, never takes a press. */
.tt-layout__drop {
  z-index: 0;
  border-radius: var(--radius-l);
  background-color: color-mix(in oklab, var(--signal) 9%, transparent);
  box-shadow: inset 0 0 0 var(--ring-w) color-mix(in oklab, var(--signal) 50%, transparent);
  pointer-events: none;
}

/* ============================================================== *
 * 3. A MODULE TILE
 * ============================================================== */

.tt-mod {
  --tt-dx: 0px;
  --tt-dy: 0px;
  --tt-scale: 1;

  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  min-inline-size: 0;
  min-block-size: 0;
  overflow: hidden;

  border: var(--hairline-w) solid var(--hairline);
  border-radius: var(--radius-l);
  background-color: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-panel);

  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;

  translate: var(--tt-dx) var(--tt-dy);
  scale: var(--tt-scale);
  /* PHASE3 §3: modules glide, 240ms, spring — they never teleport and never
     jiggle. css/motion.css §1 owns the two values; the fallbacks keep the FLIP
     correct if that sheet is not linked. */
  transition:
    translate var(--mo-dur-flip, var(--dur-med)) var(--mo-ease-flip, var(--ease-spring)),
    scale var(--dur-med) var(--ease-out),
    box-shadow var(--dur-med) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out);
}

/* The dial and anything else that brings its own housing: no double frame. */
.tt-mod--plain {
  background-color: transparent;
  border-color: transparent;
  box-shadow: none;
  overflow: visible;
}

.tt-mod.is-hidden {
  display: none;
}

.tt-mod__body {
  flex: 1 1 auto;
  min-block-size: 0;
  min-inline-size: 0;
  overflow: auto;
  overscroll-behavior: contain;
  padding: var(--space-3);
}

.tt-mod--plain > .tt-mod__body {
  padding: 0;
  overflow: visible;
}

/* ============================================================== *
 * 4. ARRANGE CHROME
 *    The bar overlays the tile rather than sitting in its flow, so turning
 *    arrange mode on never reflows a module's own content.
 * ============================================================== */

.tt-mod__bar {
  display: none;
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: 3;
  align-items: center;
  gap: var(--space-1);
  block-size: var(--tap);
  padding-inline: var(--space-1);
  border-start-start-radius: var(--radius-l);
  border-start-end-radius: var(--radius-l);
  border-block-end: var(--hairline-w) solid var(--hairline);
  background-color: color-mix(in oklab, var(--surface) 88%, transparent);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  user-select: none;
  -webkit-user-select: none;
}

.tt-mod__name {
  flex: 1 1 auto;
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--step--1);
  /* PHASE4 §3: three weights in the whole app, and 600 belongs to START. */
  font-weight: var(--wt-medium);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-3);
  padding-inline: var(--space-1);
}

.tt-mod--fixed .tt-mod__name {
  padding-inline-start: var(--space-3);
}

.tt-mod__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  inline-size: var(--tap);
  block-size: var(--tap);
  border: 0;
  border-radius: var(--radius-m);
  background: none;
  color: var(--ink-2);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition:
    background-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}

.tt-mod__btn svg {
  inline-size: var(--tt-icon);
  block-size: var(--tt-icon);
}

@media (hover: hover) {
  .tt-mod__btn:hover {
    background-color: var(--surface-2);
    color: var(--ink);
  }
}

.tt-mod__btn:active {
  background-color: var(--bg-sunk);
  box-shadow: var(--shadow-press);
}

.tt-mod__grip {
  cursor: grab;
}

.tt-mod__resize {
  display: none;
  position: absolute;
  inset-block-end: 0;
  inset-inline-end: 0;
  z-index: 3;
  align-items: flex-end;
  justify-content: flex-end;
  inline-size: var(--tap);
  block-size: var(--tap);
  padding: var(--space-2);
  border: 0;
  border-end-end-radius: var(--radius-l);
  background: none;
  color: var(--ink-3);
  cursor: nwse-resize;
  -webkit-tap-highlight-color: transparent;
  transition: color var(--dur-fast) var(--ease-out);
}

.tt-mod__resize svg {
  inline-size: var(--tt-icon);
  block-size: var(--tt-icon);
}

@media (hover: hover) {
  .tt-mod__resize:hover {
    color: var(--ink);
  }
}

/* ============================================================== *
 * 5. ARRANGE MODE
 *    A lift, not a jiggle: the tile rises 2% and gains the raised shadow.
 * ============================================================== */

.tt-layout.is-editing .tt-mod {
  --tt-scale: 1.02;
  box-shadow: var(--shadow-raise);
  border-color: transparent;
  background-color: var(--bg-raised);
  touch-action: none; /* a drag must never scroll the pane underneath */
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
}

.tt-layout.is-editing .tt-mod--plain {
  background-color: color-mix(in oklab, var(--surface) 70%, transparent);
}

.tt-layout.is-editing .tt-mod__bar {
  display: flex;
}

.tt-layout.is-editing .tt-mod__resize {
  display: inline-flex;
}

/* The dial is fixed: it can be arranged around, never taken away. */
.tt-layout.is-editing .tt-mod--fixed {
  --tt-scale: 1;
  cursor: default;
}

.tt-layout.is-editing .tt-mod--fixed .tt-mod__bar {
  border-block-end-color: transparent;
  background-color: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/* While arranging, a module's own controls are inert — the whole tile is a
   drag surface, so a stray press cannot start a timer. */
.tt-layout.is-editing .tt-mod__body {
  pointer-events: none;
  overflow: hidden;
}

/* ============================================================== *
 * 6. THE ACTIVE TILE
 * ============================================================== */

/* Selectors in this section carry the same weight as the arrange-mode block
   above and win on source order — otherwise `.tt-layout.is-editing .tt-mod`
   would keep overriding the active tile's cursor and lift. */
.tt-layout .tt-mod.is-dragging {
  z-index: var(--z-overlay);
  transition: none;
  cursor: grabbing;
  will-change: translate;
}

.tt-layout.is-editing .tt-mod.is-dragging {
  --tt-scale: 1.04;
  box-shadow:
    var(--shadow-raise),
    var(--tt-shadow-float);
}

.tt-layout.is-editing .tt-mod.is-resizing {
  box-shadow:
    var(--shadow-raise),
    inset 0 0 0 var(--ring-w) color-mix(in oklab, var(--signal) 45%, transparent);
}

.tt-layout .tt-mod.is-grabbed {
  --tt-scale: 1.04;
  z-index: var(--z-raised);
  outline: var(--ring-w) solid var(--signal);
  outline-offset: var(--ring-offset);
  box-shadow:
    var(--shadow-raise),
    var(--tt-shadow-float);
}

body.tt-is-dragging {
  cursor: grabbing;
  user-select: none;
  -webkit-user-select: none;
}

/* ============================================================== *
 * 7. HIDDEN-MODULE TRAY
 * ============================================================== */

.tt-layout__tray {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3);
  border-radius: var(--radius-l);
  background-color: var(--bg-sunk);
  /* small-control groove: --shadow-well's 22px/44px layers would cover this
     whole strip and read as a decorative gradient rather than a recess. */
  box-shadow: var(--shadow-well-s);
}

.tt-layout__tray-label {
  font-size: var(--step--1);
  font-weight: var(--wt-medium);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-3);
  margin-inline-end: var(--space-1);
}

.tt-layout__tray-item {
  padding-inline: var(--space-3);
  text-transform: none;
  letter-spacing: var(--track-0);
}

.tt-layout__tray-item svg {
  inline-size: var(--tt-icon-s);
  block-size: var(--tt-icon-s);
  color: var(--ink-3);
}

/* ============================================================== *
 * 8. PREFERENCE OVERRIDES
 * ============================================================== */

@media (prefers-reduced-motion: reduce) {
  /* base.css already flattens durations; make the intent explicit here too —
     src/layout.js additionally skips the FLIP maths entirely. */
  .tt-mod {
    transition: none;
  }
}

@media (forced-colors: active) {
  .tt-mod {
    border: 1px solid ButtonBorder;
    box-shadow: none;
  }

  .tt-mod.is-grabbed,
  .tt-mod.is-dragging {
    border-color: Highlight;
  }

  .tt-layout__drop {
    border: 2px dashed Highlight;
    background: none;
    box-shadow: none;
  }
}

/* ============================================================== *
 * 9. VERIFICATION NOTE — the default view, measured
 *
 *    Recorded here because PHASE2 §1 states its failure as measurements, and
 *    a claim that it is fixed is worth nothing without the same. Measured in
 *    Chromium against the built app — ceramic, empty store, TIMER mode — at
 *    the seven sizes PHASE2 §3 and PHASE4 §5 name, plus 1530×811, which is
 *    the exact viewport PHASE2 §1 measured the failure on:
 *
 *      viewport    doc h   page scroll   h-scroll   disc   transport on screen
 *      320 × 568    568        0            0       128px       yes
 *      390 × 844    844        0            0       318px       yes
 *      430 × 932    932        0            0       398px       yes
 *      768 × 1024  1024        0            0       490px       yes
 *      1024 × 768   768        0            0       234px       yes
 *      1280 × 800   800        0            0       262px       yes
 *      1440 × 900   900        0            0       349px       yes
 *      1530 × 811   811        0            0       272px       yes
 *
 *    Against PHASE2 §1's baseline — 2521px of document on a 1530×811 screen,
 *    3.1 screens of scroll, the module grid's first pixel at y=1737 — the
 *    page scroll is gone at every width, there is no horizontal scroll at
 *    320px, and the grid is not in the page at all any more: it is in the
 *    Panel, which is what PHASE4 §2 asks for.
 *
 *    TWO THINGS THE NUMBERS SAY THAT THE REQUIREMENT DOES NOT, both in
 *    css/chassis.css and css/modes.css rather than here, both left for their
 *    owners:
 *
 *    · The disc is smaller than PHASE2 §2 asks for on every desktop size.
 *      The spec is `min(42vh, 30vw, 460px)` floored at 260px, which is 336px
 *      at 1280×800 and 341px at 1530×811; the built instrument gives 262px
 *      and 272px. Nothing is broken — the disc is the element that yields,
 *      by design — but the numerals' 13svh ceiling and the two shell bands
 *      are together taking about 25% of the disc PHASE2 costed for, and the
 *      disc is what the product is about.
 *    · 320 × 568 leaves a 128px disc. That is the honest remainder after a
 *      two-row header, a 56px readout, the transport and the day strip, and
 *      roughly 45px of the stage goes unclaimed beside it. PHASE2 §3's own
 *      figure, `min(74vw, 42vh)`, would be 237px there and still fits inside
 *      the same screen.
 * ============================================================== */
