C-Well In-Place Reel Player
Codebook entry. The player from c-wellpics.com/investment — InvestmentReel.svelte. Copy it, drop it in, it works.
The analogy: a picture frame that turns into a television when you tap it, plays the film, then turns back into the picture when the film ends. The frame never moves, nothing pops open over the page, and you never have to close anything yourself.
The demo below is that exact player. It's set to stop after 10 seconds so you don't have to sit through the whole reel — on the real site it runs to the end and closes itself then.
The four things that make it work
1. It plays in place. No lightbox, no overlay over the whole page. The poster and the player are two layers stacked in the same box, and the player sits on top:
.inv-reel-frame { position: relative; padding-top: 56.25%; }
.inv-reel-layer { position: absolute; inset: 0; width: 100%; height: 100%; }
padding-top: 56.25% is the 16:9 box — 9 ÷ 16. The frame holds its shape before anything loads, so the page never jumps.
2. The poster stays mounted. This one is a scar, not a preference. An earlier version crossfaded the poster and the player as two separate branches. When the transition failed to resolve, both were left in the DOM at opacity 0 — and a stranded iframe keeps playing audio behind a page that looks stopped. Overlaying instead means the player always unmounts and the poster is simply revealed again.
3. It closes itself when the film ends. Subscribe on ready, not before — asking for ended before the player exists silently never fires, which is the bug that makes people believe Vimeo doesn't report the end of playback:
if (ev === 'ready') post({ method: 'addEventListener', value: 'ended' });
if (ev === 'ended' || ev === 'finish') playing = false;
4. The player wears the brand colour. One query param on the embed URL, honoured on paid Vimeo plans:
p.set('color', PLAYER_COLOR); // 'c6613f' == --terra
One constant, not a hex pasted into three components, so the player accent can't quietly drift from the token the buttons use.
If you lift this into another site
The embed needs permission to start itself. If the host sends a Permissions-Policy header, autoplay must be delegated to Vimeo explicitly or the player loads, reports ready, and sits frozen on frame one:
Permissions-Policy: autoplay=(self "https://player.vimeo.com")
allow="autoplay" on the iframe only passes along a permission the page itself was granted. This exact thing froze the demo above at 00:02.
Full source
InvestmentReel.svelte:
<script lang="ts">
import { srcset, SIZES } from '$lib/img';
// The reel plays IN PLACE on the page — no lightbox — and returns to its own
// thumbnail when the film ends, on a Svelte transition. (KC, 2026-07-26)
import { base } from '$app/paths';
import { scale, fade } from 'svelte/transition';
import { cubicOut } from 'svelte/easing';
import { prefersReducedMotion } from 'svelte/motion';
import { INVESTMENT, reelEmbeds, REELS_LIVE } from '$lib/content';
import { playerSrc } from '$lib/vimeo';
// Poster frames are real files in static/media so the section paints instantly
// and doesn't wait on Vimeo just to show a still.
const POSTERS: Record<string, { poster: string; title: string }> = {
'462882464': { poster: '/media/reel-462882464.jpg', title: 'COLORLESS | TROY' },
'108753983': { poster: '/media/reel-108753983.jpg', title: 'Frustrated Chapter 2' }
};
// Studio list wins; the seeded reels are the fallback so /investment is never
// left with an empty frame.
const EMBEDS = REELS_LIVE.length
? REELS_LIVE.map((r) => ({ id: r.id, h: r.h }))
: reelEmbeds(INVESTMENT.reels);
const POSTER_OF = new Map(REELS_LIVE.map((r) => [r.id, { poster: r.poster, title: r.title }]));
// Rotate per visit, the way the dynamic page re-rolled per request.
let index = $state(0);
$effect(() => {
if (EMBEDS.length > 1) index = Math.floor(Math.random() * EMBEDS.length);
});
const reel = $derived(EMBEDS[index]);
const meta = $derived(reel ? (POSTER_OF.get(reel.id) ?? POSTERS[reel.id]) : undefined);
let playing = $state(false);
let frame = $state<HTMLIFrameElement | null>(null);
// The poster stays MOUNTED and the player sits on top of it. An earlier pass
// crossfaded between the two as separate branches; when the transition
// couldn't resolve, BOTH were left in the DOM at opacity 0 — and a stranded
// iframe keeps playing audio behind a page that looks stopped. Overlaying
// means the player always unmounts and the thumbnail is simply revealed.
const dur = $derived(prefersReducedMotion.current ? 0 : 420);
// Auto-close when the film ends. Subscribing on 'ready' matters: asking for
// 'ended' before the player exists silently never fires — the bug that makes
// people think Vimeo doesn't report the end of playback.
$effect(() => {
if (!playing) return;
const ORIGIN = 'https://player.vimeo.com';
const post = (msg: unknown) => {
try { frame?.contentWindow?.postMessage(JSON.stringify(msg), ORIGIN); } catch { /* frame gone */ }
};
const onMsg = (e: MessageEvent) => {
if (e.origin !== ORIGIN) return;
let d: unknown = e.data;
if (typeof d === 'string') { try { d = JSON.parse(d); } catch { return; } }
const ev = (d as { event?: string })?.event;
if (ev === 'ready') post({ method: 'addEventListener', value: 'ended' });
if (ev === 'ended' || ev === 'finish') playing = false;
};
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') playing = false; };
window.addEventListener('message', onMsg);
window.addEventListener('keydown', onKey);
return () => {
window.removeEventListener('message', onMsg);
window.removeEventListener('keydown', onKey);
};
});
</script>
{#if reel}
<section class="inv-reel">
<div class="inv-reel-frame">
<button
class="inv-reel-layer inv-reel-poster"
onclick={() => (playing = true)}
aria-label={`Play ${meta?.title ?? 'the reel'}`}
tabindex={playing ? -1 : 0}
aria-hidden={playing}
>
{#if meta}
<img src={meta.poster.startsWith('http') ? meta.poster : `${base}${meta.poster}`} srcset={meta.poster.startsWith('http') ? undefined : srcset(meta.poster)} sizes={SIZES.hero} alt="" width="1280" height="720" />
{/if}
<span class="inv-reel-play" aria-hidden="true">
<svg width="58" height="58" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="3" stroke-linejoin="round"><polygon points="8 5.5 18.5 12 8 18.5" /></svg>
</span>
{#if meta?.title}<span class="inv-reel-title">{meta.title}</span>{/if}
</button>
{#if playing}
<!-- Sits ON the thumbnail. On exit it scales back down and fades,
so the reel visibly returns to the frame it came from. -->
<div
class="inv-reel-layer inv-reel-player"
in:scale={{ start: 0.96, opacity: 0, duration: dur, easing: cubicOut }}
out:scale={{ start: 0.96, opacity: 0, duration: dur, easing: cubicOut }}
>
<iframe
bind:this={frame}
src={playerSrc(reel.id, reel.h)}
title={meta?.title ?? 'C-Well Pictures reel'}
allow="autoplay; fullscreen; picture-in-picture"
allowfullscreen
></iframe>
<button class="inv-reel-stop" onclick={() => (playing = false)} aria-label="Stop the reel" transition:fade={{ duration: dur / 2 }}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><line x1="6" y1="6" x2="18" y2="18" /><line x1="18" y1="6" x2="6" y2="18" /></svg>
</button>
</div>
{/if}
</div>
</section>
{/if}
<style>
.inv-reel { margin: 0 auto clamp(56px, 8vh, 96px); max-width: 960px; }
.inv-reel-frame {
position: relative; padding-top: 56.25%; border-radius: 16px; overflow: hidden;
background: #0c0c0b; box-shadow: 0 24px 70px -36px rgba(12,12,11,.6);
}
.inv-reel-layer { position: absolute; inset: 0; width: 100%; height: 100%; }
.inv-reel-player { z-index: 2; background: #0c0c0b; border-radius: 16px; overflow: hidden; }
.inv-reel-layer iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.inv-reel-poster { padding: 0; border: none; background: none; cursor: pointer; display: block; }
.inv-reel-poster img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .7s var(--ease-word), filter .4s ease; }
.inv-reel-poster::after {
content: ''; position: absolute; inset: 0; pointer-events: none;
background: linear-gradient(to top, rgba(12,12,11,.55) 2%, rgba(12,12,11,.1) 45%, rgba(12,12,11,.25));
}
.inv-reel-poster:hover img { transform: scale(1.03); }
.inv-reel-play {
position: absolute; inset: 0; z-index: 2; display: flex; align-items: center; justify-content: center;
color: rgba(255,255,255,.98); padding-left: 6px;
filter: drop-shadow(0 0 1.5px rgba(0,0,0,.9)) drop-shadow(0 3px 14px rgba(0,0,0,.5));
transition: transform .2s ease, color .2s ease;
}
.inv-reel-poster:hover .inv-reel-play { transform: scale(1.1); color: var(--terra-h); }
.inv-reel-title {
position: absolute; left: 0; right: 0; bottom: 0; z-index: 2; padding: 24px clamp(16px, 3vw, 28px) 16px;
font-family: var(--font-mark), sans-serif; font-size: .72rem; font-weight: 600;
letter-spacing: .18em; text-transform: uppercase; color: rgba(250,249,245,.92); text-align: left;
}
/* stop control — the canonical close chip, scoped inside the frame */
.inv-reel-stop {
position: absolute; top: 12px; right: 12px; z-index: 3;
width: 40px; height: 40px; display: flex; align-items: center; justify-content: center;
border: none; border-radius: 50%; cursor: pointer; color: #fff;
background: rgba(20,20,19,.72); box-shadow: 0 2px 14px rgba(0,0,0,.45);
transition: background .2s var(--ease-snap), transform .25s var(--ease-snap);
}
.inv-reel-stop svg { width: 18px; height: 18px; }
.inv-reel-stop:hover { background: var(--terra); transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) {
.inv-reel-poster img, .inv-reel-poster:hover img, .inv-reel-play { transition: none; transform: none; }
}
</style>
vimeo.ts — the URL builder it calls:
// Vimeo player accent. NOT an API call — it's a URL parameter on the embed, and
// Vimeo only honours it on paid plans (C-Well is `plus`).
//
// This hex must stay equal to --terra in app.css, rgb(198,97,63). It lived
// hardcoded in three components, which is three chances to drift from the token
// the rest of the site uses. One constant now, so the player accent and the
// buttons can never disagree.
export const PLAYER_COLOR = 'c6613f';
/** Player URL for a normal, user-initiated play. */
export function playerSrc(id: string, hash = '', opts: { autoplay?: boolean } = {}) {
const p = new URLSearchParams();
if (hash) p.set('h', hash);
if (opts.autoplay !== false) p.set('autoplay', '1');
p.set('title', '0');
p.set('byline', '0');
p.set('portrait', '0');
p.set('badge', '0');
p.set('pip', '0');
// Strip Vimeo's own social chrome — the heart, watch-later clock and share
// arrow that sat over the top-right corner of the frame (and collided with
// our stop button). These ARE controllable per-embed; the Vimeo wordmark
// bottom-right is NOT — that one is an account setting on vimeo.com.
p.set('like', '0');
p.set('watchlater', '0');
p.set('share', '0');
p.set('dnt', '1'); // do-not-track: no Vimeo analytics cookies on our visitors
p.set('color', PLAYER_COLOR);
return `https://player.vimeo.com/video/${id}?${p}`;
}
/** Muted looping preview (Vimeo "background" mode) for hover states. */
export function previewSrc(id: string, hash = '') {
const p = new URLSearchParams();
if (hash) p.set('h', hash);
p.set('background', '1');
p.set('autoplay', '1');
p.set('loop', '1');
p.set('muted', '1');
p.set('dnt', '1');
return `https://player.vimeo.com/video/${id}?${p}`;
}