/* ══════════════════════════════════════════════════════════════════════════
   play.css — the playground, and nothing else.

   Two panes side by side: the editor left, the figure right. The glass, the
   labels, the `run` pill and the editor's own metrics all come from site.css;
   the card in the picker comes from cards.css. What is here is the
   arrangement.
   ══════════════════════════════════════════════════════════════════════════ */

/* This file is only ever linked by /play, so overriding a shell class here
   reaches that page and nothing else. The playground is a workbench rather
   than a column of prose: 1240px is the measure the landing page's copy wants
   and it wastes a third of a wide screen on two panes of code. The nav above
   moves out with it, which is the point — a 1680px working area under a
   1240px bar reads as two pages stacked. */
.l-wrap{max-width:min(1680px,100%)}

/* THE PLAYGROUND'S PANES ARE NOT GLASS, AND THIS IS THE ONE RULE THAT SAYS SO.
   `.pane` in site.css carries `backdrop-filter: blur(6px)`; that declaration is
   a read-back, it puts the page's compositing on a chain of render passes over
   a backdrop that never stops moving, and when a link in the chain misses its
   frame the nav and a pane blank for it. The pane block there has the whole
   mechanism and the reason the landing page and /gallery take that risk: on a
   page being read, a field line crossing a pane at full contrast is worse than
   an occasional one-frame blank.

   /play is not a page being read. It is a tool with a text cursor in it, held
   open for as long as someone is working, and a blank frame under a cursor is a
   different thing from a blank frame under a paragraph. It also has the least
   to gain: one figure and one editor cover most of the window, so there is
   barely any backdrop left showing through to soften. Flat here is the choice,
   not an oversight — and it makes /play the control case, the page to compare
   against when the flash is reported on one of the other two.

   `.pane` rather than `.play-pane`, though the two panes on this page are the
   same two elements today: the rule being stated is "no read-back on this
   page", and a third pane added here later should be flat without anyone
   remembering this. It wins on source order, which is what the `.l-wrap`
   override above relies on as well — this file is linked after site.css,
   always. */
.pane{-webkit-backdrop-filter:none;backdrop-filter:none}

/* The working area fills what the nav leaves, so the editor is as tall as the
   window allows rather than as tall as its content. A tool that scrolls the
   whole page to show more source is a document, not a tool.

   `min-height:0` on the panes is what lets the editor inside them scroll: a
   grid item's default `min-height:auto` refuses to shrink below its content,
   so without it a long source pushes the pane past the viewport.

   `--play-chrome` is everything the working area is not, added up, and with
   the footer gone that sum is now the whole of the rest of the page — so the
   page is exactly one viewport and the only scrollbar on it is the editor's
   own. The nav is site.css's: `padding:clamp(18px,2.2vw,30px)` around a row
   whose tallest child is `.nav-right` at 38px — not the 27px mark, which is
   what this sum said for a long time and is why the panes only ever cleared
   the bottom of the window by 9px while the comment claimed 16. Measured, the
   bar is 38 + 2×30 = 98px at the clamp's ceiling. Then an inset so the panes
   do not sit on the bottom edge of the window:

     98 nav  +  0 margin-top  +  24 bottom inset  =  122px

   The margin was 14, then 4, and is now none: the nav's own bottom padding is
   already the gap, and anything on top of it read as a seam. Every pixel of
   air the page has left is spent at the bottom instead, where there was too
   little — the panes ended a hair off the fold and the window looked cut.

   It belongs in this sum and not in a `margin-bottom`, which is the whole
   reason the sum exists. A margin is added to a height this rule has already
   fixed at the viewport, so it does not shorten the panes, it overflows them
   — a few pixels of it is invisible and any more is a page scrollbar beside
   the editor's, which is the one thing this page must not have.

   The nav is shorter than 87 below ~1364px, where the clamp comes off its
   ceiling; taking the tallest it is ever is what makes the arithmetic a floor
   on the space left over rather than a guess at it. Being a few pixels short
   of the fold costs nothing; being a few pixels over costs a page scrollbar
   beside the editor's.

   IT IS ALSO THIS PAGE'S POSITIONED LAYER, WHICH IS WHAT KEEPS THE GLOWS
   BEHIND IT. `.bg-glows` is `position:fixed` at `--z-backdrop`, which is 0 —
   and a positioned element at z-index 0 paints *after* every in-flow,
   non-positioned box in the same stacking context, whatever the source order.
   So a backdrop layer only stays a backdrop for content that has opted into the
   tier scale: site.css sweeps `.s-range,.s-core,.s-ascii,.s-footer` into
   `--z-field`, gallery.css puts `.s-gallery` there, home.css puts the hero and
   the showcase at `--z-content`. The working area is this page's section and it
   never joined them, so the four drifting balls painted over the editor and the
   figure — a pink wash across a schematic, worst where a glow was densest.

   It went unseen because the panes used to carry `backdrop-filter`. A filter
   makes an element a stacking context, and Chromium paints a non-positioned
   stacking context in the positioned phase at z-index 0, in tree order — after
   `.bg-glows`, which is earlier in the document. Every pane was clearing the
   backdrop by accident, and it stopped the moment the filter went. The filter
   is back on the other two pages and deliberately not here, so this rule is
   what holds on /play; it would be owed anyway, since a pane must not depend on
   its own filter to know which side of the backdrop it is on.

   `--z-field` rather than a higher tier: this is a section's content, which is
   what that tier is for, and the nav above it stays at `--z-nav`. */
