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.
<!--
================================================================================
NOTES FOR THE OWNER — PLEASE READ BEFORE FRIDAY NIGHT
================================================================================
Hi — everything is built and tested in this single file. You asked me to flag
anything that couldn't be done as written, or anything I'd warn you about before
you have a queue out the door on a Friday night. Here are the five things you
should know:
1. THE BASKET CIRCLE ("ONE DIGIT" vs. "MUST READ 12" FOR SCHOOL BOOKINGS)
Those two instructions contradict each other directly: "12" is two digits, so
the badge cannot literally never show more than a single digit AND still read
"12" for the teacher on her phone.
- What I did: I kept the badge locked to its exact fixed small circle (30×30px,
strictly round — it will NEVER stretch into an oval or pill, so the header
shape still matches your nephew's printed lobby board). When 10 or more
seats are picked, the digits automatically shrink slightly inside that same
small circle so "12" or "14" fits cleanly and the teacher won't ring the
counter to check.
- If you ever decide matching a strict 1-digit lobby print matters more than
showing "12", change `CAP_BADGE_SINGLE_DIGIT` from `false` to `true` at the
top of the <script> near the bottom of this file (it will cap at "9" / "+").
2. TODAY'S EURO RATE vs. NO INTERNET CONNECTION
Your nephew's spec says the machine is not on the internet ("no network at
runtime"). Without an internet connection, the page cannot pull a live market
exchange rate automatically.
- What I did: I put an offline guide rate next to the dollar total
($1.00 = €0.92) and labeled it "Approx. guide rate · charged in USD" so
tourists know roughly what they're paying without arguing over a couple of
cents difference from their card bank's rate at the till.
- To update that rate later, edit `EUR_PER_USD` at the top of the <script>
(note: if someone has JavaScript turned off, the CSS fallback table also
uses 0.92).
3. JAVASCRIPT SWITCHED OFF (YOUR SISTER'S LAPTOP) vs. SMOOTH COUNT-UP
- With JavaScript OFF, your sister can still pick either film and showtime,
see sold seats blocked out, pick available seats on the 12×14 plan, see the
top-right basket circle update, and get accurate $8.00 / $11.50 and Euro
totals.
- Two limits when JavaScript is OFF:
(a) Smoothly counting up through the numbers requires JavaScript, so on her
laptop the total updates immediately instead of counting up.
(b) Without JavaScript or a server, all four showings share Screen One's
fallback sold-seat chart (with JavaScript ON, each showing loads its
own sold-seat map).
4. IMPORTANT FOR FRIDAY NIGHTS — SEATS DO NOT LOCK ACROSS DIFFERENT PHONES
Because this page runs completely offline with no server and no browser
storage, a teacher or customer picking seats on her phone is NOT sending that
booking to your till or locking those seats on anyone else's screen. Clicking
"Prepare Counter Pass" creates a clear ticket slip for them to show at the
desk, and your staff still need to ring those seats through on your box-office
system. Before taking unattended bookings away from the counter, your nephew
will need to hook this page up to a small local server.
5. BEFORE / AFTER FIVE O'CLOCK PRICING
Showings before 5:00 PM are $8.00 (2:30 PM and 3:15 PM tonight); showings from
5:00 PM onwards are $11.50 (7:15 PM and 8:00 PM tonight). If you later add a
screening dead on 5:00 PM, the code treats 5:00 PM (17:00) as $11.50.
================================================================================
-->
<!DOCTYPE html>
<html lang="en" class="no-js">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The Savoy Twin Cinema — Tonight's Tickets</title>
<script>document.documentElement.className = 'js';</script>
<style>
/* =========================================================================
NO-JS EXACT PRICE TABLES (@counter-style)
Covers 0–60 tickets to the exact cent when JavaScript is disabled.
($8.00 USD matinee uses native integer counter multiplication; these
three rules handle $11.50 half-dollars and 0.92 EUR conversions.)
========================================================================= */
@counter-style evening-usd {
system: fixed 0;
symbols:
"$0.00" "$11.50" "$23.00" "$34.50" "$46.00" "$57.50" "$69.00" "$80.50" "$92.00" "$103.50"
"$115.00" "$126.50" "$138.00" "$149.50" "$161.00" "$172.50" "$184.00" "$195.50" "$207.00" "$218.50"
"$230.00" "$241.50" "$253.00" "$264.50" "$276.00" "$287.50" "$299.00" "$310.50" "$322.00" "$333.50"
"$345.00" "$356.50" "$368.00" "$379.50" "$391.00" "$402.50" "$414.00" "$425.50" "$437.00" "$448.50"
"$460.00" "$471.50" "$483.00" "$494.50" "$506.00" "$517.50" "$529.00" "$540.50" "$552.00" "$563.50"
"$575.00" "$586.50" "$598.00" "$609.50" "$621.00" "$632.50" "$644.00" "$655.50" "$667.00" "$678.50"
"$690.00";
fallback: decimal;
}
@counter-style matinee-eur {
system: fixed 0;
symbols:
"€0.00" "€7.36" "€14.72" "€22.08" "€29.44" "€36.80" "€44.16" "€51.52" "€58.88" "€66.24"
"€73.60" "€80.96" "€88.32" "€95.68" "€103.04" "€110.40" "€117.76" "€125.12" "€132.48" "€139.84"
"€147.20" "€154.56" "€161.92" "€169.28" "€176.64" "€184.00" "€191.36" "€198.72" "€206.08" "€213.44"
"€220.80" "€228.16" "€235.52" "€242.88" "€250.24" "€257.60" "€264.96" "€272.32" "€279.68" "€287.04"
"€294.40" "€301.76" "€309.12" "€316.48" "€323.84" "€331.20" "€338.56" "€345.92" "€353.28" "€360.64"
"€368.00" "€375.36" "€382.72" "€390.08" "€397.44" "€404.80" "€412.16" "€419.52" "€426.88" "€434.24"
"€441.60";
fallback: decimal;
}
@counter-style evening-eur {
system: fixed 0;
symbols:
"€0.00" "€10.58" "€21.16" "€31.74" "€42.32" "€52.90" "€63.48" "€74.06" "€84.64" "€95.22"
"€105.80" "€116.38" "€126.96" "€137.54" "€148.12" "€158.70" "€169.28" "€179.86" "€190.44" "€201.02"
"€211.60" "€222.18" "€232.76" "€243.34" "€253.92" "€264.50" "€275.08" "€285.66" "€296.24" "€306.82"
"€317.40" "€327.98" "€338.56" "€349.14" "€359.72" "€370.30" "€380.88" "€391.46" "€402.04" "€412.62"
"€423.20" "€433.78" "€444.36" "€454.94" "€465.52" "€476.10" "€486.68" "€497.26" "€507.84" "€518.42"
"€529.00" "€539.58" "€550.16" "€560.74" "€571.32" "€581.90" "€592.48" "€603.06" "€613.64" "€624.22"
"€634.80";
fallback: decimal;
}
/* =========================================================================
WARM CLASSIC CINEMA PALETTE & BASE TYPOGRAPHY
========================================================================= */
:root {
--velvet-deep: #180709;
--velvet-mid: #280c11;
--velvet-card: #311016;
--velvet-seat: #7c1c28;
--velvet-seat-hover: #9a2533;
--gold-bright: #f2cf7e;
--gold-main: #d8a848;
--gold-muted: #9f772b;
--gold-faint: rgba(216, 168, 72, 0.28);
--cream: #f7eedc;
--cream-soft: #dfd0b6;
--taupe: #baa68c;
--sold-bg: #241719;
--sold-border: #432c30;
--sold-ink: #6a5256;
--paper-stub: #f5ead2;
--paper-ink: #241114;
--font-serif: "Palatino Linotype", Palatino, "Book Antiqua", Georgia, "Times New Roman", serif;
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
*, *::before, *::after {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
background-color: var(--velvet-deep);
color: var(--cream);
font-family: var(--font-serif);
line-height: 1.5;
}
body {
margin: 0;
min-height: 100vh;
/* Subtle plush theatre curtain ribbing */
background-image:
radial-gradient(ellipse at 50% 0%, rgba(138, 28, 41, 0.35) 0%, rgba(24, 7, 9, 0) 68%),
repeating-linear-gradient(
90deg,
rgba(255, 220, 160, 0.014) 0px,
rgba(255, 220, 160, 0.014) 14px,
rgba(0, 0, 0, 0.14) 14px,
rgba(0, 0, 0, 0.14) 28px
);
padding: 16px 12px 48px;
}
html.no-js .js-only { display: none !important; }
html.js .nojs-only { display: none !important; }
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* =========================================================================
SHELL & LOBBY BOARD HEADER (MATCHING PRINTED BOARD)
========================================================================= */
.cinema-shell {
max-width: 960px;
margin: 0 auto;
position: relative;
}
.lobby-header {
position: relative;
background: linear-gradient(180deg, #3c1218 0%, #240a0e 100%);
border: 2px solid var(--gold-main);
border-radius: 6px;
padding: 20px 64px 18px;
text-align: center;
box-shadow:
0 10px 28px rgba(0, 0, 0, 0.6),
inset 0 0 0 4px #240a0e,
inset 0 0 0 5px var(--gold-faint);
margin-bottom: 22px;
}
.header-kicker {
font-size: 0.75rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--gold-main);
margin: 0 0 4px;
}
.header-title {
margin: 0;
font-size: clamp(1.45rem, 4vw, 2.35rem);
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--gold-bright);
text-shadow: 0 2px 6px rgba(0, 0, 0, 0.75);
font-weight: 700;
line-height: 1.15;
}
.header-sub {
margin: 6px 0 0;
font-size: 0.92rem;
color: var(--cream-soft);
font-style: italic;
}
/* Small circular basket in the top-right corner of the header */
.header-basket {
position: absolute;
top: 16px;
right: 16px;
z-index: 10;
display: inline-flex;
align-items: center;
gap: 7px;
text-decoration: none;
color: var(--gold-bright);
}
.basket-caption {
font-family: var(--font-sans);
font-size: 0.68rem;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--cream-soft);
display: inline-flex;
align-items: center;
gap: 4px;
}
.basket-icon {
width: 14px;
height: 14px;
stroke: var(--gold-main);
fill: none;
flex-shrink: 0;
}
/* Strictly fixed small circle — outer dimensions NEVER change */
.basket-circle {
width: 30px;
height: 30px;
min-width: 30px;
max-width: 30px;
min-height: 30px;
max-height: 30px;
border-radius: 50%;
background: radial-gradient(circle at 35% 30%, #7f1b27 0%, #3d0a11 100%);
border: 2px solid var(--gold-bright);
box-shadow:
0 2px 8px rgba(0, 0, 0, 0.65),
inset 0 1px 2px rgba(255, 236, 190, 0.35);
display: inline-flex;
align-items: center;
justify-content: center;
overflow: hidden;
font-family: var(--font-sans);
font-variant-numeric: tabular-nums;
font-weight: 700;
line-height: 1;
color: var(--cream);
transition: transform 0.18s ease, background-color 0.18s ease;
}
.header-basket:hover .basket-circle,
.header-basket:focus-visible .basket-circle {
border-color: #fff2c6;
transform: scale(1.05);
}
.header-basket:focus-visible {
outline: 2px solid var(--gold-bright);
outline-offset: 4px;
border-radius: 20px;
}
.basket-circle[data-digits="1"] {
font-size: 14px;
letter-spacing: 0;
}
/* Fits "12", "14", etc. cleanly inside the exact same 30x30px circle */
.basket-circle[data-digits="2"] {
font-size: 11.5px;
letter-spacing: -0.05em;
}
.basket-circle[data-digits="3"] {
font-size: 9.5px;
letter-spacing: -0.06em;
}
@keyframes badgePop {
0% { transform: scale(1); }
45% { transform: scale(1.16); }
100% { transform: scale(1); }
}
.basket-circle.pop {
animation: badgePop 0.22s ease-out;
}
@media (max-width: 620px) {
.lobby-header {
padding: 18px 50px 16px;
}
.header-basket {
top: 12px;
right: 12px;
}
.basket-caption {
display: none;
}
}
/* =========================================================================
BOOKING FORM & CSS COUNTER ENGINE
========================================================================= */
.booking-form {
counter-reset: seats 0 mat-usd 0;
}
.seat-cb:checked:not(:disabled) {
counter-increment: seats 1 mat-usd 8;
}
html.no-js .basket-circle::after {
content: counter(seats);
font-size: 12px;
letter-spacing: -0.04em;
}
/* =========================================================================
SECTION HEADINGS & TARIFF RIBBON
========================================================================= */
.section-card {
background: linear-gradient(180deg, rgba(49, 16, 22, 0.96) 0%, rgba(33, 10, 14, 0.98) 100%);
border: 1px solid var(--gold-muted);
border-radius: 6px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
}
.step-header {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: space-between;
gap: 8px 16px;
border-bottom: 1px solid var(--gold-faint);
padding-bottom: 10px;
margin-bottom: 16px;
}
.step-title {
margin: 0;
font-size: 1.2rem;
color: var(--gold-bright);
letter-spacing: 0.04em;
}
.step-badge {
display: inline-block;
font-family: var(--font-sans);
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--gold-main);
margin-right: 8px;
}
.tariff-note {
margin: 0;
font-size: 0.88rem;
color: var(--cream-soft);
font-variant-numeric: tabular-nums;
}
.tariff-note strong {
color: var(--gold-bright);
}
/* =========================================================================
STEP 1: TONIGHT'S TWO FILMS & SHOWTIMES
========================================================================= */
.films-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
@media (max-width: 700px) {
.films-grid {
grid-template-columns: 1fr;
}
}
.film-card {
background: rgba(20, 6, 8, 0.58);
border: 1px solid rgba(216, 168, 72, 0.32);
border-radius: 5px;
padding: 16px;
display: flex;
flex-direction: column;
justify-content: space-between;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.screen-tag {
font-family: var(--font-sans);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--gold-main);
margin-bottom: 4px;
}
.film-title {
margin: 0 0 4px;
font-size: 1.3rem;
color: var(--cream);
line-height: 1.2;
}
.film-meta {
margin: 0 0 10px;
font-family: var(--font-sans);
font-size: 0.78rem;
color: var(--taupe);
}
.film-synopsis {
margin: 0 0 14px;
font-size: 0.92rem;
color: var(--cream-soft);
}
.showtimes-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.time-btn {
flex: 1 1 130px;
cursor: pointer;
user-select: none;
background: linear-gradient(180deg, #43141b 0%, #2a0c10 100%);
border: 1.5px solid var(--gold-muted);
border-radius: 4px;
padding: 9px 12px;
text-align: center;
transition: background 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.time-btn:hover {
border-color: var(--gold-bright);
background: linear-gradient(180deg, #571923 0%, #370f15 100%);
}
.time-clock {
font-size: 1.08rem;
font-weight: 700;
color: var(--cream);
font-variant-numeric: tabular-nums;
}
.time-rate {
font-family: var(--font-sans);
font-size: 0.74rem;
color: var(--gold-bright);
letter-spacing: 0.03em;
font-variant-numeric: tabular-nums;
}
/* Active showtime state driven purely by :checked radio buttons (works with JS off!) */
#sh-1:checked ~ .programme-section label[for="sh-1"],
#sh-2:checked ~ .programme-section label[for="sh-2"],
#sh-3:checked ~ .programme-section label[for="sh-3"],
#sh-4:checked ~ .programme-section label[for="sh-4"] {
background: linear-gradient(180deg, #f0cb78 0%, #c99635 100%);
border-color: #fff3cc;
box-shadow: 0 0 14px rgba(242, 207, 126, 0.38);
}
#sh-1:checked ~ .programme-section label[for="sh-1"] .time-clock,
#sh-2:checked ~ .programme-section label[for="sh-2"] .time-clock,
#sh-3:checked ~ .programme-section label[for="sh-3"] .time-clock,
#sh-4:checked ~ .programme-section label[for="sh-4"] .time-clock,
#sh-1:checked ~ .programme-section label[for="sh-1"] .time-rate,
#sh-2:checked ~ .programme-section label[for="sh-2"] .time-rate,
#sh-3:checked ~ .programme-section label[for="sh-3"] .time-rate,
#sh-4:checked ~ .programme-section label[for="sh-4"] .time-rate {
color: var(--paper-ink);
font-weight: 700;
}
#sh-1:checked ~ .programme-section .film-card-1,
#sh-2:checked ~ .programme-section .film-card-1,
#sh-3:checked ~ .programme-section .film-card-2,
#sh-4:checked ~ .programme-section .film-card-2 {
border-color: var(--gold-main);
box-shadow: inset 0 0 0 1px rgba(242, 207, 126, 0.22);
}
#sh-1:focus-visible ~ .programme-section label[for="sh-1"],
#sh-2:focus-visible ~ .programme-section label[for="sh-2"],
#sh-3:focus-visible ~ .programme-section label[for="sh-3"],
#sh-4:focus-visible ~ .programme-section label[for="sh-4"] {
outline: 2px solid #ffffff;
outline-offset: 3px;
}
/* =========================================================================
STEP 2: AUDITORIUM SCREEN & 12×14 SEATING PLAN
========================================================================= */
.active-showing-strip {
text-align: center;
font-size: 0.95rem;
color: var(--cream-soft);
margin-bottom: 14px;
}
.active-showing-strip strong {
color: var(--gold-bright);
}
.nojs-sh-label { display: none; }
#sh-1:checked ~ .auditorium-section .nojs-sh-1,
#sh-2:checked ~ .auditorium-section .nojs-sh-2,
#sh-3:checked ~ .auditorium-section .nojs-sh-3,
#sh-4:checked ~ .auditorium-section .nojs-sh-4 {
display: inline;
}
.swap-notice {
background: rgba(216, 168, 72, 0.14);
border: 1px solid var(--gold-main);
color: var(--gold-bright);
border-radius: 4px;
padding: 8px 12px;
margin: 0 auto 14px;
max-width: 640px;
font-family: var(--font-sans);
font-size: 0.82rem;
text-align: center;
}
. swap-notice[hidden],
[hidden] {
display: none !important;
}
/* Glowing curved picture screen */
.screen-stage {
max-width: 580px;
margin: 4px auto 22px;
text-align: center;
}
.screen-arc {
height: 16px;
border-top: 4px solid #f7ecd5;
border-radius: 50% 50% 0 0 / 100% 100% 0 0;
background: radial-gradient(ellipse at 50% 0%, rgba(247, 236, 213, 0.34) 0%, rgba(247, 236, 213, 0) 75%);
box-shadow: 0 -4px 16px rgba(247, 236, 213, 0.25);
}
.screen-caption {
font-family: var(--font-sans);
font-size: 0.68rem;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--taupe);
margin-top: 2px;
}
/* Seat Legend */
.seat-legend {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 18px;
margin-bottom: 16px;
font-family: var(--font-sans);
font-size: 0.8rem;
color: var(--cream-soft);
}
.legend-item {
display: inline-flex;
align-items: center;
gap: 7px;
}
.legend-swatch {
width: 20px;
height: 18px;
border-radius: 5px 5px 2px 2px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.65rem;
font-weight: 700;
}
.legend-swatch.avail {
background: var(--velvet-seat);
border: 1px solid #b53a48;
border-bottom: 3px solid #4d0e16;
}
.legend-swatch.picked {
background: linear-gradient(180deg, #f5d68a 0%, #cfa03d 100%);
border: 1px solid #fff5d6;
border-bottom: 3px solid #8c6518;
color: var(--paper-ink);
}
.legend-swatch.gone {
background: var(--sold-bg);
border: 1px dashed var(--sold-border);
color: var(--sold-ink);
}
/* 12 rows × 14 seats auditorium grid */
.seating-scroll {
overflow-x: auto;
padding-bottom: 6px;
}
.seating-grid {
position: relative;
display: grid;
grid-template-columns:
20px
repeat(7, minmax(20px, 1fr))
clamp(8px, 2vw, 20px)
repeat(7, minmax(20px, 1fr))
20px;
gap: 6px 4px;
max-width: 680px;
min-width: 318px;
margin: 0 auto;
align-items: center;
}
.row-lbl {
font-family: var(--font-sans);
font-size: 0.74rem;
font-weight: 700;
color: var(--gold-main);
text-align: center;
user-select: none;
}
.aisle {
display: block;
height: 1px;
}
.seat-cb {
position: absolute;
opacity: 0;
width: 1px;
height: 1px;
pointer-events: none;
}
.seat {
height: 30px;
border-radius: 7px 7px 3px 3px;
background: var(--velvet-seat);
border: 1px solid #ad3745;
border-bottom: 4px solid #4a0d15;
color: var(--cream);
font-family: var(--font-sans);
font-variant-numeric: tabular-nums;
font-size: 0.72rem;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
user-select: none;
transition: transform 0.1s ease, background-color 0.12s ease, border-color 0.12s ease;
}
.seat:hover {
background: var(--velvet-seat-hover);
border-color: var(--gold-bright);
}
/* Selected seat */
.seat-cb:checked:not(:disabled) + .seat {
background: linear-gradient(180deg, #f6d88e 0%, #d2a13c 100%);
border-color: #fff6da;
border-bottom-color: #876014;
color: var(--paper-ink);
font-weight: 800;
box-shadow: 0 0 8px rgba(242, 207, 126, 0.45);
transform: translateY(-1px);
}
/* Sold / Gone seat — unpickable */
.seat-cb:disabled + .seat {
background: repeating-linear-gradient(
-45deg,
#221517,
#221517 4px,
#2b1b1e 4px,
#2b1b1e 8px
);
border: 1px dashed var(--sold-border);
border-bottom-width: 1px;
color: var(--sold-ink);
cursor: not-allowed;
transform: none;
box-shadow: none;
font-size: 0;
}
.seat-cb:disabled + .seat::after {
content: "×";
font-size: 0.85rem;
font-weight: 700;
color: var(--sold-ink);
}
/* Keyboard focus ring */
.seat-cb:focus-visible + .seat {
outline: 2px solid #ffffff;
outline-offset: 2px;
z-index: 2;
}
.aisle-caption {
text-align: center;
font-family: var(--font-sans);
font-size: 0.74rem;
color: var(--taupe);
margin: 12px 0 0;
}
@media (max-width: 480px) {
.section-card {
padding: 14px 10px;
}
.seating-grid {
gap: 5px 2px;
grid-template-columns:
15px
repeat(7, minmax(18px, 1fr))
6px
repeat(7, minmax(18px, 1fr))
15px;
}
.seat {
height: 27px;
font-size: 0.64rem;
border-radius: 5px 5px 2px 2px;
}
.row-lbl {
font-size: 0.66rem;
}
}
/* =========================================================================
STEP 3: BOX OFFICE RUNNING TOTAL & COUNTER TICKET SLIP
========================================================================= */
.box-office-card {
background: linear-gradient(180deg, #381117 0%, #230a0e 100%);
border: 2px solid var(--gold-main);
border-radius: 6px;
padding: 20px;
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.55);
}
.box-office-grid {
display: grid;
grid-template-columns: 1.2fr 1fr;
gap: 20px;
align-items: center;
}
@media (max-width: 680px) {
.box-office-grid {
grid-template-columns: 1fr;
}
}
.selection-heading {
margin: 0 0 6px;
font-size: 1.15rem;
color: var(--gold-bright);
}
.selection-film {
margin: 0 0 6px;
font-size: 0.95rem;
color: var(--cream);
}
.selection-seats {
margin: 0;
font-family: var(--font-sans);
font-size: 0.86rem;
color: var(--cream-soft);
line-height: 1.45;
}
.totals-panel {
background: rgba(16, 4, 6, 0.62);
border: 1px solid var(--gold-faint);
border-radius: 5px;
padding: 14px 16px;
text-align: right;
}
@media (max-width: 680px) {
.totals-panel {
text-align: left;
}
}
.totals-label {
font-family: var(--font-sans);
font-size: 0.72rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--taupe);
margin-bottom: 4px;
}
.totals-figures {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: flex-end;
gap: 8px 14px;
font-variant-numeric: tabular-nums;
}
@media (max-width: 680px) {
.totals-figures {
justify-content: flex-start;
}
}
.usd-figure {
font-size: 2.05rem;
font-weight: 700;
color: var(--gold-bright);
line-height: 1.1;
}
.eur-figure {
font-family: var(--font-sans);
font-size: 1.05rem;
font-weight: 600;
color: var(--cream-soft);
}
.fx-disclaimer {
margin: 6px 0 0;
font-family: var(--font-sans);
font-size: 0.73rem;
color: var(--taupe);
}
/* No-JS CSS Counter Running Total Rules */
.nojs-matinee-tot,
.nojs-evening-tot {
display: none;
}
#sh-1:checked ~ .box-office-card .nojs-matinee-tot,
#sh-3:checked ~ .box-office-card .nojs-matinee-tot {
display: flex;
}
#sh-2:checked ~ .box-office-card .nojs-evening-tot,
#sh-4:checked ~ .box-office-card .nojs-evening-tot {
display: flex;
}
.nojs-matinee-usd::after {
content: "$" counter(mat-usd) ".00";
}
.nojs-matinee-eur::after {
content: "≈ " counter(seats, matinee-eur);
}
.nojs-evening-usd::after {
content: counter(seats, evening-usd);
}
.nojs-evening-eur::after {
content: "≈ " counter(seats, evening-eur);
}
.nojs-seat-tally::after {
content: counter(seats) " seat(s) marked on the seating plan above.";
}
.actions-row {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 10px;
margin-top: 16px;
padding-top: 14px;
border-top: 1px solid var(--gold-faint);
}
.btn {
font-family: var(--font-sans);
font-size: 0.86rem;
font-weight: 700;
letter-spacing: 0.04em;
padding: 10px 18px;
border-radius: 4px;
cursor: pointer;
transition: filter 0.15s ease, transform 0.1s ease;
}
.btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.btn-secondary {
background: transparent;
color: var(--cream-soft);
border: 1px solid var(--gold-muted);
}
.btn-secondary:hover:not(:disabled) {
border-color: var(--gold-bright);
color: var(--cream);
}
.btn-primary {
background: linear-gradient(180deg, #f3ce7b 0%, #c89430 100%);
color: var(--paper-ink);
border: 1px solid #fff2c9;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.4);
}
.btn-primary:hover:not(:disabled) {
filter: brightness(1.06);
}
/* Perforated paper counter pass */
.counter-pass {
margin-top: 18px;
background: var(--paper-stub);
color: var(--paper-ink);
border-radius: 5px;
border-left: 6px dashed #381117;
border-right: 6px dashed #381117;
padding: 18px 20px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5);
}
.pass-top {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: baseline;
border-bottom: 1.5px dashed #8c6d46;
padding-bottom: 8px;
margin-bottom: 10px;
gap: 8px;
}
.pass-title {
margin: 0;
font-size: 1.15rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #4a1119;
}
.pass-ref {
font-family: var(--font-sans);
font-size: 0.8rem;
font-weight: 700;
color: #5c3e18;
font-variant-numeric: tabular-nums;
}
.pass-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 10px 16px;
font-family: var(--font-sans);
font-size: 0.86rem;
}
.pass-details dt {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: #6f5638;
}
.pass-details dd {
margin: 2px 0 0;
font-weight: 700;
color: var(--paper-ink);
}
.pass-footer {
margin: 12px 0 0;
padding-top: 8px;
border-top: 1px solid rgba(140, 109, 70, 0.35);
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 10px;
font-family: var(--font-sans);
font-size: 0.78rem;
color: #5a4228;
}
.btn-print {
background: #4a1119;
color: var(--cream);
border: none;
padding: 7px 14px;
border-radius: 3px;
font-family: var(--font-sans);
font-size: 0.78rem;
font-weight: 700;
cursor: pointer;
}
@media print {
body {
background: #ffffff;
color: #000000;
padding: 0;
}
.programme-section .time-btn,
.actions-row,
.btn-print {
display: none;
}
.lobby-header,
.section-card,
.box-office-card,
.counter-pass {
box-shadow: none;
border-color: #333333;
}
}
</style>
</head>
<body>
<div class="cinema-shell">
<!-- LOBBY BOARD HEADER (MATCHES PRINTED BOARD SHAPE) -->
<header class="lobby-header">
<p class="header-kicker">Est. 1954 · Two Screens · Tonight's Programme</p>
<h1 class="header-title">The Savoy Twin</h1>
<p class="header-sub">Doors open thirty minutes before the first picture</p>
</header>
<form class="booking-form" id="booking-form" action="#box-office" method="get">
<!-- Showtime radio inputs placed first so keyboard focus & no-JS CSS selectors work cleanly -->
<input class="sr-only sh-radio" type="radio" name="showing" id="sh-1" value="sh-1" checked>
<input class="sr-only sh-radio" type="radio" name="showing" id="sh-2" value="sh-2">
<input class="sr-only sh-radio" type="radio" name="showing" id="sh-3" value="sh-3">
<input class="sr-only sh-radio" type="radio" name="showing" id="sh-4" value="sh-4">
<!-- ===================================================================
STEP 1: TONIGHT'S TWO FILMS & TIMES
=================================================================== -->
<section class="section-card programme-section" aria-labelledby="step1-heading">
<div class="step-header">
<h2 class="step-title" id="step1-heading">
<span class="step-badge">Step 1</span>Pick Tonight's Film & Time
</h2>
<p class="tariff-note">
Before 5:00 PM: <strong>$8.00</strong> · After 5:00 PM: <strong>$11.50</strong>
</p>
</div>
<div class="films-grid">
<!-- FILM 1 -->
<article class="film-card film-card-1">
<div>
<div class="screen-tag">Screen One</div>
<h3 class="film-title">The Last Harbor</h3>
<p class="film-meta">Cert PG · 1 hr 54 mins · Coastal Mystery</p>
<p class="film-synopsis">
When an autumn gale washes an unmarked sea-chest onto the breakwater, a retiring keeper uncovers forty years of harbour secrets.
</p>
</div>
<div class="showtimes-row" role="group" aria-label="Showtimes for The Last Harbor">
<label class="time-btn" for="sh-1">
<span class="time-clock">2:30 PM</span>
<span class="time-rate">Matinee · $8.00</span>
</label>
<label class="time-btn" for="sh-2">
<span class="time-clock">7:15 PM</span>
<span class="time-rate">Evening · $11.50</span>
</label>
</div>
</article>
<!-- FILM 2 -->
<article class="film-card film-card-2">
<div>
<div class="screen-tag">Screen Two</div>
<h3 class="film-title">Midnight on the Orient Line</h3>
<p class="film-meta">Cert PG-13 · 2 hrs 06 mins · Classic Suspense</p>
<p class="film-synopsis">
Snowbound above the Simplon Pass with the telegraph wire cut, two strangers in the dining car realise someone aboard is travelling under a borrowed name.
</p>
</div>
<div class="showtimes-row" role="group" aria-label="Showtimes for Midnight on the Orient Line">
<label class="time-btn" for="sh-3">
<span class="time-clock">3:15 PM</span>
<span class="time-rate">Matinee · $8.00</span>
</label>
<label class="time-btn" for="sh-4">
<span class="time-clock">8:00 PM</span>
<span class="time-rate">Evening · $11.50</span>
</label>
</div>
</article>
</div>
</section>
<!-- ===================================================================
STEP 2: 12 ROWS × 14 SEATS AUDITORIUM PLAN
=================================================================== -->
<section class="section-card auditorium-section" aria-labelledby="step2-heading">
<div class="step-header">
<h2 class="step-title" id="step2-heading">
<span class="step-badge">Step 2</span>Pick Your Seats
</h2>
<p class="tariff-note">12 rows (A–L) · 14 seats per row</p>
</div>
<p class="active-showing-strip">
<span class="js-only" id="js-showing-banner">
Showing: <strong>Screen One — The Last Harbor · 2:30 PM</strong> ($8.00 per seat)
</span>
<span class="nojs-only">
<span class="nojs-sh-label nojs-sh-1">Showing: <strong>Screen One — The Last Harbor · 2:30 PM</strong> ($8.00 per seat)</span>
<span class="nojs-sh-label nojs-sh-2">Showing: <strong>Screen One — The Last Harbor · 7:15 PM</strong> ($11.50 per seat)</span>
<span class="nojs-sh-label nojs-sh-3">Showing: <strong>Screen Two — Midnight on the Orient Line · 3:15 PM</strong> ($8.00 per seat)</span>
<span class="nojs-sh-label nojs-sh-4">Showing: <strong>Screen Two — Midnight on the Orient Line · 8:00 PM</strong> ($11.50 per seat)</span>
</span>
</p>
<p class="swap-notice" id="swap-notice" role="status" aria-live="polite" hidden></p>
<div class="screen-stage" aria-hidden="true">
<div class="screen-arc"></div>
<div class="screen-caption">Screen</div>
</div>
<div class="seat-legend" aria-label="Seat status key">
<span class="legend-item"><span class="legend-swatch avail" aria-hidden="true"></span>Available</span>
<span class="legend-item"><span class="legend-swatch picked" aria-hidden="true"></span>Your Seats</span>
<span class="legend-item"><span class="legend-swatch gone" aria-hidden="true">×</span>Already Sold</span>
</div>
<div class="seating-scroll">
<div class="seating-grid" role="group" aria-label="Auditorium seating plan, rows A to L, seats 1 to 14">
<!-- ROW A (Front) -->
<span class="row-lbl" aria-hidden="true">A</span>
<input class="seat-cb" type="checkbox" id="s-A1" name="seat" value="A1" aria-label="Row A Seat 1"><label class="seat" for="s-A1">1</label>
<input class="seat-cb" type="checkbox" id="s-A2" name="seat" value="A2" aria-label="Row A Seat 2"><label class="seat" for="s-A2">2</label>
<input class="seat-cb" type="checkbox" id="s-A3" name="seat" value="A3" aria-label="Row A Seat 3"><label class="seat" for="s-A3">3</label>
<input class="seat-cb" type="checkbox" id="s-A4" name="seat" value="A4" aria-label="Row A Seat 4"><label class="seat" for="s-A4">4</label>
<input class="seat-cb" type="checkbox" id="s-A5" name="seat" value="A5" aria-label="Row A Seat 5"><label class="seat" for="s-A5">5</label>
<input class="seat-cb" type="checkbox" id="s-A6" name="seat" value="A6" aria-label="Row A Seat 6"><label class="seat" for="s-A6">6</label>
<input class="seat-cb" type="checkbox" id="s-A7" name="seat" value="A7" aria-label="Row A Seat 7"><label class="seat" for="s-A7">7</label>
<span class="aisle" aria-hidden="true"></span>
<input class="seat-cb" type="checkbox" id="s-A8" name="seat" value="A8" aria-label="Row A Seat 8"><label class="seat" for="s-A8">8</label>
<input class="seat-cb" type="checkbox" id="s-A9" name="seat" value="A9" aria-label="Row A Seat 9"><label class="seat" for="s-A9">9</label>
<input class="seat-cb" type="checkbox" id="s-A10" name="seat" value="A10" aria-label="Row A Seat 10"><label class="seat" for="s-A10">10</label>
<input class="seat-cb" type="checkbox" id="s-A11" name="seat" value="A11" aria-label="Row A Seat 11"><label class="seat" for="s-A11">11</label>
<input class="seat-cb" type="checkbox" id="s-A12" name="seat" value="A12" aria-label="Row A Seat 12"><label class="seat" for="s-A12">12</label>
<input class="seat-cb" type="checkbox" id="s-A13" name="seat" value="A13" aria-label="Row A Seat 13"><label class="seat" for="s-A13">13</label>
<input class="seat-cb" type="checkbox" id="s-A14" name="seat" value="A14" aria-label="Row A Seat 14"><label class="seat" for="s-A14">14</label>
<span class="row-lbl" aria-hidden="true">A</span>
<!-- ROW B -->
<span class="row-lbl" aria-hidden="true">B</span>
<input class="seat-cb" type="checkbox" id="s-B1" name="seat" value="B1" aria-label="Row B Seat 1"><label class="seat" for="s-B1">1</label>
<input class="seat-cb" type="checkbox" id="s-B2" name="seat" value="B2" aria-label="Row B Seat 2"><label class="seat" for="s-B2">2</label>
<input class="seat-cb" type="checkbox" id="s-B3" name="seat" value="B3" aria-label="Row B Seat 3"><label class="seat" for="s-B3">3</label>
<input class="seat-cb" type="checkbox" id="s-B4" name="seat" value="B4" aria-label="Row B Seat 4"><label class="seat" for="s-B4">4</label>
<input class="seat-cb" type="checkbox" id="s-B5" name="seat" value="B5" aria-label="Row B Seat 5"><label class="seat" for="s-B5">5</label>
<input class="seat-cb" type="checkbox" id="s-B6" name="seat" value="B6" disabled aria-label="Row B Seat 6 — Sold"><label class="seat" for="s-B6">6</label>
<input class="seat-cb" type="checkbox" id="s-B7" name="seat" value="B7" disabled aria-label="Row B Seat 7 — Sold"><label class="seat" for="s-B7">7</label>
<span class="aisle" aria-hidden="true"></span>
<input class="seat-cb" type="checkbox" id="s-B8" name="seat" value="B8" aria-label="Row B Seat 8"><label class="seat" for="s-B8">8</label>
<input class="seat-cb" type="checkbox" id="s-B9" name="seat" value="B9" aria-label="Row B Seat 9"><label class="seat" for="s-B9">9</label>
<input class="seat-cb" type="checkbox" id="s-B10" name="seat" value="B10" aria-label="Row B Seat 10"><label class="seat" for="s-B10">10</label>
<input class="seat-cb" type="checkbox" id="s-B11" name="seat" value="B11" aria-label="Row B Seat 11"><label class="seat" for="s-B11">11</label>
<input class="seat-cb" type="checkbox" id="s-B12" name="seat" value="B12" aria-label="Row B Seat 12"><label class="seat" for="s-B12">12</label>
<input class="seat-cb" type="checkbox" id="s-B13" name="seat" value="B13" aria-label="Row B Seat 13"><label class="seat" for="s-B13">13</label>
<input class="seat-cb" type="checkbox" id="s-B14" name="seat" value="B14" aria-label="Row B Seat 14"><label class="seat" for="s-B14">14</label>
<span class="row-lbl" aria-hidden="true">B</span>
<!-- ROW C -->
<span class="row-lbl" aria-hidden="true">C</span>
<input class="seat-cb" type="checkbox" id="s-C1" name="seat" value="C1" aria-label="Row C Seat 1"><label class="seat" for="s-C1">1</label>
<input class="seat-cb" type="checkbox" id="s-C2" name="seat" value="C2" aria-label="Row C Seat 2"><label class="seat" for="s-C2">2</label>
<input class="seat-cb" type="checkbox" id="s-C3" name="seat" value="C3" aria-label="Row C Seat 3"><label class="seat" for="s-C3">3</label>
<input class="seat-cb" type="checkbox" id="s-C4" name="seat" value="C4" aria-label="Row C Seat 4"><label class="seat" for="s-C4">4</label>
<input class="seat-cb" type="checkbox" id="s-C5" name="seat" value="C5" aria-label="Row C Seat 5"><label class="seat" for="s-C5">5</label>
<input class="seat-cb" type="checkbox" id="s-C6" name="seat" value="C6" aria-label="Row C Seat 6"><label class="seat" for="s-C6">6</label>
<input class="seat-cb" type="checkbox" id="s-C7" name="seat" value="C7" aria-label="Row C Seat 7"><label class="seat" for="s-C7">7</label>
<span class="aisle" aria-hidden="true"></span>
<input class="seat-cb" type="checkbox" id="s-C8" name="seat" value="C8" aria-label="Row C Seat 8"><label class="seat" for="s-C8">8</label>
<input class="seat-cb" type="checkbox" id="s-C9" name="seat" value="C9" aria-label="Row C Seat 9"><label class="seat" for="s-C9">9</label>
<input class="seat-cb" type="checkbox" id="s-C10" name="seat" value="C10" aria-label="Row C Seat 10"><label class="seat" for="s-C10">10</label>
<input class="seat-cb" type="checkbox" id="s-C11" name="seat" value="C11" aria-label="Row C Seat 11"><label class="seat" for="s-C11">11</label>
<input class="seat-cb" type="checkbox" id="s-C12" name="seat" value="C12" aria-label="Row C Seat 12"><label class="seat" for="s-C12">12</label>
<input class="seat-cb" type="checkbox" id="s-C13" name="seat" value="C13" aria-label="Row C Seat 13"><label class="seat" for="s-C13">13</label>
<input class="seat-cb" type="checkbox" id="s-C14" name="seat" value="C14" aria-label="Row C Seat 14"><label class="seat" for="s-C14">14</label>
<span class="row-lbl" aria-hidden="true">C</span>
<!-- ROW D -->
<span class="row-lbl" aria-hidden="true">D</span>
<input class="seat-cb" type="checkbox" id="s-D1" name="seat" value="D1" aria-label="Row D Seat 1"><label class="seat" for="s-D1">1</label>
<input class="seat-cb" type="checkbox" id="s-D2" name="seat" value="D2" aria-label="Row D Seat 2"><label class="seat" for="s-D2">2</label>
<input class="seat-cb" type="checkbox" id="s-D3" name="seat" value="D3" aria-label="Row D Seat 3"><label class="seat" for="s-D3">3</label>
<input class="seat-cb" type="checkbox" id="s-D4" name="seat" value="D4" aria-label="Row D Seat 4"><label class="seat" for="s-D4">4</label>
<input class="seat-cb" type="checkbox" id="s-D5" name="seat" value="D5" aria-label="Row D Seat 5"><label class="seat" for="s-D5">5</label>
<input class="seat-cb" type="checkbox" id="s-D6" name="seat" value="D6" aria-label="Row D Seat 6"><label class="seat" for="s-D6">6</label>
<input class="seat-cb" type="checkbox" id="s-D7" name="seat" value="D7" aria-label="Row D Seat 7"><label class="seat" for="s-D7">7</label>
<span class="aisle" aria-hidden="true"></span>
<input class="seat-cb" type="checkbox" id="s-D8" name="seat" value="D8" disabled aria-label="Row D Seat 8 — Sold"><label class="seat" for="s-D8">8</label>
<input class="seat-cb" type="checkbox" id="s-D9" name="seat" value="D9" disabled aria-label="Row D Seat 9 — Sold"><label class="seat" for="s-D9">9</label>
<input class="seat-cb" type="checkbox" id="s-D10" name="seat" value="D10" aria-label="Row D Seat 10"><label class="seat" for="s-D10">10</label>
<input class="seat-cb" type="checkbox" id="s-D11" name="seat" value="D11" aria-label="Row D Seat 11"><label class="seat" for="s-D11">11</label>
<input class="seat-cb" type="checkbox" id="s-D12" name="seat" value="D12" aria-label="Row D Seat 12"><label class="seat" for="s-D12">12</label>
<input class="seat-cb" type="checkbox" id="s-D13" name="seat" value="D13" aria-label="Row D Seat 13"><label class="seat" for="s-D13">13</label>
<input class="seat-cb" type="checkbox" id="s-D14" name="seat" value="D14" aria-label="Row D Seat 14"><label class="seat" for="s-D14">14</label>
<span class="row-lbl" aria-hidden="true">D</span>
<!-- ROW E -->
<span class="row-lbl" aria-hidden="true">E</span>
<input class="seat-cb" type="checkbox" id="s-E1" name="seat" value="E1" aria-label="Row E Seat 1"><label class="seat" for="s-E1">1</label>
<input class="seat-cb" type="checkbox" id="s-E2" name="seat" value="E2" aria-label="Row E Seat 2"><label class="seat" for="s-E2">2</label>
<input class="seat-cb" type="checkbox" id="s-E3" name="seat" value="E3" aria-label="Row E Seat 3"><label class="seat" for="s-E3">3</label>
<input class="seat-cb" type="checkbox" id="s-E4" name="seat" value="E4" aria-label="Row E Seat 4"><label class="seat" for="s-E4">4</label>
<input class="seat-cb" type="checkbox" id="s-E5" name="seat" value="E5" disabled aria-label="Row E Seat 5 — Sold"><label class="seat" for="s-E5">5</label>
<input class="seat-cb" type="checkbox" id="s-E6" name="seat" value="E6" disabled aria-label="Row E Seat 6 — Sold"><label class="seat" for="s-E6">6</label>
<input class="seat-cb" type="checkbox" id="s-E7" name="seat" value="E7" disabled aria-label="Row E Seat 7 — Sold"><label class="seat" for="s-E7">7</label>
<span class="aisle" aria-hidden="true"></span>
<input class="seat-cb" type="checkbox" id="s-E8" name="seat" value="E8" disabled aria-label="Row E Seat 8 — Sold"><label class="seat" for="s-E8">8</label>
<input class="seat-cb" type="checkbox" id="s-E9" name="seat" value="E9" disabled aria-label="Row E Seat 9 — Sold"><label class="seat" for="s-E9">9</label>
<input class="seat-cb" type="checkbox" id="s-E10" name="seat" value="E10" aria-label="Row E Seat 10"><label class="seat" for="s-E10">10</label>
<input class="seat-cb" type="checkbox" id="s-E11" name="seat" value="E11" aria-label="Row E Seat 11"><label class="seat" for="s-E11">11</label>
<input class="seat-cb" type="checkbox" id="s-E12" name="seat" value="E12" aria-label="Row E Seat 12"><label class="seat" for="s-E12">12</label>
<input class="seat-cb" type="checkbox" id="s-E13" name="seat" value="E13" aria-label="Row E Seat 13"><label class="seat" for="s-E13">13</label>
<input class="seat-cb" type="checkbox" id="s-E14" name="seat" value="E14" aria-label="Row E Seat 14"><label class="seat" for="s-E14">14</label>
<span class="row-lbl" aria-hidden="true">E</span>
<!-- ROW F -->
<span class="row-lbl" aria-hidden="true">F</span>
<input class="seat-cb" type="checkbox" id="s-F1" name="seat" value="F1" aria-label="Row F Seat 1"><label class="seat" for="s-F1">1</label>
<input class="seat-cb" type="checkbox" id="s-F2" name="seat" value="F2" aria-label="Row F Seat 2"><label class="seat" for="s-F2">2</label>
<input class="seat-cb" type="checkbox" id="s-F3" name="seat" value="F3" aria-label="Row F Seat 3"><label class="seat" for="s-F3">3</label>
<input class="seat-cb" type="checkbox" id="s-F4" name="seat" value="F4" disabled aria-label="Row F Seat 4 — Sold"><label class="seat" for="s-F4">4</label>
<input class="seat-cb" type="checkbox" id="s-F5" name="seat" value="F5" disabled aria-label="Row F Seat 5 — Sold"><label class="seat" for="s-F5">5</label>
<input class="seat-cb" type="checkbox" id="s-F6" name="seat" value="F6" disabled aria-label="Row F Seat 6 — Sold"><label class="seat" for="s-F6">6</label>
<input class="seat-cb" type="checkbox" id="s-F7" name="seat" value="F7" disabled aria-label="Row F Seat 7 — Sold"><label class="seat" for="s-F7">7</label>
<span class="aisle" aria-hidden="true"></span>
<input class="seat-cb" type="checkbox" id="s-F8" name="seat" value="F8" aria-label="Row F Seat 8"><label class="seat" for="s-F8">8</label>
<input class="seat-cb" type="checkbox" id="s-F9" name="seat" value="F9" aria-label="Row F Seat 9"><label class="seat" for="s-F9">9</label>
<input class="seat-cb" type="checkbox" id="s-F10" name="seat" value="F10" disabled aria-label="Row F Seat 10 — Sold"><label class="seat" for="s-F10">10</label>
<input class="seat-cb" type="checkbox" id="s-F11" name="seat" value="F11" disabled aria-label="Row F Seat 11 — Sold"><label class="seat" for="s-F11">11</label>
<input class="seat-cb" type="checkbox" id="s-F12" name="seat" value="F12" aria-label="Row F Seat 12"><label class="seat" for="s-F12">12</label>
<input class="seat-cb" type="checkbox" id="s-F13" name="seat" value="F13" aria-label="Row F Seat 13"><label class="seat" for="s-F13">13</label>
<input class="seat-cb" type="checkbox" id="s-F14" name="seat" value="F14" aria-label="Row F Seat 14"><label class="seat" for="s-F14">14</label>
<span class="row-lbl" aria-hidden="true">F</span>
<!-- ROW G -->
<span class="row-lbl" aria-hidden="true">G</span>
<input class="seat-cb" type="checkbox" id="s-G1" name="seat" value="G1" aria-label="Row G Seat 1"><label class="seat" for="s-G1">1</label>
<input class="seat-cb" type="checkbox" id="s-G2" name="seat" value="G2" aria-label="Row G Seat 2"><label class="seat" for="s-G2">2</label>
<input class="seat-cb" type="checkbox" id="s-G3" name="seat" value="G3" aria-label="Row G Seat 3"><label class="seat" for="s-G3">3</label>
<input class="seat-cb" type="checkbox" id="s-G4" name="seat" value="G4" aria-label="Row G Seat 4"><label class="seat" for="s-G4">4</label>
<input class="seat-cb" type="checkbox" id="s-G5" name="seat" value="G5" aria-label="Row G Seat 5"><label class="seat" for="s-G5">5</label>
<input class="seat-cb" type="checkbox" id="s-G6" name="seat" value="G6" disabled aria-label="Row G Seat 6 — Sold"><label class="seat" for="s-G6">6</label>
<input class="seat-cb" type="checkbox" id="s-G7" name="seat" value="G7" disabled aria-label="Row G Seat 7 — Sold"><label class="seat" for="s-G7">7</label>
<span class="aisle" aria-hidden="true"></span>
<input class="seat-cb" type="checkbox" id="s-G8" name="seat" value="G8" disabled aria-label="Row G Seat 8 — Sold"><label class="seat" for="s-G8">8</label>
<input class="seat-cb" type="checkbox" id="s-G9" name="seat" value="G9" disabled aria-label="Row G Seat 9 — Sold"><label class="seat" for="s-G9">9</label>
<input class="seat-cb" type="checkbox" id="s-G10" name="seat" value="G10" aria-label="Row G Seat 10"><label class="seat" for="s-G10">10</label>
<input class="seat-cb" type="checkbox" id="s-G11" name="seat" value="G11" aria-label="Row G Seat 11"><label class="seat" for="s-G11">11</label>
<input class="seat-cb" type="checkbox" id="s-G12" name="seat" value="G12" aria-label="Row G Seat 12"><label class="seat" for="s-G12">12</label>
<input class="seat-cb" type="checkbox" id="s-G13" name="seat" value="G13" aria-label="Row G Seat 13"><label class="seat" for="s-G13">13</label>
<input class="seat-cb" type="checkbox" id="s-G14" name="seat" value="G14" aria-label="Row G Seat 14"><label class="seat" for="s-G14">14</label>
<span class="row-lbl" aria-hidden="true">G</span>
<!-- ROW H -->
<span class="row-lbl" aria-hidden="true">H</span>
<input class="seat-cb" type="checkbox" id="s-H1" name="seat" value="H1" aria-label="Row H Seat 1"><label class="seat" for="s-H1">1</label>
<input class="seat-cb" type="checkbox" id="s-H2" name="seat" value="H2" aria-label="Row H Seat 2"><label class="seat" for="s-H2">2</label>
<input class="seat-cb" type="checkbox" id="s-H3" name="seat" value="H3" disabled aria-label="Row H Seat 3 — Sold"><label class="seat" for="s-H3">3</label>
<input class="seat-cb" type="checkbox" id="s-H4" name="seat" value="H4" disabled aria-label="Row H Seat 4 — Sold"><label class="seat" for="s-H4">4</label>
<input class="seat-cb" type="checkbox" id="s-H5" name="seat" value="H5" aria-label="Row H Seat 5"><label class="seat" for="s-H5">5</label>
<input class="seat-cb" type="checkbox" id="s-H6" name="seat" value="H6" aria-label="Row H Seat 6"><label class="seat" for="s-H6">6</label>
<input class="seat-cb" type="checkbox" id="s-H7" name="seat" value="H7" aria-label="Row H Seat 7"><label class="seat" for="s-H7">7</label>
<span class="aisle" aria-hidden="true"></span>
<input class="seat-cb" type="checkbox" id="s-H8" name="seat" value="H8" aria-label="Row H Seat 8"><label class="seat" for="s-H8">8</label>
<input class="seat-cb" type="checkbox" id="s-H9" name="seat" value="H9" aria-label="Row H Seat 9"><label class="seat" for="s-H9">9</label>
<input class="seat-cb" type="checkbox" id="s-H10" name="seat" value="H10" aria-label="Row H Seat 10"><label class="seat" for="s-H10">10</label>
<input class="seat-cb" type="checkbox" id="s-H11" name="seat" value="H11" disabled aria-label="Row H Seat 11 — Sold"><label class="seat" for="s-H11">11</label>
<input class="seat-cb" type="checkbox" id="s-H12" name="seat" value="H12" disabled aria-label="Row H Seat 12 — Sold"><label class="seat" for="s-H12">12</label>
<input class="seat-cb" type="checkbox" id="s-H13" name="seat" value="H13" aria-label="Row H Seat 13"><label class="seat" for="s-H13">13</label>
<input class="seat-cb" type="checkbox" id="s-H14" name="seat" value="H14" aria-label="Row H Seat 14"><label class="seat" for="s-H14">14</label>
<span class="row-lbl" aria-hidden="true">H</span>
<!-- ROW I -->
<span class="row-lbl" aria-hidden="true">I</span>
<input class="seat-cb" type="checkbox" id="s-I1" name="seat" value="I1" aria-label="Row I Seat 1"><label class="seat" for="s-I1">1</label>
<input class="seat-cb" type="checkbox" id="s-I2" name="seat" value="I2" aria-label="Row I Seat 2"><label class="seat" for="s-I2">2</label>
<input class="seat-cb" type="checkbox" id="s-I3" name="seat" value="I3" aria-label="Row I Seat 3"><label class="seat" for="s-I3">3</label>
<input class="seat-cb" type="checkbox" id="s-I4" name="seat" value="I4" aria-label="Row I Seat 4"><label class="seat" for="s-I4">4</label>
<input class="seat-cb" type="checkbox" id="s-I5" name="seat" value="I5" aria-label="Row I Seat 5"><label class="seat" for="s-I5">5</label>
<input class="seat-cb" type="checkbox" id="s-I6" name="seat" value="I6" aria-label="Row I Seat 6"><label class="seat" for="s-I6">6</label>
<input class="seat-cb" type="checkbox" id="s-I7" name="seat" value="I7" aria-label="Row I Seat 7"><label class="seat" for="s-I7">7</label>
<span class="aisle" aria-hidden="true"></span>
<input class="seat-cb" type="checkbox" id="s-I8" name="seat" value="I8" aria-label="Row I Seat 8"><label class="seat" for="s-I8">8</label>
<input class="seat-cb" type="checkbox" id="s-I9" name="seat" value="I9" aria-label="Row I Seat 9"><label class="seat" for="s-I9">9</label>
<input class="seat-cb" type="checkbox" id="s-I10" name="seat" value="I10" aria-label="Row I Seat 10"><label class="seat" for="s-I10">10</label>
<input class="seat-cb" type="checkbox" id="s-I11" name="seat" value="I11" aria-label="Row I Seat 11"><label class="seat" for="s-I11">11</label>
<input class="seat-cb" type="checkbox" id="s-I12" name="seat" value="I12" aria-label="Row I Seat 12"><label class="seat" for="s-I12">12</label>
<input class="seat-cb" type="checkbox" id="s-I13" name="seat" value="I13" aria-label="Row I Seat 13"><label class="seat" for="s-I13">13</label>
<input class="seat-cb" type="checkbox" id="s-I14" name="seat" value="I14" aria-label="Row I Seat 14"><label class="seat" for="s-I14">14</label>
<span class="row-lbl" aria-hidden="true">I</span>
<!-- ROW J -->
<span class="row-lbl" aria-hidden="true">J</span>
<input class="seat-cb" type="checkbox" id="s-J1" name="seat" value="J1" aria-label="Row J Seat 1"><label class="seat" for="s-J1">1</label>
<input class="seat-cb" type="checkbox" id="s-J2" name="seat" value="J2" aria-label="Row J Seat 2"><label class="seat" for="s-J2">2</label>
<input class="seat-cb" type="checkbox" id="s-J3" name="seat" value="J3" aria-label="Row J Seat 3"><label class="seat" for="s-J3">3</label>
<input class="seat-cb" type="checkbox" id="s-J4" name="seat" value="J4" aria-label="Row J Seat 4"><label class="seat" for="s-J4">4</label>
<input class="seat-cb" type="checkbox" id="s-J5" name="seat" value="J5" aria-label="Row J Seat 5"><label class="seat" for="s-J5">5</label>
<input class="seat-cb" type="checkbox" id="s-J6" name="seat" value="J6" aria-label="Row J Seat 6"><label class="seat" for="s-J6">6</label>
<input class="seat-cb" type="checkbox" id="s-J7" name="seat" value="J7" disabled aria-label="Row J Seat 7 — Sold"><label class="seat" for="s-J7">7</label>
<span class="aisle" aria-hidden="true"></span>
<input class="seat-cb" type="checkbox" id="s-J8" name="seat" value="J8" disabled aria-label="Row J Seat 8 — Sold"><label class="seat" for="s-J8">8</label>
<input class="seat-cb" type="checkbox" id="s-J9" name="seat" value="J9" aria-label="Row J Seat 9"><label class="seat" for="s-J9">9</label>
<input class="seat-cb" type="checkbox" id="s-J10" name="seat" value="J10" aria-label="Row J Seat 10"><label class="seat" for="s-J10">10</label>
<input class="seat-cb" type="checkbox" id="s-J11" name="seat" value="J11" aria-label="Row J Seat 11"><label class="seat" for="s-J11">11</label>
<input class="seat-cb" type="checkbox" id="s-J12" name="seat" value="J12" aria-label="Row J Seat 12"><label class="seat" for="s-J12">12</label>
<input class="seat-cb" type="checkbox" id="s-J13" name="seat" value="J13" aria-label="Row J Seat 13"><label class="seat" for="s-J13">13</label>
<input class="seat-cb" type="checkbox" id="s-J14" name="seat" value="J14" aria-label="Row J Seat 14"><label class="seat" for="s-J14">14</label>
<span class="row-lbl" aria-hidden="true">J</span>
<!-- ROW K -->
<span class="row-lbl" aria-hidden="true">K</span>
<input class="seat-cb" type="checkbox" id="s-K1" name="seat" value="K1" aria-label="Row K Seat 1"><label class="seat" for="s-K1">1</label>
<input class="seat-cb" type="checkbox" id="s-K2" name="seat" value="K2" aria-label="Row K Seat 2"><label class="seat" for="s-K2">2</label>
<input class="seat-cb" type="checkbox" id="s-K3" name="seat" value="K3" aria-label="Row K Seat 3"><label class="seat" for="s-K3">3</label>
<input class="seat-cb" type="checkbox" id="s-K4" name="seat" value="K4" aria-label="Row K Seat 4"><label class="seat" for="s-K4">4</label>
<input class="seat-cb" type="checkbox" id="s-K5" name="seat" value="K5" aria-label="Row K Seat 5"><label class="seat" for="s-K5">5</label>
<input class="seat-cb" type="checkbox" id="s-K6" name="seat" value="K6" aria-label="Row K Seat 6"><label class="seat" for="s-K6">6</label>
<input class="seat-cb" type="checkbox" id="s-K7" name="seat" value="K7" aria-label="Row K Seat 7"><label class="seat" for="s-K7">7</label>
<span class="aisle" aria-hidden="true"></span>
<input class="seat-cb" type="checkbox" id="s-K8" name="seat" value="K8" aria-label="Row K Seat 8"><label class="seat" for="s-K8">8</label>
<input class="seat-cb" type="checkbox" id="s-K9" name="seat" value="K9" aria-label="Row K Seat 9"><label class="seat" for="s-K9">9</label>
<input class="seat-cb" type="checkbox" id="s-K10" name="seat" value="K10" aria-label="Row K Seat 10"><label class="seat" for="s-K10">10</label>
<input class="seat-cb" type="checkbox" id="s-K11" name="seat" value="K11" aria-label="Row K Seat 11"><label class="seat" for="s-K11">11</label>
<input class="seat-cb" type="checkbox" id="s-K12" name="seat" value="K12" aria-label="Row K Seat 12"><label class="seat" for="s-K12">12</label>
<input class="seat-cb" type="checkbox" id="s-K13" name="seat" value="K13" aria-label="Row K Seat 13"><label class="seat" for="s-K13">13</label>
<input class="seat-cb" type="checkbox" id="s-K14" name="seat" value="K14" aria-label="Row K Seat 14"><label class="seat" for="s-K14">14</label>
<span class="row-lbl" aria-hidden="true">K</span>
<!-- ROW L (Back) -->
<span class="row-lbl" aria-hidden="true">L</span>
<input class="seat-cb" type="checkbox" id="s-L1" name="seat" value="L1" aria-label="Row L Seat 1"><label class="seat" for="s-L1">1</label>
<input class="seat-cb" type="checkbox" id="s-L2" name="seat" value="L2" aria-label="Row L Seat 2"><label class="seat" for="s-L2">2</label>
<input class="seat-cb" type="checkbox" id="s-L3" name="seat" value="L3" aria-label="Row L Seat 3"><label class="seat" for="s-L3">3</label>
<input class="seat-cb" type="checkbox" id="s-L4" name="seat" value="L4" aria-label="Row L Seat 4"><label class="seat" for="s-L4">4</label>
<input class="seat-cb" type="checkbox" id="s-L5" name="seat" value="L5" aria-label="Row L Seat 5"><label class="seat" for="s-L5">5</label>
<input class="seat-cb" type="checkbox" id="s-L6" name="seat" value="L6" aria-label="Row L Seat 6"><label class="seat" for="s-L6">6</label>
<input class="seat-cb" type="checkbox" id="s-L7" name="seat" value="L7" aria-label="Row L Seat 7"><label class="seat" for="s-L7">7</label>
<span class="aisle" aria-hidden="true"></span>
<input class="seat-cb" type="checkbox" id="s-L8" name="seat" value="L8" aria-label="Row L Seat 8"><label class="seat" for="s-L8">8</label>
<input class="seat-cb" type="checkbox" id="s-L9" name="seat" value="L9" aria-label="Row L Seat 9"><label class="seat" for="s-L9">9</label>
<input class="seat-cb" type="checkbox" id="s-L10" name="seat" value="L10" aria-label="Row L Seat 10"><label class="seat" for="s-L10">10</label>
<input class="seat-cb" type="checkbox" id="s-L11" name="seat" value="L11" aria-label="Row L Seat 11"><label class="seat" for="s-L11">11</label>
<input class="seat-cb" type="checkbox" id="s-L12" name="seat" value="L12" aria-label="Row L Seat 12"><label class="seat" for="s-L12">12</label>
<input class="seat-cb" type="checkbox" id="s-L13" name="seat" value="L13" aria-label="Row L Seat 13"><label class="seat" for="s-L13">13</label>
<input class="seat-cb" type="checkbox" id="s-L14" name="seat" value="L14" aria-label="Row L Seat 14"><label class="seat" for="s-L14">14</label>
<span class="row-lbl" aria-hidden="true">L</span>
</div>
</div>
<p class="aisle-caption">Row A is closest to the screen · Row L is the back row</p>
</section>
<!-- ===================================================================
TOP-RIGHT SMALL CIRCULAR BASKET BADGE
Placed after the checkboxes in DOM order so CSS counter(seats) works
even with JavaScript switched off; visually anchored in the header's
top-right corner.
=================================================================== -->
<a class="header-basket" href="#box-office" id="header-basket-link" aria-label="Ticket basket: 0 seats selected">
<span class="basket-caption" aria-hidden="true">
<svg class="basket-icon" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 10l2-6h10l2 6"/>
<path d="M3 10h18l-2 10H5L3 10z"/>
</svg>
Basket
</span>
<span class="basket-circle" id="basket-circle" data-digits="1">
<span class="js-only" id="js-badge-count">0</span>
</span>
</a>
<!-- ===================================================================
STEP 3: RUNNING TOTAL & COUNTER PASS
=================================================================== -->
<section class="box-office-card" id="box-office" aria-labelledby="box-office-heading">
<div class="box-office-grid">
<div>
<h2 class="selection-heading" id="box-office-heading">Your Ticket Basket</h2>
<p class="selection-film js-only" id="js-summary-film">
Screen One · <strong>The Last Harbor</strong> — 2:30 PM ($8.00 Matinee)
</p>
<p class="selection-seats js-only" id="js-summary-seats">
No seats picked yet — click any red seat above to add it to your basket.
</p>
<p class="selection-seats nojs-only nojs-seat-tally"></p>
</div>
<div class="totals-panel" aria-live="polite" aria-atomic="true">
<div class="totals-label">Running Total</div>
<!-- Smoothly animated JS running total -->
<div class="totals-figures js-only">
<span class="usd-figure" id="js-usd-total">$0.00</span>
<span class="eur-figure" id="js-eur-total">≈ €0.00</span>
</div>
<!-- Pure CSS no-JS running totals -->
<div class="totals-figures nojs-only nojs-matinee-tot">
<span class="usd-figure nojs-matinee-usd"></span>
<span class="eur-figure nojs-matinee-eur"></span>
</div>
<div class="totals-figures nojs-only nojs-evening-tot">
<span class="usd-figure nojs-evening-usd"></span>
<span class="eur-figure nojs-evening-eur"></span>
</div>
<p class="fx-disclaimer" id="fx-note">
Approx. tourist guide rate ($1 = €0.92 offline ref.) · Charged in USD at the till
</p>
</div>
</div>
<div class="actions-row js-only">
<button type="button" class="btn btn-secondary" id="clear-btn" disabled>Clear Seats</button>
<button type="button" class="btn btn-primary" id="pass-btn" disabled>Prepare Counter Pass</button>
</div>
<p class="fx-disclaimer nojs-only" style="margin-top: 12px;">
JavaScript is switched off on this browser — show this screen (or print with Ctrl+P / Cmd+P) at the box-office window to issue your tickets.
</p>
<!-- Generated Counter Slip for Box Office / School Block Bookings -->
<div class="counter-pass" id="counter-pass" hidden>
<div class="pass-top">
<h3 class="pass-title">The Savoy Twin — Counter Pass</h3>
<span class="pass-ref" id="pass-ref">Ref: SAV-1</span>
</div>
<dl class="pass-details">
<div>
<dt>Picture & Screen</dt>
<dd id="pass-film">—</dd>
</div>
<div>
<dt>Tonight's Time</dt>
<dd id="pass-time">—</dd>
</div>
<div>
<dt>Seats Chosen</dt>
<dd id="pass-seats">—</dd>
</div>
<div>
<dt>Amount Due at Desk</dt>
<dd id="pass-amount">—</dd>
</div>
</dl>
<div class="pass-footer">
<span>Present this pass at the box-office counter to collect your printed tickets.</span>
<button type="button" class="btn-print" id="print-btn">Print Pass</button>
</div>
</div>
</section>
</form>
</div>
<script>
(function () {
'use strict';
/* =====================================================================
OWNER CONFIGURATION
===================================================================== */
// Offline guide exchange rate ($1.00 USD -> EUR). Edit here when needed:
var EUR_PER_USD = 0.92;
// Ticket tariffs ($8.00 before 5:00 PM, $11.50 from 5:00 PM onwards):
var PRICE_BEFORE_FIVE = 8.00;
var PRICE_AFTER_FIVE = 11.50;
// Set to `true` ONLY if you ever prefer capping the circle at 1 character
// ("9" / "+") instead of showing "12" for school block-bookings:
var CAP_BADGE_SINGLE_DIGIT = false;
/* =====================================================================
TONIGHT'S FOUR SHOWINGS & REALISTIC SOLD-SEAT MAPS
(Rows A, I, K, L are kept open across showings for 12–14 seat school
block-bookings while middle rows reflect realistic sold seats.)
===================================================================== */
var SHOWINGS = {
'sh-1': {
screen: 'Screen One',
film: 'The Last Harbor',
timeLabel: '2:30 PM',
hour24: 14.5,
sold: [
'B6', 'B7',
'D8', 'D9',
'E5', 'E6', 'E7', 'E8', 'E9',
'F4', 'F5', 'F6', 'F7', 'F10', 'F11',
'G6', 'G7', 'G8', 'G9',
'H3', 'H4', 'H11', 'H12',
'J7', 'J8'
]
},
'sh-2': {
screen: 'Screen One',
film: 'The Last Harbor',
timeLabel: '7:15 PM',
hour24: 19.25,
sold: [
'B4', 'B5',
'C6', 'C7', 'C8',
'D5', 'D6', 'D7', 'D10', 'D11',
'E3', 'E4', 'E6', 'E7', 'E8', 'E9', 'E10',
'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11',
'G4', 'G5', 'G7', 'G8', 'G9',
'H6', 'H7', 'H8', 'H9',
'J5', 'J6', 'J7'
]
},
'sh-3': {
screen: 'Screen Two',
film: 'Midnight on the Orient Line',
timeLabel: '3:15 PM',
hour24: 15.25,
sold: [
'C9', 'C10',
'E6', 'E7', 'E8',
'F5', 'F6', 'F8', 'F9',
'G6', 'G7', 'G10', 'G11',
'H4', 'H5', 'H6',
'J8', 'J9'
]
},
'sh-4': {
screen: 'Screen Two',
film: 'Midnight on the Orient Line',
timeLabel: '8:00 PM',
hour24: 20.0,
sold: [
'B8', 'B9',
'C4', 'C5',
'D6', 'D7', 'D8', 'D9',
'E5', 'E6', 'E7', 'E10', 'E11',
'F3', 'F4', 'F6', 'F7', 'F8', 'F9', 'F12',
'G5', 'G6', 'G7', 'G8', 'G9', 'G10',
'H7', 'H8', 'H10', 'H11',
'J4', 'J5'
]
}
};
/* =====================================================================
DOM ELEMENTS
===================================================================== */
var form = document.getElementById('booking-form');
var showingRadios = Array.prototype.slice.call(document.querySelectorAll('.sh-radio'));
var seatBoxes = Array.prototype.slice.call(document.querySelectorAll('.seat-cb'));
var badgeCircle = document.getElementById('basket-circle');
var badgeCountEl = document.getElementById('js-badge-count');
var basketLink = document.getElementById('header-basket-link');
var showingBanner = document.getElementById('js-showing-banner');
var swapNotice = document.getElementById('swap-notice');
var summaryFilm = document.getElementById('js-summary-film');
var summarySeats = document.getElementById('js-summary-seats');
var usdTotalEl = document.getElementById('js-usd-total');
var eurTotalEl = document.getElementById('js-eur-total');
var fxNoteEl = document.getElementById('fx-note');
var clearBtn = document.getElementById('clear-btn');
var passBtn = document.getElementById('pass-btn');
var counterPass = document.getElementById('counter-pass');
var passRef = document.getElementById('pass-ref');
var passFilm = document.getElementById('pass-film');
var passTime = document.getElementById('pass-time');
var passSeats = document.getElementById('pass-seats');
var passAmount = document.getElementById('pass-amount');
var printBtn = document.getElementById('print-btn');
fxNoteEl.textContent =
'Approx. tourist guide rate ($1 = €' + EUR_PER_USD.toFixed(2) + ' offline ref.) · Charged in USD at the till';
var prefersReducedMotion = false;
if (window.matchMedia) {
prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
/* =====================================================================
HELPERS
===================================================================== */
function getActiveShowingId() {
for (var i = 0; i < showingRadios.length; i++) {
if (showingRadios[i].checked) return showingRadios[i].value;
}
return 'sh-1';
}
function getTicketPrice(showing) {
return showing.hour24 >= 17 ? PRICE_AFTER_FIVE : PRICE_BEFORE_FIVE;
}
function getSelectedSeats() {
var picked = [];
for (var i = 0; i < seatBoxes.length; i++) {
if (seatBoxes[i].checked && !seatBoxes[i].disabled) {
picked.push(seatBoxes[i].value);
}
}
return picked;
}
function formatSeatsByRow(seatCodes) {
var byRow = {};
var rowOrder = [];
for (var i = 0; i < seatCodes.length; i++) {
var code = seatCodes[i];
var row = code.charAt(0);
var num = parseInt(code.slice(1), 10);
if (!byRow[row]) {
byRow[row] = [];
rowOrder.push(row);
}
byRow[row].push(num);
}
var parts = [];
for (var r = 0; r < rowOrder.length; r++) {
var rk = rowOrder[r];
var nums = byRow[rk].sort(function (a, b) { return a - b; });
parts.push('Row ' + rk + ': ' + nums.join(', '));
}
return parts.join(' · ');
}
/* =====================================================================
SMOOTH COUNT-UP ANIMATION FOR RUNNING TOTAL
===================================================================== */
var displayedUsd = 0;
var displayedEur = 0;
var animFrame = null;
function renderTotalsText(usdVal, eurVal) {
usdTotalEl.textContent = '$' + Math.max(0, usdVal).toFixed(2);
eurTotalEl.textContent = '≈ €' + Math.max(0, eurVal).toFixed(2);
}
function animateTotalsTo(targetUsd, targetEur, immediate) {
if (animFrame) {
cancelAnimationFrame(animFrame);
animFrame = null;
}
if (immediate || prefersReducedMotion) {
displayedUsd = targetUsd;
displayedEur = targetEur;
renderTotalsText(targetUsd, targetEur);
return;
}
var startUsd = displayedUsd;
var startEur = displayedEur;
var deltaUsd = targetUsd - startUsd;
var deltaEur = targetEur - startEur;
if (Math.abs(deltaUsd) < 0.005 && Math.abs(deltaEur) < 0.005) {
displayedUsd = targetUsd;
displayedEur = targetEur;
renderTotalsText(targetUsd, targetEur);
return;
}
var durationMs = 420;
var startTime = performance.now();
function step(now) {
var t = Math.min(1, Math.max(0, (now - startTime) / durationMs));
// Smooth ease-out cubic so counting settles gently onto the final cent
var ease = 1 - Math.pow(1 - t, 3);
displayedUsd = startUsd + deltaUsd * ease;
displayedEur = startEur + deltaEur * ease;
renderTotalsText(displayedUsd, displayedEur);
if (t < 1) {
animFrame = requestAnimationFrame(step);
} else {
displayedUsd = targetUsd;
displayedEur = targetEur;
renderTotalsText(targetUsd, targetEur);
animFrame = null;
}
}
animFrame = requestAnimationFrame(step);
}
/* =====================================================================
APPLY SHOWING SOLD MAP & UPDATE UI
===================================================================== */
var lastCount = 0;
function applySoldSeatsForShowing(showingId) {
var showing = SHOWINGS[showingId];
var soldLookup = {};
for (var i = 0; i < showing.sold.length; i++) {
soldLookup[showing.sold[i]] = true;
}
var droppedSeats = [];
for (var j = 0; j < seatBoxes.length; j++) {
var cb = seatBoxes[j];
var code = cb.value;
var row = code.charAt(0);
var num = code.slice(1);
var isSold = Boolean(soldLookup[code]);
if (isSold && cb.checked) {
cb.checked = false;
droppedSeats.push(code);
}
cb.disabled = isSold;
cb.setAttribute(
'aria-label',
isSold
? 'Row ' + row + ' Seat ' + num + ' — Sold'
: 'Row ' + row + ' Seat ' + num
);
}
if (droppedSeats.length > 0) {
swapNotice.textContent =
(droppedSeats.length === 1 ? 'Seat ' + droppedSeats[0] + ' is' : 'Seats ' + droppedSeats.join(', ') + ' are') +
' already sold for the ' + showing.timeLabel + ' showing and removed from your basket.';
swapNotice.hidden = false;
} else {
swapNotice.hidden = true;
}
}
function updateUI(immediateAnimation) {
var showingId = getActiveShowingId();
var showing = SHOWINGS[showingId];
var unitPrice = getTicketPrice(showing);
var tariffWord = showing.hour24 >= 17 ? 'Evening' : 'Matinee';
var selected = getSelectedSeats();
var count = selected.length;
// 1. Update top-right small circular basket badge
var badgeText;
if (CAP_BADGE_SINGLE_DIGIT && count > 9) {
badgeText = '+';
} else {
badgeText = String(count);
}
badgeCountEl.textContent = badgeText;
badgeCircle.setAttribute('data-digits', String(Math.min(3, Math.max(1, badgeText.length))));
basketLink.setAttribute(
'aria-label',
'Ticket basket: ' + count + (count === 1 ? ' seat' : ' seats') + ' selected'
);
if (count !== lastCount && !immediateAnimation) {
badgeCircle.classList.remove('pop');
void badgeCircle.offsetWidth;
badgeCircle.classList.add('pop');
}
lastCount = count;
// 2. Update active showing banner & summary text
showingBanner.innerHTML = '';
showingBanner.appendChild(document.createTextNode('Showing: '));
var strongBanner = document.createElement('strong');
strongBanner.textContent = showing.screen + ' — ' + showing.film + ' · ' + showing.timeLabel;
showingBanner.appendChild(strongBanner);
showingBanner.appendChild(document.createTextNode(' ($' + unitPrice.toFixed(2) + ' per seat)'));
summaryFilm.innerHTML = '';
summaryFilm.appendChild(document.createTextNode(showing.screen + ' · '));
var strongFilm = document.createElement('strong');
strongFilm.textContent = showing.film;
summaryFilm.appendChild(strongFilm);
summaryFilm.appendChild(
document.createTextNode(' — ' + showing.timeLabel + ' ($' + unitPrice.toFixed(2) + ' ' + tariffWord + ')')
);
if (count === 0) {
summarySeats.textContent = 'No seats picked yet — click any red seat above to add it to your basket.';
counterPass.hidden = true;
} else {
summarySeats.textContent =
count + (count === 1 ? ' seat' : ' seats') + ' (' + count + ' × $' + unitPrice.toFixed(2) + ') — ' +
formatSeatsByRow(selected);
}
clearBtn.disabled = count === 0;
passBtn.disabled = count === 0;
// 3. Smoothly count up / interpolate running total in USD & guide EUR
var totalUsd = count * unitPrice;
var totalEur = totalUsd * EUR_PER_USD;
animateTotalsTo(totalUsd, totalEur, Boolean(immediateAnimation));
// Keep open counter pass synchronised if already visible
if (!counterPass.hidden && count > 0) {
populateCounterPass(showing, selected, totalUsd, totalEur);
}
}
function populateCounterPass(showing, selected, totalUsd, totalEur) {
var screenDigit = showing.screen === 'Screen One' ? '1' : '2';
var refCode = 'SAV-' + screenDigit + '-' + selected[0] + '-' + selected.length;
passRef.textContent = 'Pass: ' + refCode;
passFilm.textContent = showing.film + ' (' + showing.screen + ')';
passTime.textContent = 'Tonight at ' + showing.timeLabel;
passSeats.textContent = selected.length + (selected.length === 1 ? ' seat: ' : ' seats: ') + formatSeatsByRow(selected);
passAmount.textContent = '$' + totalUsd.toFixed(2) + ' USD (guide ≈ €' + totalEur.toFixed(2) + ')';
}
/* =====================================================================
EVENT LISTENERS
===================================================================== */
form.addEventListener('change', function (e) {
var target = e.target;
if (!target) return;
if (target.classList.contains('sh-radio')) {
applySoldSeatsForShowing(target.value);
updateUI(false);
} else if (target.classList.contains('seat-cb')) {
swapNotice.hidden = true;
updateUI(false);
}
});
form.addEventListener('submit', function (e) {
e.preventDefault();
});
clearBtn.addEventListener('click', function () {
for (var i = 0; i < seatBoxes.length; i++) {
seatBoxes[i].checked = false;
}
swapNotice.hidden = true;
counterPass.hidden = true;
updateUI(false);
});
passBtn.addEventListener('click', function () {
var selected = getSelectedSeats();
if (selected.length === 0) return;
var showing = SHOWINGS[getActiveShowingId()];
var totalUsd = selected.length * getTicketPrice(showing);
var totalEur = totalUsd * EUR_PER_USD;
populateCounterPass(showing, selected, totalUsd, totalEur);
counterPass.hidden = false;
counterPass.scrollIntoView({ behavior: prefersReducedMotion ? 'auto' : 'smooth', block: 'nearest' });
});
printBtn.addEventListener('click', function () {
window.print();
});
// Initialise cleanly on load
applySoldSeatsForShowing(getActiveShowingId());
updateUI(true);
})();
</script>
</body>
</html>