/*-------------------IV4 LINK---------------------------*/

/*应用于页面中的所有标签*/
*{
	/*清除/设定所有默认属性*/
	padding: 0;
	margin: 0;
	font-size: 12px;
	/*清除所有表格风格*/
	list-style: none;
	font-family: "Helvetica";
	/*阻止用户选择文字*/
	user-select: none;
	/*沒用：會導致字體被鎖定在200px，且不會再被調整窗口比例影響*/
	/*text-size-adjust: none;*/
	/*-webkit-text-size-adjust: none;*/
	/*-webkit-text-size-adjust: 100%;*/
}

html, body{
	height: 100%;
	scrollbar-width: none;
	-ms-overflow-style: none;
}

html::-webkit-scrollbar,
body::-webkit-scrollbar{
	display: none;
}

body{
	overflow-x: hidden;
}

/*-------------------滚动舞台---------------------------*/
.stage{
	position: fixed;
	inset: 0;
	overflow: hidden;
	background: #fff;
	z-index: 1;
	cursor: help;
}

.stage.hubOpen{
	cursor: pointer;
}

.scrollSpacer{
	position: relative;
	width: 100%;
	height: 400vh;
}

.iv4Link{
	width: 700px;
	height: 730px;
	margin: 0 auto;
	background-color: black;
	box-shadow: 0px 0px 13px black;
	position: absolute;
	margin-left: auto;
	margin-right: auto;
	margin-bottom: auto;
	margin-top: auto;
	left: 0;
	right: 0;
	bottom: 0;
	top: 0;
	vertical-align: middle;
	z-index: 2;
}

.iv4Link div span{
	font-family: "megrim";
	font-size: 200px;
	color: white;
}

.iv4Link img:hover{
	background-color: gray;
	transition: all 0.3s ease-in-out 0s;
	cursor: pointer;
}

.iv4Link #titleIv4{
	font-family: "Helvetica";
	position: absolute;
	bottom: 10px;
	right: 10px;
	font-size: 13px;
	color: white;
}

.iv4Link #titleArt:hover{
	cursor: pointer;
}

.iv4Link #titleDigital:hover{
	cursor: pointer;
}

#titleIv4{
	text-align: right;
	width: 75px;
	height: 15px;
}

#titleDigital{
	width: 690px;
	height: 468px;
	margin-left: 10px;
}

#titleArt{
	width: 690px;
	height: 234px;
	margin-left: 10px;
}

#titleDigital span{
	transition: all 0.3s ease-in-out 0s;
}

#titleArt span{
	transition: all 0.3s ease-in-out 0s;
}

/*-------------------glitch 过场层---------------------------*/
#glitchLayer{
	position: absolute;
	inset: 0;
	z-index: 3;
	pointer-events: none;
	opacity: 0;
}

.glitchClone{
	pointer-events: none;
}

.glitchRed{
	filter: contrast(1.28) saturate(1.9) hue-rotate(-38deg);
	animation: iv4GlitchRedFront 0.58s steps(2, end) infinite;
	mix-blend-mode: screen;
}

.glitchCyan{
	filter: contrast(1.25) saturate(1.8) hue-rotate(150deg);
	animation: iv4GlitchCyanFront 0.76s steps(2, end) infinite;
	mix-blend-mode: screen;
}

@keyframes iv4GlitchRedFront{
	0%, 100% { transform: translate3d(0,0,0); clip-path: inset(0 0 0 0); }
	25% { transform: translate3d(6px,-2px,0); clip-path: inset(8% 0 54% 0); }
	53% { transform: translate3d(-8px,2px,0); clip-path: inset(42% 0 18% 0); }
	78% { transform: translate3d(4px,-1px,0); clip-path: inset(64% 0 2% 0); }
}

@keyframes iv4GlitchCyanFront{
	0%, 100% { transform: translate3d(0,0,0); clip-path: inset(0 0 0 0); }
	20% { transform: translate3d(-5px,1px,0); clip-path: inset(16% 0 46% 0); }
	49% { transform: translate3d(7px,-2px,0); clip-path: inset(48% 0 14% 0); }
	74% { transform: translate3d(-4px,2px,0); clip-path: inset(70% 0 0 0); }
}

