/* The try stage — one sheet, two frames: 9:16 on /try/mobile/ and 16:9 on
   /try/, which sets data-layout="wide" on the stage.
   Tokens are copied from assets/css/main.css so the pages read as the same
   site; this file is standalone on purpose, the template CSS is not loaded.

   The stage is a size container (see .stage), so a dial written in cqmin is
   "of the stage's short side" -- its width upright, its HEIGHT on a wide
   frame. That is what carries the phone's proportions across without a second
   set of numbers. Dials of the height, and of a control's own box, are
   percentages and stay that way. */

:root {
	--zen-ink: #0b1413;
	--zen-surface: rgba(14, 24, 22, 0.58);
	--zen-border: rgba(255, 255, 255, 0.14);
	--zen-border-strong: rgba(255, 255, 255, 0.28);
	--zen-text: #f0f4f1;
	--zen-text-dim: rgba(240, 244, 241, 0.72);
	--zen-accent: #a8cea4;
	/* Controls sit on cream sand, so they take the sand's own family rather
	   than the site's ink. Dark brown on the clay is 6.1:1. */
	--zen-clay: #cbae83;
	--zen-clay-text: #3e2e1e;
	--zen-clay-border: rgba(62, 46, 30, 0.22);
	/* The /get pill, and nothing else. The owner asked for it redder twice:
	   once on 2026-09-06 to #f07260, which was as far as a coral could go
	   while a brown label still cleared AA, and again straight after, past
	   the point where any dark label works. It is a red now rather than a
	   salmon -- 6 degrees of hue against 7.5, and darker -- and it carries a
	   white label at 4.84:1, with a warm rim and a shadow under the glyphs so
	   white on a saturated red does not vibrate. */
	--zen-coral: #d6321f;
	--zen-coral-text: #ffffff;
	--zen-coral-border: rgba(255, 244, 234, 0.42);
	--zen-coral-shade: rgba(90, 16, 8, 0.45);
	/* Every tag on the stage wears the game's own Tag_BG board instead of a
	   clay pill, so the label has to be read against the board's own sand,
	   #f9ddb0. The owner asked for a light brown-orange; this is as light as
	   that goes while 11px caps still clear AA on it, at 4.9:1. */
	--zen-tag-text: #96491b;
	--zen-radius: 18px;
	--zen-shadow: 0 18px 50px rgba(0, 0, 0, 0.38);
	--stage-inset: 12px;

	/* The menu. Every size here is a fraction of the plaque row, which spans
	   the stage, so nothing has to restate the stage's own width formula: a
	   viewport copy of it would use 100vh where the stage uses 100dvh and
	   would drop the safe-area insets, and both errors show as plaques a few
	   px too wide on a notched phone. These are dials for the owner's eye. */
	--menu-art-pos: 50% 50%;
	--plaque-top: 43%;
	--plaque-shift-x: -10cqmin;
	--plaque-w: 28.9cqmin;
	/* Fake perspective. The far sign stands further up the sand, so it is
	   smaller, higher, and its edge passes behind the near one. The lift and
	   the overlap are read off the sign width, so resizing the pair keeps the
	   depth it was tuned to; CSS resolves both against the inline axis. */
	--plaque-far-scale: 0.82;
	--plaque-far-lift: calc(var(--plaque-w) * 0.265);
	--plaque-overlap: calc(var(--plaque-w) * -0.059);
	--plaque-text-size: 16cqw;
	--zen-plaque-text: #713600;

	/* The game. Every value here is an initial one; the owner tunes on the
	   phone. The fan's root is the app's Tex_UI_MainBtn, about 156 of a 1080
	   canvas; its children 90, on a radius of 160. The close-up's three
	   dials are its "camera": where the render sits and how big. */
	--fan-left: 4cqmin;          /* of the stage's SHORT side (its width on 9:16) */
	--fan-bottom: 15%;           /* of the stage height: the root's centre on the
	                                object rail's last card (owner, 2026-09-10) */
	--fan-size: 16cqmin;         /* the root, of the stage's short side */
	--fan-radius: 110%;          /* of the root, along the fan */
	--fan-child: 57.7%;          /* a child, of the root: 90 / 156 */
	--cell-size: 13cqmin;        /* a rail cell, of the stage's short side */
	--rail-right: 12px;          /* the rail's gap from the stage edge */
	--cell-gap: 2px;
	/* The veil over the object in hand (owner, 2026-09-20). The app marks the
	   object with a halo on the ground; the owner wants the object itself
	   tinted instead, and still readable through the tint. The stage's own
	   coral itself, so edit mode speaks in the red the rest of the site speaks
	   in and there is one place to change it. Override this dial, not
	   --zen-coral, to tune the veil alone. */
	--select-veil: var(--zen-coral);
	--select-veil-alpha: 0.55;   /* enough red to be unmistakable, little enough
	                                that the render still reads through it. The
	                                owner's eye: 0.4 on 2026-09-20, raised to
	                                0.55 the same day because it read too
	                                quietly on the pink blossom trees */
	--select-veil-in: 0.18s;     /* the veil fades in rather than snapping. The
	                                halo it replaces popped on the app's
	                                ElasticScaleOnEnable; a tint has no size to
	                                overshoot, so this is a plain fade */
	--edit-btn: 9.3cqmin;        /* an edit button, of the stage's short side: 100 / 1080 */
	/* The open row's spread, as a fraction of A BUTTON'S OWN WIDTH, because
	   that is what a percentage in translateX resolves against. The app's
	   panelFinalSpacing is 100 against a 100-wide button, so one button width
	   is its spacing exactly -- but the owner asked for the two closed up
	   against each other on the phone (2026-09-10), and 0 is that. The spread
	   animation still runs; it just has nowhere to go. Put 100% back to have
	   the app's own gap. */
	--edit-gap: 0%;
	/* The calm bar: BreathBoxOnDemand's pair, on the object a long press was
	   made on. The app's button is 150 of a canvas that is 1920 wide -- it is
	   ScaleWithScreenSize, reference 1920 x 1080, matching WIDTH -- which works
	   out at 7.8% of the screen and about 30 px on a 390 px phone, under any
	   tap-target floor. This stage already departs the same way and in the same
	   direction (--fan-size is 16% where the app's root is 8.1%), and one of
	   these two buttons is the only way into the breathing box, so it has to
	   clear 44 px.

	   The stage is NOT the viewport: --stage-inset takes 12 px off each side,
	   so a 390 px phone gives a 366 px stage. 12% of that is 43.9 px, under
	   the floor on every common phone but the largest -- the first value here
	   was sized against the viewport by mistake. 13.5% of the narrowest common
	   stage (360 - 24 = 336) is 45.4 px, and 49.4 px at 390. The owner's dial;
	   7.8% is the app exactly. */
	--calm-btn: 13.5cqmin;       /* a calm button, of the stage's short side */
	/* _buttonSpacing is 180 against a 150 button: centres 1.2 button widths
	   apart, which is 30 of clear air between two 150s -- 20% of a button.
	   This is an EDGE gap, exactly as --edit-gap is: the flex row leaves the
	   pair flush and each button's own translateX(+/- gap/2) opens it. 120%
	   was the first value here and it was wrong twice over -- it read
	   --edit-gap as centre-to-centre, which it is not, and it put the pair
	   2.2 button widths apart with a whole empty button between them. */
	--calm-gap: 20%;
	--calm-fade: 0.4s;           /* _fadeDuration, the app's own dismiss */
	--calm-pop: 1.2s;            /* _fadeDuration * 3, the OutElastic open */
	--calm-lift: 0%;             /* of the bar's own height. The app anchors on
	                                the object itself, so 0 is its answer; the
	                                dial is here for an owner who wants the row
	                                off a small sprite */
	/* The end of the trial: the menu's own art with nothing on it but the
	   store. The centre of the lower third is 83% down, which on a 9:16 stage
	   sits close enough to the bottom edge to read as page chrome rather than
	   as the screen's own call to action -- 72% is inside the lower third and
	   clear of both edges. The owner's dial, like every other placement here. */
	--over-cta-y: 72%;           /* the pill's centre, down the stage */
	--over-cta-scale: 1;         /* the pill's own size; larger now that it is
	                                the only thing on the screen, if wanted */
	--stone-w: 86cqmin;          /* the close-up, of the stage's short side */
	--fog-gain: 1;               /* 1 = the sprites haze exactly as the sand does */
	/* The renders come out far darker than the app's own presentation. The
	   owner's worry-stone reference reads a mean luminance of 0.691; the same
	   rock's render reads 0.454. This is that exposure, solved against it:
	   applied to p_3dObject_25 it lands on 0.693 with nothing clipped. A row
	   whose highlights cannot take it carries its own light in objects.js. */
	--object-light: contrast(0.82) brightness(1.5);
	--stone-x: 0px;              /* the close-up's offset from the centre */
	/* The caption is pinned 142px down and the close-up is centred, so on a
	   short stage they meet -- upright that is a 320px viewport (a 296 x 526
	   stage), which is the first iPhone SE and every phone narrower. It has
	   been true since the close-up shipped; the wide layout has the same floor
	   for the same reason. Drop the render until its top edge clears 154px:
	   the top is 0.4505 of the stage's SHORT side here, so the shift is
	   154px - 45cqmin, and it is 0 on every phone from 342px across. */
	--stone-y: max(0px, 154px - 45cqmin);
}

* {
	box-sizing: border-box;
}

html,
body {
	margin: 0;
	padding: 0;
	height: 100%;
	overflow: hidden;
}

body {
	background-color: var(--zen-ink);
	color: var(--zen-text);
	font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
	-webkit-text-size-adjust: 100%;
	-webkit-tap-highlight-color: transparent;
}

/* Same fixed background layer as the homepage. background-attachment is
   unreliable on iOS; a fixed element is not. */
.site-bg {
	position: fixed;
	top: 0;
	right: 0;
	bottom: 0;
	left: 0;
	z-index: -1;
	pointer-events: none;
	background: url("../../assets/css/images/overlay.png"), url("../../images/one.jpg");
	background: url("../../assets/css/images/overlay.png"), image-set(url("../../images/one.webp") type("image/webp"), url("../../images/one.jpg") type("image/jpeg"));
	background-size: 256px 256px, cover;
	background-position: top left, center center;
	background-repeat: repeat, no-repeat;
}

/* The page is one full-bleed canvas, so its heading has no place to sit on
   screen. It is still the page's name for a crawler and a screen reader, so
   it is clipped rather than dropped: display:none would take it out of the
   accessibility tree too. */
