/* Gala · the nine-canvas shell
 * ============================================================================
 * ONE grid, many pages. Standing rule (ops/CANVAS-TEMPLATE.md, 16 Sep 2026):
 * "keep these nine canvases as containers. If we change anything, we will
 * change it in that container. This should be like a template."
 *
 * WHY THIS FILE EXISTS. By 20 September there were three nine-canvas pages —
 * vista.html, learn-vista.html, agentimeter/index.html — each with its own
 * copy of the grid, and they had already drifted: two used `.canvas` and the
 * runtime weight contract, the third used `.c` and hardcoded 1fr 1.35fr 1fr.
 * Three copies of a rule is no rule. This is the one copy.
 *
 * IT INVENTS NO NEW VOCABULARY. The custom property names below are the ones
 * vista.html and learn-vista.html already write from /api/vista. That is
 * deliberate: those pages must be able to adopt this file without touching a
 * line of their JavaScript. A shell that required a rewrite would not get
 * adopted, and we would have four copies instead of three.
 *
 * WHAT IT OWNS        the grid, the lift, the ground, the stacking order
 * WHAT IT DOES NOT    proportions (runtime, from weight) and colour (the
 *                     pack). Both arrive as custom properties from outside.
 *
 * THE THREE AXES, set on the container:
 *   data-fit    fill  · the grid is the viewport        (landing pages)
 *               frame · 16:9 letterboxed, centred       (the Vistas)
 *   data-ground paper · light, for reading
 *               stage · dark, the canvases float on it
 *               pack  · inherit --paper/--card from packs/*.css
 *   data-weight static · proportions from this file     (a landing page)
 *               live   · proportions from /api/vista    (a Vista)
 * ========================================================================= */

/* ── the reset the shell needs ────────────────────────────────────────────
   NOT a house reset — exactly the two declarations without which the grid is
   wrong, scoped so the shell cannot restyle a host page's other content.

   box-sizing is the one that bites. Without it the frame's `width` excludes
   its own padding, the columns are laid out against a container 16px wider
   than the space available, and the page scrolls sideways by exactly the two
   gaps. It looks like a content overflow and it is a box-model one. Measured
   on cbse/grade9 at 1440px before this rule existed. */
.vista, .vista *{ box-sizing:border-box; }

/* ── tokens ──────────────────────────────────────────────────────────────
   THE SHELL READS TOKENS. IT DOES NOT DECLARE THEM.

   Every value below appears as a var() fallback at its point of use and
   nowhere as a declaration, and that is a rule rather than a style. A
   declaration on `.vista` beats a page's `:root` value — `.vista` is a nearer
   ancestor to a canvas than `:root` is — so a shell that declared its own
   defaults would quietly outrank the page it is serving.

   That is not hypothetical. learn-vista.html writes its six live weights onto
   `:root`, and the first version of this file declared --gap on .vista. The
   gap went from 17px to 8px with nothing in the page changed, and a static
   default for --col-work outranked a real weight of 52fr, collapsing the
   centre column to 51px while the flanks kept their live 26fr.

     --gap         8px      space between canvases
     --radius      11px     a card's corner
     --lift        two      contact shadow + the float above the ground
     --rail        66px     a side column, folded shut
     --shift       .42s     how weight moves
     --canvas-pad  24/26    a canvas's own inset
     --col-input --col-work --col-output --row-top --row-mid --row-bot
                            the proportions; set these wherever you like

   A page sets any of them on :root, on .vista, or inline from JavaScript, and
   the nearest one wins — which is how the cascade is supposed to behave. */

/* ── the grid ─────────────────────────────────────────────────────────────
   Three columns, three rows, always. A market changes what is inside a
   container; it never adds a tenth, never moves one, never leaves one empty.

   The fallbacks in these var() calls are the STATIC proportions — the centre
   column wider than its neighbours, the middle row taller. A landing page
   never overrides them and gets a fixed composition. A Vista overwrites all
   six at load from `weight`, and again whenever weight shifts, which is why
   they are custom properties and not literals: a stylesheet cannot be told
   to shift by an agent. */
