/* ------------------------------------------------------------------
 * HyperKit — Collage Board
 *
 * Architecture
 * ------------
 * .hk-cb            root, fixed height, overflow hidden (optional)
 *   .hk-cb__stage   positioning context every item is absolute inside
 *     .hk-cb__item  one scattered element — position + transform live here
 *       .hk-cb__float   idle bobbing animation (own layer, see below)
 *         .hk-cb__frame entrance transition + card/polaroid chrome
 *           .hk-cb__media  the img / video / icon / text
 *           .hk-cb__caption
 *   .hk-cb__cursor  the single Figma-style label that trails the pointer
 *
 * Why three nested wrappers instead of one
 * ----------------------------------------
 * Three things want to write `transform` on the same box at the same
 * time: the drag offset, the idle float loop, and the entrance
 * animation. A CSS animation and a CSS transition on the same property
 * fight each other (the animation always wins, and the transition
 * silently does nothing). Splitting them across three elements lets all
 * three run simultaneously without any of them clobbering the others:
 *
 *   __item   transform: position + drag offset + rotation + hover lift
 *   __float  transform: the idle bob keyframe animation
 *   __frame  transform + opacity: the one-shot entrance transition
 *
 * Position model
 * --------------
 * Items are placed with left/top as a PERCENTAGE of the stage, so the
 * composition keeps its proportions at any board width. The item is then
 * pulled back by half its own size with translate(-50%, -50%) — meaning
 * --hk-cb-x / --hk-cb-y always refer to the item's CENTER, which is the
 * intuitive thing when you are nudging sliders in the editor.
 *
 * Drag offsets are added on top as PIXELS (--hk-cb-dx / --hk-cb-dy),
 * written by JS. Keeping them separate from the base % position means a
 * drag never destroys the authored layout — resetting is just setting
 * the two offsets back to 0.
 *
 * Sizing
 * ------
 * Item width is `--hk-cb-w * --hk-cb-size-scale`. The per-item width is
 * responsive on its own, but the scale multiplier lets you shrink the
 * whole composition for one breakpoint with a single slider instead of
 * editing every item.
 * ------------------------------------------------------------------ */

/* ==================================================================
 * Root + stage
 * ================================================================== */
.hk-cb {
	position: relative;
	width: 100%;
	--hk-cb-size-scale: 1;
	--hk-cb-cursor-bg: #0d99ff;
	--hk-cb-cursor-fg: #ffffff;

	/* Board-level defaults live on the root, never on the element that
	 * consumes them. A custom property declared on .hk-cb__float would
	 * shadow the inherited value, and the editor's control — which
	 * writes to .hk-cb — would silently do nothing. */
	--hk-cb-float-distance: 8px;
	--hk-cb-stack-rest: 3px;
	--hk-cb-stack-rest-rot: 1deg;
	--hk-cb-stack-spread: 16px;
	--hk-cb-stack-spread-rot: 5deg;
}

.hk-cb--clip {
	overflow: hidden;
}

.hk-cb__stage {
	position: relative;
	width: 100%;
	height: 100%;
	min-height: inherit;
}

/* ==================================================================
 * Item
 * ================================================================== */
.hk-cb__item {
	--hk-cb-x: 50%;
	--hk-cb-y: 50%;
	--hk-cb-w: 220px;
	--hk-cb-dx: 0px;
	--hk-cb-dy: 0px;
	--hk-cb-rot: 0deg;
	--hk-cb-tilt: 0deg;
	--hk-cb-lift: 1;

	position: absolute;
	left: var(--hk-cb-x);
	top: var(--hk-cb-y);
	width: calc(var(--hk-cb-w) * var(--hk-cb-size-scale));
	max-width: 90%;

	/* --hk-cb-rot is the authored tilt; --hk-cb-tilt is the live lean a
	 * spillable item takes on while it's being swung around. Two
	 * separate rotations rather than one summed value, so the drag can
	 * write its own without ever disturbing the editor's setting. */
	transform:
		translate(-50%, -50%)
		translate3d(var(--hk-cb-dx), var(--hk-cb-dy), 0)
		rotate(var(--hk-cb-rot))
		rotate(var(--hk-cb-tilt))
		scale(var(--hk-cb-lift));
	transform-origin: center center;
	transition: transform 0.28s cubic-bezier(0.22, 0.8, 0.24, 1);
}