.a11y-only {
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

/* The stage is the whole experience: everything interactive lives inside it,
   and the night background is all that shows around it. 100dvh keeps the
   mobile URL bar out of the sum; 100vh is the fallback. */
/* The wide frame. /try/ sets data-layout="wide" on the stage; /try/mobile/
   sets "portrait", which is also the default. Everything not named here is
   the phone's own value, carried over by cqmin -- these are the places where
   the phone's number does not fit the room a 16:9 frame gives it.

   Four of them are arithmetic, not eye:

   The rail. Nine cards at 13cqmin would be 117% of the stage's height. At
   8cqmin the palette is 75% of the height, centred, and its bottom clears the
   pill's 56px band by 25.6px on a 640-tall stage and 84px at 1056; 9cqmin
   overlaps the pill, which paints over the palette and takes the tap.

   The fan. The root's centre sits on the object rail's last card (owner,
   2026-09-10) AND the open fan has to stay inside the frame: Exit travels
   110% of the root straight down, so its bottom edge stands
   fan-bottom - 0.8885 * root above the stage's edge. At 16cqmin that is
   -35px -- most of Exit under overflow: hidden. The two rules hold together
   only at a smaller root: 10cqmin with 12% puts the root within 3px of the
   last card at 640 and 1056 tall, and clears Exit by 20 and 33px.

   The close-up. 86cqmin of the height is 908px on a 1056-tall stage against a
   767 x 782 render, which stands 926px tall and goes under the caption. On the
   phone 86% of the width is 48% of the height; 50cqmin is that same fraction.

   ALL THREE OF THOSE HOLD ONLY ON A TALL ENOUGH STAGE, and the first draft of
   this block asked the question at 640 and 1056 and nowhere else. The pill,
   the sound switch and the close-up's caption are FIXED PIXELS -- a 56px band
   at the bottom, a caption whose bottom sits 142px down -- while every control
   here scales with the stage. Shrink the stage and the scaled things walk into
   the fixed ones:

     the rail's bottom reaches the pill's band at   H = 457px
     the fan root's bottom reaches the switch at    H = 467px
     the close-up's top reaches the caption at      H = 579px

   A 16:9 stage is 457px tall in an 836px-wide window, and 579px tall in a
   1054px one -- a half-screen window on a 1600px display, or 1024x768, which
   is not a window this page will never see. So each of the three carries a
   floor as well as a fraction: min() where the control must stop growing,
   max() where it must stop falling. Above about 1060px of stage width every
   floor is inert and the values are exactly the ones derived above.

   The rest are the phone's own numbers, restated. The menu's four are the
   first to look at: the art is the app's landscape backdrop and this crop is
   closer to the app than the phone's is. */
.stage[data-layout="wide"] {
	aspect-ratio: 16 / 9;
	width: min(100%, (100vh - 2 * var(--stage-inset)) * 16 / 9);
	width: min(100%, (100dvh - 2 * var(--stage-inset)) * 16 / 9);
	/* Initial values, the owner's eye. The second term in each is the floor
	   from the comment above, not a dial: it only bites on a short stage. */

	/* 9 cards + 8 gaps of 2px, centred, must end 16px above the pill's 56px
	   band: 9C + 16 <= H - 144. */
	--cell-size: min(8cqmin, (100cqmin - 160px) / 9);
	--fan-size: 10cqmin;
	/* The root's bottom edge, off the stage's. 12% of a 583px stage is 70px,
	   which is the switch's 56px band and 14px of air; below that the percentage
	   would put the root under the switch. */
	--fan-bottom: max(12%, 70px);
	/* The caption is fixed at the top; the close-up is centred. On a short
	   stage they meet, and the answer is to MOVE the close-up, not to shrink
	   it -- holding it centred and capping its width instead turns it into a
	   14px speck in a 600x900 window, which is worse than the overlap.
	   So: --stone-y drops it until its top edge clears 154px, and --stone-w is
	   capped only by what the band under the caption can actually hold
	   (H - 166 tall, and the render is 1.0196 as tall as it is wide). Both are
	   inert above a 340px stage, which is every window this page will meet --
	   at 1280 and 1920 the close-up is where it was. */
	--stone-w: min(50cqmin, 98cqmin - 163px);
	--stone-y: max(0px, 154px - 24.5cqmin);
	--menu-art-pos: 50% 50%;
	--plaque-w: 28.9cqmin;
	--plaque-top: 43%;
	--plaque-shift-x: -10cqmin;
}

/* Container units are Chrome 105, Safari 16 and Firefox 110 -- late 2022. An
   older browser parses `--fan-size: 16cqmin` as a custom property without
   complaint and then drops every declaration that USES it to auto, which
   collapses every control on the stage. iOS 15 is the case that matters: the
   iPhone 6s, 7 and the first SE are held there.

   So the dials keep their original percentages behind a support query -- the
   eight widths, and --stone-y, which is not a width at all but reads cqmin.
   TWO rules, not one, and the second is the reason: a `:root` fallback is
   specificity (0,1,0) and `.stage[data-layout="wide"]` is (0,2,0), so on the
   wide page the block above would win the cascade even here -- specificity
   beats source order -- and five controls would take a cqmin value the browser
   cannot compute and fall to `auto`. The wide rule below matches that
   specificity and follows it in source order, so it wins where it applies.

   The wide numbers are the cqmin ones times 9/16, because on a 16:9 stage a
   percentage is of the WIDTH and a cqmin is of the height. The floors above
   are dropped: they are min()/max() against cqmin, which is exactly what these
   browsers cannot resolve, and a pre-2022 desktop browser in a 836px window is
   not the case worth carrying arithmetic for. It gets the right proportions at
   ordinary sizes instead of nothing.

   One dial is NOT here and cannot be: --plaque-text-size (16cqw) is of the
   plaque's own box, not the stage's, and has been since before this file had a
   wide layout. On these browsers the plaque's label loses its size.

   ANY new dial written in a container unit needs a line in BOTH rules below,
   whether or not it is a width. --stone-y was the first one that was not. */
@supports not (width: 1cqmin) {
	:root {
		--plaque-shift-x: -10%;
		--plaque-w: 28.9%;
		--fan-left: 4%;
		--fan-size: 16%;
		--cell-size: 13%;
		--edit-btn: 9.3%;
		--calm-btn: 13.5%;
		--stone-w: 86%;
		/* Not a size: the close-up's drop away from a caption it would meet on
		   a short stage. There is no percentage that says the same thing -- it
		   is a pixel clearance against a pixel band -- so these browsers keep
		   the centred close-up and the overlap that comes with it below a
		   320px viewport, which is what they had before the floor existed. */
		--stone-y: 0px;
	}

	.stage[data-layout="wide"] {
		--plaque-shift-x: -5.625%;
		--plaque-w: 16.256%;
		--fan-left: 2.25%;
		--fan-size: 5.625%;
		--cell-size: 4.5%;
		--edit-btn: 5.231%;
		--calm-btn: 7.594%;
		--stone-w: 28.125%;
		--stone-y: 0px;
	}
}

.stage-wrap {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 100%;
	height: 100vh;
	height: 100dvh;
	padding:
		calc(var(--stage-inset) + env(safe-area-inset-top)) calc(var(--stage-inset) + env(safe-area-inset-right)) calc(var(--stage-inset) + env(safe-area-inset-bottom)) calc(var(--stage-inset) + env(safe-area-inset-left));
}

.stage {
	position: relative;
	aspect-ratio: 9 / 16;
	width: min(100%, (100vh - 2 * var(--stage-inset)) * 9 / 16);
	width: min(100%, (100dvh - 2 * var(--stage-inset)) * 9 / 16);
	max-height: 100%;
	/* The container every cqmin dial above reads. The stage's size comes from
	   its own aspect-ratio and width, never from its contents, so size
	   containment costs nothing -- and it is what makes "of the short side"
	   mean the width on 9:16 and the HEIGHT on 16:9, which is the whole reason
	   one stylesheet can dress both frames. */
	container-type: size;
	/* The stage's own family, not the page's. The plates, tags, tips, the pill
	   and Skip all inherit, and /try/ sets Source Sans Pro on its body for the
	   prose -- which would set every plate in it, against <br> breaks that were
	   measured in Helvetica. The entry scopes its family to the prose; this
	   keeps the stage's regardless. */
	font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
	border: solid 1px var(--zen-border);
	border-radius: var(--zen-radius);
	box-shadow: var(--zen-shadow);
	overflow: hidden;
	background: var(--zen-ink);
	touch-action: none;
}

/* iOS Safari answers a long press on an image with its own callout — Save
   Image, Copy — and that is exactly the gesture the palette hold (0.6 s) and
   the worry stone's press (1.0 s) are made of, so the callout eats them. The
   whole stage is a control surface: nothing on it is text to select, an image
   to save or a thing to drag. */
.stage,
.stage * {
	-webkit-touch-callout: none;
	-webkit-user-select: none;
	user-select: none;
	-webkit-user-drag: none;
}

/* The one line a browser with no script gets. The stage is empty behind it --
   the markup is fetched and the fallback is only shown by script -- so this is
   centred on the frame itself. */
.stage-noscript {
	position: absolute;
	top: 50%;
	left: 0;
	right: 0;
	margin: 0;
	padding: 0 24px;
	transform: translateY(-50%);
	text-align: center;
	font-size: 14px;
	line-height: 1.5;
	color: var(--zen-text-dim, var(--zen-text));
}

/* The sand fills the stage; the canvas is the stage's own background. */
#sand-canvas {
	display: block;
	width: 100%;
	height: 100%;
}

/* Hairline along the top edge of the stage. No digits, by design. */
.progress {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 2px;
	background: var(--zen-accent);
	opacity: 0;
	transform: scaleX(0);
	transform-origin: left center;
	pointer-events: none;
	transition: opacity 0.4s ease;
}

.progress.is-visible {
	opacity: 0.55;
}

/* --- The tags -------------------------------------------------------------
   One idiom for every tag the stage puts up: the game's own Tag_BG board, a
   sand-coloured plate with the rake's rings in it, under a light brown-orange
   line of caps. The board is drawn with border-image rather than a stretched
   background so the corners keep their radius whatever the sentence is: slice
   28 of the 419px art holds the whole 20px curve, and a 10px border edge
   draws it at 7px. Never a tap target -- a tag answers a gesture it must not
   be able to swallow. */
.tag {
	padding: 0.25em 0.5em;
	border: solid 10px transparent;
	border-image: url("tag-bg.webp") 28 fill / 10px / 0 stretch;
	color: var(--zen-tag-text);
	font-size: 11px;
	font-weight: 700;
	letter-spacing: 0.08em;
	line-height: 1.3;
	text-transform: uppercase;
	white-space: nowrap;
	pointer-events: none;
	transition: opacity 0.6s ease;
}

/* A tag that is one invitation -- the fan's, since the mode plates became
   permanent on 2026-09-11. Spent, it fades and never comes back; the session
   says when. The headphones tag is the exception: it is raised and dropped by
   .is-visible, which is written later and out-ranks this, so adding it to the
   session's TAG_ELEMENTS would give it a spend it never obeys. */
.tag.is-spent {
	opacity: 0;
}

/* The mode's head: the plate that says what this part of the game is for, and
   under it that mode's tip. Big enough to be read at a glance, centred on the
   stage's top line, where nothing else stands while a game plays: the
   headphones tag is spent before a scene starts, and Skip belongs to the
   breathing run. Permanent since 2026-09-11 (owner): nothing spends it, and
   the rules below only say which mode and which moment it is shown in.
   It paints over the sprites, so objects.js refuses a drop under any head and
   holds a drag clear of them (2026-09-11).

   A child of the stage, not of the rail it speaks for. An absolute box is laid
   out against its nearest positioned ancestor, and inside a rail 13% of the
   stage wide, standing at the stage's edge, no length resolves to the stage's
   own centre.

   A flex column, so a tip sits under its own plate whatever that plate's
   height -- the objects plate takes a line more than the explore plate inside
   its corridor -- with no measurement and no resize handler. The geometry, the
   caps, the size and every hide rule are the head's; the look is the plate's
   and the tip's.

   The head is a plain box and not a .tag, so it restates the two things .tag
   gives a plate. `pointer-events: none`: at z-index 4 over the canvas and the
   sprites, a head that took the finger -- at opacity 0 as much as at 1 --
   would swallow every stroke, press and tap on water inside its box. And its
   own opacity transition, without which every hide rule below would fade the
   head out and snap it back in.

   `max-content` has to be stated. A box with `left` set and `right` and
   `width` auto is sized shrink-to-fit against the space left inside its
   containing block -- here half the stage -- so left to itself the head would
   be half the stage wide whatever the cap says. `max-content` is the basis;
   the cap then clamps it. */
