/**
 * Reels swipe viewer -- chrome and geometry (SMASH-2049 prototype).
 *
 * Every selector here is prefixed `cff-swipeview-`, and the mount is
 * #cff-swipeview-root on <body>. That is a hard requirement rather than a
 * convention: twelve sites in assets/js/cff-scripts.js pause
 * `#cff-lightbox-wrapper video.cff-lightbox-video`, and `:5063` removes every
 * <video> under the lightbox container on each image change. Slice 2 is the
 * slice that creates a <video>, so nothing here may match those selectors or
 * live under that wrapper. The tier-1 element is `.cff-swipeview-video`.
 *
 * ==========================================================================
 * THE HEIGHT MODEL -- AND WHY THERE IS NO `vh` OR `dvh` IN THIS FILE
 * ==========================================================================
 *
 * RATIFIED (2026-08-24 team feedback item C): the sibling viewers' bottom-right
 * controls were reported CLIPPED on a real phone -- "I can see sometimes
 * they're getting clipped on my phone (especially instagram)" (Aman) -- because
 * they sized on `100vh`, which excludes mobile browsers' dynamic toolbars. The
 * recommended fix there was `dvh` with a `vh` fallback.
 *
 * REVERSED 2026-09-09, deliberately, after the same clipping was reported on
 * this viewer (Asmita, on the permanent demo at 4.12.0.3: the Facebook icon
 * "has no spacing below the icon, it's at the bottom of the reel").
 *
 * The original reasoning was that this viewer needed neither unit, because
 * `position: fixed; inset: 0` IS the visual viewport box and "tracks the
 * toolbars for free". THAT CLAIM DOES NOT HOLD. A fixed element's containing
 * block is the initial containing block -- the LAYOUT viewport -- which is not
 * the visual viewport, and `dvh` exists precisely because the two differ. The
 * behaviour is also engine-dependent (Chrome/Android sizes fixed boxes to the
 * layout viewport, so the bottom band sits behind the toolbar; iOS Safari has
 * varied across versions), which is the very "second-guessing about which
 * engine resolves what how" the old note claimed to escape by avoiding the
 * units. It did not escape it -- it moved the problem from a unit to a
 * positioning scheme, and stopped testing for it.
 *
 * The rail's own numbers were never the bug: measured live at 390x844 the
 * clearance was exactly the reference 18px, at six viewports, growing to 34px
 * under a simulated safe-area inset. What was wrong was the FRAME'S BOX.
 *
 * So the height is now the IG pair, matching the prototype (spec 2.6 sizes on
 * `--sc-frame-max-h: 100dvh`) and the fix the siblings were given:
 *
 *     height: 100vh;      <- fallback, the OLD behaviour, on engines without dvh
 *     height: 100dvh;     <- the visible box, where supported
 *
 * The insets stay. With `top`, `bottom` and `height` all non-auto the box is
 * over-constrained, and CSS 2.1 10.6.4 resolves that by IGNORING `bottom` for
 * a fixed-position element -- so `height` wins and the root stays anchored at
 * `top: 0`. That is specified behaviour, not a quirk, and `bottom: 0` is left
 * as the anchor that would apply only if `height` were ever invalid. Do not
 * "tidy" the redundancy away without reading this paragraph.
 *
 * Where the two viewports agree -- every emulated viewport, and all of desktop
 * -- `100dvh == 100vh == innerHeight`, which is the same number the inset-0 box
 * resolved to. So this is provably a NO-OP off-device, and the harness asserts
 * that parity rather than trusting it.
 *
 * The guard still forbids a LONE `vh`/`dvh`: the bug it was written against is
 * a `min-height: 100vh` added "for safety", and an unpaired `dvh` that silently
 * does nothing on an older engine. It now permits exactly the paired form, as
 * IG's own guard does.
 *
 * ON-DEVICE CONFIRMATION IS STILL PENDING -- it belongs with the AC 35 iOS
 * pass. Nothing here has been verified on a real phone: `setVisibleSize` has no
 * effect in CDP and the visual and layout viewports cannot be made to disagree
 * in this harness.
 *
 * Safe-area insets are applied to the CHROME (not the stage) with px fallbacks
 * first, so an engine without `env()` gets the plain value rather than nothing.
 *
 * ==========================================================================
 * WHO WINS A CLICK -- spec S6.3, and it is geometry, not preference
 * ==========================================================================
 *
 * The gesture layer is a SIBLING of the viewport, because the track carries a
 * live `transform` and a transform establishes a stacking context that TRAPS
 * every descendant z-index inside it. So a z-index on slide content is never
 * compared against the gesture layer at all; the comparison that decides a
 * click is viewport(2) vs gesture(1).
 *
 * That makes the viewport win, which is correct -- slide content must be
 * reachable -- and it means the track subtree has to opt OUT of pointer events
 * so the gesture layer can own the empty stage, with genuine controls opting
 * back in one by one. Non-interactive chrome opts out TOO: a caption element
 * was measured stealing a link's clicks at ~390x660 on a sibling once its text
 * grew over it, and it was also swallowing tap-to-toggle in its own region --
 * a silent S3.3 violation.
 *
 * ==========================================================================
 * AND THE SESSION CHROME NEEDS AN EXPLICIT z-index -- FOUND BY HIT TEST
 * ==========================================================================
 *
 * The close and mute buttons are direct children of the root, appended AFTER
 * the gesture layer in DOM order. That is NOT enough, and the first run of the
 * behavioural suite proved it: `elementFromPoint` at each button's own centre
 * returned `div.cff-swipeview-gesture`, so both were POINTER-DEAD -- visible,
 * keyboard-reachable, and inert to a mouse.
 *
 * The reason is a stacking rule that DOM order does not override: the gesture
 * layer is positioned WITH a z-index, so it forms a stacking level above every
 * positioned sibling whose z-index is `auto`, whatever their document order.
 * "Later in the DOM" only decides between siblings at the SAME level.
 *
 * This is exactly the S6.3 defect class the spec says shipped into three
 * implementations and was found by a maintainer clicking a button -- and here
 * it was found by the hit test instead, which is the whole point of demanding
 * that proof by name. So every INTERACTIVE session control is raised to level
 * 3 (above the viewport's 2, so it also clears slide content), and the
 * non-interactive readouts are deliberately left alone: they already carry
 * `pointer-events: none`, so their stacking level cannot steal a click, and
 * raising them would only create a new way for one to start doing so.
 *
 * @since 4.13.0 (SMASH-2049 prototype)
 */

/* The stacking fix the hit test forced -- see the note above. Declared once,
   here, rather than as a z-index scattered through each control's own rule:
   the level is a property of the LAYER, and a copy per control is a place for
   one to drift below the gesture layer again.

   The list SHRANK when the top row moved inside the frame. Mute and the CTA
   pill used to be root children needing their own level; they are now children
   of `.cff-swipeview-chromelayer`, which carries level 3 for the whole session
   frame chrome in one declaration. What remains here is the chrome that is
   still anchored to the WINDOW rather than to the frame: the close disc, and
   the chevrons until step (d) moves them into the rail column.

   Close and the chrome layer both sit at 3, and close is appended AFTER the
   layer, so document order puts it on top -- which is what we want on touch,
   where the full-bleed frame's top row runs underneath the window close. The
   62px left pad on that row is the belt to this braces. */
.cff-swipeview-close,
.cff-swipeview-nav {
	z-index: 3;
}

.cff-swipeview-root {
	/* The `inset` shorthand with a longhand fallback pair, in that order, so an
	   engine that knows only the longhands still gets the full box. */
	position: fixed;
	top: 0;
	right: 0;
	bottom: 0;
	left: 0;
	inset: 0;

	/* The IG pair -- see "REVERSED 2026-09-09" in the header for why the
	   insets stay alongside it and which declaration wins. `vh` FIRST so the
	   fallback on an engine without `dvh` is the previous behaviour rather
	   than an invalid declaration. */
	height: 100vh;
	height: 100dvh;

	z-index: 100000;
	overflow: hidden;
	background: #000;
	color: #fff;
	font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
	-webkit-tap-highlight-color: transparent;

	/* Rule 1.13's fade -- see the note below the @supports block. IN ONLY. */
	opacity: 0;
	transition: opacity 180ms ease;

	/* Tokens. The gutter is the reserved right-hand column the counts readout
	   and the mute button share; deriving the mute button's offset from the
	   same token is what keeps the two stacked rather than overlapping when
	   either changes size. */
	--cff-sv-gutter: 12px;
	--cff-sv-edge: 16px;
	--cff-sv-control: 44px;
	--cff-sv-safe-bottom: 0px;
	--cff-sv-safe-top: 0px;
	/* AC 17's expanded-caption cap: a LINE COUNT at the design's own line
	   height, never a viewport fraction. 12 x 18.2px = 218.4px, which is 24.7%
	   of the 884px desktop frame and 25.9% of the 844px touch frame -- inside a
	   percentage point of the design's "≈26%" at both reference viewports, with
	   no viewport unit anywhere. */
	--cff-sv-caption-cap: calc(18.2px * 12);
}

@supports (padding: env(safe-area-inset-bottom)) {
	.cff-swipeview-root {
		--cff-sv-safe-bottom: env(safe-area-inset-bottom, 0px);
		--cff-sv-safe-top: env(safe-area-inset-top, 0px);
	}
}

/* ── Rule 1.13's fade: IN ONLY, and the asymmetry is deliberate ──────────── *
 * The root opens at `opacity: 0` and is raised to 1 by a class the JS adds on
 * the frame after mount, so the first paint is not a hard cut onto a black
 * screen.
 *
 * There is NO fade-OUT, and that is a correctness call rather than a missing
 * feature. A fade-out has to defer the teardown behind a timer or a
 * `transitionend` -- and every deferred teardown owns the scroll lock
 * (`html.cff-swipeview-open` below) for the length of the deferral. If the
 * transition never fires (reduced motion, a backgrounded tab throttling
 * timers, the element removed from the flow first) the handler never runs, the
 * class is never dropped, and the visitor is left on a page they cannot
 * scroll with no viewer on screen to explain why. Closing is therefore
 * synchronous: unlock, unmount, restore focus, in that order, in one task.
 */
.cff-swipeview-root-in {
	opacity: 1;
}

/* Reduced motion is handled in ONE block, at the very END of this file -- see
   the note there for why source order rather than specificity is what makes it
   win. Nothing motion-related is suppressed here. */

/* Scroll lock while the viewer owns the screen. */
html.cff-swipeview-open,
html.cff-swipeview-open body {
	overflow: hidden;
}

/* ─────────────────────────── Track and slots ─────────────────────────── */

.cff-swipeview-viewport {
	position: absolute;
	top: 0;
	right: 0;
	bottom: 0;
	left: 0;
	inset: 0;
	/* Above the gesture layer -- see the header note. This is the ONE z-index
	   comparison that decides who receives a click. */
	z-index: 2;
	overflow: hidden;
	/* The subtree opts OUT; genuine controls opt back in individually. */
	pointer-events: none;
}

.cff-swipeview-track {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	/* Written only by setTrackOffset(). The initial value is stated so the
	   first paint is not an untransformed track for one frame. */
	transform: translate3d(0, 0, 0);
	will-change: transform;
}

.cff-swipeview-slot {
	position: absolute;
	left: 0;
	width: 100%;
	height: 100%;
	/* `top` is written per slot by assignSlot() as `index * 100%`. A slot with
	   no data-index has never been assigned, so it must not paint -- otherwise
	   an unassigned slot sits at y=0 as an opaque black rectangle over the
	   active slide for the first frames of an open. */
	display: none;
}

.cff-swipeview-slot[data-index] {
	display: block;
}