/* While the pointer owns the item, every transform change must be
 * immediate — a transition here would make the item lag behind the
 * cursor by its duration, which feels like dragging through syrup.
 * The same applies during the inertia loop, where JS is already
 * animating frame by frame. */
.hk-cb__item.is-dragging,
.hk-cb__item.is-flinging {
	transition: none;
}

/* Only grabbable when dragging is actually enabled, so a static board
 * doesn't advertise an interaction it won't honour. */
.hk-cb--draggable .hk-cb__item {
	cursor: grab;
	touch-action: none;
	-webkit-user-select: none;
	user-select: none;
}

.hk-cb--draggable .hk-cb__item.is-dragging {
	cursor: grabbing;
	z-index: 9000;
}

.hk-cb--lift .hk-cb__item:hover {
	--hk-cb-lift: 1.045;
}

/* Native image/link dragging would hijack our pointer sequence and
 * show the browser's ghost-image affordance. */
.hk-cb__item img,
.hk-cb__item a {
	-webkit-user-drag: none;
}

/* ==================================================================
 * Float (idle bob) — its own element so the keyframe animation never
 * competes with the drag transform or the entrance transition.
 * ================================================================== */
/* --hk-cb-float-duration is set per item as an inline style, so each
 * piece drifts on its own clock. */
.hk-cb__float {
	display: block;
}

.hk-cb--float .hk-cb__float {
	animation: hk-cb-float var(--hk-cb-float-duration, 6s) ease-in-out infinite alternate;
}

/* Bobbing while the user is holding the item reads as the element
 * fighting the cursor — freeze it for the duration of the grab. */
.hk-cb__item.is-dragging .hk-cb__float,
.hk-cb__item.is-flinging .hk-cb__float {
	animation-play-state: paused;
}

@keyframes hk-cb-float {
	from { transform: translate3d(0, calc(var(--hk-cb-float-distance) * -1), 0); }
	to   { transform: translate3d(0, var(--hk-cb-float-distance), 0); }
}

/* ==================================================================
 * Frame (chrome + entrance)
 * ================================================================== */
.hk-cb__frame {
	position: relative;
	display: block;
	transition:
		opacity 0.5s ease,
		transform 0.55s cubic-bezier(0.22, 1.2, 0.36, 1);
}

.hk-cb__link {
	display: block;
	color: inherit;
	text-decoration: none;
}

/* Entrance: JS adds .hk-cb--enter only when an entrance is configured,
 * then flips .is-in on each item with a staggered delay. The :not(.is-in)
 * state is the "before" frame. */
.hk-cb--enter .hk-cb__item:not(.is-in) .hk-cb__frame {
	opacity: 0;
}

.hk-cb--enter-drop .hk-cb__item:not(.is-in) .hk-cb__frame {
	transform: translate3d(0, -28px, 0) scale(0.94);
}

.hk-cb--enter-pop .hk-cb__item:not(.is-in) .hk-cb__frame {
	transform: scale(0.72);
}