.scene-head {
	position: absolute;
	/* On the top line since 2026-09-10 (owner). It was a shelf below it while
	   the sound switch held the top-left corner; the switch is at the foot of
	   the stage now, and nothing else is on this line while a game runs. */
	top: 14px;
	left: 50%;
	z-index: 4;
	display: flex;
	flex-direction: column;
	align-items: center;
	width: -webkit-max-content;
	width: max-content;
	max-width: calc(100% - 3em);
	transform: translateX(-50%);
	pointer-events: none;
	transition: opacity 0.6s ease;
	/* 16px wherever it fits, and no smaller than it has to be. "DRAW TO
	   REGAIN CALM! SAND OR WATER" sets on two lines at 16px from about a
	   330px stage up; below that it takes three, and the third line runs into
	   the draw rail, whose column is centred and grew a row and a line when
	   the surface pair and the two-line readout arrived. At a 320px viewport
	   -- a 296px stage, the narrowest this page targets -- 14.5px is the
	   largest that sets on two, and it leaves under a pixel of slack on the
	   longer line, which the wrong font fallback would spend. 13.5px keeps
	   5px of it.

	   What it has to clear is the draw rail's top, and the rail has grown
	   twice since: the surface pair and the two-line readout, then the fill
	   card on 2026-09-10, making it six rows. Recomputed from the shipped
	   values on a 296px stage -- cell 38.5px, readout 62.1px, five 2px gaps
	   -- the column is 264.5px tall and its top lands at 130.9px, against
	   this plate's bottom at 81.2px (14px top, two lines at 13.5px, 0.3em
	   padding, 12px border). About 50px of daylight, and every row the rail
	   gains takes 19px of it. Arithmetic from the values in this file, not a
	   browser measurement -- the phone settles it.
	
	   Scaled rather than stepped, because this stylesheet has no width
	   breakpoints and the stage is already sized off the viewport
	   (min(100%, 100dvh...)). A phone is width-bound at this aspect, so vw
	   tracks the stage.

	   That reasoning used to end "a short, wide desktop window is not, and
	   that is /try/'s audience, not this page's" -- which stopped being true
	   the day /try/ started running this stage. On a wide frame vw no longer
	   tracks the stage, and the clamp simply sits at its 16px cap in every
	   desktop window, which is the value the plates were tuned to anyway. The
	   cap, the 13.5px floor and the three fixed type sizes on the stage are
	   the owner's dials on a wide frame; .stage[data-layout="wide"] is where
	   they would be set, and none of them is guessed here.

	   From 2026-09-10 the two lines are no longer this size's to buy: each
	   plate carries its own <br>, at the point the owner chose. The
	   arithmetic above is not wrong, it is simply no longer what holds them
	   -- and it never worked for all three, which is why the objects plate
	   was taking a third line on the owner's phone. That break alone did not
	   cure it either: the objects plate is capped to the corridor between the
	   rails (see .scene-head.for-objects below), and LONG TAP TO CREATE did
	   not fit the corridor at any width this page targets, so it wrapped
	   again. What cured it was the copy, on 2026-09-12: the owner dropped the
	   gesture and the plate reads CREATE / YOUR ZEN PLACE. The longest line
	   on the stage is now DRAW TO REGAIN CALM! at 20 characters, on the draw
	   plate, which keeps the stage's own cap and has the room. The value is
	   unchanged: no one asked for a resize and these were tuned by eye.
	   `white-space: normal` stays, so a line that still cannot fit wraps
	   again rather than overflowing the board.

	   Set on the head since 2026-09-11 rather than on the plate, so the caps'
	   em resolve against the plate's own size; the plate inherits it, and
	   the tip keeps .tag's 11px. */
	font-size: clamp(13.5px, 4.1vw, 16px);
}

/* The plate: the board at the head's size, its edge grown to 12px against the
   same 28px slice so the corner keeps its proportion, as .cue's does at 14px.
   Its box is the one it had before it moved into the head: `max-content`,
   capped at the head, which is capped exactly as the plate used to be. The head's
   `align-items: center` is what keeps it from being stretched sideways. `inherit`, because .tag's
   11px is set on this element and would otherwise beat the head's size.
   .cue's text-indent is not copied: it answers letter-spacing's trailing gap
   on the first line only, and on a plate that wraps it would offset one line
   against the other. */
.scene-tag {
	width: -webkit-max-content;
	width: max-content;
	max-width: 100%;
	padding: 0.3em 0.9em;
	border-width: 12px;
	border-image-width: 12px;
	font-size: inherit;
	white-space: normal;
	text-align: center;
	/* .tag makes every plate on this stage bold. These three carry two
	   voices from 2026-09-10 -- the owner named which half of each leans --
	   so the light one is set here, on .scene-tag alone, and the other
	   plates keep .tag's 700. 400 and not 500: "not bold" is regular.

	   The colour does not move with the weight. --zen-tag-text on the
	   board's own sand is 4.9:1, which clears AA's 4.5:1 for normal-weight
	   text at any size -- a contrast ratio is colour, not stroke. The :root
	   comment justifies that colour by the 11px caps it was set at, so this
	   is worth saying where the weight actually changes. */
	font-weight: 400;
}

.scene-tag b {
	font-weight: 700;
}

/* The tip under the plate, in the owner's words, written by session.js from
   what the visitor has done and what the garden holds (2026-09-11). The same
   board a step down -- .tag's own 11px against the plate's 13.5 to 16, .tag's
   10px edge against 12, .tag's 700 throughout as one voice where the plate
   carries two, and 6px of air between -- so it reads as a second line and not
   a second headline. Not regular weight: every other 11px plate on the stage
   is 700, and a regular 11px caps sentence on the textured board would be the
   thinnest text there. `max-content` capped at the head, so a long tip adds
   lines and never width: in objects mode that cap is the corridor between
   the rails. */
.tip-tag {
	width: -webkit-max-content;
	width: max-content;
	max-width: 100%;
	margin-top: 6px;
	white-space: normal;
	text-align: center;
}

/* No tip for this garden. It fades on .tag's own 0.6 s with its last words
   still in it, so it goes out as a sentence and not as an empty board;
   session.js writes new words only when it has a tip to show. */
.tip-tag.is-off {
	opacity: 0;
}

/* A tip with nothing left to say for the rest of the page: the objects tip,
   once edit mode has been entered. Out of the layout, so its box stops
   refusing drops over sand where nothing is drawn (objects.js judges a drop
   against the head's box). */
.tip-tag.is-gone {
	display: none;
}

/* The full-version tag (owner, 2026-09-12). The tip's opposite: it stands in
   the tip's slot, under the plate, exactly where that mode's tip has nothing
   left to say -- always in draw, which has no ladder, and in objects and
   explore once theirs run out. session.js paints the pair from one verdict, so
   a head never shows both.

   It takes the tip's box whole (`max-content` capped at the head, 6px of air,
   wrapping rather than widening), because it stands in the same slot and the
   room reserved for the longest tip is the room it has to fit.

   The board is the same Tag_BG art in the /get pill's red, not a colour laid
   over the sand one: the plate is drawn with border-image, so its middle is
   pixels and no `background` or `color` can reach them. tag-bg-coral.webp is
   tag-bg.webp with every opaque pixel moved to the pill's hue and saturation
   (hsl 6.2, 74.7%) and its lightness scaled by one factor, 0.5615, aimed so the
   art's LIGHTEST pixel lands on --zen-coral itself. That is the number that
   matters: the art carries the rake's rings across 11 points of lightness, and
   a tint aimed at the AVERAGE would put #d6321f in the middle and leave the
   light rings at 4.55:1 under white. Aimed at the lightest, the worst pixel on
   the shipped plate measures #d63321 at 4.8224:1 -- two units of blue off the
   aim, which is the lossy webp encode and not the mapping -- and every other
   pixel is darker, so the whole board clears AA with room to spare. The fill
   averages #cd301e, eight units of red from the pill, which no eye reads as a
   different colour. The shadow under the glyphs is the pill's own and is not
   decoration: white on a saturated red vibrates without it. */
.full-tag {
	width: -webkit-max-content;
	width: max-content;
	max-width: 100%;
	margin-top: 6px;
	border-image-source: url("tag-bg-coral.webp");
	color: var(--zen-coral-text);
	text-shadow: 0 1px 2px var(--zen-coral-shade);
	white-space: normal;
	text-align: center;
}

/* This mode still has a tip, so the tag has nothing to add. Always set with
   is-gone below, which wins -- so nothing is ever seen to fade on this class.
   It is kept, and shipped in the markup, so the tag's resting state is dark as
   well as out of the layout: a tag that reached the slot before its opacity
   did would flash at full strength for a frame. */
.full-tag.is-off {
	opacity: 0;
}

/* Out of the layout, so a head that is showing neither tip nor tag -- the
   objects head between its tip retiring and the tag arriving, and every head
   before its first paint -- keeps no room it is not using, and the tag's box
   does not refuse drops over sand it is not drawn on. */
.full-tag.is-gone {
	display: none;
}

/* The one place the tag does not fit: draw's head, on a narrow stage (owner,
   2026-09-12, "hide the tag only [where] the overlap happens"). Draw's head
   keeps the STAGE's cap rather than the corridor the objects head is held to,
   and the draw readout hangs about 67px left of its rail column
   (`.tools { align-items: flex-end }`, on a child wider than the column), so
   the two boxes meet. Measured, as a rectangle intersection and not a gap:

     stage 296   97 x 6.8 px, over the readout
     stage 312   32 x 2.6 px, over the rail
     stage 320   clear by 0.9
     stage 328   clear by 4.3
     stage 336   clear by 7.8

   And the rail is z-index 10 against the head's 4, so it is the sand readout
   that paints over the coral board -- occlusion, not contact. Objects and
   explore are clear at every width: the corridor cap holds the objects head off
   its rail, and the explore head's own overlap with the palette is unchanged by
   the tag, identical with the tip in the slot, and against a palette that is
   `opacity: 0` in explore anyway.

   Set at stage 336 and not at 320 where the overlap actually stops: 320 clears
   by 0.9px and 328 by 4.3, which is inside the slack a different font fallback
   would spend, and this stylesheet already warns about exactly that a few rules
   up. Every stage that keeps the tag clears by 7.8px or more.

   Two queries and not one, because the stage is `min(100%, (100dvh - 2 *
   inset) * 9 / 16)` -- so it is narrow when the VIEWPORT is narrow or when it
   is SHORT, and a width breakpoint alone would miss the short, wide window this
   file's own comment raises as the reason it has no width breakpoints. Stage
   width is viewport width less the two 12px insets, so stage < 336 is viewport
   < 360; height-bound, (100dvh - 24) * 9 / 16 < 336 is dvh < 621.3.
   The tip is not hidden with it: a tip on a narrow stage is the same two lines
   it always was, and it is the tag's extra board that spends the daylight.

   Scoped to the portrait layout, because both thresholds are derived from the
   9:16 formula above and neither is true of a wide stage: a 621px window is
   1061px wide there, with all the room in the world for the tag. The modern
   form is `@container (max-width: 335px)` -- the stage's own width at the
   measured threshold -- and it is the right one the day this sheet drops the
   @supports fallback above, which needs the viewport query to keep working. */
