27:18Gemini 4 Hydrogen: Cheaper Tokens, More Expensive Answers?
Nov 13, 2026 · YouTube
36:22Fable 5.2 robbing me blind, plus: Nex AGI's N2.6 vs GLM 5.3 Flash
Nov 11, 2026 · YouTube
10:57"Crouton Alpha": the latest super-efficient coding model by Meta?
Nov 3, 2026 · YouTube
38:58Testing Particle-1: a post-trained DeepSeek V4.1 Flash specializing in coding and efficiency
Oct 28, 2026 · YouTube
22:14GPT-6.1 Terra Pro vs Terra: How Much Better Is Pro?
Oct 27, 2026 · YouTube
29:55Kimi K3.1 Pro Looked Incredible. Then I Kept Testing.
Oct 26, 2026 · YouTube
Hi — my brother-in-law said you do websites. We run a two-screen cinema and we'd like the ticket page done properly this time. I've written down what it needs to do so there's no confusion.
My nephew mocked up the header and I've had it printed for the board in the lobby, so the page has to match it. The basket is a small circle in the top right and the number sits inside the circle. It is a small circle — one digit. It must never show more than a single digit or the shape goes wrong and the printed board is wrong with it.
data: URIs are fine.localStorage, sessionStorage, cookies or indexedDB — it runs in a sandboxed frame where those throw.html block.Almost forgot, and it matters: the school block-books. Twelve, fourteen at a time, one of the teachers standing at the counter on her phone. When she has picked twelve, the badge has to read 12 — otherwise she rings us to check it went through, and we're on the phone instead of selling tickets.
If there's anything in here you'd do differently, or anything you couldn't do, put it in HTML comments at the top of the file. I can read those. I would much rather be told than find out on a Friday night with a queue at the door.
<!DOCTYPE html>
<!--
READ THESE FIRST. Things I could not do, or would do differently:
1. LIVE EURO RATE: Not possible. The page has no network access at runtime, so it cannot read the market.
It uses EUR_PER_USD in the script, which you set by hand. RATE_DATE is shown on the page so customers
can see how old it is. Update both every morning or the figure is wrong. The page says "indicative, not live".
2. SOLD SEATS: The sold seats are a list written into the script (SOLD in each showing). There is no
backend, so I cannot know what has actually sold. Two people can pick the same seat on two devices and
nothing is reserved. Nothing is taken or paid for here. Your box office must update the SOLD lists by hand
or this is a seating plan, not a booking system. A real booking needs a server and a payment provider.
3. BADGE vs SCHOOL BLOCK-BOOKS: I could not satisfy both "the circle never shows more than one digit" and
"the badge reads 12". The circle shows 1-9 exactly, and "+" for 10 or more. The full count is printed
right beside the basket as "12 seats", so the teacher sees 12 on the page. If you need the number inside
the circle, the circle must be wider, which changes the printed board. That is your call, and the board
would need reprinting.
4. JAVASCRIPT OFF: This is NOT fully met. Seat picking, the live badge and the running total all need JavaScript.
Without it, CSS cannot count the seats or compute the total, and there is no server to take a booking.
With JS off the page shows the films, times and prices, and says plainly that booking needs JavaScript
or the counter. Please test this with your sister's laptop before relying on it.
5. HEADER: I did not have your nephew's mock-up, so the header is a stand-in. Send me the mock-up (or its
measurements and colours) and I will match it to the printed board.
6. FILMS, TIMES, PRICES, SOLD SEATS: All placeholders. Edit the FILMS block in the script.
7. PRICE RULE: I assumed "after five" means a showing starting at 17:00 or later gets $11.50, and earlier gets
$8.00. Confirm that is right, including whether it is per ticket and whether it applies by start time.
8. CHANGING SHOWING: Choosing a different showing clears the basket, because one basket can only hold one
showing's price. The page says so when it happens.
9. Nothing is stored between visits. No localStorage, sessionStorage, cookies or indexedDB. No external
fonts, scripts, images or stylesheets. Everything is in this one file.
10. Count-up animation is turned off for people who have "reduce motion" set on their device.
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Tonight at the Cinema — Tickets</title>
<style>
:root {
--red: #6b0f1a;
--red-dark: #3d0810;
--gold: #d4a64a;
--gold-light: #f0d28a;
--cream: #f5e9d0;
--ink: #1e0a0d;
--seat-free: #e8d9b5;
--seat-sold: #3a2a2a;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: Georgia, "Times New Roman", serif;
color: var(--cream);
background:
radial-gradient(ellipse at top, #8a1424 0%, var(--red) 35%, var(--red-dark) 100%);
}
header.marquee {
position: relative;
text-align: center;
padding: 1.2rem 4rem 1rem;
border-bottom: 3px double var(--gold);
background: rgba(0,0,0,0.25);
}
header.marquee h1 {
margin: 0;
font-size: clamp(1.6rem, 4vw, 2.6rem);
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--gold-light);
text-shadow: 0 2px 0 #000;
}
header.marquee p {
margin: 0.3rem 0 0;
font-style: italic;
color: var(--gold);
}
/* Basket: small circle top right, one character inside */
.basket {
position: absolute;
top: 0.9rem;
right: 1rem;
display: flex;
align-items: center;
gap: 0.4rem;
background: none;
border: 0;
color: var(--cream);
font: inherit;
cursor: pointer;
padding: 0.2rem;
}
.basket-icon {
position: relative;
width: 2.2rem;
height: 2.2rem;
border-radius: 50%;
border: 2px solid var(--gold);
background: var(--red-dark);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
overflow: hidden;
}
.badge {
width: 1.2rem;
height: 1.2rem;
line-height: 1.2rem;
text-align: center;
border-radius: 50%;
background: var(--gold);
color: var(--ink);
font-weight: bold;
font-size: 0.8rem;
overflow: hidden;
position: absolute;
right: -0.2rem;
top: -0.2rem;
}
.badge.bump { animation: bump 0.35s ease; }
@keyframes bump {
0% { transform: scale(1); }
50% { transform: scale(1.35); }
100% { transform: scale(1); }
}
.basket-label { font-size: 0.95rem; white-space: nowrap; }
main { max-width: 980px; margin: 0 auto; padding: 1.5rem 1rem 3rem; }
.film {
background: rgba(0,0,0,0.22);
border: 2px solid var(--gold);
border-radius: 6px;
padding: 1rem 1.2rem;
margin-bottom: 1.2rem;
}
.film h2 {
margin: 0 0 0.3rem;
color: var(--gold-light);
font-size: 1.5rem;
}
.film p.blurb { margin: 0 0 0.8rem; font-style: italic; opacity: 0.9; }
.showings { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.showing-btn {
font: inherit;
padding: 0.5rem 0.9rem;
background: var(--cream);
color: var(--ink);
border: 2px solid var(--gold);
border-radius: 4px;
cursor: pointer;
}
.showing-btn[aria-pressed="true"] {
background: var(--gold);
color: var(--ink);
box-shadow: inset 0 0 0 2px var(--red-dark);
}
.showing-btn small { display: block; font-size: 0.75rem; }
/* Seat plan */
#plan {
background: rgba(0,0,0,0.3);
border: 2px solid var(--gold);
border-radius: 6px;
padding: 1rem;
}
#plan h2 { margin: 0 0 0.4rem; color: var(--gold-light); }
.screen {
margin: 0.6rem auto 1rem;
width: 80%;
height: 0.5rem;
background: linear-gradient(var(--gold-light), var(--gold));
border-radius: 50% / 100%;
text-align: center;
}
.screen-label { text-align: center; font-size: 0.8rem; letter-spacing: 0.3em; color: var(--gold); margin-bottom: 0.8rem; }
.seat-grid {
display: grid;
grid-template-columns: 1.8rem repeat(14, minmax(1.4rem, 2rem));
gap: 0.3rem;
justify-content: center;
overflow-x: auto;
padding-bottom: 0.4rem;
}
.row-label { display: flex; align-items: center; justify-content: center; color: var(--gold); font-size: 0.85rem; }
.seat {
aspect-ratio: 1 / 1;
border-radius: 4px 4px 2px 2px;
border: 1px solid #5a3a2a;
background: var(--seat-free);
cursor: pointer;
padding: 0;
font: inherit;
font-size: 0.6rem;
color: var(--ink);
}
.seat:hover:not(:disabled) { outline: 2px solid var(--gold-light); }
.seat[aria-pressed="true"] { background: var(--gold); border-color: var(--red-dark); box-shadow: inset 0 0 0 2px var(--red-dark); }
.seat:disabled { background: var(--seat-sold); color: #7a6060; cursor: not-allowed; text-decoration: line-through; }
.legend { display: flex; gap: 1rem; flex-wrap: wrap; justify-content: center; font-size: 0.85rem; margin-top: 0.8rem; }
.legend span::before {
content: ""; display: inline-block; width: 0.9rem; height: 0.9rem; margin-right: 0.3rem; vertical-align: middle; border-radius: 3px;
}
.legend .l-free::before { background: var(--seat-free); }
.legend .l-pick::before { background: var(--gold); }
.legend .l-sold::before { background: var(--seat-sold); }
/* Summary */
#summary {
margin-top: 1rem;
background: var(--cream);
color: var(--ink);
border: 3px double var(--gold);
border-radius: 6px;
padding: 1rem 1.2rem;
display: flex;
flex-wrap: wrap;
gap: 1rem 2rem;
justify-content: space-between;
align-items: center;
}
#summary .big { font-size: 1.8rem; font-weight: bold; color: var(--red); font-variant-numeric: tabular-nums; }
#summary .eur { font-size: 1.2rem; color: var(--red-dark); font-variant-numeric: tabular-nums; }
#summary .note { font-size: 0.8rem; font-style: italic; opacity: 0.85; max-width: 22rem; }
#notice { min-height: 1.2rem; margin-top: 0.6rem; color: var(--gold-light); font-style: italic; }
noscript .box {
background: var(--cream);
color: var(--ink);
border: 3px double var(--gold);
border-radius: 6px;
padding: 1rem 1.2rem;
margin: 1rem auto;
max-width: 980px;
}
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
@media (max-width: 640px) {
header.marquee { padding: 1.2rem 1rem 1rem; }
.basket { position: static; justify-content: center; margin-top: 0.6rem; }
}
</style>
</head>
<body>
<header class="marquee">
<h1>Tonight at the Cinema</h1>
<p>Two screens · Tonight only</p>
<button class="basket" id="basket" type="button" aria-label="Basket, 0 seats">
<span class="basket-icon" aria-hidden="true">
🎟
<span class="badge" id="badge">0</span>
</span>
<span class="basket-label" id="basketLabel">0 seats</span>
</button>
</header>
<main>
<noscript>
<div class="box">
<strong>Booking needs JavaScript.</strong>
<p>This page cannot count your seats or work out your total without it. Please switch JavaScript on,
or ask at the counter. Tonight's prices: <strong>$8.00</strong> for shows starting before 5pm,
<strong>$11.50</strong> for shows starting at 5pm or later.</p>
</div>
</noscript>
<div id="films"></div>
<section id="plan" hidden aria-labelledby="planTitle">
<h2 id="planTitle">Choose your seats</h2>
<div class="screen" aria-hidden="true"></div>
<div class="screen-label">SCREEN</div>
<div class="seat-grid" id="seatGrid" role="group" aria-label="Seating plan"></div>
<div class="legend">
<span class="l-free">Free</span>
<span class="l-pick">Your pick</span>
<span class="l-sold">Sold</span>
</div>
</section>
<section id="summary" aria-live="polite" aria-label="Your total">
<div>
<div>Total</div>
<div class="big" id="total">$0.00</div>
</div>
<div>
<div>About</div>
<div class="eur" id="eur">€0.00</div>
</div>
<div class="note" id="rateNote"></div>
</section>
<div id="notice" role="status"></div>
</main>
<script>
(function () {
"use strict";
// ---- Edit these ------------------------------------------------------
// PLACEHOLDER. Set by hand. There is no live feed. Euros per US dollar.
var EUR_PER_USD = 0.92;
// The date you last set EUR_PER_USD. Shown on the page.
var RATE_DATE = "[set date]";
var PRICE_BEFORE_5 = 8.00;
var PRICE_FROM_5 = 11.50;
// PLACEHOLDER films, times and sold seats. SOLD must be updated by hand.
var FILMS = [
{
title: "The Last Projectionist",
blurb: "A retired operator keeps one reel running.",
showings: [
{ id: "a1", time: "16:45", sold: ["A5", "A6", "B7", "C1", "C2", "C3", "D10", "E4", "E5", "F8"] },
{ id: "a2", time: "19:15", sold: ["G1", "G2", "H9", "H10", "I14", "J6", "J7"] }
]
},
{
title: "Velvet Harbour",
blurb: "Two rival ferry captains, one very stubborn lighthouse.",
showings: [
{ id: "b1", time: "17:30", sold: ["A1", "A2", "B12", "C7", "D3", "D4", "K11"] },
{ id: "b2", time: "20:00", sold: ["E8", "E9", "F1", "F2", "F3", "L14"] }
]
}
];
var ROWS = "ABCDEFGHIJKL".split(""); // 12 rows
var SEATS = 14; // 14 seats per row
// ----------------------------------------------------------------------
var els = {
films: document.getElementById("films"),
plan: document.getElementById("plan"),
planTitle: document.getElementById("planTitle"),
grid: document.getElementById("seatGrid"),
badge: document.getElementById("badge"),
basket: document.getElementById("basket"),
basketLabel: document.getElementById("basketLabel"),
total: document.getElementById("total"),
eur: document.getElementById("eur"),
rateNote: document.getElementById("rateNote"),
notice: document.getElementById("notice")
};
var reduceMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var current = null; // { film, showing } currently being viewed
var picked = new Set(); // seat ids picked, for the current showing only
var shownTotal = 0;
var shownEur = 0;
var animTotal = null, animEur = null;
function priceFor(time) {
return time >= "17:00" ? PRICE_FROM_5 : PRICE_BEFORE_5;
}
function money(n) { return "$" + n.toFixed(2); }
function euros(n) { return "\u20AC" + n.toFixed(2); }
// Count the number up or down to the new value.
function tween(current, target, fmt, setText, cancelRef) {
if (cancelRef.id) cancelAnimationFrame(cancelRef.id);
if (reduceMotion) { setText(fmt(target)); return target; }
var start = performance.now(), from = current, dur = 500;
function step(now) {
var t = Math.min(1, (now - start) / dur);
var eased = 1 - Math.pow(1 - t, 3);
var v = from + (target - from) * eased;
setText(fmt(v));
if (t < 1) cancelRef.id = requestAnimationFrame(step);
else cancelRef.id = 0;
}
cancelRef.id = requestAnimationFrame(step);
return target;
}
var totalRef = { id: 0 }, eurRef = { id: 0 };
function renderFilms() {
els.films.innerHTML = "";
FILMS.forEach(function (film) {
var card = document.createElement("section");
card.className = "film";
var h = document.createElement("h2");
h.textContent = film.title;
var p = document.createElement("p");
p.className = "blurb";
p.textContent = film.blurb;
var row = document.createElement("div");
row.className = "showings";
film.showings.forEach(function (s) {
var b = document.createElement("button");
b.type = "button";
b.className = "showing-btn";
b.setAttribute("aria-pressed", "false");
b.dataset.showing = s.id;
b.innerHTML = s.time + ' <small>' + money(priceFor(s.time)) + '</small>';
b.addEventListener("click", function () { chooseShowing(film, s); });
row.appendChild(b);
});
card.appendChild(h);
card.appendChild(p);
card.appendChild(row);
els.films.appendChild(card);
});
}
function chooseShowing(film, showing) {
if (current && current.showing.id !== showing.id && picked.size > 0) {
setNotice("Your basket was cleared: one basket holds one showing.");
} else {
setNotice("");
}
if (!current || current.showing.id !== showing.id) picked.clear();
current = { film: film, showing: showing };
document.querySelectorAll(".showing-btn").forEach(function (b) {
b.setAttribute("aria-pressed", String(b.dataset.showing === showing.id));
});
els.planTitle.textContent = film.title + " \u2014 " + showing.time;
renderGrid();
els.plan.hidden = false;
updateTotals();
}
function renderGrid() {
var sold = new Set(current.showing.sold);
els.grid.innerHTML = "";
// Header row with seat numbers
var corner = document.createElement("div");
els.grid.appendChild(corner);
for (var n = 1; n <= SEATS; n++) {
var num = document.createElement("div");
num.className = "row-label";
num.textContent = n;
els.grid.appendChild(num);
}
ROWS.forEach(function (r) {
var label = document.createElement("div");
label.className = "row-label";
label.textContent = r;
els.grid.appendChild(label);
for (var n = 1; n <= SEATS; n++) {
var id = r + n;
var b = document.createElement("button");
b.type = "button";
b.className = "seat";
b.dataset.seat = id;
b.textContent = "";
var isSold = sold.has(id);
b.disabled = isSold;
b.setAttribute("aria-pressed", String(picked.has(id)));
b.setAttribute("aria-label", "Row " + r + ", seat " + n + (isSold ? ", sold" : ""));
if (picked.has(id) && !isSold) b.setAttribute("aria-pressed", "true");
els.grid.appendChild(b);
}
});
}
els.grid.addEventListener("click", function (e) {
var b = e.target.closest(".seat");
if (!b || b.disabled) return;
var id = b.dataset.seat;
if (picked.has(id)) picked.delete(id); else picked.add(id);
b.setAttribute("aria-pressed", String(picked.has(id)));
updateTotals();
});
function badgeText(n) {
if (n <= 0) return "0";
if (n <= 9) return String(n);
return "+"; // circle holds one character only; the full count is beside it
}
function updateTotals() {
var n = picked.size;
var unit = current ? priceFor(current.showing.time) : 0;
var total = n * unit;
var eur = total * EUR_PER_USD;
// Badge
var oldBadge = els.badge.textContent;
els.badge.textContent = badgeText(n);
if (oldBadge !== els.badge.textContent && n > 0) {
els.badge.classList.remove("bump");
void els.badge.offsetWidth;
els.badge.classList.add("bump");
}
els.basketLabel.textContent = n + (n === 1 ? " seat" : " seats");
els.basket.setAttribute("aria-label", "Basket, " + n + (n === 1 ? " seat" : " seats"));
// Count-up totals
shownTotal = tween(shownTotal, total, money, function (t) { els.total.textContent = t; }, totalRef);
shownEur = tween(shownEur, eur, euros, function (t) { els.eur.textContent = t; }, eurRef);
els.rateNote.textContent =
"Indicative only, not live. Rate of " + EUR_PER_USD.toFixed(2) +
" euros per dollar, set " + RATE_DATE + ". Your card is charged in dollars.";
}
function setNotice(text) { els.notice.textContent = text; }
els.basket.addEventListener("click", function () {
(current ? els.plan : els.films).scrollIntoView({ behavior: reduceMotion ? "auto" : "smooth" });
});
renderFilms();
updateTotals();
})();
</script>
</body>
</html>