/* Frame styles ---------------------------------------------------- */
.hk-cb__item--frame-card .hk-cb__frame,
.hk-cb__item--frame-polaroid .hk-cb__frame {
	background: var(--hk-cb-frame-bg, #ffffff);
	padding: var(--hk-cb-frame-pad, 10px);
	border-radius: var(--hk-cb-frame-radius, 10px);
	box-shadow: var(--hk-cb-shadow, 0 12px 32px rgba(15, 23, 42, 0.14));
}

.hk-cb__item--frame-polaroid .hk-cb__frame {
	padding-bottom: var(--hk-cb-polaroid-pad, 34px);
}

.hk-cb__item--frame-none .hk-cb__frame {
	filter: drop-shadow(var(--hk-cb-shadow-drop, 0 10px 24px rgba(15, 23, 42, 0.16)));
}

.hk-cb__caption {
	margin-top: 8px;
	font-size: 12px;
	line-height: 1.35;
	text-align: center;
	color: var(--hk-cb-caption-color, #6b7280);
}

.hk-cb__item--frame-polaroid .hk-cb__caption {
	position: absolute;
	left: var(--hk-cb-frame-pad, 10px);
	right: var(--hk-cb-frame-pad, 10px);
	bottom: 10px;
	margin-top: 0;
}

/* ==================================================================
 * Media
 * ================================================================== */
.hk-cb__media {
	position: relative;
	display: block;
	width: 100%;
	overflow: hidden;
	border-radius: var(--hk-cb-radius, 0px);
}

.hk-cb__media img,
.hk-cb__media video {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
	border-radius: inherit;
}

/* Shapes ----------------------------------------------------------
 * Anything non-rectangular forces a 1:1 box, because a circle or a
 * hexagon cut out of an arbitrary aspect ratio reads as a squashed
 * blob rather than the shape the user picked. */
.hk-cb__item--shape-rounded .hk-cb__media {
	border-radius: var(--hk-cb-radius, 14px);
}

.hk-cb__item--shape-circle .hk-cb__media,
.hk-cb__item--shape-squircle .hk-cb__media,
.hk-cb__item--shape-blob .hk-cb__media,
.hk-cb__item--shape-hex .hk-cb__media,
.hk-cb__item--shape-diamond .hk-cb__media {
	aspect-ratio: 1 / 1;
}

.hk-cb__item--shape-circle .hk-cb__media {
	border-radius: 50%;
}

.hk-cb__item--shape-pill .hk-cb__media {
	border-radius: 999px;
}

.hk-cb__item--shape-squircle .hk-cb__media {
	border-radius: 30%;
}

.hk-cb__item--shape-blob .hk-cb__media {
	border-radius: 62% 38% 54% 46% / 45% 56% 44% 55%;
}

.hk-cb__item--shape-hex .hk-cb__media {
	clip-path: polygon(50% 0%, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%);
}

.hk-cb__item--shape-diamond .hk-cb__media {
	clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
}

/* Photo stack -----------------------------------------------------
 * A pile of sheets that barely peek at rest and fan out on hover.
 *
 * Every sheet shares ONE transform rule; the only thing that differs
 * between them is --hk-cb-d, the depth index PHP stamps on each one.
 * Multiplying the offsets by that index means a pile of three and a
 * pile of twelve both work with no extra CSS and no per-sheet styles.
 *
 * --hk-cb-dir alternates 1 / -1 down the pile so consecutive sheets
 * lean opposite ways — a stack that all leans one way reads as a
 * rendering mistake rather than as paper.
 *
 * The box: the top sheet sits in normal flow and gives the stack its
 * height from its own aspect ratio; the rest are absolutely positioned
 * over it. So the media box is exactly the size of the photo you see,
 * however tall or wide it happens to be.
 * ------------------------------------------------------------------ */
/* A note on the doubled-up selectors below: the generic media rules
 * (.hk-cb__media img) and the shape rules (.hk-cb__item--shape-hex
 * .hk-cb__media) both out-rank a lone .hk-cb__stack-sheet, so every
 * override here is qualified with .hk-cb__media--stack to win on
 * specificity rather than on source order alone. */
.hk-cb__item .hk-cb__media--stack {
	/* The fanned sheets travel outside the media box, so the clipping
	 * every other media type wants would decapitate them on hover. */
	overflow: visible;
	clip-path: none;
}

.hk-cb__media--stack .hk-cb__stack-sheet {
	--hk-cb-d: 0;
	--hk-cb-dir: 1;

	display: block;
	width: 100%;
	height: auto;
	border-radius: inherit;
	box-shadow: var(--hk-cb-stack-shadow, 0 4px 14px rgba(15, 23, 42, 0.16));
	transform:
		translate3d(
			calc(var(--hk-cb-d) * var(--hk-cb-stack-rest)),
			calc(var(--hk-cb-d) * var(--hk-cb-stack-rest)),
			0
		)
		rotate(calc(var(--hk-cb-d) * var(--hk-cb-dir) * var(--hk-cb-stack-rest-rot)));
	transition: transform 0.38s cubic-bezier(0.2, 0.8, 0.2, 1);
}

/* Everything below the top sheet is stacked over it. Later siblings
 * paint above earlier ones, and PHP emits the pile deepest-first, so
 * the natural paint order is already correct — no z-index needed. */
.hk-cb__media--stack .hk-cb__stack-sheet:not(:last-child) {
	position: absolute;
	inset: 0;
	height: 100%;
	object-fit: cover;
}

/* Fan out. Dragging counts as a hover here: the item is under the
 * pointer and being handled, which is exactly when the pile should be
 * open — and it's the only way a touch user ever sees it. */
.hk-cb__item:hover .hk-cb__media--stack .hk-cb__stack-sheet,
.hk-cb__item.is-dragging .hk-cb__media--stack .hk-cb__stack-sheet {
	transform:
		translate3d(
			calc(var(--hk-cb-d) * var(--hk-cb-stack-spread)),
			calc(var(--hk-cb-d) * var(--hk-cb-stack-spread) * 0.45),
			0
		)
		rotate(calc(var(--hk-cb-d) * var(--hk-cb-dir) * var(--hk-cb-stack-spread-rot)));
}

/* Square-cropping shapes take their height from the container's
 * aspect-ratio instead of from the top sheet, so every sheet — the top
 * one included — becomes absolute and fills that box. */
.hk-cb__item--shape-circle .hk-cb__media--stack .hk-cb__stack-sheet,
.hk-cb__item--shape-squircle .hk-cb__media--stack .hk-cb__stack-sheet,
.hk-cb__item--shape-blob .hk-cb__media--stack .hk-cb__stack-sheet,
.hk-cb__item--shape-hex .hk-cb__media--stack .hk-cb__stack-sheet,
.hk-cb__item--shape-diamond .hk-cb__media--stack .hk-cb__stack-sheet {
	position: absolute;
	inset: 0;
	height: 100%;
	object-fit: cover;
}

/* Deck mode ------------------------------------------------------
 * Click to throw the top photo aside until only the last is left,
 * which is then the one that carries the link. */
.hk-cb__media--deck {
	cursor: pointer;
}

/* Discarded sheets keep their place in the document — the last child
 * is what gives the stack its height, so removing it from flow would
 * collapse the whole pile the moment you flipped past it. They're only
 * moved and faded. Four classes deep so this beats the hover-fan rule
 * whatever order they end up in. */
.hk-cb__item .hk-cb__media--deck .hk-cb__stack-sheet.is-gone {
	transform:
		translate3d(calc(var(--hk-cb-dir) * 64%), -20%, 0)
		rotate(calc(var(--hk-cb-dir) * 17deg))
		scale(0.94);
	opacity: 0;
	pointer-events: none;
	transition: transform 0.46s cubic-bezier(0.3, 0.9, 0.3, 1), opacity 0.4s ease;
}

/* The link covers the pile but can't be clicked through until the
 * pile is down to one — that's the whole mechanic. */
.hk-cb__stack-link {
	position: absolute;
	inset: 0;
	z-index: 4;
	pointer-events: none;
}

.hk-cb__media--deck.is-last .hk-cb__stack-link {
	pointer-events: auto;
}

/* border-radius inherits; clip-path doesn't, and it now has to live on
 * the sheets because the container had to stop clipping. */
.hk-cb__item--shape-hex .hk-cb__media--stack .hk-cb__stack-sheet {
	clip-path: polygon(50% 0%, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%);
}

.hk-cb__item--shape-diamond .hk-cb__media--stack .hk-cb__stack-sheet {
	clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
}

/* Records ---------------------------------------------------------
 * The vinyl is drawn, not shipped: concentric grooves from a repeating
 * radial gradient, a soft sheen raked across it, and the cover art as
 * the centre label. Nothing to download, and it recolours from a
 * single custom property.
 *
 * Two transforms again, two elements: __disc-wrap owns the slide out
 * of the sleeve (a transition) and __disc owns the rotation (a
 * keyframe animation). On one element the animation would win and the
 * record would never leave its sleeve.
 * ------------------------------------------------------------------ */
.hk-cb__item .hk-cb__media--music {
	/* The record slides beyond the artwork's box. */
	overflow: visible;
	clip-path: none;
}

.hk-cb__music {
	position: relative;
	display: block;
	aspect-ratio: 1 / 1;
}

.hk-cb__sleeve {
	position: relative;
	z-index: 2;
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
	border-radius: inherit;
	box-shadow: var(--hk-cb-sleeve-shadow, 0 10px 26px rgba(15, 23, 42, 0.24));
}

.hk-cb__disc-wrap {
	position: absolute;
	inset: 0;
	z-index: 1;
	transition: transform 0.55s cubic-bezier(0.22, 0.9, 0.24, 1);
}

/* Sleeve template: the disc hides behind the artwork and peeks out to
 * the right, then pulls further out on hover. */
.hk-cb__music--sleeve .hk-cb__disc-wrap {
	transform: translateX(var(--hk-cb-disc-peek, 26%));
}

.hk-cb__item:hover .hk-cb__music--sleeve .hk-cb__disc-wrap {
	transform: translateX(var(--hk-cb-disc-pull, 54%));
}

.hk-cb__disc {
	position: relative;
	width: 100%;
	height: 100%;
	border-radius: 50%;
	background:
		/* the sheen, raked across the whole face */
		linear-gradient(118deg, rgba(255, 255, 255, 0.16) 0 10%, rgba(255, 255, 255, 0) 34%, rgba(255, 255, 255, 0) 66%, rgba(255, 255, 255, 0.08) 88%, rgba(255, 255, 255, 0) 100%),
		/* the grooves */
		repeating-radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.05) 0 1px, rgba(0, 0, 0, 0) 1px 4px),
		/* the disc itself */
		radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--hk-cb-vinyl, #121212) 78%, #ffffff) 0 20%, var(--hk-cb-vinyl, #121212) 34%, #050505 100%);
	box-shadow: 0 10px 26px rgba(15, 23, 42, 0.3);
	animation: hk-cb-spin var(--hk-cb-disc-speed, 3.4s) linear infinite;
}