@media (max-width: 359px), (max-height: 621px) {
	.stage[data-layout="portrait"] .scene-head.for-draw .full-tag {
		display: none;
	}
}

/* The same rule, the same reason, on the wide frame -- the owner's 2026-09-12
   decision was "hide the tag only where the overlap happens", and it happens
   here too. The tag is a fixed 407px at every stage size (its type sits at the
   16px cap), so it is the STAGE that has to be wide enough, and the draw
   readout hangs left of its rail column exactly as it does upright. Measured
   as a rectangle intersection, tag against readout:

     stage 496   73 x 33 px
     stage 536   53 x 38
     stage 576   33 x 40
     stage 616   13 x 40
     stage 676   clear

   Zero is at about stage 642; set at 660, the same margin over the crossing
   that the portrait pair takes. The wide stage is min(100%, (100dvh - 24) *
   16 / 9), so it is under 660 when the viewport is under 684 wide, or under
   395 tall. Objects and explore are clear at every width for the same reason
   they are upright: the corridor cap holds the objects head off its rail, and
   the palette is `opacity: 0` in explore anyway. */
@media (max-width: 683px), (max-height: 395px) {
	.stage[data-layout="wide"] .scene-head.for-draw .full-tag {
		display: none;
	}
}

/* A head that stands by default and a list of rules that take it down. Each
   is true in one mode only, and none is true anywhere but in a game. */
.stage:not([data-state="playing"]) .scene-head {
	opacity: 0;
}

/* The three heads share one place, so a mode change cross-fades them through
   each other: for as long as the fade runs, two boards with two sentences are
   superimposed in the middle of the stage. A transition takes its duration
   from the rules that match once the change has landed, so this one is read
   only on the way out -- the plate that is leaving goes in 0.25 s, and the
   one arriving still takes .tag's 0.6 s. Out first, in after, which is what
   the eye reads as one plate replacing another. */
.stage:not([data-mode="draw"]) .scene-head.for-draw,
.stage:not([data-mode="objects"]) .scene-head.for-objects,
.stage:not([data-mode="explore"]) .scene-head.for-explore {
	opacity: 0;
	transition-duration: 0.25s;
}

/* The object rail is nine cards tall -- 66% of the stage's height, centred on
   it -- so its top rises above this shelf, and a plate wide enough to reach
   the rail's column is painted over by the cards (z-index 10 against 4). It
   was, on every width: 34.3px of the sentence sat under the first two cards
   at 375px. So this one plate is capped to the corridor between the rails
   rather than to the stage. The plate is centred on the stage, so the cap is
   mirrored -- twice the rail's own width off, not once -- with a gutter
   either side. Under the copy this plate carried until 2026-09-12 the
   sentence took a third line inside that, and a fourth at 320px; since the
   owner dropped the gesture it sets on the two lines its <br> asks for from
   360px up, and takes a third only at 320, where the corridor is 173px.
   Either way it costs nothing: what it has to clear is beside it, not under
   it. The cap is the head's, so the tip under the plate is held in the same
   corridor: the rail's top rises past the tip as well as the plate.

   The draw plate keeps the stage's own cap. Its rail is six rows against
   nine -- the readout, the surface pair, and four tools since the fill card
   arrived on 2026-09-10 -- and seven on sand since the Rake (2026-10-18), but
   that row grows the column downward only (the margin rule further down), so
   its top is the six-row column's on both surfaces. The readout is fixed
   pixels where a cell
   scales with the stage, so the draw column is still the shorter of the two
   at every width this page targets and its top sits lower: about 49px lower
   on a 296px stage, 70px on a 375px one. Capping it here as well takes the
   sentence to four lines at 320px and undoes the clamp above. The explore
   plate has no rail beside it at all. */
.scene-head.for-objects {
	max-width: calc(100% - 2 * (var(--rail-right) + var(--cell-size)) - 1.5em);
}

/* AlphaMapPainter.HideRightMenu clears the screen under a stroke. The fan and
   both rails go on 0.15 s; the head goes with them and comes back on the
   lift, which is not a spend. */
.stage.is-stroking .scene-head {
	opacity: 0;
	transition-duration: 0.15s;
}

/* The two bars float over whatever object is in hand, at z-index 10 and
   clamped only to the frame, so either can stand in the head's band. The head
   stands down for as long as one is up -- as the rails and the fan already do
   for edit mode -- and comes back when it goes; nothing is spent. The calm
   bar's close is also the moment the explore tip's new words are first seen,
   which is the owner's "upon return or dismisses the buttons".

   Under the garden door's black as well. The Breathing Box button closes the
   calm bar and raises the black in the same task, and without this the head
   would fade back in under a cover that is still clear, carrying words the
   owner reserved for the return. */
.stage[data-edit="on"] .scene-head,
.stage[data-calm="on"] .scene-head,
.stage[data-veil="up"] .scene-head {
	opacity: 0;
	transition-duration: 0.15s;
}

/* The breathing word, and the close-up's caption: the same board as every
   other tag, at the one size on the stage that is meant to be read across the
   room. The board's edge grows with the type -- 14px of border against the
   same 28px slice keeps the corner in proportion to the bigger plate. The box
   shrinks to the word, so it hugs "HOLD" and "BREATHE OUT" alike, and a long
   caption still wraps rather than running off the stage. */
.cue {
	position: absolute;
	top: 50%;
	left: 50%;
	max-width: calc(100% - 3em);
	margin: 0;
	padding: 0.35em 1.1em;
	opacity: 0;
	transform: translate(-50%, -50%);
	border-width: 14px;
	border-image-width: 14px;
	font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
	font-size: 20px;
	letter-spacing: 0.1em;
	/* letter-spacing adds a trailing gap after the last glyph; this pulls the
	   line back so it sits centred inside the plate. */
	text-indent: 0.1em;
	text-align: center;
	white-space: normal;
}

/* In the close-up the caption would sit over the stone, which is the whole
   picture there. In that state only it moves to the top of the stage, under
   the sound switch's row. The breathing words keep the middle. */
.stage[data-state="stone"] .cue {
	top: calc(14px + 44px + 16px);
	transform: translate(-50%, 0);
}

/* Skip stands beside the breathing word for the second half of the scene, so
   it takes the same board. It cannot take the .tag class with it: a tag is
   pointer-events: none by definition and this one has to be pressed.
   The board has rounded corners and real alpha outside them, so this is the
   one plate that has to say what is behind it: a button carries the UA's own
   ButtonFace fill, which the old clay rule happened to paint over, and which
   showed as four white shoulders once the board took the fill's place. */
.skip {
	position: absolute;
	display: none;
	padding: 0.4em 1.1em;
	background: none;
	border: solid 10px transparent;
	border-image: url("tag-bg.webp") 28 fill / 10px / 0 stretch;
	color: var(--zen-tag-text);
	font: inherit;
	font-size: 13px;
	font-weight: 700;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	cursor: pointer;
	-webkit-appearance: none;
	appearance: none;
	-webkit-tap-highlight-color: transparent;
}

.skip.is-visible {
	display: block;
}

.skip {
	top: 14px;
	right: 12px;
}

/* The menu: the app's own day art and wood signs, over the sand. It fades
   out over the scene prepared under it and back in when the scene ends.
   Opacity rather than display, so it can fade; pointer-events so the hidden
   menu never sits between the finger and the sand. The duration is set
   inline by session.js from its own TIMING, so the two cannot drift. */
.menu {
	position: absolute;
	top: 0;
	right: 0;
	bottom: 0;
	left: 0;
	z-index: 20;
	opacity: 0;
	pointer-events: none;
	transition: opacity 0.6s ease;
}

.menu.is-visible {
	opacity: 1;
	pointer-events: auto;
}

/* The art is the game's landscape menu backdrop, a 2039 px square of which
   the app shows the middle band. It is never scaled to fit: the stage crops
   it. --menu-art-pos moves the crop window. */
.menu-art {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
	object-position: var(--menu-art-pos);
}

/* The app's own label face. The owner confirmed the licence on 2026-09-03.
   The OTF is shipped as it is: there is no font toolchain on this machine,
   and every browser that clears this page's WebGL floor reads OpenType. */
@font-face {
	font-family: "Relaxing";
	src: url("Relaxing.otf") format("opentype");
	font-weight: 400;
	font-style: normal;
	font-display: swap;
}

/* Two signs planted in the sand, one nearer than the other. The row spans
   the stage, so a plaque width is a plain percentage of it and no rule here
   has to know the stage's width in viewport units. The tops align, and the
   far sign lifts itself off that line. */
.plaques {
	position: absolute;
	top: var(--plaque-top);
	right: 0;
	left: 0;
	display: flex;
	align-items: flex-start;
	justify-content: center;
	transform: translateX(var(--plaque-shift-x));
}

/* Nearest the eye: full size, lowest, and in front of its neighbour. */
#menu-play {
	z-index: 2;
}

/* Further up the sand: smaller, higher, and behind. The overlap is small
   enough that the label clears the near sign at every stage width. */
#menu-breathe {
	width: calc(var(--plaque-w) * var(--plaque-far-scale));
	margin-top: calc(-1 * var(--plaque-far-lift));
	margin-left: var(--plaque-overlap);
	z-index: 1;
}

/* The app's Tex_UI_MenuScene_Btn sprite, 267 x 385: a board on a post. The
   label sits on the board, rows 19-147 of the sprite, where the app places
   its own text. Pressed is the app's ColorTint, 0.784. */
.plaque {
	position: relative;
	width: var(--plaque-w);   /* of the stage, through .plaques */
	aspect-ratio: 267 / 385;
	container-type: inline-size;
	padding: 0;
	border: 0;
	background: url("plaque.webp") center / 100% 100% no-repeat;
	cursor: pointer;
	-webkit-appearance: none;
	appearance: none;
}

.plaque:active {
	filter: brightness(0.784);
}

.plaque span {
	position: absolute;
	top: 5%;
	right: 8%;
	left: 8%;
	display: flex;
	align-items: center;
	justify-content: center;
	height: 32%;
	color: var(--zen-plaque-text);
	font-family: "Relaxing", "Helvetica Neue", Helvetica, Arial, sans-serif;
	font-size: var(--plaque-text-size);
	font-weight: 400;
	line-height: 1.15;
	letter-spacing: 0.04em;
	text-align: center;
}

/* Always there, on the menu and in both scenes, never animated. */
.pill {
	position: absolute;
	right: 12px;
	bottom: 12px;
	display: none;
	align-items: center;
	height: 44px;
	padding: 0 1.3em 0 0.5em;
	background: var(--zen-coral);
	border: solid 1px var(--zen-coral-border);
	border-radius: 3em;
	color: var(--zen-coral-text);
	font-size: 13px;
	font-weight: 700;
	letter-spacing: 0.16em;
	text-decoration: none;
	text-shadow: 0 1px 2px var(--zen-coral-shade);
	text-transform: uppercase;
}

/* The game icon, ahead of the label. Decorative: the label already names the
   destination, so it carries an empty alt. */
.pill img {
	flex: none;
	margin-right: 0.7em;
	border-radius: 8px;
}

.pill.is-visible {
	display: flex;
}

