// The public clip gallery: authored screenplay scenes recorded through // the reel, each in two takes โ€” through the wizard's eyes and from the // board above. Standalone pages, no app bundle: a clip link must play // anywhere a browser lands, and unfurl anywhere it's pasted. import type { ClipMeta } from "./store.js"; const esc = (s: string) => s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); const PAGE_CSS = ` * { box-sizing: border-box; } body { margin: 0; background: #171a20; color: #d8d2c0; font-family: "Archivo Narrow", sans-serif; } a { color: #e0b34a; } .wrap { max-width: 1100px; margin: 0 auto; padding: 1.4rem 1.25rem 3rem; } .mast { display: flex; align-items: baseline; gap: 1rem; margin-bottom: 1.6rem; border-bottom: 1px solid rgba(233, 225, 203, 0.18); padding-bottom: 0.7rem; } .mast a { font-family: "Oswald", sans-serif; text-transform: uppercase; letter-spacing: 0.18em; font-size: 0.95rem; color: #e9e1cb; text-decoration: none; } .mast .crumb { color: #8d8672; font-size: 0.85rem; letter-spacing: 0.06em; } .mast .crumb a { font: inherit; letter-spacing: inherit; text-transform: none; color: inherit; } .mast .deal { margin-left: auto; font-size: 0.8rem; letter-spacing: 0.12em; color: #e0b34a; } h1 { font-family: "Oswald", sans-serif; font-weight: 500; font-size: 1.7rem; letter-spacing: 0.06em; text-transform: uppercase; margin: 0 0 0.3rem; color: #e9e1cb; } h1 a { color: inherit; text-decoration: none; } p.sub { color: #a49c86; margin: 0 0 1.6rem; font-size: 1.05rem; max-width: 46rem; } .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1.4rem; } .card { background: #1f232d; border: 1px solid rgba(233, 225, 203, 0.18); border-radius: 8px; overflow: hidden; text-decoration: none; color: inherit; display: block; } .card:hover { border-color: #e0b34a; } .card img { width: 100%; display: block; aspect-ratio: 736 / 577; object-fit: cover; } .card .meta { padding: 0.7rem 0.9rem 0.9rem; } .card .t { font-family: "Oswald", sans-serif; color: #e0b34a; letter-spacing: 0.06em; text-transform: uppercase; font-size: 0.95rem; } .card .b { font-size: 0.95rem; line-height: 1.4; margin-top: 0.3rem; color: #b5b0a1; } .card .s { float: right; color: #8d8672; font-size: 0.85rem; font-family: "Courier Prime", monospace; } video { width: 100%; border-radius: 6px; display: block; background: #000; } .takes { display: grid; gap: 1.5rem; margin-top: 1.4rem; } .take h2 { font-family: "Oswald", sans-serif; font-size: 0.9rem; color: #e0b34a; font-weight: 500; letter-spacing: 0.14em; text-transform: uppercase; margin: 0 0 0.5rem; } .foot { margin-top: 2.2rem; color: #8d8672; font-size: 0.95rem; } .share { font: inherit; font-size: 0.85rem; letter-spacing: 0.12em; text-transform: uppercase; cursor: pointer; font-family: "Oswald", sans-serif; color: #43331f; background: #e9e1cb; border: 1.5px solid #43331f; border-radius: 3px; padding: 0.4rem 1rem; margin: -0.6rem 0 0.4rem; } .share:hover { background: #f2d27a; } .share.done { background: #d8d2c0; } `; // The share button: the phone's own share sheet where there is one, // the clipboard everywhere else. The page URL is the canonical link. const SHARE_JS = ` var b = document.querySelector(".share"); b.addEventListener("click", function () { var data = { title: b.dataset.title, text: b.dataset.text, url: location.origin + location.pathname }; if (navigator.share) { navigator.share(data).catch(function () {}); return; } navigator.clipboard.writeText(data.url).then(function () { b.textContent = "Link copied"; b.classList.add("done"); setTimeout(function () { b.textContent = "Share this clip"; b.classList.remove("done"); }, 1800); }); }); `; const shell = (metas: string[], body: string) => ` ${metas.join("\n")}
${body}
`; /** The 1200x630 share card cut by deploy/clips-prep.mjs, with the * dimensions that let Facebook show it on the very first share. Falls * back to the poster on a set that was never prepped. */ function cardImageMetas(clip: ClipMeta, base: string): string[] { const prepped = clip.width !== undefined; const image = `${base}/clips/${clip.name}${prepped ? "-card" : ""}.jpg`; return [ ``, ...(prepped ? [ ``, ``, ] : []), ``, ]; } export function clipsIndexHtml(clips: ClipMeta[], base: string): string { const metas = [ `Wiz-War clips from the labyrinth`, ``, ``, ``, ``, ``, ...(clips[0] ? cardImageMetas(clips[0], base) : []), ``, ]; const cards = clips.map((c) => ` ${esc(c.title)}
${c.seconds}s ${esc(c.title)}
${esc(c.blurb)}
`).join("\n"); const body = `

Clips from the labyrinth

Scenes of Wiz-War, played by the rules and filmed through the wizards' own eyes. Each clip has a first-person take and a board take of the same moves.

${cards}

Wiz-War is Tom Jolly's classic of magical combat. This is a fan-built table, and you can deal yourself in.

`; return shell(metas, body); } export function clipPageHtml(clip: ClipMeta, base: string): string { const metas = [ `${esc(clip.title)} ยท Wiz-War clip`, ``, ``, ``, ``, ``, ...cardImageMetas(clip, base), ``, ``, ``, ...(clip.width && clip.height ? [ ``, ``, ] : []), ``, ]; const body = `

${esc(clip.title)}

${esc(clip.blurb)}

Through the wizard's eyes

The same scene, from the board

Played by the real rules engine, so every move on screen is a legal move. Deal yourself in.

`; return shell(metas, body); }