.play{position:relative;z-index:var(--z-field);
  display:grid;grid-template-columns:var(--play-left,1fr) var(--space-gap) 1fr;
  height:calc(100vh - var(--play-chrome));
  --play-chrome:122px}
/* The seam between the panes is the split. The whole gap is the grip — a
   column the width of the gutter, full height, `col-resize` under the
   pointer — and a short pill in its middle is the only ink, there to say a
   grip exists; it takes the accent while the pointer is on the seam or
   dragging it. play.js writes `--play-left` in pixels as the pointer moves
   and remembers it, so the split is the reader's from one visit to the next.
   `.is-dragging` also lifts pointer events off the editor and the figure, or
   the drag would select source and stall over an <svg>. */
.play-divider{position:relative;cursor:col-resize;touch-action:none;
  user-select:none}
.play-divider::before{content:"";position:absolute;left:50%;top:50%;
  width:4px;height:28px;border-radius:var(--radius-pill);
  transform:translate(-50%,-50%);background:var(--color-text-soft);opacity:.55;
  transition:background-color var(--duration-hover) var(--ease-out),
             opacity var(--duration-hover) var(--ease-out)}
.play-divider:hover::before,.play.is-dragging .play-divider::before{
  background:var(--color-accent-solid);opacity:1}
.play.is-dragging{cursor:col-resize}
.play.is-dragging .play-pane{pointer-events:none}
/* The 24px outer radius every top-level pane wears. `.pane` in site.css
   deliberately carries no radius of its own — the component that applies the
   class is what knows whether it is a card, a beat or a working pane — so this
   is the pane saying which it is, the same way `.example-card` does. */
.play-pane{display:flex;flex-direction:column;min-height:0;padding:14px 16px 16px;
  border-radius:var(--radius-pane)}

/* One row per pane, and the two read as a pair: the gap pushes what is on the
   right to the right, so `run` and `download` line up down the page. */
.play-head{display:flex;align-items:center;gap:9px;margin-bottom:12px;flex:none}
.play-head-gap{flex:1}

/* Every control in a head is a real button now, one spec: type at reading
   size, a paper pill, the sheet's cast scaled to a control — offset sideways,
   blurred wide, pulled back hard by the negative spread, so what lands reads
   as the glow pooling beside it rather than as a shadow under it. The
   micro-label treatment these wore (10px uppercase mono, no ground) read as
   annotations on the pane rather than as things a hand could press, and a
   tool's controls have to look pressable before anyone learns what they do.

   Two of them carry the brand and the rest stay paper, which is the whole
   hierarchy: `Examples` because it is the one control a newcomer needs —
   what can this thing draw — and `run` because it is the one that changes
   what the figure is. Only colour moves on hover; the site's rule is that
   CTAs move on nothing, and a pill that lifts under a pointer is a twitch
   rather than a response. */
.play-examples,
.play-head .play-tool,.play-head .copy,.play-head .flip,.play-head .run,
.picker-head .play-tool{flex:none;display:inline-flex;align-items:center;gap:7px;
  font-family:inherit;font-size:12.5px;font-weight:500;
  letter-spacing:normal;text-transform:none;
  color:var(--color-ink);background:var(--color-paper);
  padding:6px 13px;border-radius:var(--radius-pill);
  box-shadow:6px 0 22px -12px var(--color-sheet-cast);
  transition:color var(--duration-hover) var(--ease-out),
             background-color var(--duration-hover) var(--ease-out)}
.play-head .play-tool:hover,.play-head .copy:hover,.play-head .flip:hover,
.picker-head .play-tool:hover{color:var(--color-accent-deep);background:var(--color-paper)}
.play-examples,.play-head .run{background:var(--color-brand);
  color:var(--color-on-brand)}
.play-examples:hover,.play-head .run:hover{background:var(--color-brand-hover);
  color:var(--color-on-brand)}
/* The picker is paper, so a paper pill in its head is a label pretending to
   be a button — the wash is the ground that shows on this ground, and the
   cast goes with it: a shadow under a translucent tint reads as dirt. */
.picker-head .play-tool{background:var(--color-wash);box-shadow:none}