/* The master switch: the app's own menu sprite, on every screen, bottom-left
   as the app itself puts it (owner, 2026-09-10). It shares the `/get` pill's
   line -- both are 44px tall, 12px off the bottom -- so the two read as one
   row across the foot of the stage, and it is centred under the fan's root
   (owner, the same day). Left edges were flush first, and that was the wrong
   axis: the root is --fan-size across, 64.6px on a 430px phone, and the switch
   is 44px, so two discs flush on the left have centres 10.3px apart and read
   as staggered. Half the difference in width is the offset, so the two stay
   on one vertical axis at every stage size. The foot of the stage is no
   longer inset symmetrically -- the switch follows the control it stands
   under, not the pill it stands beside. 44px is the smallest comfortable target;
   the art is 170px square with its own alpha, so it needs no fill, no border
   and no radius of ours. */
.sound-toggle {
	position: absolute;
	bottom: 12px;
	left: calc(var(--fan-left) + (var(--fan-size) - 44px) / 2);
	width: 44px;
	height: 44px;
	padding: 0;
	background: none;
	border: 0;
	line-height: 0;
	cursor: pointer;
	-webkit-tap-highlight-color: transparent;
	/* .fan-wrap.is-open takes it down; the fan's own fade back. */
	transition: opacity 0.6s ease;
}

.sound-toggle img {
	width: 100%;
	height: 100%;
}

/* The page starts silent, and nothing may play before a gesture, so the button
   has to ask for one. A slow breath, well under the sand's own rhythm, that
   stops the moment the visitor answers so it never nags.
   The art breathes, not the button: the box stays 44px, so the target never
   shrinks under the finger while the sprite is at the bottom of its swing. */
.sound-toggle.is-attract img {
	animation: sound-attract 3.4s ease-in-out infinite;
}

@keyframes sound-attract {
	0%, 100% { transform: scale(0.9); }
	50% { transform: scale(1.18); }
}

/* The invitation, in the shared tag idiom above. It followed the switch to
   the foot of the stage until 2026-09-10 and now sits at the top, centred
   (owner): the switch is a long way from it, but the top middle is the one
   place on this stage that is empty whenever the tag is up. It is only ever
   shown on the menu -- a scene entry spends it -- so it shares that line with
   nothing: .skip belongs to the breathing run and the mode plates to a game.
   A whole sentence on a stage nine units wide, so it keeps the switch's own
   44px row and takes its width from the words. */
.sound-hint {
	position: absolute;
	top: 14px;
	left: 50%;
	display: flex;
	align-items: center;
	height: 44px;
	max-width: calc(100% - 24px);
	margin: 0;
	padding: 0 0.6em;
	opacity: 0;
	transform: translateX(-50%);
}

.sound-hint.is-visible {
	opacity: 1;
}

/* A visitor who asked for less motion still gets the tag; it just sits still. */
@media (prefers-reduced-motion: reduce) {
	.sound-toggle.is-attract img {
		animation: none;
	}
}

/* Shown only when WebGL is unavailable (spec section 7). */
.stage-fallback {
	position: absolute;
	top: 0;
	right: 0;
	bottom: 0;
	left: 0;
	display: none;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 1.4em;
	padding: 2em;
	background: var(--zen-surface);
	color: var(--zen-text-dim);
	font-size: 15px;
	line-height: 1.5;
	text-align: center;
}

.stage-fallback.is-visible {
	display: flex;
}

.cta {
	display: inline-block;
	padding: 0.9em 1.8em;
	border: solid 1px rgba(255, 255, 255, 0.28);
	border-radius: 3em;
	color: var(--zen-text);
	font-size: 0.85em;
	font-weight: 600;
	letter-spacing: 0.15em;
	text-decoration: none;
	text-transform: uppercase;
}

.cta:hover,
.cta:focus-visible {
	background-color: rgba(255, 255, 255, 0.08);
	border-color: var(--zen-accent);
}

/* The consent manager's re-open icon sits bottom-left, on top of the stage and
   under the fan, so it is hidden on both trial pages. It is NOT hidden here
   alone: assets/css/main.css:2172 hides it on the home page too, which makes
   hiding it the site's convention rather than the stage's exception -- this
   comment claimed the opposite until 2026-09-14. The consent banner itself is
   untouched, and the script writes `display` inline, so this has to out-rank an
   inline style. There is no re-open control on /try/ yet; the footer is where
   one would go.  */
#silktide-cookie-icon {
	display: none !important;
}

/* --- The game: stacking -------------------------------------------------------
   The trail canvas and the fan carry a z-index, so every control that must
   stay above them carries one too. Sand 0; objects 3; the mode's head
   4, over the sprites and under the ghost that carries one; ghost 5;
   close-up 6; trail 7; fan and cue 8; Back 9; scene chrome 10; menu 20; the
   chrome that outlives the menu 21; fallback 30. */
.progress,
.skip {
	z-index: 10;
}

/* --- The end of the trial ------------------------------------------------------
   The 300 s cap lands on `over`: the main menu's own background art, no wood
   signs on it, and the Get-the-app pill moved off its corner to sit centred,
   inside the lower third -- see --over-cta-y for why not its middle.
   Owner, 2026-09-10 -- and terminal by their decision, so the
   only ways on are the store link and a page reload.

   The art, the fade and the z-index are the menu's own: `over` shows the same
   .menu element, and only these two rules tell the two screens apart. */
.stage[data-state="over"] .plaques {
	display: none;
}

/* The same pill, moved -- not a second CTA. One /get link on the page is one
   thing to keep in step, and its own click tracking keeps working. `right:
   auto` is needed because the base rule pins it to the right edge, and a left
   without it would set both. */
.stage[data-state="over"] .pill {
	right: auto;
	bottom: auto;
	left: 50%;
	top: var(--over-cta-y);
	transform: translate(-50%, -50%) scale(var(--over-cta-scale));
}

/* The sound switch, its tag and the /get pill belong to the page, not to a
   scene: they are the only controls the opaque menu must not cover. */
.pill,
.sound-toggle,
.sound-hint {
	z-index: 21;
}

.cue {
	z-index: 8;
}

.stage-fallback {
	z-index: 30;
}

/* --- The game: the sprites ------------------------------------------------------
   Pinned to the sand by objects.js: left, top and width from the projected
   base point, z-index from depth. The layer takes no finger; a worry stone
   does, in explore mode only. */
.objects {
	position: absolute;
	top: 0;
	right: 0;
	bottom: 0;
	left: 0;
	z-index: 3;
	overflow: hidden;
	pointer-events: none;
}

.object {
	position: absolute;
	pointer-events: none;
}

.object img {
	display: block;
	width: 100%;
	height: auto;
	filter: var(--object-light);
	-webkit-user-drag: none;
	user-select: none;
	-webkit-user-select: none;
}

/* The shader fades the sand into a haze with distance, and a flat sprite that
   ignores it reads as pasted on right where it shows most — at the horizon.
   objects.js writes --fog from sand.fogAt, which is the shader's own
   smoothstep, and --sprite so the haze is painted through the sprite's own
   alpha and nowhere else. --fog-gain is the owner's dial: 1 matches the sand
   exactly, above 1 pushes the distance further than the sand does. */