/* ── The stage row: rule 1.1's frame, expressed without a viewport unit ──── *
 * ONE per slot (inside the track, holding the painted frame and the per-post
 * rail), plus ONE at session level (holding the session frame chrome and the
 * chevrons). Both are `position: absolute; inset: 0` over their own container,
 * so the row's CONTENT box is the window minus the 8px desktop margins, and
 * the frame's `height: 100%` resolves against that -- rule 1.1's
 * "viewport - 16px" with no arithmetic and no `vh`.
 *
 * WHY A FLEX ROW AND NOT INSTAGRAM'S CLAMP. Instagram anchors everything off a
 * continuous `--sbi-qs-stage-half: min(calc(50vw - 30px), calc((50vh - 8px) *
 * 9 / 16))` pair. That formula is BUILT OUT OF `vh`/`dvh`, and this file's
 * ratified rule (see the height-model note in the header) is that neither unit
 * appears here at all. The naive port -- swapping `vh` for `%` -- is a SILENT
 * wrong answer, not a build error: a percentage inside a custom property
 * resolves against whichever property it is finally substituted into, so one
 * token holding both a width term and a height term would resolve the
 * height-derived half against the wrong axis.
 *
 * The flex row needs no viewport unit and no arithmetic at all. It is also
 * Aman's own model (spec §2.1: `display: flex; align-items: flex-end;
 * gap: 16px`), so this is the design's geometry rather than a substitute for
 * it. The rail column becoming a flex SIBLING is what makes rule 1.3's
 * "outside the frame, 16px to its right" free of any `right:` arithmetic, and
 * it is why the frame and the rail centre TOGETHER (rule 1.1) rather than the
 * frame centring alone with the rail hung off it.
 *
 * AND THE TRANSFORM IS GONE, WHICH IS THE POINT OF DOING THIS FIRST.
 * `.cff-swipeview-stage` used to be `position: absolute; top: 50%; left: 50%;
 * transform: translate(-50%, -50%)`. A transform mints a stacking context,
 * which was harmless only while the stage held nothing but
 * `pointer-events: none` media -- and rules 1.5 / IG-4 / IG-7 move FOUR
 * interactive elements inside the frame (mute, CTA, Follow, more/less). The
 * moment one of those lands inside a stacking context, its z-index stops being
 * compared against the gesture layer and it goes pointer-dead: this file's
 * founding §6.3 defect, the one the header note records the hit test catching
 * once already. Instagram's `.sbi-qs-media` carries the same reasoning for the
 * same removal. So the transform goes here, in the geometry step, BEFORE any
 * chrome moves inside -- and the whole hit-test tier re-runs immediately,
 * because removing it changes what `elementFromPoint` returns for every point
 * inside the stage.
 */
.cff-swipeview-stagerow {
	position: absolute;
	top: 0;
	right: 0;
	bottom: 0;
	left: 0;
	inset: 0;
	display: flex;
	justify-content: center;
	align-items: flex-end;
	/* Touch base: full-bleed, no margins, no gap (rule 1.1). Desktop overrides
	   both inside the ONE capability query. */
	gap: 0;
	padding: 0;
	/* §6.3: the row is inert; genuine controls opt back in one by one. */
	pointer-events: none;
}

/* ── The painted frame (per-slide) ───────────────────────────────────────── *
 * Holds the media and, from step (e), the bottom row. `background: #000` lives
 * HERE and not on the session twin below: the twin is an overlay above the
 * video, and a background on it would paint the video out entirely.
 *
 * `isolation: isolate` is safe on this element specifically. It mints a
 * stacking context INSIDE the track, which is already inside one (the track's
 * own live transform), so it changes no comparison that decides pointer
 * ownership -- the only comparison that does is viewport-z vs gesture-z, one
 * level up. It is NOT put on the session twin, whose children are the
 * interactive session controls that must stay comparable against the gesture
 * layer.
 *
 * THE GEOMETRY BELOW IS STATED TWICE -- here and on .cff-swipeview-framechrome
 * -- and that duplication is the deliberate cost of splitting session chrome
 * out of the per-slide frame. Instagram records the same trade at its own
 * `.sbi-qs-framechrome`. The two rules are written SOLO rather than as one
 * grouped selector list on purpose: a shared list would make
 * "the two frame boxes resolve identically" a tautology about one block of
 * text, where two solo rules make it a real assertion that can fail.
 * tests/js/swipeview-geometry.test.js re-derives both and fails if they
 * disagree.
 */
.cff-swipeview-stage {
	position: relative;
	width: 100%;
	height: 100%;
	overflow: hidden;
	background: #000;
	isolation: isolate;
	border-radius: 0;
}

/* The session-level twin of the frame's geometry. Transparent by design -- see
   the note above. Raised to level 3 so the interactive controls it will hold
   from step (c) clear both the gesture layer (1) and the viewport (2); the
   container itself stays pointer-inert. `overflow: hidden` is what lets a
   full-frame-width progress bar sit flush on the rounded bottom edge without
   its square ends poking past the corner curve. */
.cff-swipeview-framechrome {
	position: relative;
	width: 100%;
	height: 100%;
	overflow: hidden;
	border-radius: 0;
}

.cff-swipeview-chromelayer {
	position: absolute;
	top: 0;
	right: 0;
	bottom: 0;
	left: 0;
	inset: 0;
	z-index: 3;
	pointer-events: none;
}

/* The rail column (rule 1.3) is a flex sibling of the frame, so the pair
   centres together. Its own rules live with the rail's, further down -- ONE
   rule per selector, because `cssmodel.block()` returns the FIRST match and a
   second rule for the same selector is silently invisible to every assertion
   about the first. A placeholder rule lived here through step (a); it is gone
   now that the real one exists. */

/* Progressive-enhancement floor. `aspect-ratio` is Baseline since 2021 and is
   a LOWER bar than several things this viewer already hard-requires
   (`clip-path` for rule 1.1's corner containment, `color-mix`, `env()`), so it
   is not guarded per-declaration. But without it a desktop frame sized
   `width: auto` would shrink-wrap to zero -- its media is absolutely
   positioned -- so the floor restores the touch layout's full-bleed frame
   rather than leaving a zero-width one. Stated at top level, not nested inside
   the capability query, so both geometry paths stay readable by the suite. */
@supports not (aspect-ratio: 9 / 16) {
	/* Solo rules, not a grouped list -- cssmodel.js's discipline 4 cannot read
	   the last member of a group, so a grouped floor would be untestable. */
	.cff-swipeview-stage {
		width: 100%;
		height: 100%;
	}

	.cff-swipeview-framechrome {
		width: 100%;
		height: 100%;
	}
}

/* ── The letterbox backdrop (rule 1.14 / AC 12) ──────────────────────────── *
 * A blurred, over-scaled copy of the POSTER behind media that is wider than
 * 9:16, so the bands either side read as an extension of the video rather than
 * as two dead black bars.
 *
 * `inset: -10%` plus `scale(1.1)` is the design's own over-scale, and both are
 * needed: the blur samples beyond the element's box, so without the bleed the
 * edges would fade to transparent and show a hard seam against the frame.
 *
 * ── THE PAINT-ORDER AUDIT (four sites, and one of them was the bug) ─────────
 * Instagram shipped this invisible and fixed it in `49d18a6b`: "the media's own
 * black background hid the letterbox backdrop". Every static probe passed while
 * nothing was on screen, which is why a screenshot is the acceptance step here
 * and not a DOM assertion.
 *
 * This stylesheet has five `background: #000` declarations. Audited, one by one:
 *
 *   .cff-swipeview-root        SAFE -- the window behind the frame, and the
 *                              desktop backdrop overrides it anyway.
 *   .cff-swipeview-stage       SAFE and REQUIRED -- it is the frame's own
 *                              black, and the backdrop is its CHILD, so the
 *                              stage paints behind it. It is also what shows
 *                              when there is no poster to blur.
 *   .cff-swipeview-embedcard   REMOVED. A full-frame opaque child ABOVE the
 *                              backdrop. Tier 2 needs no letterbox (it is an
 *                              iframe), so this never showed the bug -- but it
 *                              is the same shape, and the stage already
 *                              provides the black it was duplicating.
 *   .cff-swipeview-terminal    REMOVED, and this is the one that WOULD have
 *                              hidden it. The terminal tier paints a real
 *                              poster with `object-fit: contain`, so it has a
 *                              genuine letterbox case -- and its opaque
 *                              full-frame background sat directly over the
 *                              backdrop. Exactly Instagram's defect, one tier
 *                              across.
 *   .cff-swipeview-embedcard-mask  SAFE -- 56px tall at the frame's top, and
 *                              its whole job is to be opaque there.
 *
 * The media elements themselves carry NO background, which is the other half of
 * the same audit: a background on the <video> or the poster would hide the
 * backdrop from directly in front of it.
 */
.cff-swipeview-backdrop {
	position: absolute;
	top: -10%;
	right: -10%;
	bottom: -10%;
	left: -10%;
	/* Set by applyFit() from the poster URL, through safeCssUrl(). */
	background-position: center;
	background-size: cover;
	background-repeat: no-repeat;
	filter: blur(28px) brightness(0.55) saturate(1.2);
	transform: scale(1.1);
	/* Hidden until applyFit() measures a genuinely wider-than-9:16 source, so
	   it only earns its compositor pass when it is actually visible. */
	display: none;
	pointer-events: none;
}

.cff-swipeview-video,
.cff-swipeview-embedcard-poster,
.cff-swipeview-terminal-poster {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	/* `cover` is the BASE, which is the design's call for the common case: a
	   source TALLER than 9:16 is cropped rather than letterboxed, because a
	   Reel is a 9:16 format and a taller source is nearly always a 9:16 video
	   with padding. `contain` is switched on per-slide by applyFit() only for
	   sources genuinely WIDER than the frame. */
	object-fit: cover;
	/* NO background here, deliberately -- see the paint-order audit above. A
	   background on the media element hides the backdrop from in front of it,
	   which is precisely the defect Instagram shipped. */
	/* A <video> is pointer-transparent by default with no controls attribute,
	   which is what lets the gesture layer own the stage centre. Stated rather
	   than relied on. */
	pointer-events: none;
}

/* Rule 1.14's boundary, applied by applyFit() when the source is wider than
   9:16 x 1.04. The 4% tolerance is what stops a nominally-9:16 source whose
   encoded dimensions round a pixel out from flickering into letterbox mode. */
.cff-swipeview-stage[data-fit="contain"] .cff-swipeview-video,
.cff-swipeview-stage[data-fit="contain"] .cff-swipeview-terminal-poster {
	object-fit: contain;
}

/* ─────────────────────────── Gesture layer ─────────────────────────── */

.cff-swipeview-gesture {
	position: absolute;
	top: 0;
	right: 0;
	bottom: 0;
	left: 0;
	inset: 0;
	z-index: 1;
	/* The one element that WANTS pointer events across the whole viewport. */
	pointer-events: auto;
	/* No tabindex, and none may be added -- see the JS note. A div is not
	   focusable, so tabindex="-1" would ADD click focusability to an
	   aria-hidden element, which Chrome then refuses to hide from AT. */
	touch-action: none;
}

/* ─────────────────────────── Loading affordance ─────────────────────── */

.cff-swipeview-loader {
	position: absolute;
	top: 50%;
	left: 50%;
	width: 36px;
	height: 36px;
	margin: -18px 0 0 -18px;
	pointer-events: none;
	opacity: 0;
	transition: opacity 120ms linear;
}

.cff-swipeview-loader-visible {
	opacity: 1;
}

.cff-swipeview-spinner {
	display: block;
	width: 100%;
	height: 100%;
	border: 2px solid rgba(255, 255, 255, 0.28);
	border-top-color: #fff;
	border-radius: 50%;
	animation: cffSwipeViewSpin 800ms linear infinite;
}

@keyframes cffSwipeViewSpin {
	to { transform: rotate(360deg); }
}

/* The spinner's reduced-motion override lives in the single block at the end
   of this file, with every other one. */

/* ─────────────────────────── Tier 2: the embed card ─────────────────── */

.cff-swipeview-embedcard {
	position: absolute;
	top: 0;
	right: 0;
	bottom: 0;
	left: 0;
	inset: 0;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 14px;
	/* NO background -- see the paint-order audit on .cff-swipeview-backdrop.
	   Tier 2 needs no letterbox, so this never showed the bug, but it is the
	   same shape and the frame already provides the black. */
}

.cff-swipeview-embedcard-play {
	position: relative;
	z-index: 1;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 68px;
	height: 68px;
	padding: 0;
	color: #fff;
	cursor: pointer;
	background: rgba(0, 0, 0, 0.45);
	border: 1px solid rgba(255, 255, 255, 0.55);
	border-radius: 50%;
	/* A genuine control, so it opts back INTO pointer events (S6.3). Its centre
	   is hit-tested by the geometry suite. */
	pointer-events: auto;
}

.cff-swipeview-embedcard-label {
	position: relative;
	z-index: 1;
	max-width: 78%;
	font-size: 13px;
	line-height: 1.35;
	text-align: center;
	/* Non-interactive chrome opts OUT -- the S6.3 half that is easy to forget,
	   and the one that swallowed tap-to-toggle on a sibling. */
	pointer-events: none;
	opacity: 0.86;
}

.cff-swipeview-embedcard-playing .cff-swipeview-embedcard-play,
.cff-swipeview-embedcard-playing .cff-swipeview-embedcard-label,
.cff-swipeview-embedcard-playing .cff-swipeview-embedcard-poster {
	display: none;
}

.cff-swipeview-embedcard-frame {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	border: 0;
	/* Facebook's frame owns its own controls and we have none, so it MUST
	   receive pointer events -- otherwise tier 2 has no way to play at all.
	   The accepted cost, recorded in the JS: a vertical swipe STARTED on the
	   frame body does not navigate the track. */
	pointer-events: auto;
}