/*-------------------碎片爆开层---------------------------*/
#shardLayer{
	position: absolute;
	inset: 0;
	z-index: 4;
	pointer-events: none;
}

.shardPiece{
	position: absolute;
	width: 700px;
	height: 730px;
	left: 0; right: 0; top: 0; bottom: 0;
	margin: auto;
	will-change: transform, opacity;
}

.shardPiece .iv4Link{
	box-shadow: none;
}

/*-------------------内部空间层---------------------------*/
#hubLayer{
	position: absolute;
	inset: 0;
	z-index: 0;
	opacity: 0;
	pointer-events: none;
}

.hubSlider{
	position: absolute;
	inset: 0;
}

.sliderViewport{
	position: relative;
	width: 100%;
	height: 100%;
	overflow: hidden;
	cursor: default;
	background: #050505;
}

.sliderTrack{
	display: flex;
	height: 100%;
	transition: transform 0.5s cubic-bezier(.65,0,.35,1);
}

.sliderGlitchFlash{
	position: absolute;
	inset: 0;
	pointer-events: none;
	z-index: 1;
}

.sliderGlitchFlash .flashRed,
.sliderGlitchFlash .flashCyan{
	position: absolute;
	inset: 0;
	mix-blend-mode: screen;
	opacity: 0;
}

.sliderGlitchFlash .flashRed{
	background: rgba(255,30,60,0.45);
}

.sliderGlitchFlash .flashCyan{
	background: rgba(0,220,255,0.4);
}

/* the animation only exists while .isActive is present, so re-adding that class after a
   forced reflow reliably restarts it from 0% every time — a bare animation on the element
   itself only ever plays once and then just sits frozen at its finished (unclipped) frame */
.sliderGlitchFlash.isActive .flashRed{
	animation: sliderGlitchBurstRed 0.36s steps(3, end) 1 forwards;
}

.sliderGlitchFlash.isActive .flashCyan{
	animation: sliderGlitchBurstCyan 0.42s steps(3, end) 1 forwards;
}

@keyframes sliderGlitchBurstRed{
	0%   { opacity: 0.85; clip-path: inset(0% 0 0% 0);  transform: translate3d(-6px,0,0); }
	25%  { opacity: 0.75; clip-path: inset(30% 0 8% 0);  transform: translate3d(8px,0,0); }
	55%  { opacity: 0.55; clip-path: inset(4% 0 46% 0);  transform: translate3d(-9px,0,0); }
	100% { opacity: 0;    clip-path: inset(50% 0 4% 0);  transform: translate3d(0,0,0); }
}

@keyframes sliderGlitchBurstCyan{
	0%   { opacity: 0.8;  clip-path: inset(0% 0 0% 0);  transform: translate3d(7px,0,0); }
	30%  { opacity: 0.7;  clip-path: inset(10% 0 40% 0); transform: translate3d(-8px,0,0); }
	60%  { opacity: 0.45; clip-path: inset(42% 0 10% 0); transform: translate3d(6px,0,0); }
	100% { opacity: 0;    clip-path: inset(42% 0 10% 0); transform: translate3d(0,0,0); }
}

.sliderSlide{
	position: relative;
	height: 100%;
	overflow: hidden;
	flex: none;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 20px;
	color: rgba(255,255,255,0.55);
}

.sliderSlide .lockGlyph{
	font-size: 38px;
	opacity: 0.6;
}

.sliderSlide .slideTitle{
	font-family: "megrim";
	font-size: 80px;
	letter-spacing: 3px;
	color: rgba(255,255,255,0.85);
}

.sliderSlide .slideTag{
	font-size: 13px;
	letter-spacing: 4px;
}

/*-------------------项目档案版式：文字为主，截图为档案图版---------------------------*/
/* Ambient wash: the shot blown up to cover the whole slide, blurred almost to abstraction
   and pushed dark, so it colours the screen without competing with the type. Scaled past
   the edges so the blur doesn't fade out into transparency at the sides. */
.slidePreviewBlur{
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	object-fit: cover;
	object-position: center center;
	display: block;
	pointer-events: none;
	filter: blur(52px) saturate(1.15) brightness(0.3);
	transform: scale(1.18);
}

