# Split-media hero

> An opening section with the headline, copy and actions beside an image or a muted looping video. The media reserves its aspect ratio, stacks under the copy on phones and swaps sides with one prop.

- ID: `cmp_hero_split_media_01`
- Slug: `hero-split-media-01`
- Version: `1.0.0` (current)
- Status: published
- Published: 2026-09-30
- Updated: 2026-09-30
- Available versions: `1.0.0`
- Kind: section
- Primary category: `heroes`
- Detail page: https://pagesugar.com/components/hero-split-media-01?variant=blue
- Preview: https://pagesugar.com/preview/hero-split-media-01

## Variants

| Variant | Label | Default | Artifact digest |
| --- | --- | --- | --- |
| `neutral` | Neutral | yes | `sha256-a81c4d1b65c84c87dfd7197d0586c7cebfaa376955310632cac06ff01f8c149f` |
| `blue` | Blue accent | no | `sha256-e0f699429b946f61c50054358964b7b1696a3051314cbe5f460d1553fa566dd2` |
| `amber` | Amber accent | no | `sha256-72740cfd1f944d36c71e1f4150a15648d268136f760865fcf72ae781147be55f` |

## Runtime and compatibility

- Runtime: svelte
- Svelte: 5
- SvelteKit required: no (portable Svelte component)
- Tailwind CSS: 4
- SSR: supported
- Requires client-side JavaScript: no
- Integration level: presentational
- Appearance modes: light
- Suggested directory: `src/lib/components/hero-split-media-01`

## Dependencies

No third-party runtime packages.

## Services

No external services required.

## Usage

Presentational: renders your headline, copy, links and media exactly as supplied. It does not host, resize, transcode or optimise media, and it ships no images; supply files sized for the column (about 1400 px wide covers a 2x screen). Both actions are ordinary links. Without JavaScript the video shows its poster and does not play.

Required props: `title`, `primaryAction`

```svelte
<script lang="ts">
	import SplitMediaHero from '$lib/components/hero-split-media-01/SplitMediaHero.svelte';
</script>

<SplitMediaHero
	eyebrow="Riverside Lido"
	title="Fifty metres of heated water, open all year."
	description="Lane swimming from 6:30 every morning and lessons for children from four."
	primaryAction={{ label: 'Book a lane', href: '/book' }}
	secondaryAction={{ label: 'See the timetable', href: '/timetable' }}
	media={{
		type: 'image',
		src: '/images/pool.jpg',
		alt: 'The main pool from the gallery, eight lanes with lane ropes.',
		width: 1600,
		height: 1200
	}}
/>
```

Limitations:

- Ships no media. The preview's illustrations and clip are preview-only and are not part of the export.
- One image or one video. For art direction (a different crop per breakpoint) pass a picture element through mediaSlot; the slot then owns its own sizing and aspect ratio.
- The image loads eagerly with fetchpriority="high" because a hero image is usually the page's largest paint. Below the fold, pass the image through mediaSlot with loading="lazy" instead.
- The video is muted and has no controls beyond pause and play. It is for short ambient loops, not for anything with speech or sound; use a video player for those.
- Media with a white background may need its own dark version on a dark page; the component cannot recolour an image.
- Light appearance by default. The tokens retone it for a dark or tinted page, but no dark mode is declared or selected automatically.

## Usage guide

### Split-media hero

An opening section with the headline, copy and actions on one side and an image or a short
muted video on the other. From the `lg` breakpoint the copy takes five parts of the width and
the media seven; below it, the copy comes first and the media follows at full width.

#### Media

Pass the file's own `width` and `height`. They set the frame's aspect ratio before anything
loads, so the page never jumps. The image is loaded eagerly with `fetchpriority="high"`,
because a hero image is usually the largest thing on the first screen.

```svelte
<SplitMediaHero
	title="Fifty metres of heated water, open all year."
	primaryAction={{ label: 'Book a lane', href: '/book' }}
	media={{
		type: 'image',
		src: '/images/pool.jpg',
		alt: 'The main pool from the gallery, eight lanes with lane ropes.',
		width: 1600,
		height: 1200
	}}
/>
```