/*
 * THE TITLE-BAR MASK -- fact 4 in the JS note, and the reason it is a strip
 * rather than a full cover.
 *
 * The embed document paints a Facebook title/attribution bar across its top,
 * and that bar is a LINK: a tap there leaves the site for facebook.com in the
 * middle of a swipe session. `show_text=false` suppresses the caption block
 * BELOW the video but not that bar, and no parameter does.
 *
 * Since the frame is cross-origin we cannot style or query it, so the only
 * available fix is geometric: an opaque, pointer-eating strip of our own over
 * that band. 56px is the measured height of the bar plus its padding at the
 * stage widths this viewer produces; it is a MEASURED CONSTANT, not a guess,
 * and the geometry suite asserts the reservation COVERS the widest capture
 * rather than merely existing.
 *
 * It opts INTO pointer events on purpose -- swallowing the tap is its whole
 * job -- and it is aria-hidden with no tabindex, because it is a guard rather
 * than a control.
 */
/* ── Tier 2's title-bar mask, under the new top row (AC 24) ──────────────── *
 * Facebook's own plugins/video.php frame paints its title bar over our stage,
 * no parameter suppresses it, and A TAP ON IT NAVIGATES THE VISITOR AWAY
 * mid-swipe. The mask covers it and eats the tap; a mask with
 * `pointer-events: none` paints identically and fails invisibly.
 *
 * THE COLLISION THIS STEP HAD TO RESOLVE. The frame now also holds a 48px top
 * row with the mute chip and the CTA pill in it, and both sit in the mask's
 * band. The ordering cannot be reasoned about -- the mask's whole job is to
 * swallow taps -- so it is PROVEN by a 27-point sweep across the band in the
 * behavioural tier, asserting that no point reaches the iframe and every point
 * resolves to something safe.
 *
 * The outcome of that sweep is why the mask keeps `z-index: 2` INSIDE the
 * track rather than being raised: the top row lives in the session chrome
 * layer at level 3, so the row's controls sit ABOVE the mask by construction.
 * That is the right way round. The mask still covers the whole band, so a tap
 * anywhere the row does not occupy is eaten by it, and a tap on the mute chip
 * or the CTA is handled by a real control -- mute being DISABLED on this tier
 * (AC 24), so it fires nothing.
 */
.cff-swipeview-embedcard-mask {
	position: absolute;
	top: 0;
	left: 0;
	z-index: 2;
	width: 100%;
	height: 56px;
	background: #000;
	pointer-events: auto;
}

/* ─────────────────────────── Tier 3: terminal ─────────────────────────── */

.cff-swipeview-terminal {
	position: absolute;
	top: 0;
	right: 0;
	bottom: 0;
	left: 0;
	inset: 0;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 14px;
	padding: 24px;
	box-sizing: border-box;
	/* NO background -- see the paint-order audit on .cff-swipeview-backdrop.
	   This tier paints a real poster with `object-fit: contain`, so it has a
	   genuine letterbox case, and an opaque full-frame background here sat
	   directly over the backdrop. The frame's own `background: #000` provides
	   the black this was duplicating. */
}

/*
 * The deliberate black slide: a Reel with neither mp4 nor poster. Poster data
 * is thin (present on only 2 of 7 videos-edge fixture items, absent entirely
 * from the timeline DASH reel), and fbcdn posters expire -- so this state is
 * ordinary rather than exotic. It gets a class of its own so the geometry suite
 * can assert the state exists and is laid out, rather than its absence being
 * indistinguishable from a rendering bug.
 */
.cff-swipeview-terminal-noposter {
	background: #0a0a0a;
}

/* The scrim over the poster (rule 1.19). It is what makes 13px text legible on
   an arbitrary video frame, and it is a SEPARATE element rather than a
   background on the terminal itself -- the terminal's own background had to go,
   because it sat over the letterbox backdrop (see the paint-order audit). */
.cff-swipeview-terminal-scrim {
	position: absolute;
	top: 0;
	right: 0;
	bottom: 0;
	left: 0;
	background: rgba(0, 0, 0, 0.4);
	pointer-events: none;
}

.cff-swipeview-terminal-msg {
	position: relative;
	max-width: 30ch;
	margin: 0;
	/* Rule 1.19's measured treatment: 13px at 70% white, not 14px at 86%
	   opacity. The difference matters more than it looks -- `opacity` fades the
	   text AND anything it inherits, where a colour with alpha leaves the type
	   at full weight and only lightens the ink. */
	color: rgba(255, 255, 255, 0.7);
	font-size: 13px;
	line-height: 1.45;
	text-align: center;
	pointer-events: none;
}

/* ── The terminal tier's own route-out pill (rule 1.19 / AC 24) ──────────── *
 * Styled EXACTLY like the top-row CTA and carrying its OWN CLASS, which is the
 * point rather than a detail.
 *
 * Instagram shipped these two sharing a class and had to split them
 * (`897e6ea8`): a document-order query for "the CTA" returns whichever comes
 * first in the DOM, and on a terminal-tier slide that is the PER-SLIDE pill
 * inside the track rather than the session-level control in the frame chrome.
 * Every consumer that refreshed "the CTA's href" was then writing to the wrong
 * element on exactly the tier where the route out is the only thing left.
 *
 * So the paint is duplicated deliberately and the selectors stay distinct. A
 * geometry assertion pins that they do.
 */
.cff-swipeview-terminal-link {
	position: relative;
	box-sizing: border-box;
	display: inline-flex;
	align-items: center;
	gap: 7px;
	height: 34px;
	padding: 0 13px 0 11px;
	color: #fff;
	font-size: 13px;
	font-weight: 600;
	line-height: 1;
	white-space: nowrap;
	text-decoration: none;
	background: rgba(0, 0, 0, 0.35);
	border: 0;
	border-radius: 999px;
	-webkit-backdrop-filter: blur(6px);
	backdrop-filter: blur(6px);
	filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));
	/* This tier's ONLY action (S7.1), so it opts back in and its centre is
	   hit-tested. */
	pointer-events: auto;
	transition: background 150ms ease;
}

.cff-swipeview-terminal-link:hover,
.cff-swipeview-terminal-link:focus-visible {
	color: #fff;
	background: rgba(0, 0, 0, 0.55);
	text-decoration: none;
}

.cff-swipeview-terminal-link svg {
	width: 16px;
	height: 16px;
	flex: 0 0 auto;
}

/* Vertical only -- same reasoning as the CTA pill's ring. */
.cff-swipeview-terminal-link::before {
	content: '';
	position: absolute;
	top: -5px;
	right: 0;
	bottom: -5px;
	left: 0;
	border-radius: 999px;
}

.cff-swipeview-terminal-link[aria-disabled="true"] {
	cursor: default;
	opacity: 0.5;
}

/* ═══════════════════════════════════════════════════════════════════════════
 * PER-SLIDE CHROME: THE BOTTOM ROW (rules IG-4 / IG-5 / AC 9 / AC 17 / AC 18)
 * ═══════════════════════════════════════════════════════════════════════════
 *
 * MOVED INSIDE THE FRAME. It was a window-anchored band hugging the stage from
 * outside it, which on a letterboxed desktop put the byline and caption on the
 * backdrop pillars rather than on the video they describe (AC 2).
 *
 * The scrim comes with it: 42% of the frame's height, under the row, and there
 * is deliberately NO TOP scrim (IG-1). The top row carries its own text-shadow
 * instead, because a second gradient up there fights the clean top edge that
 * removing the wordmark created.
 */
.cff-swipeview-scrim {
	position: absolute;
	right: 0;
	bottom: 0;
	left: 0;
	height: 42%;
	z-index: 1;
	background: linear-gradient(to top, rgba(0, 0, 0, 0.72), rgba(0, 0, 0, 0.28) 55%, rgba(0, 0, 0, 0) 100%);
	pointer-events: none;
}

/*
 * The row itself.
 *
 * PINS `bottom` ONLY, NEVER `top` AS WELL. An absolutely-positioned box with
 * both pinned resolves to its full containing block on Safari 15.4-16.3 where
 * a `fit-content` height is unsupported -- which here means a full-frame band
 * sitting over the gesture layer eating every vertical swipe. Bottom-anchored
 * chrome pins bottom.
 *
 * `padding-right` is a SEPARATE LONGHAND and must stay one: it clears the rail
 * on touch (where the rail is inside the frame) and drops back to the plain
 * inset on desktop (where the rail is outside). Folding it into the shorthand
 * also breaks the geometry suite, which parses that shorthand's values.
 *
 * NO `> *` POINTER-EVENTS OPT-IN, and this is a blocking finding from
 * Instagram's own review rather than a stylistic choice. A blanket opt-in was
 * once justified on the theory that these children have nothing clickable
 * inside them -- which misses that pointer-events needs no click handler to do
 * damage. This is a COLUMN flex container with the default
 * `align-items: stretch`, so a blanket rule turns the author row and the
 * caption into opaque, FULL-WIDTH bands in the mobile thumb zone on every
 * slide, silently swallowing both tap-to-pause (bound to the gesture layer,
 * never reached) and touch-swipe navigation. §6.3 names this exact case, and a
 * caption element was measured doing it at ~390x660 on a sibling. So: per
 * control opt-in, and the author row and Follow pill additionally
 * `align-self: flex-start` so they shrink-wrap rather than stretch.
 */
.cff-swipeview-bottom {
	position: absolute;
	right: 0;
	bottom: 0;
	left: 0;
	z-index: 2;
	box-sizing: border-box;
	display: flex;
	flex-direction: column;
	gap: 10px;
	padding: 0 16px 16px 16px;
	padding-bottom: max(16px, var(--cff-sv-safe-bottom));
	padding-right: 76px;
	font-size: 14px;
	line-height: 18.2px;
	pointer-events: none;
}

/* ── Author row (rule IG-4 / AC 9) ───────────────────────────────────────── */
.cff-swipeview-authorrow {
	display: flex;
	align-items: center;
	gap: 10px;
	/* Shrink-wrapped, not stretched -- see the note on the row above. */
	align-self: flex-start;
	min-width: 0;
	max-width: 100%;
}

.cff-swipeview-author {
	display: flex;
	align-items: center;
	gap: 10px;
	min-width: 0;
	color: #fff;
	text-decoration: none;
	pointer-events: auto;
}

.cff-swipeview-author:hover .cff-swipeview-handle-text,
.cff-swipeview-author:focus-visible .cff-swipeview-handle-text {
	text-decoration: underline;
	text-underline-offset: 3px;
}

.cff-swipeview-avatar {
	box-sizing: border-box;
	width: 32px;
	height: 32px;
	flex: 0 0 auto;
	border-radius: 50%;
	object-fit: cover;

	/* The same iOS square-corner mitigation as `.cff-swipeview-close` -- see the
	   long note there. This is the only other 50% circle in the viewer with no
	   compositing hint of its own, and unlike the close it holds an IMAGE, where
	   an unclipped raster is just as visible. No `overflow: hidden` here either:
	   it is an <img>, so the radius already clips the content box, and this
	   element has no hit ring to lose. */
	isolation: isolate;
	transform: translateZ(0);
	background: #262626;
}

/* The last-resort avatar (AC 10): the design's initial-letter gradient disc.
   The hue is DERIVED FROM THE PAGE NAME so the same Page always gets the same
   disc rather than a colour that changes between slides -- a per-slide random
   hue reads as a different account. */
.cff-swipeview-avatar-fallback {
	display: flex;
	align-items: center;
	justify-content: center;
	color: #fff;
	font-size: 13px;
	font-weight: 600;
	line-height: 1;
	text-transform: uppercase;
	background-image: linear-gradient(135deg, hsl(var(--cff-sv-avatar-hue, 212) 70% 58%), hsl(calc(var(--cff-sv-avatar-hue, 212) + 50) 65% 38%));
}

.cff-swipeview-handle {
	display: flex;
	align-items: center;
	gap: 4px;
	min-width: 0;
	font-size: 14px;
	font-weight: 600;
	line-height: 18.2px;
}

.cff-swipeview-handle-text {
	overflow: hidden;
	white-space: nowrap;
	text-overflow: ellipsis;
}

/* AC 13 -- the verified badge is NOT SOURCEABLE on Facebook and is omitted.
   The rule is kept and wired dark so the branch exists rather than having to
   be invented later: `is_verified` / `verification_status` appear nowhere in
   the plugin, the header call requests exactly
   `id,picture,cover,name,link` plus `fan_count,about`, and no post-level edge
   carries one. Sourcing it means adding a PAGE-LEVEL request with an
   unverified permission scope -- a decision, not an implementation detail.
   Never faked. */
.cff-swipeview-verified {
	width: 14px;
	height: 14px;
	flex: 0 0 auto;
}

/* Outline pill, NOT a filled brand-blue button -- the design's own treatment,
   and the same shape TikTok fills solid. Aman's stated rationale is CTA
   density. */
.cff-swipeview-follow {
	font: inherit;
	-webkit-appearance: none;
	appearance: none;
	position: relative;
	box-sizing: border-box;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	flex: 0 0 auto;
	height: 28px;
	margin-left: 2px;
	padding: 0 12px;
	color: #fff;
	font-size: 13px;
	font-weight: 600;
	line-height: 1;
	white-space: nowrap;
	text-decoration: none;
	background: transparent;
	border: 1px solid rgba(255, 255, 255, 0.75);
	border-radius: 8px;
	pointer-events: auto;
	transition: background 150ms ease;
}