/* The archive plate. No fixed width/height: it's absolutely positioned with width:auto so
   it SHRINK-WRAPS the image, and the image itself is bounded by max-width/max-height. That
   way the frame is always exactly the image's shape — no black filler inside the mat. */
.slidePreviewCard{
	position: absolute;
	right: 96px;
	top: 50%;
	transform: translateY(-50%);
	padding: 10px;
	background: #050505;
	border: 1px solid rgba(255,255,255,0.16);
	box-shadow: 0 26px 70px rgba(0,0,0,0.6);
	line-height: 0;
	/* the blur wash is appended after this in the DOM, so the plate needs an explicit
	   layer to stay above it (info panel sits above both at z-index 2) */
	z-index: 1;
}

.slidePreviewMain{
	display: block;
	width: auto;
	height: auto;
	max-width: 900px;
	/* --canvasH is the viewport height expressed in canvas px (see index.html). The
	   subtracted margin is small so short/wide windows still get a large plate — this
	   bound is what was capping the image on letterbox-shaped screens. */
	max-height: calc(var(--canvasH, 900px) - 110px);
	pointer-events: none;
}

/*-------------------图版载入指示器---------------------------*/
/* The plate shrink-wraps its image, so while the shot is still downloading there is
   nothing to give it size — this holds it open and shows a rotating sphere. */
.slidePreviewCard.isLoading{
	width: 220px;
	height: 220px;
}

.plateLoader{
	position: absolute;
	inset: 0;
	display: flex;
	align-items: center;
	justify-content: center;
}

.plateLoader span{
	position: absolute;
	border-radius: 50%;
	border: 2px solid transparent;
	display: block;
}

/* two counter-rotating arcs in the site red/cyan, so the wait reads as part of the
   same glitch language rather than a generic spinner */
.plateLoader span:nth-child(1){
	width: 62px;
	height: 62px;
	border-top-color: #00c8c8;
	border-right-color: rgba(0,200,200,0.35);
	animation: plateSpin 0.9s linear infinite;
}

.plateLoader span:nth-child(2){
	width: 40px;
	height: 40px;
	border-bottom-color: #ff1e3c;
	border-left-color: rgba(255,30,60,0.3);
	animation: plateSpin 1.35s linear infinite reverse;
}

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

/*-------------------收尾图版：更多项目---------------------------*/
/* mirrors the archive plate framing, but dashed and empty — reads as a reserved slot */
.outroMark{
	position: absolute;
	/* Sits exactly where a real archive plate sits, and takes exactly its size. */
	top: 50%;
	right: 96px;
	transform: translateY(-50%);
	/* --plateW / --plateH are the MEASURED box of a real plate image (front.js
	   publishPlateBox), so this frame is identical to one by construction */
	width: var(--plateW, 900px);
	height: var(--plateH, 563px);
	padding: 10px;
	display: flex;
	align-items: center;
	justify-content: center;
	border: 1px dashed rgba(255,255,255,0.22);
	background: rgba(255,255,255,0.02);
	z-index: 1;
}

.outroMark span{
	font-family: "megrim";
	font-size: 92px;
	line-height: 1;
	color: rgba(255,255,255,0.32);
}

/* the outro slide has no artwork, so it keeps the plain dark ground */
.sliderSlideOutro{
	background: #050505;
}

/* Text is the hero now: no card, no border — it sits directly on the wash, left aligned
   and vertically centred, opposite the plate. Sized in canvas px, not vw. */
.slideInfoPanel{
	position: absolute;
	/* pushed inward so the type sits close to the plate — the two columns read as one
	   composition centred on the canvas, rather than two things pinned to opposite edges */
	left: 112px;
	top: 50%;
	transform: translateY(-50%);
	width: 418px;
	max-width: none;
	box-sizing: border-box;
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: 18px;
	padding: 0;
	background: none;
	border: none;
	z-index: 2;
}

.slideInfoIndex{
	font-size: 12px;
	letter-spacing: 5px;
	color: rgba(255,255,255,0.42);
}

.slideInfoTitle{
	font-family: "megrim";
	font-size: 76px;
	line-height: 1.04;
	letter-spacing: 3px;
	color: rgba(255,255,255,0.95);
	text-shadow: 0 4px 40px rgba(0,0,0,0.7);
}