.object::after {
	content: '';
	position: absolute;
	top: 0;
	right: 0;
	bottom: 0;
	left: 0;
	background: var(--haze, #efe6d6);
	opacity: calc(var(--fog, 0) * var(--fog-gain));
	-webkit-mask-image: var(--sprite);
	mask-image: var(--sprite);
	-webkit-mask-size: 100% 100%;
	mask-size: 100% 100%;
	-webkit-mask-repeat: no-repeat;
	mask-repeat: no-repeat;
	pointer-events: none;
}

/* Explore mode is where the press lives, as BreathBoxOnDemand is armed in
   NAVIGATION alone -- and it lives on every object, not the worry stones
   alone: p_3dObject_base gives every placed object a BreathBox-tagged child,
   so the app's press is armed on all of them. Which buttons the press opens is
   the object's business, not the gate's. */
.stage[data-state="playing"][data-mode="explore"] .object {
	pointer-events: auto;
	cursor: pointer;
	touch-action: none;
}

/* The veil over the object in hand. The app marks it with a child plane
   tagged ObjectSelection, a halo on the ground switched on for the whole of
   edit mode (ObjectMover.cs:627-630, :663-666). Ported that way until
   2026-09-20, when the owner asked for the object itself to carry the mark:
   a halo under an object says something is here, not this is the one you are
   moving.

   So the flat colour goes through the sprite's OWN alpha and nowhere else --
   the very trick .object::after uses for the distance haze -- which is why
   the veil needs no footing, no ellipse and no size of its own. The render
   still reads through it at --select-veil-alpha.

   It has to be pinned OVER the sprite with a z-index, and tree order will not
   do it: `.object img { filter: var(--object-light) }` builds a stacking
   context, which lifts the image into Appendix E's step 8 where tree order
   decides -- and a ::before precedes the img there, so it would paint under.
   `z-index: 1` is the fix, the same lesson the halo's `z-index: -1` carried
   the other way round, and it stays inside .object, which sortDepth already
   makes a stacking context by writing an integer z-index on it.

   It takes no finger either: the box around a sprite is deliberately not a
   target (objects.js reads the art's own alpha), and a veil that could be
   pressed would give that back.

   One consequence of that z-index, named so it is not read as a bug: the
   distance haze at .object::after has no z-index of its own, so the veil
   paints OVER it. A far object is hazed and its veil is not -- full strength
   at the horizon. Deliberate: the veil is a selection cue, and a cue that
   faded into the distance would be weakest where the sprite is hardest to
   pick out. */
.object.is-editing::before {
	content: '';
	position: absolute;
	z-index: 1;
	top: 0;
	right: 0;
	bottom: 0;
	left: 0;
	background: var(--select-veil);
	-webkit-mask-image: var(--sprite);
	mask-image: var(--sprite);
	-webkit-mask-size: 100% 100%;
	mask-size: 100% 100%;
	-webkit-mask-repeat: no-repeat;
	mask-repeat: no-repeat;
	pointer-events: none;
	/* The resting strength is declared HERE, not left to the animation's fill
	   mode. A pseudo-element's initial opacity is 1, so a veil that owned its
	   0.4 only through `both` would paint a solid coral silhouette -- hiding
	   the very render the owner asked to keep seeing -- the moment anything
	   defeated the animation: a user stylesheet or accessibility extension
	   forcing `animation: none`, or --select-veil-in going missing, which
	   makes the whole shorthand invalid and takes the fill mode with it. The
	   halo could fail safely, because its resting opacity WAS 1 and it sat
	   behind the sprite at z-index -1. A veil over the sprite cannot. */
	opacity: var(--select-veil-alpha);
	animation: edit-veil-in var(--select-veil-in) linear both;
}

/* A pseudo-element that only exists while the class is on cannot transition
   in from nothing, so the fade is a keyframe. It ends on the same dial the
   rule rests at, so the animation only supplies the way in. */
@keyframes edit-veil-in {
	from { opacity: 0; }
	to { opacity: var(--select-veil-alpha); }
}

/* Forced colours. `background-color` is a forced property (CSS Color Adjust 1
   3.1): the coral would be replaced by a system colour, keeping only the 0.4,
   so the veil would come out as a wash of Canvas over the art -- a dimming,
   not a mark. The halo never had this problem, sitting behind the sprite. So
   in a forced-colours theme the veil stands down and the object is ringed
   instead, in the system's own selection colour, which is a cue that does not
   depend on hue at all. */
@media (forced-colors: active) {
	.object.is-editing::before {
		display: none;
	}

	.object.is-editing {
		outline: 3px solid Highlight;
		outline-offset: 2px;
	}
}

/* --- The game: edit mode ---------------------------------------------------------
   ObjectMover.cs. A long press on any placed object in objects mode opens it:
   one row of buttons above the sprite, the finger drags the object under
   them, Confirm closes it and Delete takes the object with it.

   The object answers the finger here as a worry stone answers it in explore.
   Nothing else on the stage takes a finger in objects mode -- the sand's own
   input is off outside draw -- so this costs no other gesture. */
.stage[data-state="playing"][data-mode="objects"] .object {
	pointer-events: auto;
	cursor: pointer;
	touch-action: none;
}

/* The app's panel is a screen-space row on a bottom pivot, following a
   per-object anchor raised to about the model's own height, so it stands
   above it. objects.js writes left and top in the canvas pixels projectUv
   answers in, and this puts the row's bottom edge on that point.

   z-index 10 is the scene chrome's, over the sprites and over both rails --
   which are off the screen anyway for as long as this is up. */
.edit-bar {
	position: absolute;
	z-index: 10;
	display: flex;
	align-items: center;
	gap: 0;
	/* The row is sized here and the buttons take half of it each. A
	   percentage on the buttons would resolve against this box, which
	   shrink-wraps them -- 9.3% of itself, not of the stage. This box is
	   absolutely positioned, so its own percentage is the stage's. */
	width: calc(2 * var(--edit-btn));
	opacity: 0;
	transform: translate(-50%, -100%);
	pointer-events: none;
	/* `visibility` as well as opacity, and this one is the whole point:
	   opacity alone leaves both buttons in the tab order, inside a box that
	   carries aria-hidden. It is delayed out so the fade is seen, and
	   immediate in. The `.fan-item` rules use the same pattern. */
	visibility: hidden;
	transition: opacity 0.12s cubic-bezier(0.645, 0.045, 0.355, 1), visibility 0s linear 0.12s;
}

.edit-bar.is-visible {
	opacity: 1;
	pointer-events: auto;
	visibility: visible;
	transition: opacity 0.12s cubic-bezier(0.645, 0.045, 0.355, 1), visibility 0s;
}

/* The row fades out from under the finger while the object is being dragged,
   and comes back on release. ObjectMover's fadeDuration, 0.12 s, is the only
   fade the app's panel has -- it is created at full alpha and destroyed, never
   faded in or out around that -- so the same duration stands above in both
   directions. A transition takes its duration from the rules that match after
   the change, so a shorter one written only here would fade out in 0.12 s and
   back in over whatever the base rule said. It is what lets the visitor see
   what they are placing, so it must not take the buttons out of reach: the
   finger that owns the drag already has them. */
.edit-bar.is-dragging {
	opacity: 0;
	pointer-events: none;
}

/* 100 x 100 of the app's 1080-wide canvas, which is the same canvas the rail's
   140-unit cards come from: 100 / 1080 = 9.26%. */
.edit-btn {
	flex: none;
	width: 50%;
	aspect-ratio: 1;
	padding: 0;
	background: none;
	border: 0;
	cursor: pointer;
	-webkit-appearance: none;
	appearance: none;
	-webkit-tap-highlight-color: transparent;
	touch-action: none;
	/* HorizontalLayoutGroup.spacing tweens 0 -> panelFinalSpacing over
	   panelAnimDuration on Ease.InOutCubic, so the two buttons part from each
	   other as the row opens. Two opposed translates rather than an animated
	   `gap`: a transform animates everywhere this page runs, and it leaves the
	   row's own centre on the object while the buttons move. */
	transform: translateX(0);
	transition: transform 0.34s cubic-bezier(0.645, 0.045, 0.355, 1);
}

.edit-bar.is-visible .edit-btn:first-child {
	transform: translateX(calc(var(--edit-gap) / -2));
}

.edit-bar.is-visible .edit-btn:last-child {
	transform: translateX(calc(var(--edit-gap) / 2));
}

/* p_EditModeBtn_Remove: the button root is 100 square and its image child 90,
   so the glyph stands inside the target rather than filling it. */
.edit-btn img {
	display: block;
	width: 90%;
	height: 90%;
	margin: 5%;
	object-fit: contain;
	-webkit-user-drag: none;
}

.edit-btn:active {
	filter: brightness(0.85);
}

/* --- The game: the calm bar -------------------------------------------------
   BreathBoxOnDemand.SpawnUIButtonsAtWorld. A long press on any object in
   explore mode opens the app's pair of wooden buttons over it.

   The app's container is anchored at the object's own world position with a
   0.5/0.5 pivot, so the row is centred ON the object rather than hung above
   it as the edit bar is. objects.js writes left and top in the canvas pixels
   projectUv answers in; the translate is what puts the row's middle there.

   There is NO scrim. Both button prefabs carry a 3500-square Blocker at alpha
   0.176 and both have it inactive (m_IsActive: 0), so nothing behind the row
   is dimmed and the outside tap reaches the sand. */
.calm-bar {
	position: absolute;
	z-index: 10;
	display: flex;
	align-items: center;
	justify-content: center;
	gap: 0;
	/* One button wide by default -- the app centres a lone Breathe button on
	   the object -- and two when the object is a worry stone. The buttons take
	   half of the pair; on their own they take the whole of the one. */
	width: var(--calm-btn);
	opacity: 0;
	transform: translate(-50%, calc(-50% - var(--calm-lift)));
	pointer-events: none;
	/* `visibility` as well as opacity, and this one is the whole point:
	   opacity alone leaves both buttons in the tab order, inside a box that
	   carries aria-hidden. Delayed out so the fade is seen, immediate in --
	   the same pattern .edit-bar and the fan items use. */
	visibility: hidden;
	transition: opacity var(--calm-fade) cubic-bezier(0.645, 0.045, 0.355, 1), visibility 0s linear var(--calm-fade);
}

.calm-bar.has-stone {
	width: calc(2 * var(--calm-btn));
}

.calm-bar.is-visible {
	opacity: 1;
	pointer-events: auto;
	visibility: visible;
	transition: opacity var(--calm-fade) cubic-bezier(0.645, 0.045, 0.355, 1), visibility 0s;
}

/* A lone Breathe button is centred and the stone's is not there at all, as the
   app instantiates its second prefab only for a stone. `display: none` rather
   than opacity, so it leaves the tab order with the layout. ONE gate, keyed on
   the bar: an earlier draft also toggled an `is-hidden` class on the button,
   which no rule ever matched -- it read as load-bearing and did nothing. */
.calm-bar:not(.has-stone) #calm-stone {
	display: none;
}

.calm-btn {
	flex: none;
	width: 100%;
	aspect-ratio: 1;
	padding: 0;
	background: none;
	border: 0;
	cursor: pointer;
	-webkit-appearance: none;
	appearance: none;
	-webkit-tap-highlight-color: transparent;
	touch-action: none;
	--calm-offset: 0%;
	transform: translateX(var(--calm-offset));
}

.calm-bar.has-stone .calm-btn {
	width: 50%;
}

/* _buttonSpacing/2 either way. The pair parts about the bar's own centre,
   which stays on the object. */
.calm-bar.has-stone .calm-btn:first-child {
	--calm-offset: calc(var(--calm-gap) / -2);
}

.calm-bar.has-stone .calm-btn:last-child {
	--calm-offset: calc(var(--calm-gap) / 2);
}

/* breathRect.DOScale(one, _fadeDuration * 3).SetEase(Ease.OutElastic), and
   worryRect's own beside it -- each button scales about ITS OWN centre, from
   zero, already parted, so every stop reads the button's own --calm-offset
   and none of them has to know which button it is on.

   The stops are DOTween's own OutElastic, at the amplitude this project
   configures -- `defaultEaseOvershootOrAmplitude: 1.70158`, and a plain
   SetEase(Ease) does not touch it, so EaseManager takes the
   `s = period/2pi * asin(1/A)` branch rather than `period/4`:

     A * 2^(-10t) * sin((t - 0.02999) * 2pi/0.3) + 1,  A = 1.70158

   It peaks at 1.867 around a tenth of the way through and dips to 0.70 at a
   quarter, which is a far bigger bounce than the amplitude-1 curve the first
   attempt at this used (1.25 and 0.875). Sampled every twentieth early, where
   it moves, and every tenth once it has settled. `linear` on the animation
   itself, so the sampling is not eased a second time between the stops.

   (This derivation used to live on @keyframes edit-select-pop, the selection
   halo's own pop, which went with the halo on 2026-09-20.) */
@keyframes calm-pop {
	0% { transform: translateX(var(--calm-offset)) scale(0); }
	5% { transform: translateX(var(--calm-offset)) scale(1.4895); }
	10% { transform: translateX(var(--calm-offset)) scale(1.8461); }
	15% { transform: translateX(var(--calm-offset)) scale(1.3536); }
	20% { transform: translateX(var(--calm-offset)) scale(0.8269); }
	25% { transform: translateX(var(--calm-offset)) scale(0.7008); }
	30% { transform: translateX(var(--calm-offset)) scale(0.875); }
	35% { transform: translateX(var(--calm-offset)) scale(1.0612); }
	40% { transform: translateX(var(--calm-offset)) scale(1.1058); }
	50% { transform: translateX(var(--calm-offset)) scale(0.9784); }
	60% { transform: translateX(var(--calm-offset)) scale(0.9844); }
	70% { transform: translateX(var(--calm-offset)) scale(1.0132); }
	80% { transform: translateX(var(--calm-offset)) scale(0.9973); }
	90% { transform: translateX(var(--calm-offset)) scale(0.998); }
	100% { transform: translateX(var(--calm-offset)) scale(1); }
}

.calm-bar.is-visible .calm-btn {
	animation: calm-pop var(--calm-pop) linear both;
}

/* p_StartBreathBox's Image child is 150 inside a root that stretches, so the
   glyph is the whole target here rather than inset as the edit button's is. */
.calm-btn img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: contain;
	-webkit-user-drag: none;
}

.calm-btn:active {
	filter: brightness(0.85);
}

/* --- The garden door's black -------------------------------------------------
   SaveGardenData.LoadSceneAsyncWithMidFade: a full-screen overlay to opaque
   over halfFadeDuration (1 s in SceneMain), the save and the scene swap at the
   midpoint under it, then back. The menu door into the breathing box has the
   menu to hide behind; this one has nothing, and without a cover the garden is
   seen to vanish and the camera to jump.

   That is the way IN. The app's return has no cover -- ExitToPrevious is a
   bare LoadSceneAsync -- but it is loading a scene there, where this stage
   swaps a texture and some sprites in one frame and would pop. The return's
   black is this stage's own, on the app's duration.

   Over everything on the stage except the menu, which brings its own cover. */
.fade-veil {
	position: absolute;
	top: 0;
	right: 0;
	bottom: 0;
	left: 0;
	/* Over the pill, the sound switch and the headphones tag, which all sit at
	   21 -- an opaque black with a lit coral pill on it is not a cover. Under
	   the WebGL fallback at 30, which is a dead end and must never be hidden. */
	z-index: 22;
	background: #000;
	opacity: 0;
	pointer-events: none;
	/* session.js counts TIMING.veilMs; the two are written to match. */
	transition: opacity 1s linear;
}

/* The app's overlay is an Image with raycastTarget on, so nothing under it is
   reachable while it is up. Here that matters more than faithfulness: without
   it every control on the stage stays live under an opaque black for a whole
   second, and a tap on Exit there left the swap underneath to fire into a
   scene nobody was in -- which wedged the page. session.js re-checks the state
   at the bottom of the fade as well; this is what stops the tap landing at
   all. */
.stage[data-veil="up"] .fade-veil {
	opacity: 1;
	pointer-events: auto;
}