/* 28px tall by design; the band takes the REACHABLE box to 44px without moving
   the outline. VERTICAL ONLY -- the pill sits 2px from the author link, so a
   horizontal expansion would overlap it. */
.cff-swipeview-follow::before {
	content: '';
	position: absolute;
	top: -8px;
	right: 0;
	bottom: -8px;
	left: 0;
	border-radius: 8px;
}

.cff-swipeview-follow:hover,
.cff-swipeview-follow:focus-visible {
	color: #fff;
	background: rgba(255, 255, 255, 0.12);
	text-decoration: none;
}

/* ── Caption (rules IG-4 / IG-5 / AC 17 / AC 18) ─────────────────────────── *
 * ONE LINE, `nowrap` + ellipsis, with a "more" control revealed only on
 * MEASURED overflow. Down from three lines -- the design's call, and it
 * NARROWS what the prototype shipped, so it is gated on spec amendment A5.
 * Until A5 lands the viewer is non-conformant to §6.1's clamp depth, and that
 * is recorded rather than reinterpreted.
 *
 * §6.1's expander contract, all four parts, each encoding a defect that has
 * shipped:
 *
 *   - the caption TEXT is never tappable; only the shrink-wrapped more/less
 *     button is interactive. The reference puts `role="button" tabindex="0"`
 *     on the caption BLOCK, so its centre hit-test lands on a text node. We
 *     keep his visual treatment exactly and move the role onto the button
 *     that already looks like the control.
 *   - revealed only on MEASURED overflow (scrollWidth vs clientWidth) -- never
 *     on a character count, which is wrong at every width but one.
 *   - expansion NEVER survives a slide change.
 *   - the expanded block is CAPPED and owns its own scroll surface; wheel and
 *     touch inside it must not navigate the track. Uncapped, a 2,500-character
 *     caption measures 782px in the reference's desktop build and escapes the
 *     frame's top edge by 463px, silently clipped -- text lost with no
 *     indication that there is more.
 *
 * THE CAP IS A LINE COUNT, NOT A VIEWPORT FRACTION, and that is a deliberate
 * deviation from IG-5's "≈26% of frame height". 12 lines at the design's own
 * 18.2px line-height is 218.4px: 24.7% of the 884px desktop frame and 25.9% of
 * the 844px touch frame -- inside a percentage point of his figure at BOTH
 * reference viewports, with no viewport unit at all.
 *
 * A percentage genuinely would not work here, which is why this is not a
 * shortcut: this element's containing block is bottom-anchored with an AUTO
 * height, so a percentage `max-height` resolves to `none`. Giving it a definite
 * height would mean pinning both `top` and `bottom` -- the Safari full-band
 * hazard named on the row above.
 */
.cff-swipeview-caption {
	margin: 0;
	display: flex;
	align-items: baseline;
	gap: 6px;
	font-size: 14px;
	line-height: 18.2px;
	/* Deliberately NOT opted back into pointer events -- the text must never be
	   tappable. Only the more/less button opts in. */
	pointer-events: none;
}

.cff-swipeview-caption-text {
	min-width: 0;
	overflow: hidden;
	white-space: nowrap;
	text-overflow: ellipsis;
	/* Third-party caption text: keep a long unbroken string inside the frame. */
	overflow-wrap: break-word;
	word-wrap: break-word;
}

.cff-swipeview-caption-more {
	font: inherit;
	-webkit-appearance: none;
	appearance: none;
	/* §6.3 explicit opt-in: this button IS the control, and the only one. */
	pointer-events: auto;
	position: relative;
	flex: 0 0 auto;
	display: none;
	margin: 0;
	padding: 0;
	color: #a8a8a8;
	font-size: 14px;
	font-weight: 400;
	line-height: 18.2px;
	background: none;
	border: 0;
	cursor: pointer;
}

/* The 44px hit target WITHOUT the layout cost, and this is the smallest of the
   four controls that needed it. The design's visible control is 33.9 x 18.2 --
   below this file's 44px convention.

   PADDING WOULD FIX THE TARGET AND BREAK THE DESIGN: this row is measured at
   18.2px tall and the bottom row at 76.2px, so a 44px-tall button grows the
   whole block by 26px and pushes the author row up off its mark. The old
   `inline-block` + `padding: 4px 2px` toggle did exactly that at a smaller
   scale.

   A positioned pseudo-element takes no part in layout but IS hit-tested, and a
   hit on it resolves to its originating element. Insets are asymmetric on
   purpose: 13px above and below the 18.2px text gives 44.2px, and the
   horizontal 6px stops short of the caption text so the grown target cannot
   start swallowing taps meant for the gesture layer. */
.cff-swipeview-caption-more::before {
	content: '';
	position: absolute;
	top: -13px;
	right: -6px;
	bottom: -13px;
	left: -6px;
}

.cff-swipeview-caption-has-more .cff-swipeview-caption-more {
	display: inline-block;
}

.cff-swipeview-caption-more:hover {
	color: #fff;
}

.cff-swipeview-caption-expanded {
	align-items: flex-end;
}

.cff-swipeview-caption-expanded .cff-swipeview-caption-text {
	/* Caption text is set via textContent, never innerHTML (§12) -- a caption's
	   literal `<br>` markers are translated to `\n` in JS instead. `pre-line`
	   turns those preserved newlines into real breaks without ever parsing the
	   text as markup. */
	white-space: pre-line;
	overflow-y: auto;
	overflow-x: hidden;
	text-overflow: clip;
	max-height: var(--cff-sv-caption-cap);
	/* Owns its scroll surface. The wheel/touch handlers additionally refuse to
	   navigate while the event originated inside here -- CSS alone cannot stop
	   the gesture layer, because the gesture layer is not an ancestor. */
	pointer-events: auto;
	padding-right: 10px;
	scrollbar-width: thin;
	scrollbar-color: rgba(255, 255, 255, 0.35) transparent;
	-webkit-overflow-scrolling: touch;
	overscroll-behavior: contain;
}

.cff-swipeview-caption-expanded .cff-swipeview-caption-text::-webkit-scrollbar {
	width: 4px;
}

.cff-swipeview-caption-expanded .cff-swipeview-caption-text::-webkit-scrollbar-track {
	background: transparent;
}

.cff-swipeview-caption-expanded .cff-swipeview-caption-text::-webkit-scrollbar-thumb {
	background: rgba(255, 255, 255, 0.35);
	border-radius: 2px;
}

/* ── First-run swipe coach (rule 1.9 / AC 12) ────────────────────────────── *
 * The ONLY onboarding overlay that ships, and it replaces the four-row intro
 * card outright -- which Aman retracts himself in the walkthrough. The final
 * design keeps exactly TWO hints: this one and the unmute label. No pause hint,
 * no desktop swipe hint, no keyboard hint card, no "reset hint" button (a dev
 * aid). Facebook never had the card, so this is a pure addition rather than a
 * removal.
 *
 * TOUCH ONLY and FIRST RUN ONLY: desktop has visible chevrons and a cursor, so
 * it needs no swipe instruction. Gated in JS on the same input-capability probe
 * the chevrons use, and persisted in localStorage under a plugin-namespaced key
 * so it shows once EVER rather than once per session.
 *
 * `pointer-events: none` is not optional. This is a full-frame layer above the
 * gesture layer, so without it the very swipe it is teaching would be
 * swallowed -- and that is this file's founding §6.3 defect shape, arrived at
 * through the one element whose whole purpose is to be in the way.
 */
.cff-swipeview-coach {
	position: absolute;
	top: 0;
	right: 0;
	bottom: 0;
	left: 0;
	z-index: 9;
	display: none;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 14px;
	color: #fff;
	text-align: center;
	background: rgba(0, 0, 0, 0.35);
	pointer-events: none;
}

.cff-swipeview-coach-on {
	display: flex;
}

.cff-swipeview-coach-text {
	font-size: 15px;
	font-weight: 600;
	line-height: 19.5px;
	text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}

.cff-swipeview-coach-hand {
	width: 44px;
	height: 64px;
	animation: cffSwipeViewCoach 1.4s ease-in-out infinite;
}

@keyframes cffSwipeViewCoach {
	0%   { transform: translateY(10px); opacity: 0.35; }
	45%  { transform: translateY(-10px); opacity: 1; }
	100% { transform: translateY(10px); opacity: 0.35; }
}

/* ── Hashtags: COLOUR ONLY, NOT LINKS (AC 18) ────────────────────────────── *
 * A CONFLICT, resolved toward behaviour and recorded rather than resolved
 * silently. Rule IG-5 asks for two things that cannot both hold: "caption text
 * never tappable; only the shrink-wrapped more/less button is interactive" AND
 * "hashtags #e0f1ff, hover underline". A hover underline is an interactivity
 * claim, and a hashtag link is tappable caption text -- which is precisely what
 * §6.1's expander contract forbids, and that contract encodes a defect that has
 * shipped three times. Real hashtag links would additionally be a §6.4
 * escalation.
 *
 * Behaviour wins on the authority split, so hashtags take the design's COLOUR
 * and stay inert spans: no href, no hover underline, no pointer-events opt-in.
 * The visual result is the design's at rest and differs only on hover.
 *
 * Built by tokenising the ALREADY-ESCAPED caption text and appending real text
 * nodes and spans -- never by parsing markup (§12).
 */
.cff-swipeview-tag {
	color: #e0f1ff;
}

/* ═══════════════════════════════════════════════════════════════════════════
 * THE RAIL COLUMN (rule 1.3 / IG-2 / IG-3 / AC 8 / AC 15)
 * ═══════════════════════════════════════════════════════════════════════════
 *
 * REPLACES `.cff-swipeview-stats`, which was a single <a> wrapping the whole
 * likes+comments cluster in the window's bottom-right gutter. Three things
 * change and each is a rule rather than a preference:
 *
 *   PLACEMENT   the cluster was window-anchored in a gutter; the rail is
 *               frame-relative (AC 2) -- a 44px column OUTSIDE the frame on
 *               desktop, an in-frame overlay at `right: 10px` on touch.
 *   SEMANTICS   the cluster was ONE link for every metric. Rule IG-3 makes
 *               each item its own thing: likes a readout, comments a link,
 *               share a button. They are not interchangeable, and one <a>
 *               around all three claimed they were.
 *   COUNT       two metrics became three, because the plugin's own tile meta
 *               box prints reactions, comments AND shares. See the change
 *               log's parity audit.
 *
 * The column lives in `.cff-swipeview-stagerow` -- the PER-SLOT one for the
 * action stack (so it travels with the drag, per the ratified rule) and the
 * SESSION one for the chevrons (navigation is not per-post). Nothing can be
 * one grid across that boundary, which is why the reference's
 * `minmax(0,1fr) auto 1fr` rail grid is reproduced as two positioned clusters
 * rather than ported.
 *
 * BOTTOM-ALIGNED, and asserted rather than assumed (AC 8): the stack grows
 * upward from the frame's bottom edge, so when a metric is absent the
 * remaining items re-pitch DOWNWARD into the space instead of leaving a hole
 * where the missing one was.
 */

/* Touch base: an in-frame overlay at the frame's bottom-right. The stage row
   is the containing block and is inset to the frame on touch (full-bleed), so
   `right: 10px` -- the design's IG inset -- lands inside the frame with no
   second rule. It is taken OUT of the flex flow here so it claims no width;
   the desktop rule below puts it back in as a 44px flex item. */
.cff-swipeview-railcol {
	position: absolute;
	right: 10px;
	bottom: 0;
	flex: 0 0 auto;
	width: auto;
	height: auto;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: flex-end;
	/* §6.3: the column is inert; each rail item opts back in. */
	pointer-events: none;
}

.cff-swipeview-rail {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 20px;
	padding-bottom: 18px;
	padding-bottom: max(18px, var(--cff-sv-safe-bottom));
	/* SMASH-2064's drag-follow. Only the DURATION is written from JS
	   (setRailTravel), so the property list and the easing stay declarative and
	   in one place.

	   `transform` ALONE, never `all`: the duration is toggled between 0ms and
	   320ms on every drag frame, and `all` would drag every other animatable
	   property into that toggle.

	   `ease-out` because that is the TRACK's easing -- moveTrack() and
	   settleBack() both write `transform <n>ms ease-out` -- and NOT Instagram's
	   cubic-bezier(0.22, 0.61, 0.36, 1). The settle-back has to move at the
	   same speed as the video it is captioning, so this follows the local track
	   rather than the sibling viewer.

	   0ms at rest, so the declaration is inert until a drag writes a duration.
	   There is no `transform` here either: a rail at rest carries none at all,
	   which is what keeps the at-rest box identical to the pinned build's. */
	transition-property: transform;
	transition-timing-function: ease-out;
	transition-duration: 0ms;
	pointer-events: none;
}