.slideInfoDesc{
	font-size: 15px;
	line-height: 1.8;
	letter-spacing: 0.4px;
	color: rgba(255,255,255,0.62);
	max-width: none;
	text-shadow: 0 2px 18px rgba(0,0,0,0.8);
}

/* text-swap transition: only the copy changes, the card itself stays put. steps() on the
   opacity keeps it flickering out rather than smoothly fading, matching the site's glitch
   language, and the small stagger makes the lines break up instead of moving as one block. */
.slideInfoIndex,
.slideInfoTitle,
.slideInfoDesc{
	transition: opacity 0.17s steps(3, end), transform 0.17s ease-out, filter 0.17s ease-out;
}

/* the button is deliberately NOT in this list — its label and styling are identical for
   every project, so animating it would just be movement with nothing to communicate */
.slideInfoPanel.isSwapping .slideInfoIndex,
.slideInfoPanel.isSwapping .slideInfoTitle,
.slideInfoPanel.isSwapping .slideInfoDesc{
	opacity: 0;
	transform: translateX(-12px) skewX(-6deg);
	filter: blur(2px);
}

.slideInfoPanel.isSwapping .slideInfoTitle{ transition-delay: 0.03s; }
.slideInfoPanel.isSwapping .slideInfoDesc{ transition-delay: 0.06s; }

.slideAccessBtn{
	align-self: flex-start;
	margin-top: 8px;
	padding: 15px 32px;
	border: 1px solid rgba(255,255,255,0.4);
	background: rgba(0,0,0,0.3);
	color: rgba(255,255,255,0.9);
	font-size: 12px;
	letter-spacing: 3.5px;
	cursor: pointer;
	transition: border-color 0.2s ease-in-out, color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}

.slideAccessBtn:hover,
.slideAccessBtn:focus-visible{
	border-color: #00c8c8;
	color: #00c8c8;
	box-shadow: 0 0 18px rgba(0,200,200,0.3);
	outline: none;
}

.sliderArrow{
	position: absolute;
	top: 50%;
	transform: translateY(-50%);
	width: 48px;
	height: 48px;
	border: 1px solid rgba(255,255,255,0.18);
	background: rgba(0,0,0,0.4);
	color: rgba(255,255,255,0.6);
	font-size: 18px;
	cursor: pointer;
	display: flex;
	align-items: center;
	justify-content: center;
	transition: border-color 0.2s ease-in-out, color 0.2s ease-in-out;
	z-index: 2;
}

.sliderArrow:hover,
.sliderArrow:focus-visible{
	border-color: #00c8c8;
	color: #00c8c8;
	outline: none;
}

/* tucked into the outer margins so they clear the text block (left:78px) and the
   plate (right:76px) instead of sitting on top of them */
.sliderArrowPrev{ left: 14px; }
.sliderArrowNext{ right: 12px; }

.sliderDots{
	position: absolute;
	left: 0; right: 0;
	bottom: 40px;
	display: flex;
	justify-content: center;
	gap: 10px;
	z-index: 2;
}

.sliderDot{
	width: 8px;
	height: 8px;
	border-radius: 50%;
	border: 1px solid rgba(255,255,255,0.35);
	background: transparent;
	cursor: pointer;
	padding: 0;
}

.sliderDot.isActive{
	background: #00c8c8;
	border-color: #00c8c8;
}

#hubTitleIv4{
	font-family: "Helvetica";
	position: absolute;
	bottom: 10px;
	right: 10px;
	font-size: 13px;
	color: rgba(255,255,255,0.55);
}

/*-------------------滚动提示---------------------------*/
#scrollHintWrap{
	position: absolute;
	left: 0; right: 0;
	bottom: 26px;
	z-index: 5;
	pointer-events: none;
	text-align: center;
	transition: opacity 0.25s ease-out;
}

.scrollHint{
	display: inline-block;
	color: rgba(255,255,255,0.4);
	font-size: 11px;
	letter-spacing: 3px;
	animation: hintPulse 2.4s ease-in-out infinite;
}

@keyframes hintPulse{
	0%, 100% { opacity: 0.25; }
	50% { opacity: 0.65; }
}