/* color-mix is recent; this is the same disc without the highlight for
 * anything that can't parse it. */
@supports not (background: color-mix(in srgb, red 50%, blue)) {
	.hk-cb__disc {
		background:
			linear-gradient(118deg, rgba(255, 255, 255, 0.16) 0 10%, rgba(255, 255, 255, 0) 34%, rgba(255, 255, 255, 0) 66%, rgba(255, 255, 255, 0.08) 88%, rgba(255, 255, 255, 0) 100%),
			repeating-radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.05) 0 1px, rgba(0, 0, 0, 0) 1px 4px),
			radial-gradient(circle at 50% 50%, var(--hk-cb-vinyl, #121212) 34%, #050505 100%);
	}
}

.hk-cb__music--spin-hover .hk-cb__disc {
	animation-play-state: paused;
}

/* Playing is what starts the record turning — hover alone is only a
 * stand-in for it while the audio is still loading. */
.hk-cb__music--spin-hover.is-playing .hk-cb__disc,
.hk-cb__item:hover .hk-cb__music--spin-hover .hk-cb__disc {
	animation-play-state: running;
}

.hk-cb__disc-label {
	position: absolute;
	top: 50%;
	left: 50%;
	width: var(--hk-cb-disc-label, 36%);
	height: var(--hk-cb-disc-label, 36%);
	transform: translate(-50%, -50%);
	border-radius: 50%;
	background-position: center;
	background-size: cover;
	box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35);
}