/* ── Rail items (IG-2 / IG-3) ────────────────────────────────────────────── *
 * BARE GLYPHS -- no discs. This is the one place the IG skin departs from
 * "all circular controls share one paint": TikTok's rail glyphs sit in
 * translucent circles and Instagram's do not, because Instagram's own Reels
 * player renders them bare. The drop-shadow does the contrast work a disc
 * would otherwise do, over arbitrary video.
 *
 * SEMANTICS DIFFER PER ITEM AND ARE NOT INTERCHANGEABLE (IG-3 / AC 15):
 *   like     a READOUT, not a control -- `role="img"` with an exact-figure
 *            label, `cursor: default`, hover transform suppressed. There is no
 *            logged-in visitor inside an embedded feed, so a heart styled like
 *            Facebook's tappable one would promise something the UI cannot do.
 *   comment  a LINK to the post, where commenting genuinely works.
 *   share    a real <button> -- Web Share, then clipboard + toast, then a
 *            toast carrying the URL. The rail's only in-page action, and the
 *            reason the parity floor's third item is worth having beyond
 *            parity itself.
 *   view     touch only; the desktop CTA pill's counterpart.
 *
 * Each item is a shrink-wrapped column, NEVER a full-width flex child. A
 * stretched item becomes an opaque dead strip across the thumb zone that eats
 * tap-to-pause and swipe both.
 */
.cff-swipeview-act {
	/* The UA form-control refusal -- see .cff-swipeview-close for the full
	   reasoning. `font: inherit` FIRST so the count's own 12px still wins. */
	font: inherit;
	-webkit-appearance: none;
	appearance: none;
	position: relative;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 5px;
	margin: 0;
	padding: 0;
	color: #fff;
	background: none;
	border: 0;
	text-decoration: none;
	cursor: pointer;
	pointer-events: auto;
	filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.45));
	transition: transform 120ms ease;
}

.cff-swipeview-act svg {
	width: 28px;
	height: 28px;
	flex: 0 0 auto;
}

/* The 44px REACHABLE box for a bare glyph. An item measures ~29 wide by ~45
   tall (glyph plus count), so the miss is on the HORIZONTAL axis. The growth
   is sized to the rail COLUMN's 44px -- space the design already reserves for
   these controls -- so a grown item can never reach out over the video, and
   adjacent items cannot overlap because the 18px desktop / 20px touch gap far
   exceeds the 1px of vertical growth.

   The likes READOUT gets one too, even though it is not a control: it carries
   `role="img"` with a label, and an AT user targeting it should not have a
   smaller target than the items either side. */
.cff-swipeview-act::before {
	content: '';
	position: absolute;
	top: -1px;
	right: -8px;
	bottom: -1px;
	left: -8px;
}

.cff-swipeview-act-count,
.cff-swipeview-act-label {
	/* `block` + `min-height` so an EMPTY count STILL RESERVES ITS ROW. This is
	   the always-rendered count row (AC 8), and it is measured rather than
	   assumed: the reference's like, comment and share items are all 43px tall
	   despite share carrying no figure, so the 12px is reserved and not
	   collapsed. An empty inline element generates no line box at all, which
	   would pull the share glyph 17px up toward its neighbour and break the
	   rail's rhythm. IG-3's "a glyph without a count" is about the FIGURE, not
	   about the space. */
	display: block;
	min-height: 12px;
	font-size: 12px;
	font-weight: 600;
	line-height: 12px;
	/* Tabular figures, so a count changing between slides cannot shift the
	   glyph above it sideways. */
	font-variant-numeric: tabular-nums;
}

.cff-swipeview-act:hover {
	color: #fff;
	text-decoration: none;
	transform: scale(1.06);
}

.cff-swipeview-act:active {
	transform: scale(0.94);
}

/* The readout's feedback is SUPPRESSED rather than merely unstyled -- a
   transform on hover is itself an affordance claim, and this item has no
   action to offer. */
.cff-swipeview-act-like {
	cursor: default;
}

.cff-swipeview-act-like:hover,
.cff-swipeview-act-like:active {
	transform: none;
}

/* The touch-only "View" item -- the desktop CTA pill's counterpart (AC 6).
   Hidden on desktop by the capability query, which is what makes the migration
   VIEWPORT-driven rather than tier-driven. */
.cff-swipeview-act-view {
	display: flex;
}

/* ── The toast (IG-3's share fallback chain) ─────────────────────────────── *
 * Web Share, then clipboard + "Link copied", then "Copy this link: <url>".
 * A polite live region so the outcome is announced rather than only painted --
 * the whole point of the fallback chain is that the visitor learns what
 * happened, and a silent copy is indistinguishable from a dead button.
 */
.cff-swipeview-toast {
	position: absolute;
	left: 50%;
	bottom: 96px;
	max-width: 80%;
	padding: 9px 14px;
	color: #fff;
	font-size: 13px;
	font-weight: 600;
	line-height: 16px;
	text-align: center;
	background: rgba(0, 0, 0, 0.82);
	border-radius: 8px;
	transform: translateX(-50%);
	opacity: 0;
	pointer-events: none;
	transition: opacity 160ms linear;
}

.cff-swipeview-toast-on {
	opacity: 1;
}

/* ═══════════════════════════════════════════════════════════════════════════
 * THE SESSION FRAME CHROME'S TOP ROW (rule 1.5 / AC 6)
 * ═══════════════════════════════════════════════════════════════════════════
 *
 * `[blank left] … [mute][View on Facebook pill]`, right-aligned.
 *
 * The frame's top-LEFT is deliberately EMPTY. The design removed the platform
 * wordmark from there and never put a counter in its place (AC 5), and that
 * corner of the WINDOW is now the close disc's. On touch the frame is
 * full-bleed, so the window close overlaps this row -- hence the 62px left pad
 * on the base rule, which is there to keep the row's contents from ever
 * sliding under it. Desktop needs no such clearance: the close button is far
 * to the left of the frame at any desktop viewport.
 *
 * There is NO top scrim (rule IG-1). The row carries its own `text-shadow`
 * instead, because a second gradient up here would fight the clean top edge
 * that removing the wordmark created. The bottom row gets its contrast from
 * the bottom scrim.
 */
.cff-swipeview-top {
	position: absolute;
	top: 0;
	right: 0;
	left: 0;
	box-sizing: border-box;
	height: 48px;
	padding: 14px 10px 0 62px;
	padding-top: max(14px, var(--cff-sv-safe-top));
	display: flex;
	align-items: center;
	justify-content: flex-end;
	gap: 8px;
	text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
	/* §6.3: the row is inert; the two controls in it opt back in. */
	pointer-events: none;
}

/* ── Mute: the shared expanding chip (rule 1.6 / AC 7) ───────────────────── *
 * Replaces BOTH of this viewer's old sound affordances at once: the discless
 * 44px glyph in the bottom-right gutter AND the separate bottom-centre unmute
 * pill. One control, one accessible name, one focus stop, one disabled state.
 *
 * THE STATE PAINT IS INVERTED from every other control in this file, and that
 * is the design's point rather than an accident: MUTED is LOUD (a solid white
 * chip with a dark glyph) and UNMUTED is QUIET (dark translucent, white
 * glyph). It is also why the chip carries NO shadow -- it is deliberately
 * flatter than the scrim-shadowed text around it, and `box-shadow: none` is
 * stated rather than omitted so a future "add a shadow like the other
 * controls" change has to argue with it.
 *
 * The label slides OUT OF the button rather than appearing beside it, which is
 * what makes 34 -> 136 one expanding box instead of two elements. The
 * transitions are deliberately asymmetric -- 700ms for the geometry, 450ms for
 * the colour, 350ms+50ms for the label's opacity -- so the label fades in
 * behind a width that is still opening.
 *
 * WHEN the label shows is OURS, not the design's (rule 1.7 / AC 27). Aman's
 * build opens muted unconditionally; we keep §4.1's unmuted attempt, so the
 * label becomes the REFUSAL arm. The JS owns that gate entirely; this file
 * only paints the two states.
 *
 * The paint is driven by a `data-state` ATTRIBUTE rather than a class so it
 * can never disagree with what updateMuteChrome() computed as the REAL state
 * -- rule 1.7(a): the button always reflects reality, including "muted for a
 * reason the visitor did not choose".
 */
.cff-swipeview-mute {
	font: inherit;
	-webkit-appearance: none;
	appearance: none;
	position: relative;
	box-sizing: border-box;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	flex: 0 0 auto;
	min-width: 34px;
	height: 34px;
	padding: 0 7px;
	color: #111;
	background: #fff;
	border: 0;
	border-radius: 17px;
	-webkit-backdrop-filter: blur(6px);
	backdrop-filter: blur(6px);
	box-shadow: none;
	text-shadow: none;
	line-height: 1;
	white-space: nowrap;
	/* This rule deliberately does NOT clip its overflow (MG-06, found by QA on
	 * Instagram and confirmed to reproduce identically here). The ::before
	 * below is the 44px hit ring, positioned absolutely at minus 5px on all
	 * four sides — so it lies OUTSIDE this 34px chip's padding box, and
	 * clipping the box clipped the ring away with it. The pseudo-element still
	 * computed as 44x44 and still accepted pointer events, which is why this
	 * read as working: nothing outside the chip was actually hit-testable, so
	 * an edge tap fell through to the gesture layer and PAUSED the Reel instead
	 * of toggling sound. Measured at 430x739 with real touch streams, 20px
	 * right of the chip centre.
	 *
	 * Nothing here needs the clip: the sliding label already clips itself, and
	 * the 20px glyph cannot overflow a 34px chip. The close disc never had this
	 * bug because it does not clip, despite using the same ring pattern at
	 * minus 2px.
	 *
	 * Kept free of literal declaration syntax on purpose — swipeview-geometry
	 * reads properties off the RAW stylesheet, so a `prop: value` pair written
	 * inside a comment is picked up as if it were a declaration. */
	cursor: pointer;
	pointer-events: auto;
	transition: padding 700ms cubic-bezier(0.22, 1, 0.36, 1),
		background 450ms ease,
		color 450ms ease;
}

.cff-swipeview-mute svg {
	width: 20px;
	height: 20px;
	flex: 0 0 auto;
	filter: none;
}

/* The 44px REACHABLE box for a 34px chip -- 5px on each side.
 *
 * The insets are asymmetric in effect rather than in value: the chip GROWS to
 * 136px while hinting, so the horizontal 5px only ever matters at rest, which
 * is where it is needed. The right-hand 5px reaches into the 8px gap between
 * this and the CTA pill and stops 3px short of it -- and the pill's own ring
 * is VERTICAL ONLY for exactly that reason. Two controls whose grown hit boxes
 * overlap hand the shared strip to whichever paints last, which is the silent,
 * position-dependent misroute §6.3's history is made of. */
.cff-swipeview-mute::before {
	content: '';
	position: absolute;
	top: -5px;
	right: -5px;
	bottom: -5px;
	left: -5px;
	border-radius: 22px;
}

.cff-swipeview-mute-label {
	max-width: 0;
	margin-left: 0;
	opacity: 0;
	overflow: hidden;
	font-size: 13px;
	font-weight: 600;
	line-height: 13px;
	letter-spacing: -0.005em;
	white-space: nowrap;
	transform: translateX(-6px);
	transition: max-width 700ms cubic-bezier(0.22, 1, 0.36, 1),
		margin 700ms cubic-bezier(0.22, 1, 0.36, 1),
		opacity 350ms ease 50ms,
		transform 700ms cubic-bezier(0.22, 1, 0.36, 1);
}

.cff-swipeview-mute-hinting {
	padding: 0 12px 0 9px;
}

.cff-swipeview-mute-hinting .cff-swipeview-mute-label {
	max-width: 140px;
	margin-left: 6px;
	opacity: 1;
	transform: none;
}

/* Unmuted -- the quiet state. */
.cff-swipeview-mute[data-state="on"] {
	color: #fff;
	background: rgba(0, 0, 0, 0.55);
}

.cff-swipeview-mute[data-state="on"]:hover,
.cff-swipeview-mute[data-state="on"]:focus-visible {
	background: rgba(0, 0, 0, 0.75);
}

/* Once unmuted there is nothing to hint, so the label cannot reappear. */
.cff-swipeview-mute[data-state="on"] .cff-swipeview-mute-label {
	display: none;
}

.cff-swipeview-mute[disabled] {
	/* DISABLED, never omitted (rule 1.20 / §6.2 / AC 24). Tier 2 has no
	   control surface at all, and the design's answer -- omit the button when
	   there is no audio -- leaves `m` flipping a state with no control and no
	   audible effect.

	   Declared AFTER the [data-state="on"] rules so it wins on source order at
	   equal specificity: a disabled chip must read disabled whichever sound
	   state it is in, and the solid-white muted paint is the louder of the two.
	   The suite pins this ordering, because reversing the two blocks is a
	   plausible tidy-up that would silently make a disabled chip look live. */
	cursor: default;
	opacity: 0.4;
}