/*-------------------闲置边框故障提示（唤起注意，不依赖悬停）---------------------------*/
/* absolute (not fixed) because the site scales <body> with `zoom`, which distorts
   fixed-element sizing and pushed the right/bottom edges off-screen. .stage is already
   fixed at inset:0, so absolute inside it tracks the viewport exactly under any zoom. */
#idleBorderHint{
	position: absolute;
	inset: 0;
	z-index: 6;
	pointer-events: none;
	/* 0 = just arrived or just interacted, 1 = deeply idle. JS ramps this up the longer
	   the visitor sits still and snaps it back to 0 on any interaction. Every value
	   below scales off it, so one number drives the whole escalation. */
	--idleLevel: 0;
	/* master on/off gate, multiplied into every opacity below. On desktop JS turns this on
	   only while the cursor is outside the black front panel; on touch devices it comes on
	   3s after load and stays on, since there's no hover to react to. */
	--hintOn: 0;
	--edgeSplit: calc(2px + 3px * var(--idleLevel));
	/* how far each band reaches inward from its edge — kept shallow so it stays a framing
	   detail at the sides rather than creeping toward the centre of the page */
	--edgeDepth: calc(46px + 46px * var(--idleLevel));
	/* the three brightness steps the glitch snaps between — subtle at rest, and still
	   restrained when fully idle so it never competes with the centre panel */
	--edgeDim: calc((0.16 + 0.16 * var(--idleLevel)) * var(--hintOn));
	--edgeMid: calc((0.30 + 0.22 * var(--idleLevel)) * var(--hintOn));
	--edgeHot: calc((0.48 + 0.26 * var(--idleLevel)) * var(--hintOn));
}

/* four independent bars — one per edge — so every trigger visibly hits all four sides
   instead of relying on a single clip-path sweep whose timing made one edge dominate */
.idleEdge{
	position: absolute;
	/* stays 0 if animations are unavailable, and `both` fill makes the randomized
	   start delay hold the 0% keyframe instead of flashing a solid bar on load */
	opacity: 0;
	/* continuous glitch — never stops. steps(1,end) means every keyframe HOLDS then SNAPS,
	   so this reads as hard digital flicker/tearing rather than a smooth colour fade.
	   Duration and delay are randomized per edge in JS and re-rolled periodically, so the
	   rate stays glitchy while the rhythm never settles into a predictable pattern. */
	animation-duration: var(--glitchDur, 1.4s);
	animation-delay: var(--glitchDelay, 0s);
	animation-timing-function: steps(1, end);
	animation-iteration-count: infinite;
	animation-fill-mode: both;
}

/* left + right edges only — the top/bottom bands crowded the composition, and the two
   side columns frame the centred panel without fighting it */
.idleEdge{ animation-name: idleEdgeGlitchV; }

.idleEdgeLeft, .idleEdgeRight{ top: 0; bottom: 0; width: var(--edgeDepth); }
.idleEdgeLeft{ left: 0; }
.idleEdgeRight{ right: 0; }

/* NO mix-blend-mode here: `screen` against the white idle background always resolves to
   white, which made every edge invisible on the front page. Solid colors instead, so the
   bars read on both the white idle state and the black broken-in state.
   Each band is a gradient: a hard bright line right at the frame edge that falls off
   toward the centre, so the glitch bleeds inward instead of hugging the very border. */
.idleEdge::before,
.idleEdge::after{
	content: "";
	position: absolute;
	inset: 0;
}