.hk-cb__disc-hole {
	position: absolute;
	top: 50%;
	left: 50%;
	width: 6.5%;
	height: 6.5%;
	transform: translate(-50%, -50%);
	border-radius: 50%;
	background: #f4f4f5;
	box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25);
}

@keyframes hk-cb-spin {
	to { transform: rotate(360deg); }
}

/* Play button ------------------------------------------------------
 * Deliberately a real <button>: on a phone there is no hover, and some
 * browsers won't let sound start without a click. */
.hk-cb__music-btn {
	position: absolute;
	left: 8px;
	bottom: 8px;
	z-index: 3;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 34px;
	height: 34px;
	padding: 0;
	border: 0;
	border-radius: 50%;
	background: rgba(12, 12, 14, 0.72);
	color: #ffffff;
	cursor: pointer;
	opacity: 0;
	transform: scale(0.85);
	transition: opacity 0.2s ease, transform 0.2s ease;
	-webkit-backdrop-filter: blur(6px);
	backdrop-filter: blur(6px);
}

.hk-cb__item:hover .hk-cb__music-btn,
.hk-cb__music.is-playing .hk-cb__music-btn,
.hk-cb__music-btn:focus-visible {
	opacity: 1;
	transform: scale(1);
}

.hk-cb__music-btn svg {
	width: 16px;
	height: 16px;
}