/* ── CTA: "View on Facebook" (rule IG-6 / AC 6) ──────────────────────────── *
 * DESKTOP ONLY, in the top row beside mute. On touch it becomes a rail item
 * instead (step d) -- and that migration is VIEWPORT-driven, not tier-driven,
 * which is what keeps §6.1's "a control MUST NOT relocate as the chain
 * escalates" true. Hidden by default because touch is the base.
 *
 * The terminal tier's own route-out pill (AC 24) is styled like this one but
 * carries its OWN class, not this one. Instagram shipped them sharing a class
 * and had to split them (`897e6ea8`): a document-order query for "the CTA"
 * returned whichever came first in the DOM, which on a terminal-tier slide is
 * the per-slide one rather than the session-level control.
 */
.cff-swipeview-cta {
	position: relative;
	display: none;
	box-sizing: border-box;
	align-items: center;
	flex: 0 0 auto;
	gap: 7px;
	height: 34px;
	padding: 0 13px 0 11px;
	color: #fff;
	font-size: 13px;
	font-weight: 600;
	line-height: 1;
	white-space: nowrap;
	text-decoration: none;
	text-shadow: none;
	background: rgba(0, 0, 0, 0.35);
	border-radius: 999px;
	-webkit-backdrop-filter: blur(6px);
	backdrop-filter: blur(6px);
	filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));
	pointer-events: auto;
	transition: background 150ms ease;
}

.cff-swipeview-cta svg {
	width: 16px;
	height: 16px;
	flex: 0 0 auto;
}

.cff-swipeview-cta:hover,
.cff-swipeview-cta:focus-visible {
	color: #fff;
	background: rgba(0, 0, 0, 0.55);
	text-decoration: none;
}

/* VERTICAL ONLY -- see .cff-swipeview-mute::before for why. The pill is
   already wider than 44px, so height is the only axis that misses it. */
.cff-swipeview-cta::before {
	content: '';
	position: absolute;
	top: -5px;
	right: 0;
	bottom: -5px;
	left: 0;
	border-radius: 999px;
}

/* ── Pause glyph (rule 1.10 / AC 11) ─────────────────────────────────────── *
 * REPLACES the 600ms tap-flash. A play triangle shown for AS LONG AS the slide
 * is paused, not a transient echo of a tap -- so the viewer always shows
 * whether it is playing, which is what §6.1's "play/pause reflects actual
 * state" asks for on the visual channel. The sr-only live region carries the
 * same fact on the AT channel.
 *
 * Deliberately NOT `inset: 0` + `place-items: center`, which is how the
 * reference does it. This is a shrink-wrapped, CENTRE-ANCHORED 84px box, so a
 * future pointer-events regression on this layer would expose an 84px disc
 * rather than the whole frame -- and a full-frame element above the gesture
 * layer is this file's founding §6.3 defect shape. The tap-flash it replaces
 * was built the same way for the same reason; this keeps the shape and resizes
 * it.
 *
 * The in-curve OVERSHOOTS and the out-curve does not, which is asymmetric on
 * purpose: appearing is a statement, disappearing gets out of the way.
 */
.cff-swipeview-pauseglyph {
	position: absolute;
	top: 50%;
	left: 50%;
	margin: -42px 0 0 -42px;
	z-index: 4;
	display: none;
	/* Decorative readout of state the layer BELOW owns. Without this it would
	   swallow the tap that resumes playback -- an 84px dead disc dead centre of
	   the frame, which is exactly where a visitor taps to resume. */
	pointer-events: none;
}

.cff-swipeview-pauseglyph svg {
	box-sizing: border-box;
	width: 84px;
	height: 84px;
	padding: 22px;
	color: #fff;
	background: rgba(0, 0, 0, 0.4);
	border-radius: 50%;
	-webkit-backdrop-filter: blur(6px);
	backdrop-filter: blur(6px);
	will-change: transform, filter;
}

.cff-swipeview-root[data-paused="true"] .cff-swipeview-pauseglyph {
	display: block;
}

.cff-swipeview-root[data-paused="true"] .cff-swipeview-pauseglyph svg {
	animation: cffSwipeViewPauseIn 200ms cubic-bezier(0.2, 0.9, 0.25, 1.15) both;
}

/* The dissolve. `-was` is left behind by the JS for one 100ms window after
   unpausing, so the glyph animates OUT rather than vanishing. */
.cff-swipeview-root[data-paused="false"] .cff-swipeview-pauseglyph-was {
	display: block;
}

.cff-swipeview-root[data-paused="false"] .cff-swipeview-pauseglyph-was svg {
	animation: cffSwipeViewPauseOut 100ms cubic-bezier(0.4, 0, 0.6, 1) both;
}

@keyframes cffSwipeViewPauseIn {
	from { opacity: 0; transform: scale(0.3); filter: blur(4px); }
	to   { opacity: 1; transform: scale(1);   filter: blur(0); }
}

@keyframes cffSwipeViewPauseOut {
	from { opacity: 1; transform: scale(1);   filter: blur(0); }
	to   { opacity: 0; transform: scale(0.8); filter: blur(4px); }
}

/* ─────────────────────────── Session chrome ─────────────────────────── */

/* ── The slide counter has NO VISIBLE BOX any more (AC 5) ────────────────── *
 * It used to paint "3 / 8" at the window's top-left in 13px semibold. The
 * design has no counter anywhere, and the frame's top-left is deliberately
 * blank -- that corner of the WINDOW is now the close button's. Facebook was
 * the only one of the four viewers still showing one, and it showed it
 * pill-less in the exact spot the close disc now occupies.
 *
 * §7.1's position announcement SURVIVES, AT-only: the element keeps its
 * `role="status"` and `aria-live="polite"` and is given the `-sr` class, so a
 * screen-reader user still hears "3 of 8" on every slide change while nothing
 * is painted. Removing the announcement along with the paint would have been
 * a real regression for the visitor who most needs to know where they are in a
 * feed that has no visible position indicator at all.
 *
 * This is a §6.4 escalation: §6.1 guarantees a VISIBLE counter, so the viewer
 * is knowingly non-conformant to that clause until spec amendment A1 lands.
 * Recorded rather than quietly reinterpreted -- do not claim §6.1 conformance
 * on this point in the PR body.
 *
 * There is no rule for `.cff-swipeview-counter` here on purpose. The suite
 * asserts its ABSENCE from the collision run, because an element with no
 * visible box cannot collide with anything and a zone standing in for one
 * would be inventing geometry.
 */

/* ── Close: window-anchored, top-LEFT, a 40px disc (rule 1.4 / AC 2) ──────── *
 * Moved from top-RIGHT and given a disc. It is the ONLY window-anchored
 * control in the viewer now -- everything else is frame-anchored inside the
 * frame (AC 2), because on a letterboxed desktop window-anchored chrome sits
 * on the backdrop pillars rather than on the video it belongs to.
 *
 * 40x40 is the design's box and it is BELOW this file's 44px convention.
 * Recorded rather than silently accepted: 40px clears WCAG 2.5.8 (24px, AA)
 * but misses 2.5.5's 44px AAA target, which this viewer's other controls meet.
 * The design's number wins on the authority split (visual design = Aman), and
 * the REACHABLE box is grown back to 44px by the `::before` ring below -- which
 * takes no part in layout, so the disc stays 40px while elementFromPoint over
 * the ring still resolves to the button. Invisible either way, so there was no
 * design question to escalate, only a gap to close.
 *
 * ── The UA's form-control defaults, refused EXPLICITLY ───────────────────────
 * A <button> is not a normal element: the UA sets its own `font` shorthand
 * (`400 13.3333px Arial` in Chrome) and its own `padding: 1px 6px`, and
 * NEITHER inherits from the viewer. Instagram measured this on its live demo
 * and found every button in the viewer reporting `font-family: Arial` while
 * the overlay reported the design's stack -- so its labels and counts were
 * rendering at the design's sizes but not its face. `padding: 0` matters just
 * as much for a glyph-only control: the UA's 6px side padding shrinks the
 * content area, and a 40px disc was laying out a 22px glyph in 28px of it.
 *
 * `font: inherit` is the FIRST declaration so each rule's own explicit
 * size/weight still wins on source order, and this trio is repeated per rule
 * rather than hoisted into a `.cff-swipeview-root button` reset -- that
 * selector has HIGHER specificity (0,1,1) than these single-class rules
 * (0,1,0), so it would override the paddings later rules set on purpose. Flat
 * specificity is what keeps the cascade here predictable.
 */
.cff-swipeview-close {
	font: inherit;
	-webkit-appearance: none;
	appearance: none;
	padding: 0;
	position: absolute;
	top: 12px;
	top: max(12px, var(--cff-sv-safe-top));
	left: 12px;
	box-sizing: border-box;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 40px;
	height: 40px;
	color: #fff;
	cursor: pointer;
	background: rgba(255, 255, 255, 0.16);
	border: 0;
	border-radius: 50%;

	/* ── iOS: SQUARE FIRST, ROUND LATER ──────────────────────────────────── *
	 * Reported on device (Asmita): "the close button appears square instead of
	 * round, and after a while it appears round."
	 *
	 * The close carries no `backdrop-filter` -- but three of its neighbours in
	 * this same stacking context do (`-mute`, `-cta`, `-terminal-link`, all
	 * `blur(6px)` on touch), the `-pauseglyph svg` adds `will-change`, and
	 * `-track` is composited via `transform` + `will-change`. The root also
	 * animates `opacity` on open. On iOS WebKit that combination puts the
	 * context on a compositing path where a neighbouring layer's raster can
	 * land before this element's rounded clip is applied -- so a translucent
	 * 40x40 background paints as a SQUARE, and any later invalidation (a
	 * scroll, a slide change, the mute chip's own transition) re-rasterizes it
	 * correctly. "After a while it appears round" is that re-raster.
	 *
	 * The mitigation is to make this element its own correctly-clipped layer
	 * from the FIRST paint:
	 *   isolation: isolate     -- its own stacking context, so it is not
	 *                             folded into a neighbour's layer
	 *   translateZ(0)          -- promote it up front, so the radius is part of
	 *                             the layer's raster rather than applied after
	 *
	 * NOT `overflow: hidden`, which several write-ups of this bug recommend: the
	 * 44px reachable ring is a `::before` that extends BEYOND this 40px box
	 * (rule 1.4 / AC 31), and clipping would silently cut the hit target back
	 * to 40px. `border-radius` clips its own background without it -- the bug is
	 * layer rasterization, not overflow.
	 *
	 * ON-DEVICE CONFIRMATION PENDING. WebKit could not be driven here
	 * (playwright 1.62.1 wants webkit-2336; only webkit-2287 is installed), and
	 * headless WebKit on macOS is a different compositor from iOS anyway, so a
	 * green run would not have been evidence. This belongs with the AC 35 iOS
	 * pass. The same audit applied to every other rounded control: the mute,
	 * CTA and terminal pills already carry `-webkit-backdrop-filter` so they
	 * composite with their own clip; the rail items are bare glyphs with no
	 * radius; the chevrons are desktop-only. `-avatar` is the one remaining
	 * 50% circle with no hint, and gets the same treatment.
	 */
	isolation: isolate;
	transform: translateZ(0);
	/* Stated rather than relied on by omission: every genuine control declares
	   its own `auto` so an opt-out ancestor cannot silently swallow it if a
	   future change wraps these in a container. */
	pointer-events: auto;
	transition: background 150ms ease, transform 150ms ease;
}

.cff-swipeview-close:hover,
.cff-swipeview-close:focus-visible {
	background: rgba(255, 255, 255, 0.28);
}

.cff-swipeview-close:active {
	transform: scale(0.94);
}

.cff-swipeview-close svg {
	width: 22px;
	height: 22px;
}

/* The 44px REACHABLE box for a 40px disc -- 2px on each side. A positioned
   pseudo-element takes no part in layout but is hit-tested, and a hit on it
   resolves to its originating element. */
.cff-swipeview-close::before {
	content: '';
	position: absolute;
	top: -2px;
	right: -2px;
	bottom: -2px;
	left: -2px;
	border-radius: 50%;
}

/* ── Progress bar (rule 1.12 / AC 29) ────────────────────────────────────── *
 * MOVED from a full-bleed bar across the WINDOW's bottom edge to the bottom
 * edge INSIDE the frame, at the design's 2px. It lives in
 * `.cff-swipeview-framechrome`, whose `overflow: hidden` plus radius is what
 * clips the bar's square ends against the frame's rounded bottom corners --
 * the one job positioning alone could not do.
 *
 * `scaleX` RATHER THAN `width`, and this is AC 29's substance rather than a
 * micro-optimisation. Two things follow from it:
 *
 * 1. The bar now has a TRANSITION at all. Facebook declared none, so the fill
 *    stepped in visible jumps between the 250ms samples §6.1 requires. The
 *    transition is what interpolates between them, and a transform animates on
 *    the compositor where a width relayouts.
 *
 * 2. The duration EQUALS the poll interval, deliberately. YouTube's sibling
 *    defect is the counter-example AC 29 names: a shared fill written only
 *    inside a 250ms interval and then swept by a 200ms `width` transition,
 *    which leaves a ~450ms window where the bar is showing a wrong value AND
 *    sliding towards another one. Matching the two means each sample's
 *    animation finishes exactly as the next arrives. The suite asserts the
 *    equality against CHROME_POLL_MS rather than against the literal 250.
 */