/* The same pill, square, holding a mark instead of a word: `fmt`, `share`,
   `copy` and `download` are icons now, and `run` and `desugar` are not. That
   split is the head's hierarchy — the two that change what you are looking at
   keep their word, and the four that act on what is already there are marks
   you learn once. The pill radius on a 32px square is a circle, so the shape
   comes for free from the spec above; what is left is the size, the lost
   padding and centring the one child.

   `.copy` wears this too and is not a `.play-tool`. The modifier names the
   shape the head's controls share rather than one component's variant, which
   is the honest reading when the head styles all of them as one row anyway. */
.play-head .play-tool--icon{width:32px;height:32px;padding:0;
  justify-content:center;position:relative}
/* Feedback without a word. site.js swaps a text button's label to `copied`;
   on one of these that would delete the SVG, so it toggles a class instead and
   moves the confirmation into the title. Both outcomes wear the brand — colour
   says *something happened*, the tooltip says which — and the ground stays
   paper, because `.copy.is-done`'s filled pill was written for a button whose
   own text had to stay legible on it. */
.play-head .play-tool--icon.is-done,
.play-head .play-tool--icon.is-failed{color:var(--color-brand);
  background:var(--color-paper)}

/* …and the word itself, under the button that was pressed.

   Colour alone was the whole confirmation, and a 32px mark going from grey to
   pink for a beat is not one: the report lived in `title`, which a pointer
   only reads after resting for a second and a tap never reads at all. So the
   button that copies a link looked exactly as inert as a button that does
   nothing, which is the review it got.

   `attr(aria-label)` rather than a fixed string, because the handlers already
   move that attribute to the outcome — so the bubble says `Copied`, `Link
   copied` or `Copy failed` without a second copy of those words here, and what
   is on screen is what a screen reader is being told. It is a bubble anchored
   to its own button rather than a toast in the corner for the reason the
   install line says `copied` in its own face: the thing you clicked is the
   thing that answers. */
.play-head .play-tool--icon.is-done::after,
.play-head .play-tool--icon.is-failed::after{content:attr(aria-label);
  position:absolute;top:calc(100% + 8px);left:50%;translate:-50% 0;
  padding:4px 9px;border-radius:var(--radius-pill);
  background:var(--color-brand);color:var(--color-on-brand);
  font-size:12px;line-height:1.45;font-weight:500;white-space:nowrap;
  box-shadow:var(--shadow-control-sm);pointer-events:none;z-index:2;
  animation:play-said 140ms ease-out}
@keyframes play-said{from{opacity:0;translate:-50% -4px}}
@media (prefers-reduced-motion:reduce){
  .play-head .play-tool--icon.is-done::after,
  .play-head .play-tool--icon.is-failed::after{animation:none}
}

/* All a `.play-tool` keeps of its own is the disabled state — its look is the
   shared button spec above. */
.play-tool:disabled{opacity:.45;cursor:default}

/* The editor fills the pane and scrolls inside it. `position:relative` is the
   gutter's and the ink's containing block — .editor is absolute-positioned
   layers over a relative box, and site.css sizes them to `inset:0`. */
.play-source-box{position:relative;flex:1;min-height:0}

/* The gutter runs the full height of the pane: its tint continues past the
   last line, and the numbers stop where the file does. Numbering empty space
   would claim lines that are not there, and a column that stopped with them
   would look like it ran out rather than like the file did.

   A wash rather than a rule. DESIGN.md's footer note is this site's position
   on drawn lines — "it would still be a line drawn between two things. The gap
   above is doing it." A tinted column separates the numbering from the code
   without drawing anything. */
.play-pane .editor-gutter{background:var(--color-wash)}
/* Before play.js runs this holds a plain <pre>, which has to look and scroll
   like what replaces it or the page jumps on load — same face, size, line
   height and tab size, and the same left inset the gutter will occupy.

   `2.6em + .55em` is site.css's `--gutter` plus the text layers' breathing
   room, spelled again because the variable is declared on `.editor`, which
   does not exist until the editor is built. Two copies of one sum is the cost
   of the page reading correctly before any JavaScript runs; if they drift the
   source visibly jumps sideways on load, which is the kind of bug that shows
   itself. */
.play-source{position:absolute;inset:0;margin:0;overflow:auto;white-space:pre;
  padding-left:calc(2.6em + .55em);font-family:var(--font-code);font-size:13.5px;
  line-height:1.55;tab-size:2;color:var(--color-ink)}

/* Diagnostics are a row under the editor, not an overlay over it.
   The carousel overlays a short pane; this editor is the height of the window,
   and an overlay would cover the lines someone is typing. Sized to its content
   up to a third of the pane, then it scrolls.

   `position:static` is the load-bearing declaration: the element wears
   `.editor-errors` too, so that it is the same component the carousel shows,
   and site.css pins that one `absolute` to the foot of its pane. Without this
   the row would leave the flow and sit over the source — the exact thing the
   rule exists to avoid. */