.idleEdgeLeft::before{
	background: linear-gradient(to right, #ff1e3c 0, #ff1e3c 2px, rgba(255,30,60,0.28) 16%, rgba(255,30,60,0) 78%);
}
.idleEdgeLeft::after{
	background: linear-gradient(to right, #00c8c8 0, #00c8c8 2px, rgba(0,200,200,0.25) 16%, rgba(0,200,200,0) 78%);
}

.idleEdgeRight::before{
	background: linear-gradient(to left, #ff1e3c 0, #ff1e3c 2px, rgba(255,30,60,0.28) 16%, rgba(255,30,60,0) 78%);
}
.idleEdgeRight::after{
	background: linear-gradient(to left, #00c8c8 0, #00c8c8 2px, rgba(0,200,200,0.25) 16%, rgba(0,200,200,0) 78%);
}

/* chromatic split across the band — widens as idle deepens */
.idleEdge::before{ transform: translateX(calc(-1 * var(--edgeSplit))); }
.idleEdge::after{  transform: translateX(var(--edgeSplit)); }

/* vertical bands tear along Y — the clip-path jumps are what sell it as a glitch:
   sections of the column blink out of existence mid-cycle */
@keyframes idleEdgeGlitchV{
	0%   { opacity: var(--edgeDim); clip-path: inset(0 0 0 0); }
	8%   { opacity: var(--edgeHot); clip-path: inset(0 0 58% 0); }
	12%  { opacity: calc(0.10 * var(--hintOn)); clip-path: inset(0 0 0 0); }
	18%  { opacity: var(--edgeHot); clip-path: inset(52% 0 0 0); }
	24%  { opacity: var(--edgeMid); clip-path: inset(0 0 0 0); }
	37%  { opacity: var(--edgeDim); clip-path: inset(20% 0 26% 0); }
	44%  { opacity: var(--edgeHot); clip-path: inset(0 0 0 0); }
	48%  { opacity: calc(0.08 * var(--hintOn)); clip-path: inset(0 0 66% 0); }
	55%  { opacity: var(--edgeMid); clip-path: inset(0 0 0 0); }
	64%  { opacity: var(--edgeDim); clip-path: inset(58% 0 6% 0); }
	74%  { opacity: var(--edgeHot); clip-path: inset(0 0 0 0); }
	79%  { opacity: calc(0.12 * var(--hintOn)); clip-path: inset(34% 0 38% 0); }
	86%  { opacity: var(--edgeMid); clip-path: inset(0 0 0 0); }
	95%  { opacity: var(--edgeDim); clip-path: inset(10% 0 0 0); }
	100% { opacity: var(--edgeDim); clip-path: inset(0 0 0 0); }
}

/* a burst doesn't swap to a different animation any more — it just drives the SAME glitch
   harder and faster, so the character never changes, only the intensity */
#idleBorderHint.isGlitching{
	--edgeDim: calc((0.26 + 0.14 * var(--idleLevel)) * var(--hintOn));
	--edgeMid: calc((0.44 + 0.18 * var(--idleLevel)) * var(--hintOn));
	--edgeHot: calc((0.62 + 0.20 * var(--idleLevel)) * var(--hintOn));
	--edgeSplit: calc(3px + 4px * var(--idleLevel));
	--edgeDepth: calc(62px + 58px * var(--idleLevel));
}

#idleBorderHint.isGlitching .idleEdge{
	animation-duration: calc(var(--glitchDur, 1.4s) * 0.34);
}

@media (prefers-reduced-motion: reduce){
	.glitchRed, .glitchCyan, .scrollHint{
		animation: none !important;
	}
	#idleBorderHint{
		display: none;
	}
}

/*-------------------移动端适配（竖屏窄屏）---------------------------*/
/* shallower bands on a phone — the desktop depth would swallow most of the screen */
html.iv4-mobile #idleBorderHint{
	--edgeDepth: calc(26px + 26px * var(--idleLevel));
}

html.iv4-mobile #idleBorderHint.isGlitching{
	--edgeDepth: calc(34px + 30px * var(--idleLevel));
}

html.iv4-mobile .iv4Link{
	width: 90vw;
	height: calc(90vw * 730 / 700);
}

html.iv4-mobile .iv4Link div span{
	font-size: calc(90vw * 200 / 700);
}

html.iv4-mobile #titleDigital{
	width: calc(90vw * 690 / 700);
	height: calc(90vw * 468 / 700);
	margin-left: calc(90vw * 10 / 700);
}

html.iv4-mobile #titleArt{
	width: calc(90vw * 690 / 700);
	height: calc(90vw * 234 / 700);
	margin-left: calc(90vw * 10 / 700);
}

html.iv4-mobile #titleIv4{
	width: calc(90vw * 75 / 700);
	height: calc(90vw * 15 / 700);
}

html.iv4-mobile .iv4Link #titleIv4{
	font-size: calc(90vw * 13 / 700);
	bottom: calc(90vw * 10 / 700);
	right: calc(90vw * 10 / 700);
}