.cff-swipeview-progress {
	position: absolute;
	right: 0;
	bottom: 0;
	left: 0;
	height: 2px;
	background: rgba(255, 255, 255, 0.28);
	overflow: hidden;
	/* A read-only readout with no click handler. Without this it swallows taps
	   landing in its band instead of letting them reach the gesture layer
	   (§6.3: non-interactive chrome MUST opt out too). */
	pointer-events: none;
}

.cff-swipeview-progress-fill {
	display: block;
	width: 100%;
	height: 100%;
	background: #fff;
	transform: scaleX(0);
	transform-origin: left center;
	transition: transform 250ms linear;
}

/* Applied by the progress write for ONE write, whenever the position moves
   BACKWARD (a slide change, a loop wrap). Without it the fill animates back
   down over 250ms, which reads as the bar rewinding -- rule 1.12's "jump on
   nav" class. The class is added, the value written, and the class removed on
   the next frame, so exactly that one write is unanimated.

   The add-write-remove has to happen in ONE TASK for the suppression to work:
   a write deferred past the class removal is animated after all, which is the
   failure this looks like it is preventing while not preventing it. */
.cff-swipeview-progress-fill-jump {
	transition: none;
}

/* ── Chevrons (rule 1.11 / AC 8) ─────────────────────────────────────────── *
 * Now the MIDDLE ROW of the session rail column, vertically centred on the
 * frame, rather than a free-floating cluster hung off the window's right edge.
 *
 * The reference builds its rail column as a `minmax(0,1fr) auto 1fr` grid
 * (measured 380/124/380 at an 884px frame) so the chevrons land on the frame's
 * centre and get pushed up if the action stack ever needs more than its half.
 * That grid cannot be ported, and the reason is ownership rather than taste:
 * the action stack is per-POST (its counts change per slide and it travels
 * with the drag) so it lives inside the track, while the chevrons are
 * session-level and cannot. Nothing can be one grid across that boundary.
 *
 * So the RESULT is reproduced without the grid: the session column is the same
 * 44px box in the same place, and `top: 50%` inside it centres the pair on the
 * frame -- which is exact, because the frame and the column are the same
 * height and share a top edge. The grid's row-growth behaviour is unreachable
 * at this rail's height anyway (the stack is ~183px against a 442px half).
 * Recorded as a deviation with a measured equivalence rather than silently.
 *
 * Both directions are built unconditionally and DISABLED at the ends, never
 * added and removed: a control that vanishes at the last slide moves its
 * sibling, and a disabled button keeps the cluster's geometry stable while
 * staying honest about what it can do (§6.2).
 */
.cff-swipeview-nav {
	/* Hidden by default and revealed only where a fine pointer exists --
	   RATIFIED: gate on INPUT CAPABILITY, never on viewport width. A narrow
	   desktop window still has a mouse; a large tablet does not. */
	display: none;
}

/* ═══════════════════════════════════════════════════════════════════════════
 * THE ONE CAPABILITY QUERY (rule 1.2)
 * ═══════════════════════════════════════════════════════════════════════════
 *
 * `(hover: hover) and (pointer: fine)` is the ONLY media query in this file
 * that switches layout, and there is NO width query anywhere -- a narrow
 * desktop window still has a mouse and a keyboard; a large tablet has neither.
 * Touch is the BASE and desktop is the override, so a device the query cannot
 * classify gets the full-bleed touch layout rather than a letterboxed desktop
 * one it cannot drive. `tests/js/swipeview-geometry.test.js` asserts both
 * halves: that no width query exists at all, and that the desktop rules live
 * inside this query rather than at top level.
 *
 * A note on reading this file with the suite's parser, because the sibling
 * Instagram suite has the opposite hazard: FB's `cssmodel.block()` is
 * LINE-START anchored, so a top-level rule remains readable wherever it sits
 * in the file, and an indented copy inside a media query is never mistaken for
 * it. There is no "first @media barrier" here. What the parser CANNOT read is
 * a rule written as the last member of a grouped selector list -- discipline 4
 * in cssmodel.js -- which is why the two frame rules above are solo.
 */
/* ── THE DESKTOP GATE: CAPABILITY *AND* WIDTH ────────────────────────────── *
 * Added 2026-09-09, and it reverses this file's "zero width queries" rule for
 * one compound condition. The authority is the prototype's own stylesheet, not
 * a preference.
 *
 * WHAT WENT WRONG. The switch was keyed on input capability ALONE. Chrome
 * DevTools' "Responsive" mode does not turn touch emulation on, so
 * `(hover: hover) and (pointer: fine)` MATCHES at 390px wide -- and the whole
 * desktop layout rendered inside a phone-sized viewport: the frame shrank to
 * 330x828 and moved to (0,8) leaving a black gutter, the rail column stood
 * outside it in that gutter, the chevrons appeared, and the touch-only View
 * item (the Facebook mark) vanished. Measured, at 390x844 / 393x852 / 412x915
 * / 375x667: `desktopQ=true`, 3 rail items instead of 4, 26px from the last
 * item to the viewport bottom instead of 18.
 *
 * WHAT THE PROTOTYPE DOES. Its entire mobile layout -- `.sc-railcol { display:
 * contents }`, the absolute in-frame `.sc-rail`, `--sc-frame-max-h: 100dvh`,
 * `.sc-nav { display: none }` -- lives in `@media (max-width: 768px)`, a pure
 * WIDTH query (shared/base.css). Its capability query governs hover
 * affordances, not layout. So at 390px wide with a mouse the prototype stays
 * mobile and we did not; measured under identical emulation, the prototype
 * kept a 390x844 full-bleed frame with all four rail items.
 *
 * So the gate is now capability AND width: `min-width: 769px` is the exact
 * complement of the prototype's `max-width: 768px`. Below that everything
 * falls back to the touch base, which is what the prototype renders. This is
 * still ONE media query -- a compound condition, not a second breakpoint --
 * and the guard permits exactly this form while still failing a bare width
 * query anywhere else.
 *
 * The two hover rules in here (the tooltip's shown state, the chevrons' hover
 * and focus paint) ride along deliberately: both attach to controls that exist
 * ONLY in this block, so gating them on width costs nothing.
 */
@media (hover: hover) and (pointer: fine) and (min-width: 769px) {
	/* Rule 1.1's desktop frame: 8px top/bottom margins as PADDING on the row
	   (not a `- 16px` subtraction), and the 16px stage gap between the frame
	   and the rail column. */
	.cff-swipeview-stagerow {
		gap: 16px;
		padding: 8px 0;
	}

	/* The frame, and its session-level twin, sized identically. Height-led:
	   the row's content box gives the height, `aspect-ratio` derives the
	   width. `max-width: 100%` is the only clamp, and it is why an unusually
	   narrow desktop window yields a taller-than-9:16 frame rather than one
	   that overflows -- see the recorded limitation in the change log.

	   `clip-path` alongside `border-radius` is rule 1.1, and it is not
	   redundant: a COMPOSITED video layer ignores an ancestor's border-radius
	   and will paint square corners straight past it. The clip-path is what
	   actually contains them. Verifying that at rest needs a real device --
	   headless Chrome's SwiftShader path does not reproduce it. */
	.cff-swipeview-stage {
		width: auto;
		height: 100%;
		max-width: 100%;
		aspect-ratio: 9 / 16;
		border-radius: 8px;
		clip-path: inset(0 round 8px);
	}

	.cff-swipeview-framechrome {
		width: auto;
		height: 100%;
		max-width: 100%;
		aspect-ratio: 9 / 16;
		border-radius: 8px;
		clip-path: inset(0 round 8px);
	}

	/* The rail column's desktop rule lives with the rail's own, further down in
	   this same query -- ONE rule per selector per query, for the same reason
	   the base rules are unduplicated: `block()` returns the FIRST match, so a
	   second rule is invisible to every assertion about the first. This was a
	   44px stub through step (a) and it shadowed the real rule the moment that
	   arrived, which the frame-centring assertions caught immediately. */

	/* Rule 1.13's desktop backdrop: the host page stays faintly visible behind
	   a blur, which is Aman's stated intent. The plain `rgba` comes FIRST as
	   the fallback for an engine without `color-mix`, and the `color-mix` line
	   overrides it where supported. Touch keeps the base rule's opaque `#000`
	   -- no blur, no translucency -- because a phone has no host page worth
	   showing through and `backdrop-filter` over a full-screen video layer is
	   the expensive case. */
	.cff-swipeview-root {
		background: rgba(0, 0, 0, 0.82);
		background: color-mix(in srgb, #000 82%, transparent);
		-webkit-backdrop-filter: blur(14px);
		backdrop-filter: blur(14px);
	}

	/* Rule 1.4 -- (16, 16) on desktop, (12, max(12, safe-top)) on touch. */
	.cff-swipeview-close {
		top: 16px;
		left: 16px;
	}

	/* Rule 1.5 -- no need to clear the window close button here: it is far to
	   the left of the frame at any desktop viewport, so the 62px touch pad
	   drops to the design's 16px. */
	.cff-swipeview-top {
		padding: 14px 14px 0 16px;
	}

	/* Rule IG-6 / AC 6 -- the CTA is a top-row pill on desktop and a rail item
	   on touch. VIEWPORT-driven, so §6.1's "MUST NOT relocate as the chain
	   escalates" is untouched: the tier chain never moves it. */
	.cff-swipeview-cta {
		display: inline-flex;
	}

	/* Rule IG-4 -- the rail is OUTSIDE the frame here, so the bottom row needs
	   no gutter reserved for it and the 76px touch pad drops to the plain
	   inset. This is the reason `padding-right` is a separate longhand. */
	.cff-swipeview-bottom {
		padding-right: 16px;
	}

	/* ── Tooltips (rule 1.9 / AC 12) ──────────────────────────────────────────
	 * On close and the two chevrons ONLY. Aman gates his own tooltips on this
	 * exact query, so the idiom is his; what this adds is that NO other
	 * control gets one -- no pause hint, no swipe hint, no desktop keyboard
	 * hint. Those three plus the coach overlay and the unmute label were five
	 * hints; rule 1.9's minimalism principle keeps two, and neither of them is
	 * a tooltip.
	 *
	 * All three tooltips point RIGHT: close is at the window's top-left and
	 * the chevrons are right of the frame, so in both cases the label has room
	 * outward.
	 *
	 * The attribute is NAMESPACED (`data-cff-swipe-tip`) rather than the
	 * reference's bare `data-tip` -- that is a name a host theme or another
	 * plugin on the page could plausibly already use, and this file's whole
	 * namespace discipline exists because twelve sites in cff-scripts.js act
	 * on selectors that must never match anything here.
	 *
	 * `::after` on a flex container would become a flex item, so it is
	 * absolutely positioned out of flow; `pointer-events: none` stops a
	 * tooltip that overhangs a sibling from stealing its clicks. Both `:hover`
	 * AND `:focus-visible` reveal it -- a tooltip a keyboard user cannot get
	 * to is not a tooltip.
	 */
	[data-cff-swipe-tip]::after {
		content: attr(data-cff-swipe-tip);
		position: absolute;
		top: 50%;
		left: calc(100% + 10px);
		padding: 6px 9px;
		color: #fff;
		font-size: 12px;
		font-weight: 500;
		line-height: 12px;
		white-space: nowrap;
		background: rgba(28, 28, 30, 0.96);
		border: 1px solid rgba(255, 255, 255, 0.08);
		border-radius: 6px;
		box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
		/* Above the session frame chrome (3) and the close disc, so a tooltip
		   on the close button is not clipped behind the chrome layer. */
		z-index: 30;
		opacity: 0;
		pointer-events: none;
		transform: translateY(-50%) translateX(4px);
		transition: opacity 120ms ease, transform 120ms ease;
	}

	[data-cff-swipe-tip]:hover::after,
	[data-cff-swipe-tip]:focus-visible::after {
		opacity: 1;
		transform: translateY(-50%) translateX(0);
	}

	/* Rule 1.3 -- the rail column becomes a 44px flex ITEM again, back in the
	   row's flow so the frame and the column centre together. Bottom-aligned:
	   `justify-content: flex-end` on the row plus the column's own
	   `flex-end` is what makes the action stack re-pitch downward when a
	   metric is absent. */
	.cff-swipeview-railcol {
		position: relative;
		right: auto;
		bottom: auto;
		width: 44px;
		height: 100%;
	}

	.cff-swipeview-rail {
		gap: 18px;
		padding-bottom: 18px;
	}

	/* 26px desktop, 28px touch (IG-2) -- the touch value is the base. */
	.cff-swipeview-act svg {
		width: 26px;
		height: 26px;
	}

	/* The CTA is the top-row pill here, so the rail's "View" item goes. */
	.cff-swipeview-act-view {
		display: none;
	}

	/* Middle row of the column, centred on the frame. */
	.cff-swipeview-nav {
		position: absolute;
		top: 50%;
		right: 0;
		left: 0;
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 12px;
		transform: translateY(-50%);
		/* §6.3: the CONTAINER is inert and the two buttons opt back in. */
		pointer-events: none;
	}

	/* Rule 1.11 -- 44x44 discs sharing rule 1.4's one circular-control paint
	   with the close button, not the old `rgba(0,0,0,.4)`. */
	.cff-swipeview-nav-prev,
	.cff-swipeview-nav-next {
		font: inherit;
		-webkit-appearance: none;
		appearance: none;
		padding: 0;
		position: relative;
		box-sizing: border-box;
		display: flex;
		align-items: center;
		justify-content: center;
		width: 44px;
		height: 44px;
		color: #fff;
		cursor: pointer;
		background: rgba(255, 255, 255, 0.16);
		border: 0;
		border-radius: 50%;
		line-height: 1;
		pointer-events: auto;
		transition: background 150ms ease, transform 150ms ease, opacity 150ms ease;
	}

	.cff-swipeview-nav-prev svg,
	.cff-swipeview-nav-next svg {
		width: 24px;
		height: 24px;
	}

	.cff-swipeview-nav-prev:hover,
	.cff-swipeview-nav-next:hover,
	.cff-swipeview-nav-prev:focus-visible,
	.cff-swipeview-nav-next:focus-visible {
		background: rgba(255, 255, 255, 0.28);
	}

	.cff-swipeview-nav-prev:active,
	.cff-swipeview-nav-next:active {
		transform: scale(0.94);
	}

	/* Rule 1.11's `opacity .3` at the ends -- the design's number, replacing
	   the prototype's .35. */
	.cff-swipeview-nav-prev[disabled],
	.cff-swipeview-nav-next[disabled] {
		cursor: default;
		opacity: 0.3;
		transform: none;
	}
}

/* ── The unmute pill and the tap-flash are BOTH GONE (AC 7 / AC 11) ──────── *
 * `.cff-swipeview-pill` was a bottom-centre "Tap to unmute" button, and
 * `.cff-swipeview-tapflash` was a 600ms transient echo of a tap. Rule 1.6
 * folds the first into the mute chip's expanding label -- one control instead
 * of two, one accessible name, one focus stop -- and rule 1.10 replaces the
 * second with the persistent pause glyph above, which reads STATE rather than
 * echoing an EVENT.
 *
 * The tap-flash's shape is what carried over rather than its behaviour: it was
 * already shrink-wrapped and centre-anchored rather than full-bleed, for
 * exactly the §6.3 reason the pause glyph keeps. What changed is that a glyph
 * showing "you tapped" cannot tell a visitor whether the video is playing,
 * which is what §6.1 asks the visual channel to carry.
 *
 * TAP_FLASH_MS went with them, and so did the cross-file assertion that pinned
 * the constant against the keyframe's duration -- its subject stopped
 * existing. That is a BEHAVIOURAL retarget rather than a selector rename, and
 * it is recorded as one: the flash-polarity pin is no longer meaningful now
 * the glyph is a state readout, and the suite says so in-file rather than
 * quietly dropping the check.
 */

/* ─────────────────────────── Accessibility ─────────────────────────── */

/*
 * A VISIBLE FOCUS INDICATOR ON EVERY CONTROL -- WCAG 2.4.7.
 *
 * The YouTube prototype shipped without one, and that is a recorded finding
 * rather than an oversight to repeat: the controls there are transparent-at-
 * rest glyphs on video, so the browser's default outline was either invisible
 * against the frame or suppressed by the reset. Every control this viewer owns
 * is listed here explicitly, and the list is the same one focusables() walks --
 * so a control added to one and not the other is a visible gap rather than a
 * silent one.
 *
 * :focus-visible so a mouse click does not paint a ring, with a :focus
 * fallback for engines without it. The ring is drawn OUTSIDE the box
 * (outline-offset) plus a dark companion shadow, because a white ring on a
 * bright video frame is otherwise as invisible as no ring at all.
 */
.cff-swipeview-close:focus-visible,
.cff-swipeview-mute:focus-visible,
.cff-swipeview-cta:focus-visible,
.cff-swipeview-nav-prev:focus-visible,
.cff-swipeview-nav-next:focus-visible,
.cff-swipeview-caption-more:focus-visible,
.cff-swipeview-author:focus-visible,
.cff-swipeview-follow:focus-visible,
/* NOT `.cff-swipeview-act-like`. It is a `div role="img"` readout, so it can
   never receive focus -- a ring for it would be dead CSS that IMPLIES
   focusability, and the two-list parity assertion in the behavioural tier
   flags exactly that. It still gets the 44px HIT ring, because an AT user
   targeting a readout should not have a smaller target than the controls
   either side of it; a hit target and a focus ring are different questions. */
.cff-swipeview-act-comment:focus-visible,
.cff-swipeview-act-share:focus-visible,
.cff-swipeview-act-view:focus-visible,
.cff-swipeview-terminal-link:focus-visible,
.cff-swipeview-embedcard-play:focus-visible {
	outline: 2px solid #fff;
	outline-offset: 2px;
	box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.72);
}