.hk-cb__music-btn .hk-cb__ico-pause,
.hk-cb__music.is-playing .hk-cb__music-btn .hk-cb__ico-play {
	display: none;
}

.hk-cb__music.is-playing .hk-cb__music-btn .hk-cb__ico-pause {
	display: block;
}

/* Hover couldn't start the sound — the browser wanted a click first.
 * Keep the button showing so there's something obvious to click. */
.hk-cb__music.is-blocked .hk-cb__music-btn {
	opacity: 1;
	transform: scale(1);
}

/* The frames the players live in are functional, not visual.
 *
 * Given a real size rather than 1×1 and hidden with opacity: embedded
 * players are entitled to refuse to play in a box with no area, and a
 * display:none one certainly would.
 *
 * The hiding has to live on this WRAPPER, never on the element passed
 * to the SDK — YouTube and Spotify both replace that element with
 * their own iframe, which inherits none of its classes. Style the
 * element they consume and the styling vanishes with it, leaving a
 * Spotify player card sitting on top of the record.
 *
 * Behind everything as well as transparent, so a browser that ignores
 * the opacity still can't paint it over the artwork. */
.hk-cb__music-host {
	position: absolute;
	left: 0;
	bottom: 0;
	z-index: -1;
	width: 300px;
	max-width: 100%;
	height: 80px;
	opacity: 0;
	overflow: hidden;
	pointer-events: none;
}

.hk-cb__music-host iframe {
	display: block;
	width: 100%;
	height: 100%;
	border: 0;
}

/* Icon items ------------------------------------------------------ */
.hk-cb__icon {
	display: flex;
	align-items: center;
	justify-content: center;
	aspect-ratio: 1 / 1;
	background: var(--hk-cb-icon-bg, transparent);
	color: var(--hk-cb-icon-color, #111827);
	font-size: var(--hk-cb-icon-size, 40px);
}

.hk-cb__icon svg {
	width: 1em;
	height: 1em;
	fill: currentColor;
}

.hk-cb__icon i {
	font-size: inherit;
	line-height: 1;
}

/* Text items ------------------------------------------------------ */
.hk-cb__text {
	padding: var(--hk-cb-text-pad, 14px 16px);
	font-size: var(--hk-cb-text-size, 15px);
	line-height: 1.45;
	color: var(--hk-cb-text-color, #111827);
	background: var(--hk-cb-text-bg, transparent);
	border-radius: inherit;
}

/* ==================================================================
 * Spills
 *
 * One layer per board holding every droplet and splat. It sits above
 * resting items but far below the ~9000 z-index a dragged item gets,
 * so the cup you're holding stays in front of the liquid leaving it
 * while the drops still fall in front of the rest of the collage.
 *
 * JS owns the droplet transforms frame by frame (a falling arc isn't
 * expressible as a keyframe when its velocity comes from how hard you
 * threw the item). The splat, whose whole life is fixed the moment it
 * lands, is pure CSS.
 * ================================================================== */
.hk-cb__spills {
	position: absolute;
	inset: 0;
	z-index: 500;
	overflow: hidden;
	pointer-events: none;
}

.hk-cb__drop {
	position: absolute;
	top: 0;
	left: 0;
	width: var(--hk-cb-spill-size, 14px);
	height: var(--hk-cb-spill-size, 14px);
	/* Teardrop: pointed at the top, heavy at the bottom. JS rotates it
	 * to face its direction of travel, so the point trails behind. */
	border-radius: 50% 50% 50% 50% / 68% 68% 32% 32%;
	will-change: transform;
}

/* Puddles are irregular rather than perfect ellipses — a spill that
 * lands as a neat oval reads as a shadow, not as liquid. Each one gets
 * its own rotation from JS, so the same lopsided shape never repeats
 * at the same angle twice. */
.hk-cb__splat {
	position: absolute;
	top: 0;
	left: 0;
	width: calc(var(--hk-cb-spill-size, 14px) * 2.1);
	height: calc(var(--hk-cb-spill-size, 14px) * 0.8);
	border-radius: 46% 54% 62% 38% / 58% 44% 56% 42%;
	opacity: 0.85;
	transform: translate(-50%, -50%) rotate(var(--hk-cb-splat-rot, 0deg)) scale(0.2);
	animation: hk-cb-splat 0.34s cubic-bezier(0.2, 1.5, 0.4, 1) forwards;
	transition: opacity 0.9s ease-in;
}

/* Set by JS once the splat has had its time on the board. */
.hk-cb__splat.is-drying {
	opacity: 0;
}

@keyframes hk-cb-splat {
	from { transform: translate(-50%, -50%) rotate(var(--hk-cb-splat-rot, 0deg)) scale(0.2); }
	to   { transform: translate(-50%, -50%) rotate(var(--hk-cb-splat-rot, 0deg)) scale(1); }
}

/* ==================================================================
 * Figma-style cursor label
 *
 * One element per board, reused by every linked item — a per-item label
 * would mean N absolutely-positioned nodes all listening for the same
 * pointer. It lives in the stage's coordinate space (absolute, not
 * fixed) so it clips with the board and needs no scroll compensation.
 *
 * JS writes the transform every frame while lerping toward the pointer,
 * which is what produces the trailing drag. No transition here: the
 * easing IS the lerp.
 * ================================================================== */
.hk-cb__cursor {
	position: absolute;
	top: 0;
	left: 0;
	z-index: 9999;
	display: flex;
	align-items: center;
	gap: 6px;
	pointer-events: none;
	opacity: 0;
	visibility: hidden;
	transform: translate3d(-9999px, -9999px, 0);
	transition: opacity 0.16s ease, visibility 0.16s ease;
	will-change: transform;
}

.hk-cb.is-cursor-visible .hk-cb__cursor {
	opacity: 1;
	visibility: visible;
}

.hk-cb__cursor-arrow {
	flex: none;
	width: 18px;
	height: 18px;
	color: var(--hk-cb-cursor-bg);
	filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.22));
}