/* --- The game: the right rail ------------------------------------------------
   One column down the right side of the stage, over the sand, holding
   whichever set of buttons the mode calls for: the object cards in objects
   mode, the three brushes, the bucket and the Rake in draw mode. Both sit in
   the same box and only their opacity differs, so a mode change reads as one
   cross-fade. The column is centred on the stage height, so it grows both
   ways as the owner adds cards -- except the sand-only Rake's row, which a
   margin further down turns into growth downward only. 0.5 s is
   LeftMenuController's fadeDuration. */
.palette,
.tools {
	position: absolute;
	top: 50%;
	right: var(--rail-right);
	z-index: 10;
	display: flex;
	flex-direction: column;
	/* Every child is the column's own width today, so this shows only on a
	   child wider than the column -- the surface row -- which then hangs off
	   to the left rather than to the right, over the sand and not over the
	   stage's edge. */
	align-items: flex-end;
	gap: var(--cell-gap);
	width: var(--cell-size);
	opacity: 0;
	/* Out of the tab order whenever it is invisible. `pointer-events: none`
	   answers the mouse and nothing else: on the menu these two rails are 15
	   focus stops on controls no one can see, and on /try/ the page's own
	   prose sits behind all of them. visibility is not animatable, so it is
	   held for the length of the fade OUT and dropped at once on the way in --
	   every rule below that raises the opacity clears the delay with it. */
	visibility: hidden;
	transform: translateY(-50%);
	pointer-events: none;
	/* LeftMenuController.FadeInMenu / FadeOutMenu: 0.5 s, and DOTween's
	   project default ease, OutQuad. The two run at once, as the app runs
	   them, so the rail never empties between the modes. */
	transition: opacity 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94), visibility 0s 0.5s;
}

.stage[data-state="playing"][data-mode="objects"] .palette {
	opacity: 1;
	visibility: visible;
	transition-delay: 0s, 0s;
}

.stage[data-state="playing"][data-mode="objects"] .palette .cell {
	pointer-events: auto;
}

.stage[data-state="playing"][data-mode="draw"] .tools {
	opacity: 1;
	visibility: visible;
	transition-delay: 0s, 0s;
}

.stage[data-state="playing"][data-mode="draw"] .tools .tool {
	pointer-events: auto;
}

/* AlphaMapPainter.HideRightMenu takes both side menus off screen while a
   stroke is under the finger, not the left one alone. The fan already goes;
   the rail goes with it, on the fan's own 0.15 s. */
.stage.is-stroking[data-state="playing"] .palette,
.stage.is-stroking[data-state="playing"] .tools {
	opacity: 0;
	visibility: hidden;
	transition-duration: 0.15s;
	transition-delay: 0s, 0.15s;
}

/* pointer-events on the column is not enough: a child that sets its own
   `auto` is still a target through a parent set to `none`, and both sets of
   cards do exactly that. They have to be named, at the depth of the rule
   that armed them. */
.stage.is-stroking[data-state="playing"] .palette .cell,
.stage.is-stroking[data-state="playing"] .tools .tool {
	pointer-events: none;
}

/* ThumbnailDragHandler hides the whole menu while a ghost is in flight.
   Written against the same depth as the rule that raises the rail: a plain
   `.palette.is-dragging` is out-ranked by it and never shows. */
.stage[data-state="playing"] .palette.is-dragging,
.stage[data-state="playing"] .palette.is-dragging .cell {
	opacity: 0;
	pointer-events: none;
}

/* Edit mode takes BOTH side menus off the screen, not the right one alone:
   ObjectMover.UpdateRightSideMenu drops the alpha and the raycasts of the
   right menu and the left one together, for as long as an object is in hand.
   The rail here, the fan further down.

   Same depth as the rule that raises the rail, and written after it, which is
   how the stroke hides above already win. The cards have to be named as well:
   a child that sets its own `auto` is still a target through a parent set to
   `none`. */
.stage[data-edit="on"][data-state="playing"] .palette,
.stage[data-edit="on"][data-state="playing"] .tools {
	opacity: 0;
	visibility: hidden;
	transition-duration: 0.15s;
	transition-delay: 0s, 0.15s;
}

.stage[data-edit="on"][data-state="playing"] .palette .cell,
.stage[data-edit="on"][data-state="playing"] .tools .tool {
	pointer-events: none;
}

.cell,
.tool {
	flex: none;
	width: 100%;
	aspect-ratio: 1;
	padding: 0;
	background: none;
	border: 0;
	cursor: pointer;
	pointer-events: none;
	-webkit-appearance: none;
	appearance: none;
	-webkit-tap-highlight-color: transparent;
	touch-action: none;
}

.cell img,
.tool img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: contain;
	-webkit-user-drag: none;
}

/* The app tints the held card white; on the wooden card a slight darkening. */
.cell.is-held,
.cell:active,
.tool:active {
	filter: brightness(0.85);
}

/* Which brush is loaded. The card in hand stands back and the two still on
   offer are full: the rail shows what can be picked up rather than what is
   already held, on the owner's eye. `is-active` keeps its meaning and
   aria-pressed still carries the state in words, so only the sighted reading
   is inverted. */
.tool {
	opacity: 1;
	transition: opacity 0.3s ease;
}

.tool.is-active {
	opacity: 0.55;
}

/* The app's Sand / Water pair: two 80px buttons on one row above a list of
   140px cards (SceneMain, DrawMenu > TopMenu). Here they are full cells --
   the app's ratio would make them 27px on a 360px stage, under the 44px the
   sound switch names as the smallest comfortable target. The row is two
   cells wide and hangs off the column's right edge, so its right card sits
   over the brushes and its left one stands over the sand. */
.surfaces {
	display: flex;
	flex: none;
	gap: var(--cell-gap);
	width: calc(200% + var(--cell-gap));
}

.surfaces .tool {
	flex: 1 1 0;
	width: auto;
}

/* WaterSandController: the surface in hand is not interactable. It still has
   to take the finger, though. `pointer-events: none` does not swallow a tap,
   it makes the element invisible to hit-testing -- the tap would land on the
   canvas underneath, paint a dot in the visitor's garden and
   hide the whole rail for the length of the press
   (session.js's onStagePointerDown fires on `e.target === dom.canvas`). A
   Unity Selectable with interactable = false consumes the raycast and does
   nothing, and that is what this pair does: the card stays hit-testable, the
   click handler ignores a surface already in hand, and only the press tint is
   dropped so the card does not read as pressable. */
.stage[data-state="playing"][data-mode="draw"] .tools .surfaces .tool.is-active {
	cursor: default;
}

.surfaces .tool.is-active:active {
	filter: none;
}

/* ExecuteDraw swaps every card's sprite when the surface changes. The stage
   carries the surface; each card holds both icons and shows one. Instant, as
   the app's swap is. */
.stage[data-surface="water"] .tool img.for-sand,
.stage:not([data-surface="water"]) .tool img.for-water {
	display: none;
}

/* The app's Rake works on sand only. In water mode its card leaves the rail
   whole -- out of the flow, the tab order and the accessibility tree -- and
   the column closes the gap, as Rev 8 of the app does with ignoreLayout and
   a CanvasGroup. */
.stage[data-surface="water"] .tool.sand-only {
	display: none;
}

/* With the Rake on the rail the column is one cell longer, and centred it
   would grow both ways: the readout would climb 23-25 px, into the red
   full-version plate by 11 px on a 360 px phone and touching it at 390
   (measured 2026-10-18, against 11.8 and 24.8 px of air today). So it grows
   downward only. Half a cell and a gap of margin cancels the half the
   translateY(-50%) above takes back up, and the top stands exactly where the
   four-card column puts it, on both surfaces and across a swap. A margin and
   not a second translateY: a percentage in a translate is of the rail's own
   height, while --cell-size falls back to a percentage of the STAGE's width
   under @supports not (width: 1cqmin) -- which is what a margin percentage
   resolves against. Not transitioned, so a surface swap moves it at once, as
   the app's swap is. */
.stage:not([data-surface="water"]) .tools {
	margin-top: calc((var(--cell-size) + var(--cell-gap)) / 2);
}

/* The readout: which brush is loaded, for as long as it is. ExecuteDraw.cs
   puts the app's own copy of this between the surface pair and the button
   list, and it sits there here too -- in the column's flow, not above it.
   Not to be confused with the mode's plate, .scene-tag, centred on the
   top line: that one names the mode and stands for as long as it, this one
   is a state and is only ever replaced.

   Right-aligned rather than centred, and wider than the column: the rail is
   --cell-size wide, about 48px on a 366px stage, and the widest label, board
   and border counted, is about 105px. `align-self` puts its right edge on
   the column's, so the extra width runs left over the sand and moves no
   card; a word of another width -- FREE DRAW against CLEAR -- grows and
   shrinks the same way. `max-content` has to be asked for, or a flex item
   would be stretched to the column's width instead.

   Nothing here sets opacity, z-index or pointer-events. It is a child of the
   rail, so the mode cross-fade, the stroke-hide and the rail's own
   `pointer-events: none` all reach it -- which is the app's behaviour too:
   AlphaMapPainter.HideRightMenu takes the whole right menu, readout and all. */
.brush-tag {
	flex: none;
	align-self: flex-end;
	width: -webkit-max-content;
	width: max-content;
	/* 4px rather than the 8px this had while it hung above the rail: the
	   column is a row taller than it was, and every pixel of its height is a
	   half pixel of its top, which is what the draw plate is measured
	   against on the narrowest stage. (The Rake's row is the exception: the
	   margin rule above .brush-tag cancels its half, so it costs the top
	   nothing.) */
	margin: 4px 0;
	/* Two lines: the surface, then the brush, as ExecuteDraw writes them.
	   `pre-line` honours the newline session.js composes and overrides .tag's
	   `nowrap`; there is nothing else to wrap, since neither line runs past two
	   words and the box is max-content. Centred because two lines of unequal
	   width read as ragged against one edge -- the mode plates centre for the
	   same reason. */
	white-space: pre-line;
	text-align: center;
	/* The duration -- one side of the app's 0.4 s cross-fade -- is set inline
	   by session.js from its own TIMING, as the menu's is, so the stylesheet
	   and the countdown that drives it cannot drift. .tag's 0.6 s stands only
	   until the session runs. */
}

/* The bottom of the fade. session.js writes the new word here, with the plate
   already out, and lets it come back. */
.brush-tag.is-swapping {
	opacity: 0;
}

/* The ghost: a copy of the cell at 0.6, 1.1 times the size, centred on the
   finger. objects.js moves it in stage pixels. */
.ghost {
	position: absolute;
	z-index: 5;
	width: var(--cell-size);
	aspect-ratio: 1;
	opacity: 0;
	transform: translate(-50%, -50%) scale(1.1);
	pointer-events: none;
}

.ghost.is-visible {
	opacity: 0.6;
}

.ghost img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: contain;
}

/* --- The game: the close-up ------------------------------------------------------
   The owner's render, centred, over the visitor's own sand. --stone-w,
   --stone-x and --stone-y are its camera. The trail canvas covers the
   stage and takes the finger; stone.js decides, pixel by pixel, whether it
   is on the stone. */
/* A sheet of plain sand between the garden and the close-up. The app frames
   the worry stone tight enough that the rest of the garden is simply out of
   shot; this camera is fixed, so the garden is covered instead — the other
   objects and every raked stroke go under it, and the close-up and its Back
   button are what is left.

   stone-sand.webp is not new art. It is sand.js's own fragment shader run
   over sand-normal.webp with h = 1 everywhere: the same sun, the same 0.55
   grain weight, the same #EFE4D2 palette top, no height field because there
   are no strokes on it. `cover` on a 512 tile blows the grain up, which is
   what a close-up eye would see. The flat colour underneath is the same
   shader at the middle of the plane, so a sheet that fails to load is still
   sand and not a hole. */