.play-errors{position:static;flex:none;margin-top:10px;max-height:30%;overflow:auto;
  background:var(--color-brand-tint);border-radius:var(--radius-pane-in);
  padding:9px 12px;font-family:var(--font-code);font-size:11.5px;
  line-height:1.5;color:var(--color-brand-deep)}
.play-errors[hidden]{display:none}

.play-figure{flex:1;min-height:0;display:grid;place-items:center;
  background:var(--color-paper);border-radius:var(--radius-pane-in);
  padding:14px;overflow:auto}
/* Capped against the figure's authored width, the same rule the carousel uses:
   a small figure blown up to fill the pane draws its type at twice the size of
   a large one. */
.play-figure svg{max-width:min(100%,calc(var(--nat-w,600px) * 1.2));
  max-height:100%;height:auto}
/* `hidden` is a UA `display:none`, and any `display` declared in a stylesheet
   beats it — so an element given `display:grid` above ignores the attribute
   entirely. `.run[hidden]` in site.css exists for the same reason. */
.play-figure[hidden]{display:none}
.play-desugar{flex:1;min-height:0;margin:0;overflow:auto;white-space:pre;
  background:var(--color-paper);border-radius:var(--radius-pane-in);
  padding:14px;font-family:var(--font-code);font-size:12.5px;line-height:1.55;
  color:var(--color-ink)}
.play-desugar[hidden]{display:none}

/* ══ the picker ════════════════════════════════════════════════════════════
   A full-width overlay over the working area rather than a sidebar: a sidebar
   would cost the editor a third of its width permanently for something used
   once a session.

   A native <dialog>, so Esc, the backdrop and the focus handling are the
   browser's and cost no script. */
.picker{width:min(1100px,92vw);max-height:82vh;padding:0;border:0;
  border-radius:var(--radius-pane);background:var(--color-paper);
  color:var(--color-ink);overflow:auto}
/* THE ONE `backdrop-filter` ON THIS PAGE, AND IT KEEPS ITS PLACE. The panes
   above have theirs switched off for the reason that rule gives; this one is
   not a side-payment on a look, it is the effect — 2px over a 34% scrim is what
   pushes the working page back far enough that the card grid reads as the only
   live thing. It also exists only while a modal is open, which is a state a
   person is holding, not the resting page the flash was reported on. If a flash
   is ever seen *while the picker is up*, this is the line to suspect. */
.picker::backdrop{background:rgb(0 0 0 / .34);backdrop-filter:blur(2px)}
.picker-head{position:sticky;top:0;z-index:2;display:flex;align-items:center;
  gap:12px;padding:16px 20px;background:var(--color-paper)}
.picker-head h2{font-size:16px;font-weight:600;margin:0}
/* One quiet line under the heading instead of a label per group: it says what
   picking does, which no group heading was going to. */
.picker-hint{margin:2px 0 0;font-size:12.5px;color:var(--color-text-soft)}
/* The same grid at full size is /gallery, one click from the corner — for
   whoever came to browse rather than to pick. */
.picker-gallery{font-size:12.5px;color:var(--color-text-soft);margin-right:6px;
  transition:color var(--duration-hover) var(--ease-out)}
.picker-gallery:hover{color:var(--color-brand)}
.picker-close{width:38px;height:38px;padding:0;justify-content:center}
.picker-body{padding:0 20px 20px}
.picker-body .label{display:block;margin:14px 0 10px}
.picker-grid{display:grid;gap:12px;
  grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}

/* ── mobile ────────────────────────────────────────────────────────────────
   One column, editor first. The landing page does the opposite at this width
   and that stays: there the figure is the pitch, here the editor is the tool.
   40vh is enough source to work in without pushing the figure off-screen.

   `flex:0 0 55vh`, not `height:55vh`: the base rule already gives this box
   `flex:1` inside `.play-pane`'s column flex, which sets `flex-basis:0%` —
   and a flex item's main-axis size comes from `flex-basis` once that is
   anything but `auto`, so a plain `height` here is never consulted at all.
   The effect was not a slightly-wrong number but a box that collapsed to its
   basis, `0`, with the source invisible above the figure. Zero grow and
   shrink keep it pinned at 40vh rather than flexing back open. */
@media (max-width:860px){
  .play{grid-template-columns:1fr;height:auto;gap:14px}
  .play-divider{display:none}
  .play-source-box{flex:0 0 40vh}
  .play-pane--figure{min-height:46vh}
  /* The desktop cap is a percentage of the pane, and a percentage max-height
     is dropped when the parent's height is indefinite — which `height:auto`
     above makes it. A viewport unit keeps the promise the desktop rule makes. */
  .play-errors{max-height:33vh}
}