.hk-cb__cursor-label {
	display: inline-block;
	padding: 4px 9px;
	border-radius: var(--hk-cb-cursor-radius, 999px);
	background: var(--hk-cb-cursor-bg);
	color: var(--hk-cb-cursor-fg);
	font-size: var(--hk-cb-cursor-size, 12px);
	font-weight: 600;
	line-height: 1.35;
	white-space: nowrap;
	box-shadow: 0 2px 10px rgba(15, 23, 42, 0.18);
}

/* Hovering a labelled item hides the OS cursor so the Figma pill reads
 * as the pointer itself rather than as a tooltip stuck next to one.
 * Keyed off the data attribute, not a class, so it tracks exactly the
 * same set of items the JS binds to — labelled, with or without a link. */
.hk-cb--hide-native-cursor .hk-cb__item[data-hk-label]:hover {
	cursor: none;
}

.hk-cb--hide-native-cursor.hk-cb--draggable .hk-cb__item[data-hk-label].is-dragging {
	cursor: none;
}

/* ==================================================================
 * Editor placeholder
 * ================================================================== */
.hk-cb--empty {
	padding: 32px;
	text-align: center;
	border: 1px dashed #ccc;
	border-radius: 12px;
	color: #666;
}

/* ==================================================================
 * Reduced motion
 *
 * Kills the ambient motion (float, entrance) that the user never asked
 * for. Dragging is deliberate, direct manipulation, so it stays — but
 * the inertia fling is switched off by JS reading the same query.
 * ================================================================== */
@media (prefers-reduced-motion: reduce) {
	.hk-cb--float .hk-cb__float {
		animation: none;
	}

	.hk-cb--enter .hk-cb__item .hk-cb__frame {
		opacity: 1;
		transform: none;
		transition: none;
	}

	.hk-cb__item {
		transition: none;
	}

	/* The pile still opens on hover — that's a direct response to the
	 * user's own pointer, not ambient motion — it just doesn't glide. */
	.hk-cb__media--stack .hk-cb__stack-sheet {
		transition: none;
	}

	/* A record turning forever is exactly the kind of perpetual motion
	 * this setting exists to stop. The music still plays. */
	.hk-cb__disc {
		animation: none;
	}
}