.stone-veil {
	position: absolute;
	top: 0;
	right: 0;
	bottom: 0;
	left: 0;
	z-index: 4;
	background: rgb(216, 206, 191) url("stone-sand.webp") center / cover no-repeat;
	opacity: 0;
	pointer-events: none;
	transition: opacity 0.6s ease;
}

.stage[data-state="stone"] .stone-veil {
	opacity: 1;
}

/* The close-up is the same render as the sprite, so it takes the same
   exposure. stone.js writes the row's own light when it has one; the
   stylesheet's value stands otherwise. The trail is a canvas above this and
   is not filtered, and neither is the alpha mask, which reads the decoded
   pixels rather than the painted ones. */
.stone-closeup {
	position: absolute;
	top: calc(50% + var(--stone-y));
	left: calc(50% + var(--stone-x));
	z-index: 6;
	width: var(--stone-w);
	height: auto;
	filter: var(--object-light);
	opacity: 0;
	transform: translate(-50%, -50%);
	pointer-events: none;
	-webkit-user-drag: none;
	transition: opacity 0.6s ease;
}

.stone-closeup.is-visible {
	opacity: 1;
}

.trail {
	position: absolute;
	top: 0;
	right: 0;
	bottom: 0;
	left: 0;
	z-index: 7;
	display: none;
	width: 100%;
	height: 100%;
	touch-action: none;
}

.trail.is-visible {
	display: block;
}

/* --- The game: the fan --------------------------------------------------------------
   LeftMenuController.cs, on either frame -- the geometry below is all
   fractions of the root, so it follows --fan-size to a wide stage; only the
   root's own size and the room under it are set per layout. The root sits bottom-left; the
   four children fan on the app's half circle, evenly spaced over it by the
   app's own step, 180 / (itemCount - 1), which for four is 60 degrees from
   270: Exit down at 270, Explore at 330, Objects at 390, Draw up at 450. The
   two on the axes are a whole radius out; the two diagonals are cos 30 of it
   across and half of it up or down. Each child sits in a box the root's size,
   centred on it, so a translate in per cent is a fraction of the root and
   --fan-radius reads as the app's radius against its root. The box is square
   (aspect-ratio: 1), so an X and a Y percentage resolve to the same length
   and the diagonals are not skewed. Open: 0.45 s OutBack and a 0.225 s fade, no
   stagger. Closed: 0.45 s InBack, a 0.675 s fade, then out of reach. The
   two cubic-beziers approximate Penner's Back with the default 1.70158
   overshoot; DOTween's are the same curves. The whole fan hides during a
   rake stroke, as the app hides its menus in AlphaMapPainter.HideRightMenu. */
.fan-wrap {
	position: absolute;
	bottom: var(--fan-bottom);
	left: var(--fan-left);
	z-index: 8;
	width: var(--fan-size);
	aspect-ratio: 1;
	opacity: 0;
	pointer-events: none;
	transition: opacity 0.6s ease;
}

.stage[data-state="playing"] .fan-wrap {
	opacity: 1;
	pointer-events: auto;
}

.stage.is-stroking .fan-wrap {
	opacity: 0;
	pointer-events: none;
	transition-duration: 0.15s;
}

/* The other half of ObjectMover.UpdateRightSideMenu: the left menu goes with
   the right one for as long as an object is in hand. The session also closes
   the fan on the way in, so nothing is left open behind this. */
.stage[data-edit="on"] .fan-wrap {
	opacity: 0;
	pointer-events: none;
	transition-duration: 0.15s;
}

.fan-root,
.fan-item button,
.stone-back {
	display: block;
	padding: 0;
	background: none;
	border: 0;
	line-height: 0;
	cursor: pointer;
	-webkit-appearance: none;
	appearance: none;
	-webkit-tap-highlight-color: transparent;
	touch-action: none;
}

.fan-root {
	position: relative;
	z-index: 2;
	width: 100%;
	height: 100%;
}

.fan-root img,
.fan-item img,
.stone-back img {
	display: block;
	width: 100%;
	height: auto;
	-webkit-user-drag: none;
}

/* Pressed is the app's ColorTint, 0.784. */
.fan-root:active img,
.fan-item button:active img,
.stone-back:active img {
	filter: brightness(0.784);
}

.fan-item {
	position: absolute;
	top: 0;
	left: 0;
	z-index: 1;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 100%;
	height: 100%;
	opacity: 0;
	visibility: hidden;
	transform: translate(0, 0);
	pointer-events: none;
	transition:
		transform 0.45s cubic-bezier(0.36, 0, 0.66, -0.56),
		opacity 0.675s ease,
		visibility 0s linear 0.675s;
}

.fan-item button {
	width: var(--fan-child);
}

.fan-wrap.is-open .fan-item {
	opacity: 1;
	visibility: visible;
	pointer-events: auto;
	transition:
		transform 0.45s cubic-bezier(0.34, 1.56, 0.64, 1),
		opacity 0.225s ease,
		visibility 0s;
}

.fan-wrap.is-open .fan-exit {
	transform: translate(0, var(--fan-radius));
}

.fan-wrap.is-open .fan-explore {
	transform: translate(calc(0.866 * var(--fan-radius)), calc(0.5 * var(--fan-radius)));
}

.fan-wrap.is-open .fan-objects {
	transform: translate(calc(0.866 * var(--fan-radius)), calc(-0.5 * var(--fan-radius)));
}

.fan-wrap.is-open .fan-draw {
	transform: translate(0, calc(-1 * var(--fan-radius)));
}

/* The switch moved to the foot of the stage on 2026-09-10 and the fan's root
   rose to the rail's last card in the same pass, which puts the Exit item --
   the one that travels straight down -- across the switch's 44px target. Both
   are controls, so neither may take the other's finger: the switch stands
   down while the fan is open, on the fan's own 0.15s, exactly as the rails do
   under a stroke, and comes back when the fan closes. The fan is open for one
   tap, and the switch is on every other screen this stage has.

   The sibling combinator reaches it because .sound-toggle follows .fan-wrap in
   the markup; if either moves, this rule stops matching and the two controls
   overlap again in silence. */
.fan-wrap.is-open ~ .sound-toggle {
	opacity: 0;
	pointer-events: none;
	transition-duration: 0.15s;
}

/* The tag beside the fan's root: what the button next to it opens, once. A
   child of the wrapper, so it arrives with the fan and hides with it during a
   stroke. It stands on the root's own centre line, off its right edge -- the
   rails' idiom, mirrored, the fan being at the left edge of the stage where
   they are at the right. There is room: the root's right edge is at 20% of
   the stage, and the sentence, padding and the board's two 10px edges
   counted, is about 180px, so on the narrowest stage this targets -- 320px --
   it ends near 250px, short of the edge.

   `max-content` has to be asked for. With `left: 100%` the space left inside
   the wrapper is zero, so a shrink-to-fit box would be sized from the
   sentence's minimum rather than its preferred width, and only .tag's nowrap
   would save it. */
.fan-tag {
	position: absolute;
	top: 50%;
	left: 100%;
	z-index: 0;
	width: -webkit-max-content;
	width: max-content;
	margin-left: 8px;
	transform: translateY(-50%);
}

/* The tap that opens the fan is the tap that spends this tag, and the Objects
   button travels straight into the plate: it is inside the tag's box by the
   time it stops. Paint order is what keeps the button readable -- z-index 0
   here against .fan-item's 1 -- and this is what keeps the plate from being
   a ghost behind it for half a second. 0.15 s is the file's own number for a
   control that has to be gone before something else moves. */
.fan-tag.is-spent {
	transition-duration: 0.15s;
}

/* The close-up's Back: the app's own sprite, in the fan root's place and
   size, cross-faded with it by the stage's state. */
.stone-back {
	position: absolute;
	bottom: var(--fan-bottom);
	left: var(--fan-left);
	z-index: 9;
	width: var(--fan-size);
	aspect-ratio: 1;
	opacity: 0;
	pointer-events: none;
	/* Out of the tab order with the rails, and for the same reason: it is one
	   more control at opacity 0 on every screen but the close-up. */
	visibility: hidden;
	transition: opacity 0.6s ease, visibility 0s 0.6s;
}

.stage[data-state="stone"] .stone-back {
	opacity: 1;
	visibility: visible;
	transition-delay: 0s, 0s;
	pointer-events: auto;
}

/* Less motion: every fade and the fan's travel are instant. The trail still
   draws; it is content, not motion. */
@media (prefers-reduced-motion: reduce) {
	.fan-wrap,
	.fan-item,
	.fan-wrap.is-open .fan-item,
	.stone-back,
	.palette,
	.tools,
	.tool,
	.stone-veil,
	.stone-closeup,
	.tag,
	.ghost,
	/* Every rule that shortens a fade for motion -- the stroke-hides, the fan
	   tag's spend, the plate that a mode change takes down -- out-ranks the
	   plain class beside it, so it survives this block unless the whole
	   selector is repeated. The rail and fan rules were already outliving it
	   before the mode plates were written. */
	.edit-bar,
	.edit-btn,
	.fan-tag.is-spent,
	/* The mode's head is a plain box and not a .tag, so its own fade is named
	   here as well as every rule that takes it down. */
	.scene-head,
	.stage.is-stroking .scene-head,
	.stage:not([data-mode="draw"]) .scene-head.for-draw,
	.stage:not([data-mode="objects"]) .scene-head.for-objects,
	.stage:not([data-mode="explore"]) .scene-head.for-explore,
	.stage[data-edit="on"] .scene-head,
	.stage[data-calm="on"] .scene-head,
	.stage[data-veil="up"] .scene-head,
	.stage.is-stroking .fan-wrap,
	.stage.is-stroking[data-state="playing"] .palette,
	.stage.is-stroking[data-state="playing"] .tools,
	/* Edit mode's own rules out-rank the plain
	   entries above for the same reason and are repeated here in full.
	   `.edit-bar.is-visible` carries its own transition shorthand, so without
	   this line the row would keep fading for as long as it is up. */
	.edit-bar.is-visible,
	.edit-bar.is-dragging,
	.calm-bar,
	.calm-bar.is-visible,
	.fade-veil,
	.stage[data-edit="on"] .fan-wrap,
	.stage[data-edit="on"][data-state="playing"] .palette,
	.stage[data-edit="on"][data-state="playing"] .tools {
		transition-duration: 0s;
		/* The rails hold `visibility` for the length of their fade so they leave
		   the tab order only once they are gone. With no fade there is nothing
		   to hold it for. */
		transition-delay: 0s;
	}

	/* The calm buttons are simply there, at their own offsets. An animation is
	   not a transition and does not answer transition-duration, so the pop
	   takes its own rule, as the selection veil's does below. session.js makes
	   the same answer on its side: raiseVeil does the swap immediately and
	   never raises the black. */
	.calm-bar.is-visible .calm-btn {
		animation: none;
		transform: translateX(var(--calm-offset)) scale(1);
	}

	/* The selection veil is simply there, at full strength. An animation is
	   not a transition and does not answer transition-duration, so it takes
	   its own rule: no fade, and the opacity it settles at. */
	.object.is-editing::before {
		animation: none;
		opacity: var(--select-veil-alpha);
	}
}