##### When alt can be empty

Write alt text that says what the image shows whenever it adds something the copy does not:
a product screen, the actual venue, a menu. Use `alt: ''` only when the image is atmosphere
and the headline and paragraph already say everything; screen readers then skip it.

##### Video

```svelte
media={{
	type: 'video',
	src: '/media/timeline.mp4',
	poster: '/media/timeline-poster.webp',
	alt: 'The timeline bar for Guest seats sliding two weeks later.',
	width: 960,
	height: 720
}}
```

The clip is always muted and loops. It starts only when the visitor has not asked for reduced
motion; otherwise the poster stays until they press play. A 44 px pause and play button sits in
the frame's end corner. Translate its names with `videoLabels`:

```svelte
videoLabels={{ pause: 'השהיית הסרטון', play: 'הפעלת הסרטון' }}
```

Keep clips short and silent, encode them as H.264 MP4 for every browser, and make the poster
work on its own.

##### Art direction

For a different crop on phones, pass a `picture` element through `mediaSlot`. It renders
inside the same frame (radius, hairline, shadow), and it sets its own aspect ratio:

```svelte
<SplitMediaHero
	title="Swimming lessons for every age"
	primaryAction={{ label: 'Find a class', href: '/lessons' }}
>
	{#snippet mediaSlot()}
		<picture>
			<source
				media="(min-width: 1024px)"
				srcset="/images/pool-wide.jpg"
				width="1600"
				height="1000"
			/>
			<img
				class="block aspect-square h-auto w-full object-cover lg:aspect-[8/5]"
				src="/images/pool-square.jpg"
				alt="The learner pool from above."
				width="1000"
				height="1000"
			/>
		</picture>
	{/snippet}
</SplitMediaHero>
```

#### No media

Omit both `media` and `mediaSlot` and the hero becomes a text-only introduction, on the same
start edge as the rest of the page, with the copy held to a readable width.

#### Retoning for a dark page

```css
.my-dark-band {
	--hero-split-media-ink: #fafafa;
	--hero-split-media-muted: #a1a1aa;
	--hero-split-media-hairline: rgb(255 255 255 / 0.1);
	--hero-split-media-accent: #fafafa;
	--hero-split-media-on-accent: #18181b;
}
```

## Props

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `title` | `string` | yes |  | Headline text, set at display size. |
| `primaryAction` | `SplitMediaHeroAction` | yes |  | The one filled action: { label, href }. |
| `eyebrow` | `string` | no |  | Short label set small and uppercase above the headline. Omitted, no eyebrow renders. |
| `description` | `string` | no |  | Supporting paragraph under the headline, held to a readable measure. |
| `secondaryAction` | `SplitMediaHeroAction` | no |  | Optional quieter link beside the primary action: { label, href }. |
| `media` | `SplitMediaHeroMedia` | no |  | { type: 'image', src, alt, width, height } or { type: 'video', src, poster, alt, width, height }. Width and height are the file's intrinsic size and reserve its aspect ratio. alt may be empty only when the media is decorative. Omit media and mediaSlot for a text-only hero. |
| `mediaPosition` | 'start' \| 'end' | no | `'end'` | Which side the media takes from the lg breakpoint. Start and end follow the text direction, and the copy always comes first in the DOM. |
| `mediaSlot` | `Snippet` | no |  | Replaces the built-in media inside the same frame, for an illustration or a picture element with art direction. The snippet sets its own size. |
| `mediaCaption` | `string` | no |  | Short caption set under the media. |
| `headingLevel` | 1 \| 2 | no | `1` | 1 when the hero opens the page; 2 when the page already has an h1. |
| `videoLabels` | `{ pause: string; play: string }` | no | `{ pause: 'Pause video', play: 'Play video' }` | Accessible names for the video's pause and play button. Translate them with the rest of the page. |

## Customization

Change content through props, retone the section through seven --hero-split-media-\* CSS variables, and edit Tailwind classes in the source for the column split, spacing or type scale.