/* keep the shatter grid's footprint matching the now-smaller front box so the
   fragments stay aligned with it instead of shattering a phantom 700x730 area */
html.iv4-mobile .shardPiece{
	width: 90vw;
	height: calc(90vw * 730 / 700);
}

html.iv4-mobile .sliderSlide .slideTitle{
	font-size: 12vw;
}

html.iv4-mobile .sliderSlide .lockGlyph{
	font-size: 8vw;
}

html.iv4-mobile .sliderSlide .slideTag{
	font-size: 3.4vw;
}

/* mobile stacks the same hierarchy vertically: archive plate up top as the exhibit,
   the type block beneath it as the main event */
/* mobile runs at zoom:100%, so vw/vh are safe here (unlike inside the zoomed desktop
   canvas). The plate still shrink-wraps — it's just centred at the top instead of right. */
/* Centred in the band above the text block, not pinned to the top — top-anchoring made
   every spare pixel pool underneath the image, which is what looked top-heavy.
   dvh repeats each value for browsers that support it (mobile URL bars change vh). */
html.iv4-mobile .slidePreviewCard{
	/* --plateTop / --plateMaxH are measured in real px by front.js (see syncInfoPanelHeight) */
	top: var(--plateTop, 40%);
	bottom: auto;
	left: 50%;
	right: auto;
	transform: translate(-50%, -50%);
	padding: 7px;
}

/* Adaptive: the plate takes ALL the height left over above the pinned text block, instead
   of a fixed vh cap. The portrait shots are very tall (≈540x1110) so height is always the
   limiting dimension — a fixed cap is what kept them small and left a dead gap below. */
html.iv4-mobile .slidePreviewMain{
	max-width: 94vw;
	max-height: var(--plateMaxH, 58vh);
}

/* the portrait plate is narrow and centred, so a full-width left-aligned text block below
   it never lined up with anything — centring the type makes the stack read as one column */
html.iv4-mobile .slideInfoPanel{
	left: 8vw;
	right: 8vw;
	/* pinned just above the dots rather than floating at a fixed vh — the plate above
	   flexes to fill whatever room this leaves */
	top: auto;
	bottom: 78px;
	transform: none;
	width: auto;
	max-width: none;
	box-sizing: border-box;
	align-items: center;
	text-align: center;
	gap: 10px;
}

/* arrows sit level with the plate instead of dead-centre, where they landed on top of
   the title and index line */
/* level with the middle of the plate — same band centre the plate is centred on */
html.iv4-mobile .sliderArrow{
	top: var(--plateTop, 40%);
	width: 40px;
	height: 40px;
	font-size: 15px;
}

html.iv4-mobile .slideInfoIndex{
	font-size: 2.9vw;
	letter-spacing: 4px;
}

html.iv4-mobile .slideInfoTitle{
	font-size: 9vw;
	letter-spacing: 2px;
}

html.iv4-mobile .slideInfoDesc{
	font-size: 3.3vw;
	line-height: 1.65;
	max-width: none;
}

/* full-width, properly tappable button — the desktop sizing came out far too small once
   scaled down to a phone */
html.iv4-mobile .slideAccessBtn{
	align-self: stretch;
	text-align: center;
	margin-top: 6px;
	padding: 14px 18px;
	font-size: 3.2vw;
	letter-spacing: 2.5px;
}

/* same box the mobile plates occupy: --plateAR carries the portrait artwork ratio, so
   the empty frame is phone-shaped and exactly as large as a real screenshot would be */
html.iv4-mobile .outroMark{
	top: var(--plateTop, 40%);
	right: auto;
	left: 50%;
	transform: translate(-50%, -50%);
	width: var(--plateW, 300px);
	height: var(--plateH, 616px);
	padding: 7px;
}

html.iv4-mobile .outroMark span{
	font-size: 76px;
}

html.iv4-mobile .sliderDots{
	bottom: 44px;
}

/* the footer sat almost on the screen edge, which made the whole bottom cluster
   (text card / dots / footer) read as squashed together */
html.iv4-mobile #hubTitleIv4{
	bottom: 16px;
	right: 14px;
}