.vista{
  display:grid;
  gap:var(--gap, 8px);
  min-width:0;
  grid-template-columns:var(--col-input,1fr) var(--col-work,1.35fr) var(--col-output,1fr);
  grid-template-rows:var(--row-top,minmax(190px,auto)) var(--row-mid,1fr) var(--row-bot,minmax(180px,auto));
  transition:grid-template-columns var(--shift,.42s cubic-bezier(.22,.61,.36,1)),
             grid-template-rows    var(--shift,.42s cubic-bezier(.22,.61,.36,1));
}

/* FILL — the grid is the viewport. Nothing scrolls on a desktop, because the
   whole proposition is meant to be one look. */
.vista[data-fit="fill"]{ min-height:100dvh; padding:var(--fill-pad, 10px); }

/* FRAME — 16:9, letterboxed on whichever axis runs out first, centred on a
   ground that fills the screen. On a wider monitor the ground grows and the
   nine canvases do not: more wheat either side, same composition. A grid whose
   proportions are read from polyprint.spaces stops meaning anything if the
   display can restretch it, and a face that gets wider on one screen is a
   different face. */
.vista[data-fit="frame"]{
  /* 100% not 100vw — 100vw includes the scrollbar gutter, so on any page
     that scrolls the frame is wider than the room it has. */
  width:min(100%, calc(100vh * 16 / 9));
  aspect-ratio:16 / 9;
  max-height:100vh;
  /* A token, for the same reason as every other value here. This rule carries
     an attribute selector, so it outranks a plain `.vista{padding:…}` in a
     page's own media query — and a media query adds no specificity. Reading a
     token instead of declaring a length lets the page win without having to
     know the shell's selector. learn-vista tightens this to 11px at phone
     width and was silently losing that. */
  padding:var(--frame-pad, calc(var(--gap, 8px) * 1.25) var(--gap, 8px) var(--gap, 8px));
}
body:has(> .vista[data-fit="frame"]){ display:grid; place-items:center; min-height:100dvh; margin:0; }

/* ── a canvas ─────────────────────────────────────────────────────────────
   min-width/min-height:0 are not cosmetic. A grid child defaults to
   min-content, so one long unbroken word or a wide <pre> in ANY canvas
   silently widens its whole column and the composition is gone. This is the
   single most common way a nine-canvas page breaks, and it breaks in a way
   that looks like a content problem rather than a layout one. */