- Accent: set --hero-split-media-accent and --hero-split-media-on-accent together, keeping on-accent at 4.5:1 or better. The accent fills the primary action and draws focus rings.
- Hover: the primary's hover fill mixes 12% of --hero-split-media-accent-hover-mix into the accent. White (the default) lightens a near-black accent; set #000000 for a mid-tone accent such as the blue and amber palettes, because lightening it lowers contrast with white text.
- Text: --hero-split-media-ink sets the headline and the secondary link; --hero-split-media-muted sets the eyebrow, paragraph and caption. Keep muted at 4.5:1 against your page.
- Media frame: --hero-split-media-frame-radius rounds the frame (16px by default; 0 for square corners) and --hero-split-media-hairline draws the one-pixel edge over the media. The frame's resting fill is mixed from the ink colour, so it follows a retone.
- Dark page retone: ink #fafafa, muted #a1a1aa, hairline rgb(255 255 255 / 0.1), accent #fafafa, on-accent #18181b (all --hero-split-media-\*). The black shadow under the frame disappears on dark; the hairline carries the edge.
- Column split: the grid's lg:grid-cols-\[minmax(0,5fr)\_minmax(0,7fr)\] gives the media the larger share. Change both fractions (and the mirrored pair for mediaPosition start) to 1fr for an even split.
- Media: supply width and height from the file itself so the frame reserves the right space. Use object-cover crops; for a different crop on phones, pass a picture element through mediaSlot.
- Video: keep clips short, silent and under a few megabytes, with a poster that works on its own, because reduced-motion visitors see the poster unless they press play.
- Headings: set headingLevel to 2 when the page already has an h1.

| Token | Public CSS variable |
| --- | --- |
| `accent` | `--hero-split-media-accent` |
| `onAccent` | `--hero-split-media-on-accent` |
| `accentHoverMix` | `--hero-split-media-accent-hover-mix` |
| `ink` | `--hero-split-media-ink` |
| `muted` | `--hero-split-media-muted` |
| `hairline` | `--hero-split-media-hairline` |
| `frameRadius` | `--hero-split-media-frame-radius` |

## Accessibility