/* ── THE RING'S CORNER IS PER CONTROL, NOT SHARED ────────────────────────── *
 * `border-radius: 6px` used to live in the rule above, and it was the reported
 * defect: "the close icon on desktop looks square outlined instead of round
 * like other platforms" (Asmita, on the 4.12.0.11 demo).
 *
 * `.cff-swipeview-close:focus-visible` is specificity (0,2,0) -- one class plus
 * one pseudo-class -- and `.cff-swipeview-close` is (0,1,0). So a
 * `border-radius` in the grouped rule BEAT each control's own resting radius,
 * and did it only while the control was keyboard-focused. The close disc's
 * `border-radius: 50%` collapsed to 6px on focus, taking its translucent
 * background with it, and the `outline` plus the halo `box-shadow` then
 * followed that 6px corner -- both properties honour `border-radius`, which is
 * why the ring looked square rather than the ring being drawn on some other
 * box. Measured on the demo: rest `50%`, keyboard-focused `6px`.
 *
 * It was eight controls, not one. Every ROUNDED control in the enumeration was
 * flattened the same way and close was merely the most visible -- it is
 * top-left, present on every tier, and a circle-to-rounded-square is the
 * largest shape delta in the set:
 *
 *   -close, -nav-prev, -nav-next, -embedcard-play    50%    -> 6px
 *   -mute                                            17px   -> 6px
 *   -cta, -terminal-link                             999px  -> 6px
 *   -follow                                          8px    -> 6px  (2px, subtle)
 *
 * How it got here: the grouped rule carried the 6px from the SMASH-2049
 * prototype shell (`b30a3d84`), where the enumerated controls were
 * square-cornered and 6px was right for all of them. `c03167b8` then made
 * close a 50% disc and added that radius to the RESTING rule without noticing
 * the higher-specificity focus rule above was still overriding it. Each later
 * rounded control -- the mute chip, the CTA and terminal pills, the chevrons,
 * the embed play button -- inherited the same silent flattening.
 *
 * THE RULE: a control's `:focus-visible` state must never restate
 * `border-radius`. The ring's corner is whatever the control's own shape is,
 * so `outline` and `box-shadow` trace the real silhouette. The 6px belongs
 * ONLY to the controls that have NO radius of their own, where it is an
 * addition rather than an override -- the three rail items, the author link
 * and the caption more/less button, all square-cornered at rest.
 *
 * Instagram's viewer is the reference and it never had this bug, because it
 * declares focus per control instead of in one group: `.sbi-qs-close:focus-
 * visible` sets only a `box-shadow`, so the disc keeps its 50% and the ring
 * traces it (`css/sbi-quickscroll.css:228`). Measured on the IG demo, every
 * control keeps its resting shape when focused -- close 50%, nav 50%, mute
 * 17px, view-on-ig 999px, follow 8px -- and 4px is added only to `-author` and
 * `-act`, which have none. Same split as below.
 *
 * NOT touched, and deliberately: the 44px reachable ring is
 * `.cff-swipeview-close::before` at its own `border-radius: 50%`, a separate
 * box from the one the outline traces. It never carried the 6px and it does
 * not change here, so AC 31's hit-test enumeration is unaffected by this fix
 * -- the two enumerations stay in step. (The `::after` on the same element is
 * the desktop tooltip, which keeps its own 6px corner.)
 */
.cff-swipeview-caption-more:focus-visible,
.cff-swipeview-author:focus-visible,
.cff-swipeview-act-comment:focus-visible,
.cff-swipeview-act-share:focus-visible,
.cff-swipeview-act-view:focus-visible {
	border-radius: 6px;
}

.cff-swipeview-sr {
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

/* ═══════════════════════════════════════════════════════════════════════════
 * REDUCED MOTION -- §3.2: motion REMOVED entirely, not shortened
 * ═══════════════════════════════════════════════════════════════════════════
 *
 * ONE block, and it is deliberately the LAST thing in this file.
 *
 * Source order, not specificity, is what makes it win. Several of the
 * transitions it suppresses are declared inside the capability query
 * `(hover: hover) and (pointer: fine)` -- the tooltip's opacity fade, for one
 * -- and a media query adds NO specificity. Two rules of equal specificity are
 * decided by which comes later in the stylesheet, so a reduced-motion block
 * placed before the capability query silently loses to it. This file had two
 * such blocks, both above the capability query, before this step consolidated
 * them here.
 *
 * `animation: none` carries `!important` because an animation shorthand
 * declared on the element itself would otherwise tie and win by source order
 * within the same rule set; the transitions do not need it.
 *
 * DELIBERATELY ABSENT: .cff-swipeview-progress-fill. Its transition is not
 * decoration -- it interpolates between the state samples §6.1 requires, and
 * removing it makes the bar step in visible jumps rather than sit still. A
 * progress bar that advances IS the content. The one motion there nobody asked
 * for, the rewind on a slide change, is suppressed for that specific write
 * instead.
 */
@media (prefers-reduced-motion: reduce) {
	.cff-swipeview-root,
	.cff-swipeview-close,
	.cff-swipeview-mute,
	.cff-swipeview-mute-label,
	.cff-swipeview-cta,
	.cff-swipeview-act,
	.cff-swipeview-follow,
	.cff-swipeview-terminal-link,
	.cff-swipeview-nav-prev,
	.cff-swipeview-nav-next,
	.cff-swipeview-toast {
		transition: none;
	}

	/* S3.2 for SMASH-2064's rail travel: the animated RETURN to rest goes, the
	   drag-follow STAYS. The follow is direct manipulation the visitor is
	   performing themselves, and it is written with a 0ms duration on every
	   path regardless -- so this removes the settle-back animation and nothing
	   else. setRailTravel() reaches the same answer in JS via
	   prefersReducedMotion(); this is the declarative half, and the two agree
	   by both resolving to "no transition" rather than by either trusting the
	   other.

	   ITS OWN RULE, not appended to the group above, and that is a testability
	   decision rather than a style one. cssmodel's block() identifies a
	   grouped selector by a trailing comma on the preceding line and skips it,
	   so a rule read of a group's LAST member returns the group's declarations
	   as though they were that selector's own -- the failure the model records
	   under "discipline 4". A comment sitting between the group and the
	   selector defeats that guard outright, because the preceding line then
	   ends in a comment terminator rather than a comma. A solo rule is soundly
	   readable and carries its own reasoning.

	   And this comment is why the stylesheet now has a balanced-delimiter
	   assertion in the geometry tier. Its first draft spelled that terminator
	   out as a literal two-character sequence -- inside a CSS comment, which
	   does not nest and has no escape. The comment ended early, the tail of
	   the prose became garbage declarations, and the browser dropped THIS RULE
	   during error recovery. The text-based geometry read still found
	   `transition: none` and passed; only the behavioural tier, which asks a
	   real engine, saw that reduced motion was doing nothing here. */
	.cff-swipeview-rail {
		transition: none;
	}

	[data-cff-swipe-tip]::after {
		transition: none;
	}

	.cff-swipeview-coach-hand {
		animation: none !important;
	}

	.cff-swipeview-spinner {
		/* The spin is decorative; under reduced motion the ring still reads as
		   a loading state without rotating. */
		animation: none !important;
	}

	/* AC 11's reduced-motion clause, and it closes a gap the prototype had: the
	   tap-flash this replaces declared its animation with NO reduced-motion
	   override at all. The glyph still APPEARS and disappears -- it is a state
	   readout and suppressing it would remove information -- it just does so
	   without the bloom and the dissolve. */
	.cff-swipeview-root[data-paused="true"] .cff-swipeview-pauseglyph svg,
	.cff-swipeview-root[data-paused="false"] .cff-swipeview-pauseglyph-was svg {
		animation: none !important;
	}
}