.canvas{
  position:relative;
  display:flex;
  flex-direction:column;
  min-width:0;
  min-height:0;
  /* A token, because not every page wants it. learn-vista's canvases have
     no padding at all — the .hd and .bd inside them carry their own — so it
     sets --canvas-pad:0 rather than fighting a blanket value. */
  padding:var(--canvas-pad, 24px 26px);
  border-radius:var(--radius, 11px);
  background:var(--sp, var(--card, #fff));
  /* Two lookups, and the order is the whole point. --lift is the page's or
     the pack's; --lift-ground is the shell's per-ground default, declared on
     BODY so it stays further away than anything the page says. The shell must
     never win a shadow argument with the page it is serving. */
  box-shadow:var(--lift, var(--lift-ground, 0 1px 2px rgba(18,18,22,.05), 0 6px 16px -10px rgba(18,18,22,.18)));
}

/* The slot tint. packs/*.css publish --sp-<space> for each of the ten space
   names; a canvas asks for its own by name and falls back to the flat card
   colour, so an unthemed page still renders correctly rather than blank.

   THE ATTRIBUTE IS data-space, NOT data-slot. polyprint.spaces calls the
   column space_id and learn-vista.html already writes data-space on every
   canvas — so this is the estate's word, not a new one. It also had to move:
   learn-vista uses data-slot for a human LABEL ("the claims · empty") and
   renders it as its own empty marker. Two meanings on one attribute name is a
   collision that only shows up on the page that has both. */
.canvas[data-space="ledger"]{ --sp:var(--sp-ledger); }
.canvas[data-space="figure"]{ --sp:var(--sp-figure); }
.canvas[data-space="main"]  { --sp:var(--sp-main);   }
.canvas[data-space="poster"]{ --sp:var(--sp-poster); }
.canvas[data-space="reel"]  { --sp:var(--sp-reel);   }
.canvas[data-space="line"]  { --sp:var(--sp-line);   }
.canvas[data-space="still"] { --sp:var(--sp-still);  }
.canvas[data-space="talk"]  { --sp:var(--sp-talk);   }
.canvas[data-space="setl"]  { --sp:var(--sp-setl);   }
.canvas[data-space="setr"]  { --sp:var(--sp-setr);   }

/* The label. One size, one weight, one colour, across the whole estate —
   this is the part that makes 48 sites read as one instrument applied to
   many trades rather than as 48 attempts. */
.canvas > h2{
  font-size:10.5px; text-transform:uppercase; letter-spacing:.09em;
  font-weight:600; color:var(--muted,#8d8d95); margin:0 0 12px;
}

/* A SCREEN IS STILL A CANVAS. Same box, same lift, same radius — it differs
   only in that it is dark and clips its contents. Media is full-bleed, so the
   padding goes; overflow:hidden is what keeps a video inside the radius. */
.canvas[data-screen]{
  background:var(--screen,#121216); color:#fff; overflow:hidden;
}
.canvas[data-screen] > h2{ color:#7e7e8c; }
.canvas[data-screen][data-bleed]{ --canvas-pad:0; }
/* A bleed canvas has no padding, so its label would sit hard in the corner.
   Float it over the media instead of insetting the media — the whole point of
   bleed is that the frame reaches the radius. The scrim is what keeps the
   label legible over a bright frame; without it the label disappears on any
   pale shot and you only find out on the one episode that is shot outdoors. */
.canvas[data-screen][data-bleed] > h2{
  position:absolute; inset:0 auto auto 0; z-index:4; margin:0;
  padding:14px 16px 20px; width:100%;
  background:linear-gradient(180deg, rgba(10,10,14,.55), transparent);
  pointer-events:none;
}

/* An empty container is a bug, not a style. The standing rule says a market
   never leaves one empty, so an empty one says so in place rather than
   rendering as a blank card that looks deliberate. */
.canvas[data-filled="0"]::after{
  content:attr(data-space) " · empty";
  position:absolute; inset:auto 0 0 0; padding:6px 10px;
  font-size:10px; letter-spacing:.06em; text-transform:uppercase;
  color:#b4442f; opacity:.75;
}

/* ── the ground ───────────────────────────────────────────────────────────
   The canvases are objects on a stage, not cells in a table, so the ground
   must be DARKER than the card or nothing reads as lifted. That single
   relationship is what the whole treatment rests on. */
body[data-ground="paper"]{
  /* warmer and a shade deeper than a white card, so the card reads as lifted
     off it rather than cut into it */
  background:#e9e7e4; color:#0c0c0d;
}
body[data-ground="stage"]{
  --lift-ground:0 1px 2px rgba(0,0,0,.13), 0 14px 34px -16px rgba(0,0,0,.42);
  /* a vignette, so the plane has a centre and the centre canvas sits in it */
  background:radial-gradient(120% 100% at 50% 40%, #4a4c52 0%, #2f3033 62%, #232427 100%);
  color:#f2f2f4;
}


/* PACK — the ground is lit from the pack's own tokens, so a ground change
   carries the stage with it instead of fighting it. Light from above and
   behind, shadow pooling at the foot of the frame. */
body[data-ground="pack"]{
  --lift-ground:0 1px 2px rgba(0,0,0,.13), 0 14px 34px -16px rgba(0,0,0,.42);
  color:var(--ink);
  background:
    radial-gradient(128% 86% at 50% -16%, color-mix(in srgb,var(--card) 62%,transparent) 0%, transparent 60%),
    radial-gradient(96% 74% at 50% 122%, color-mix(in srgb,var(--ink) 16%,transparent) 0%, transparent 56%),
    var(--paper);
}


/* ── folding a side column ────────────────────────────────────────────────
   A Vista can shut a side rail to give the centre the room. The grid keeps
   three columns — the rail is narrow, not gone — because removing a column
   would move every canvas after it, and moving a canvas is rearranging the
   argument. */
body[data-left="shut"]  .vista{ --col-input:var(--rail, 66px); }
body[data-right="shut"] .vista{ --col-output:var(--rail, 66px); }

/* ── narrow ───────────────────────────────────────────────────────────────
   One column. The nine keep their ORDER, because the nine are the argument in
   order: who we are, what this is, proof it is real, what you must have, where
   you stand, what you are missing, why it matters, do it now, buy. Stacking is
   allowed to change the shape; it is not allowed to change the sequence.

   TWO BREAKPOINTS, BECAUSE THE TWO FAMILIES HONESTLY DIFFER. A landing page
   has nine blocks of prose and stops working three-across at about 1040px. A
   Vista is a dashboard whose side columns are already rails, and it holds its
   three columns down to 900px. One number could not serve both, and a media
   query cannot read a custom property — so it is an attribute:

     <div class="vista" data-stack="900">   the Vistas
     <div class="vista">                    everything else, 1040px

   A media query adds no specificity, so both blocks are written at the same
   weight and separated by :not() rather than by order.

   GRID-COLUMN IS RESET, AND IT HAS TO BE !important. A Vista writes
   `grid-column:3` into each canvas's INLINE style from column_role. Collapse
   the template to one column without clearing that and the canvas is placed
   in an implicit third column sized by its content — three ragged columns
   instead of one, which is what learn-vista did at 1000px before this existed:
   233/466/233 became 470/319/142. Inline styles are beaten by !important and
   by nothing else. Rows are deliberately NOT reset: a page that assigns
   stacked rows by hand (learn-vista does) must keep that.

   AND THE STACKED TEMPLATE IS ITSELF A TOKEN. learn-vista's phone layout is
   deliberately TWO columns with three canvases hidden and the rest placed by
   hand. A literal `grid-template-columns:1fr` here carries an attribute
   selector and beat it, collapsing that design to one column. --stack-cols and
   --stack-rows let the page state its own and win. The !important on
   grid-column only has to beat an INLINE style, so an id selector in the page
   (#c-talk{grid-column:1/-1 !important}) still outranks it, as it must. */
@media (max-width:1040px){
  .vista:not([data-stack="900"]){
    grid-template-columns:var(--stack-cols, 1fr);
    grid-template-rows:var(--stack-rows, none); }
  .vista:not([data-stack="900"]) > .canvas{ grid-column:var(--stack-col, 1) !important; }
  .vista:not([data-stack="900"])[data-fit="frame"]{ width:100%; aspect-ratio:auto; max-height:none; }
}
@media (max-width:900px){
  .vista[data-stack="900"]{
    grid-template-columns:var(--stack-cols, 1fr);
    grid-template-rows:var(--stack-rows, none); }
  .vista[data-stack="900"] > .canvas{ grid-column:var(--stack-col, 1) !important; }
  .vista[data-stack="900"][data-fit="frame"]{ width:100%; aspect-ratio:auto; max-height:none; }
}

@media (max-width:1040px){
  /* a tighter inset when there is less room — still through the token, so a
     page that set --canvas-pad:0 stays at 0 */
  .canvas{ min-height:0; padding:var(--canvas-pad, 20px 18px); }
  /* Stacked, a screen IS the canvas, so it takes the shape of its media.
     A vertical short in a 16/10 box is a postage stamp. */
  .canvas[data-screen][data-o="horizontal"]{ aspect-ratio:16/10; }
  .canvas[data-screen][data-o="vertical"]  { aspect-ratio:9/16; max-height:78vh; }
}

@media (prefers-reduced-motion:reduce){
  .vista{ transition:none; }
}