- The section is labelled by its headline, which is an h1 by default; set headingLevel to 2 when the page already has one.
- The copy comes before the media in the DOM at every width, so reading and tab order put the headline and actions first. mediaPosition moves the media with CSS only.
- Image alt text comes from you. Describe what the image shows when it carries meaning; pass alt: '' only when it is decoration and the copy already says everything.
- A video with alt text takes it as its accessible name (aria-label); with alt: '' it is hidden from assistive technology. It has no audio track expectation and is always muted.
- The video autoplays only when prefers-reduced-motion is not set, and stops and returns to its poster if the setting is switched on while it plays. With reduced motion, the poster stays until the visitor presses play (WCAG 2.2.2).
- The pause and play button is a 44 px native button whose accessible name swaps between videoLabels.pause and videoLabels.play; it names the video through aria-controls. It appears once JavaScript runs, since without it the video never moves.
- Links and the video button show a two-pixel accent focus ring on :focus-visible only; over media the ring sits on a white band so it shows against any image.
- White on-accent text measures 17.7:1 on the neutral accent (#18181b), 6.7:1 on blue (#1d4ed8) and 5.0:1 on amber (#b45309). The hover fills keep it above 4.5:1: the neutral hover lightens (about 12:1), blue and amber darken (about 7.9:1 and 6.1:1). Muted text (#52525b) measures 7.7:1 on white. Ratios use the WCAG relative-luminance formula.
- The layout uses logical properties and text-start, so it mirrors under dir="rtl"; tracking resets to 0 for right-to-left and CJK text, and CJK headlines keep words whole.
- Element IDs come from $props.id(), so two heroes on one page stay unique.

Known limitations:

- Contrast ratios are computed for the shipped palettes only; re-check any changed token (4.5:1 for text, 3:1 for the focus ring).
- The video button sits over the media on a translucent dark fill; over a very dark clip its boundary is the white/15 hairline and the white icon, not the fill.
- The component cannot check that alt text is accurate or that a video is free of flashing content; both are the consumer's responsibility.

## License

- Declared source: MIT
- Default license approval is pending. See https://pagesugar.com/docs/license.

## Source

- Palette: Blue accent (`blue`)
- Entry: `SplitMediaHero.svelte`
- Suggested directory: `src/lib/components/hero-split-media-01`
- Files: 1
- Artifact digest: `sha256-e0f699429b946f61c50054358964b7b1696a3051314cbe5f460d1553fa566dd2`

Paths below are relative to the suggested directory. Copy the files as they are;
they import nothing from this site.

#### `SplitMediaHero.svelte`

Role: entry · 12981 bytes · SHA-256 `571490444c589a7f403123d56b3b31b2e8bebcac141abf6082b0aa244bba5435`

```svelte
<script lang="ts" module>
	export interface SplitMediaHeroAction {
		label: string;
		href: string;
	}

	/**
	 * Width and height are the file's intrinsic size. They set the frame's aspect ratio before the
	 * file loads, so nothing below the hero moves. Use alt: '' only when the media is decorative.
	 */
	export type SplitMediaHeroMedia =
		| { type: 'image'; src: string; alt: string; width: number; height: number }
		| {
				type: 'video';
				src: string;
				poster: string;
				alt: string;
				width: number;
				height: number;
		  };
</script>

<script lang="ts">
	import type { Snippet } from 'svelte';

	interface Props {
		/** Headline text. */
		title: string;
		/** Short label above the headline. */
		eyebrow?: string;
		/** Supporting paragraph under the headline. */
		description?: string;
		/** The one filled action. */
		primaryAction: SplitMediaHeroAction;
		/** Optional quieter link beside the primary action. */
		secondaryAction?: SplitMediaHeroAction;
		/** Image or muted video beside the copy. Omit it, and mediaSlot, for a text-only hero. */
		media?: SplitMediaHeroMedia;
		/** Which side the media takes from the lg breakpoint. The copy always comes first in the DOM. */
		mediaPosition?: 'start' | 'end';
		/** Replaces the built-in media, for an illustration or a picture element with art direction. */
		mediaSlot?: Snippet;
		/** Short caption set under the media. */
		mediaCaption?: string;
		/** 1 when the hero opens the page, 2 when the page already has an h1. */
		headingLevel?: 1 | 2;
		/** Accessible names for the video's pause and play button. */
		videoLabels?: { pause: string; play: string };
	}

	let {
		title,
		eyebrow,
		description,
		primaryAction,
		secondaryAction,
		media,
		mediaPosition = 'end',
		mediaSlot,
		mediaCaption,
		headingLevel = 1,
		videoLabels = { pause: 'Pause video', play: 'Play video' }
	}: Props = $props();

	const uid = $props.id();

	const hasMedia = $derived(Boolean(mediaSlot || media));
	const start = $derived(mediaPosition === 'start');
	/* Reserve the file's shape before it loads; a missing or zero size falls back to 4:3. */
	const ratio = $derived(
		media && media.width > 0 && media.height > 0 ? `${media.width} / ${media.height}` : '4 / 3'
	);

	let video = $state<HTMLVideoElement>();
	let playing = $state(false);
	/* The pause control only appears once script runs: without it, the video never starts. */
	let ready = $state(false);

	/* A refused or superseded play() leaves the label on whatever the element is actually doing. */
	function play(el: HTMLVideoElement) {
		el.play().catch(() => {
			if (video === el) playing = !el.paused;
		});
	}

	// Runs once per video element; a new src renders a new element (see the key block below).
	$effect(() => {
		const el = video;
		if (!el) return;
		ready = true;
		playing = !el.paused;
		// Safari only autoplays when the muted property itself is set, not just the attribute.
		el.muted = true;
		const reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
		// Autoplay is decided here rather than with the autoplay attribute, so reduced motion never
		// sees a first frame of movement.
		if (!reduce.matches) play(el);
		// Switching reduced motion on mid-play stops the clip and returns it to its poster.
		const onChange = () => {
			if (!reduce.matches || el.paused) return;
			el.pause();
			el.load();
		};
		reduce.addEventListener('change', onChange);
		return () => {
			reduce.removeEventListener('change', onChange);
			playing = false;
		};
	});

	function toggle() {
		if (!video) return;
		if (video.paused) play(video);
		else video.pause();
	}
</script>

<section
	class="hero-split-media px-4 py-16 sm:px-6 sm:py-24 lg:px-8 lg:py-32"
	aria-labelledby="{uid}-title"
>
	<div
		class={[
			'mx-auto grid items-center gap-12 lg:gap-16',
			hasMedia
				? start
					? 'max-w-7xl lg:grid-cols-[minmax(0,7fr)_minmax(0,5fr)]'
					: 'max-w-7xl lg:grid-cols-[minmax(0,5fr)_minmax(0,7fr)]'
				: 'max-w-7xl'
		]}
	>
		<div class={['min-w-0 text-start', !hasMedia && 'max-w-4xl']}>
			{#if eyebrow}
				<p
					class="hero-split-media__eyebrow hero-split-media__tracked mb-2 text-xs/none font-medium tracking-[0.06em] text-balance break-words text-[var(--_muted)] uppercase"
				>
					{eyebrow}
				</p>
			{/if}
			<svelte:element
				this={headingLevel === 2 ? 'h2' : 'h1'}
				id="{uid}-title"
				class={[
					'hero-split-media__title hero-split-media__tracked text-4xl/[1.1] font-semibold tracking-[-0.022em] text-balance break-words text-[var(--_ink)] sm:text-5xl/[1.05] xl:text-6xl/[1.05]',
					// Without media the headline carries the section alone, so it takes the next step up.
					!hasMedia && 'lg:text-6xl/[1.05] xl:text-7xl/[1.05]'
				]}
			>
				{title}
			</svelte:element>
			{#if description}
				<p
					class="hero-split-media__prose mt-6 max-w-xl text-base/6 text-pretty break-words text-[var(--_muted)] sm:text-lg/7"
				>
					{description}
				</p>
			{/if}
			<div class="mt-8 flex flex-col gap-3 sm:flex-row sm:flex-wrap">
				<a
					href={primaryAction.href}
					class="hero-split-media__action hero-split-media__primary inline-flex min-h-12 max-w-full items-center justify-center rounded-lg bg-[var(--_accent)] px-6 py-3 text-center text-base/6 font-medium text-[var(--_on-accent)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--_accent)]"
				>
					<span class="min-w-0 [overflow-wrap:anywhere]">{primaryAction.label}</span>
				</a>
				{#if secondaryAction}
					<a
						href={secondaryAction.href}
						class="hero-split-media__action hero-split-media__secondary inline-flex min-h-12 max-w-full items-center justify-center rounded-lg px-6 py-3 text-center text-base/6 font-medium text-[var(--_ink)] ring-1 ring-[var(--_control-border)] ring-inset focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--_accent)] sm:ring-0"
					>
						<!-- The arrow is joined to the last word (U+2060) so it follows a wrapped label. -->
						<span class="min-w-0 [overflow-wrap:anywhere]"
							>{secondaryAction.label}&#8288;<svg
								class="ms-2 inline-block size-4 align-[-0.1875em] rtl:-scale-x-100"
								viewBox="0 0 16 16"
								fill="none"
								stroke="currentColor"
								stroke-width="1.75"
								stroke-linecap="round"
								stroke-linejoin="round"
								aria-hidden="true"
							>
								<path d="M3 8h10M9 4l4 4-4 4" />
							</svg></span
						>
					</a>
				{/if}
			</div>
		</div>

		{#if hasMedia}
			<figure class={['m-0 min-w-0', start && 'lg:order-first']}>
				<div
					class="hero-split-media__frame relative overflow-hidden rounded-[var(--_frame-radius)] bg-[var(--_placeholder)]"
					style:aspect-ratio={!mediaSlot && media ? ratio : undefined}
				>
					{#if mediaSlot}
						{@render mediaSlot()}
					{:else if media?.type === 'video'}
						{#key media.src}
							<video
								bind:this={video}
								id="{uid}-video"
								class="absolute inset-0 size-full object-cover"
								src={media.src}
								poster={media.poster}
								width={media.width}
								height={media.height}
								muted
								loop
								playsinline
								preload="metadata"
								disablepictureinpicture
								aria-label={media.alt || undefined}
								aria-hidden={media.alt ? undefined : 'true'}
								onplay={() => (playing = true)}
								onpause={() => (playing = false)}
							></video>
						{/key}
					{:else if media}
						<img
							class="absolute inset-0 size-full object-cover"
							src={media.src}
							alt={media.alt}
							width={media.width}
							height={media.height}
							loading="eager"
							fetchpriority="high"
							decoding="async"
						/>
					{/if}
					<!-- The hairline sits over the media so a pale image still has an edge. -->
					<span
						class="pointer-events-none absolute inset-0 rounded-[inherit] ring-1 ring-[var(--_hairline)] ring-inset"
						aria-hidden="true"
					></span>
					{#if !mediaSlot && media?.type === 'video' && ready}
						<button
							type="button"
							class="hero-split-media__control absolute end-3 bottom-3 grid size-11 place-items-center rounded-full bg-zinc-950/60 text-white ring-1 ring-white/15 backdrop-blur-md"
							aria-controls="{uid}-video"
							aria-label={playing ? videoLabels.pause : videoLabels.play}
							onclick={toggle}
						>
							{#if playing}
								<svg class="size-4" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
									<rect x="3.5" y="2.5" width="3" height="11" rx="1" />
									<rect x="9.5" y="2.5" width="3" height="11" rx="1" />
								</svg>
							{:else}
								<!-- Nudged a pixel towards the point so the triangle looks centred. -->
								<svg
									class="size-4 translate-x-px"
									viewBox="0 0 16 16"
									fill="currentColor"
									aria-hidden="true"
								>
									<path
										d="M4.5 2.9v10.2a.9.9 0 0 0 1.36.77l8.3-5.1a.9.9 0 0 0 0-1.54l-8.3-5.1a.9.9 0 0 0-1.36.77Z"
									/>
								</svg>
							{/if}
						</button>
					{/if}
				</div>
				{#if mediaCaption}
					<figcaption
						class="hero-split-media__prose mt-3 text-[13px]/5 text-balance break-words text-[var(--_muted)]"
					>
						{mediaCaption}
					</figcaption>
				{/if}
			</figure>
		{/if}
	</div>
</section>

<style>
	/* Public tokens: set --hero-split-media-* on this section or any ancestor to retone it. */
	.hero-split-media {
		--_accent: var(--hero-split-media-accent, #1d4ed8);
		--_on-accent: var(--hero-split-media-on-accent, #ffffff);
		--_ink: var(--hero-split-media-ink, #18181b);
		--_muted: var(--hero-split-media-muted, #52525b);
		--_hairline: var(--hero-split-media-hairline, rgb(0 0 0 / 0.08));
		--_frame-radius: var(--hero-split-media-frame-radius, 16px);
		/* The frame's fill before the media paints, one step off the page in the ink colour. */
		--_placeholder: color-mix(in oklab, var(--_ink) 4%, transparent);
		/*
		 * The primary's hover mixes 12% of this colour into the accent. White lightens a near-black
		 * accent; a mid-tone accent needs black, since lightening it drops white text's contrast.
		 */
		--_accent-hover-mix: var(--hero-split-media-accent-hover-mix, #000000);
		/* The outlined secondary on phones: a control boundary at 3:1, stronger than a hairline. */
		--_control-border: color-mix(in oklab, var(--_ink) 50%, transparent);
	}

	/* The one elevation: the media frame, lit from above. */
	.hero-split-media__frame {
		box-shadow:
			0 1px 2px rgb(0 0 0 / 0.05),
			0 12px 40px rgb(0 0 0 / 0.08);
	}

	.hero-split-media__action,
	.hero-split-media__control {
		transition-property: color, background-color, box-shadow, transform, opacity;
		transition-duration: 150ms;
		transition-timing-function: cubic-bezier(0.2, 0, 0, 1);
	}
	.hero-split-media__action:active,
	.hero-split-media__control:active {
		transform: scale(0.98);
		transition-duration: 80ms;
	}
	/* Hover shifts the tone one step instead of fading the button. */
	.hero-split-media__primary:hover {
		background-color: color-mix(in oklab, var(--_accent) 88%, var(--_accent-hover-mix));
	}
	.hero-split-media__secondary:hover {
		background-color: color-mix(in oklab, var(--_ink) 5%, transparent);
	}
	.hero-split-media__control:hover {
		background-color: rgb(9 9 11 / 0.75);
	}
	/* White fills the gap under the accent ring, so focus shows over any image. */
	.hero-split-media__control:focus-visible {
		outline: 2px solid var(--_accent);
		outline-offset: 2px;
		box-shadow: 0 0 0 2px #ffffff;
	}
	/* The control fades in once script runs; it sits over the media, so nothing moves. */
	@starting-style {
		.hero-split-media__control {
			opacity: 0;
		}
	}

	/* Arabic and Hebrew are never letter-spaced, and CJK is not tightened. */
	.hero-split-media__tracked:dir(rtl),
	.hero-split-media__tracked:is(:lang(ja), :lang(zh), :lang(ko)) {
		letter-spacing: 0;
	}
	/* Uppercase does nothing for Hebrew or Arabic, so the eyebrow takes a step up in size instead. */
	.hero-split-media__eyebrow:dir(rtl) {
		font-size: 0.8125rem;
	}
	/*
	 * Japanese and Chinese have no spaces: break between phrases where the browser can
	 * (auto-phrase), keep closing punctuation on its line (strict), and never mid-word.
	 * Korean spaces its words, so it keeps them whole.
	 */
	:is(.hero-split-media__title, .hero-split-media__prose):is(:lang(ja), :lang(zh)) {
		word-break: normal;
		word-break: auto-phrase;
		line-break: strict;
	}
	/* Every Japanese or Chinese character is a full em wide, so the display size steps down to keep
	   each clause on one line. */
	.hero-split-media__title:is(:lang(ja), :lang(zh)) {
		font-size: 2rem;
	}
	@media (min-width: 40rem) {
		.hero-split-media__title:is(:lang(ja), :lang(zh)) {
			font-size: 3rem;
		}
	}
	:is(.hero-split-media__title, .hero-split-media__prose):lang(ko) {
		word-break: keep-all;
	}

	@media (prefers-reduced-motion: reduce) {
		.hero-split-media__action:active,
		.hero-split-media__control:active {
			transform: none;
		}
	}
</style>
```

## Artifacts

### Blue accent (`blue`)

- Artifact digest: `sha256-e0f699429b946f61c50054358964b7b1696a3051314cbe5f460d1553fa566dd2`
- Entry: `SplitMediaHero.svelte`
- Receipt: https://pagesugar.com/artifacts/cmp_hero_split_media_01/1.0.0/blue/sha256-e0f699429b946f61c50054358964b7b1696a3051314cbe5f460d1553fa566dd2/manifest.json
- Bundle: https://pagesugar.com/artifacts/cmp_hero_split_media_01/1.0.0/blue/sha256-e0f699429b946f61c50054358964b7b1696a3051314cbe5f460d1553fa566dd2/bundle.zip (7987 bytes, sha256 `fdec4c0545b20cca33da706a22515ffd2943af90343ef7c1a336d4aac44d459d`)

Files:

- `SplitMediaHero.svelte` (entry, 12981 bytes): https://pagesugar.com/artifacts/cmp_hero_split_media_01/1.0.0/blue/sha256-e0f699429b946f61c50054358964b7b1696a3051314cbe5f460d1553fa566dd2/source/SplitMediaHero.svelte
