/* Mobile try — the framed portrait stage.
   Tokens are copied from assets/css/main.css so the page reads as the same
   site; this file is standalone on purpose, the template CSS is not loaded. */

: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: -10%;
	--plaque-w: 28.9%;
	/* 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: 4%;              /* of the stage width */
	--fan-bottom: 12%;           /* of the stage height */
	--fan-size: 16%;             /* the root, of the stage width */
	--fan-radius: 110%;          /* of the root, along the fan */
	--fan-child: 57.7%;          /* a child, of the root: 90 / 156 */
	--cell-size: 13%;            /* a rail cell, of the stage width */
	--rail-right: 12px;          /* the rail's gap from the stage edge */
	--cell-gap: 2px;
	--stone-w: 86%;              /* the close-up, of the stage width */
	--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 */
	--stone-y: 0px;
}

* {
	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. */
.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%;
	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 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 is one invitation. 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 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. The app puts
   it bottom-left, which on this 9:16 stage is where the fan already sits in
   the raking scene, so it mirrors .skip at top-left instead and no control the
   owner has already placed has to move. 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;
	top: 14px;
	left: 12px;
	width: 44px;
	height: 44px;
	padding: 0;
	background: none;
	border: 0;
	line-height: 0;
	cursor: pointer;
	-webkit-tap-highlight-color: transparent;
}

.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 beside it, in the shared tag idiom above. It is a whole
   sentence on a stage only 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: calc(12px + 44px + 8px);
	display: flex;
	align-items: center;
	height: 44px;
	max-width: calc(100% - 76px);
	margin: 0;
	padding: 0 0.6em;
	opacity: 0;
}

.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. The stage is a full-bleed preview with its own chrome, so the
   icon is hidden here and only here; every other page on the site still carries
   it, and the consent banner itself is untouched. The script writes `display`
   inline, so this has to out-rank an inline style. */
#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; 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 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 objects 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;
}

.stage[data-state="playing"][data-mode="objects"] .object.is-stone {
	pointer-events: auto;
	cursor: pointer;
	touch-action: none;
}

/* The tag above a worry stone, in the shared tag idiom. Spent for the
   session on the first press: the layer says so, and every tag fades. It is
   only true in objects mode, so draw mode and the close-up hide it too. */
.object-tag {
	position: absolute;
	bottom: 100%;
	left: 50%;
	margin-bottom: 6px;
	transform: translateX(-50%);
}

.objects[data-hint="spent"] .object-tag,
.stage:not([data-mode="objects"]) .object-tag,
.stage:not([data-state="playing"]) .object-tag {
	opacity: 0;
}

/* --- 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 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. 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;
	gap: var(--cell-gap);
	width: var(--cell-size);
	opacity: 0;
	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);
}

.stage[data-state="playing"][data-mode="objects"] .palette {
	opacity: 1;
}

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

.stage[data-state="playing"][data-mode="draw"] .tools {
	opacity: 1;
}

.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;
	transition-duration: 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;
}

.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 that is not in use stands back rather than
   the one in use standing out, which keeps the rail as quiet as the rest of
   the stage. */
.tool {
	opacity: 0.55;
	transition: opacity 0.3s ease;
}

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

/* The tag beside the rail: what the column under it is for, once. It is a
   child of the rail it speaks for, so it rides that rail's own cross-fade and
   is only ever seen in its mode -- and goes with it during a stroke. Absolute
   takes it out of the column's flex flow, so it adds no cell.

   The width has to be asked for. A box with `right` set and `left` and
   `width` auto is sized shrink-to-fit against its containing block, which
   here is the rail: 13% of the stage, about 47px. Every word of the sentence
   is wider than that, so left to itself the tag sets one word per line, and a
   max-width can only ever narrow a shrink-to-fit box further. `max-content`
   is the basis the cap then clamps: one line where the sentence fits, a wrap
   where it does not. 600% of the rail is 78% of the stage, and the rail's own
   13% plus its 12px gap leave exactly that much room, so the widest tag still
   stops short of the stage's edge. */
.rail-tag {
	position: absolute;
	top: 50%;
	right: 100%;
	width: -webkit-max-content;
	width: max-content;
	max-width: 600%;
	margin-right: 8px;
	transform: translateY(-50%);
	white-space: normal;
	text-align: center;
}

/* 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 a portrait stage. The root sits bottom-left; the
   three children fan on the app's half circle: 270 down, 360 right, 450 up.
   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. 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;
}

.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-objects {
	transform: translate(var(--fan-radius), 0);
}

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

/* The tag over the fan's root: what the button under it opens, once. A child
   of the wrapper, so it arrives with the fan and hides with it during a
   stroke. Its left edge is the root's own, not its centre: the root sits 4%
   from the stage edge and a centred sentence would hang off it. */
.fan-tag {
	position: absolute;
	bottom: 100%;
	left: 0;
	z-index: 0;
	margin-bottom: 8px;
}

/* 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;
	transition: opacity 0.6s ease;
}

.stage[data-state="stone"] .stone-back {
	opacity: 1;
	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 {
		transition-duration: 0s;
	}
}
