A peek behind the curtain — every file the club keeps on the server, from the front-of-house code to the trophy ledger in the database.
<?php
$dbPath = __DIR__ . '/../database/tennis_club.sqlite';
$pdo = null;
try {
$pdo = new PDO('sqlite:' . $dbPath);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE IF NOT EXISTS messages (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, body TEXT, created_at INTEGER)");
$pdo->exec("CREATE TABLE IF NOT EXISTS bookings (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, court TEXT, slot_date TEXT, slot_time TEXT, created_at INTEGER, UNIQUE(court, slot_date, slot_time))");
} catch (Exception $e) { $pdo = null; }
$name = isset($_COOKIE['tennis_name']) ? substr(trim($_COOKIE['tennis_name']), 0, 30) : '';
$ADMIN_PASSWORD = '123456';
$ADMIN_TOKEN = hash('sha256', 'baseline-admin-' . $ADMIN_PASSWORD);
$isAdmin = isset($_COOKIE['tennis_admin']) && hash_equals($ADMIN_TOKEN, $_COOKIE['tennis_admin']);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['set_name'])) {
$n = trim($_POST['set_name']);
if ($n !== '') {
$n = substr($n, 0, 30);
setcookie('tennis_name', $n, ['expires' => time() + 86400 * 60, 'path' => '/', 'secure' => true, 'samesite' => 'None']);
$_COOKIE['tennis_name'] = $n;
$name = $n;
}
}
if (isset($_POST['logout'])) {
setcookie('tennis_name', '', ['expires' => time() - 3600, 'path' => '/', 'secure' => true, 'samesite' => 'None']);
unset($_COOKIE['tennis_name']);
$name = '';
}
if (isset($_POST['admin_login'])) {
$pw = $_POST['admin_password'] ?? '';
if ($pw === $ADMIN_PASSWORD) {
setcookie('tennis_admin', $ADMIN_TOKEN, ['expires' => time() + 86400, 'path' => '/', 'secure' => true, 'samesite' => 'None', 'httponly' => true]);
$_COOKIE['tennis_admin'] = $ADMIN_TOKEN;
$isAdmin = true;
header("Location: ?page=admin");
exit;
} else {
header("Location: ?page=admin&err=1");
exit;
}
}
if (isset($_POST['admin_logout'])) {
setcookie('tennis_admin', '', ['expires' => time() - 3600, 'path' => '/', 'secure' => true, 'samesite' => 'None']);
unset($_COOKIE['tennis_admin']);
$isAdmin = false;
header("Location: ?page=admin");
exit;
}
if ($isAdmin && $pdo) {
if (isset($_POST['admin_edit_msg_id'])) {
try {
$stmt = $pdo->prepare("UPDATE messages SET body = ? WHERE id = ?");
$stmt->execute([substr(trim($_POST['admin_edit_msg_body'] ?? ''), 0, 500), (int)$_POST['admin_edit_msg_id']]);
} catch (Exception $e) {}
header("Location: ?page=admin#chats");
exit;
}
if (isset($_POST['admin_delete_msg_id'])) {
try {
$stmt = $pdo->prepare("DELETE FROM messages WHERE id = ?");
$stmt->execute([(int)$_POST['admin_delete_msg_id']]);
} catch (Exception $e) {}
header("Location: ?page=admin#chats");
exit;
}
if (isset($_POST['admin_delete_booking_id'])) {
try {
$stmt = $pdo->prepare("DELETE FROM bookings WHERE id = ?");
$stmt->execute([(int)$_POST['admin_delete_booking_id']]);
} catch (Exception $e) {}
header("Location: ?page=admin#bookings");
exit;
}
if (isset($_POST['admin_rename_user'])) {
$from = trim($_POST['admin_rename_from'] ?? '');
$to = substr(trim($_POST['admin_rename_to'] ?? ''), 0, 30);
if ($from !== '' && $to !== '') {
try {
$s1 = $pdo->prepare("UPDATE messages SET name = ? WHERE name = ?");
$s1->execute([$to, $from]);
$s2 = $pdo->prepare("UPDATE bookings SET name = ? WHERE name = ?");
$s2->execute([$to, $from]);
} catch (Exception $e) {}
}
header("Location: ?page=admin#users");
exit;
}
if (isset($_POST['admin_purge_user'])) {
$u = trim($_POST['admin_purge_user']);
if ($u !== '') {
try {
$pdo->prepare("DELETE FROM messages WHERE name = ?")->execute([$u]);
$pdo->prepare("DELETE FROM bookings WHERE name = ?")->execute([$u]);
} catch (Exception $e) {}
}
header("Location: ?page=admin#users");
exit;
}
}
if ($pdo) {
if (isset($_POST['message']) && $name !== '') {
$msg = trim($_POST['message']);
if ($msg !== '') {
try {
$stmt = $pdo->prepare("INSERT INTO messages (name, body, created_at) VALUES (?, ?, ?)");
$stmt->execute([$name, substr($msg, 0, 500), time()]);
} catch (Exception $e) {}
}
}
if (isset($_POST['book']) && $name !== '' && !empty($_POST['court']) && !empty($_POST['slot_date']) && !empty($_POST['slot_time'])) {
try {
$stmt = $pdo->prepare("INSERT INTO bookings (name, court, slot_date, slot_time, created_at) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$name, $_POST['court'], $_POST['slot_date'], $_POST['slot_time'], time()]);
} catch (Exception $e) {}
}
if (isset($_POST['cancel_id']) && $name !== '') {
try {
$stmt = $pdo->prepare("DELETE FROM bookings WHERE id = ? AND name = ?");
$stmt->execute([$_POST['cancel_id'], $name]);
} catch (Exception $e) {}
}
}
$anchor = isset($_POST['anchor']) ? '#' . $_POST['anchor'] : '';
$redirect = $_SERVER['PHP_SELF'] . $anchor;
header("Location: " . $redirect);
exit;
}
if (isset($_GET['action']) && $_GET['action'] === 'messages') {
header('Content-Type: application/json');
if ($pdo) {
try {
$rows = $pdo->query("SELECT name, body, created_at FROM messages ORDER BY id DESC LIMIT 80")->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(array_reverse($rows));
} catch (Exception $e) { echo json_encode([]); }
} else {
echo json_encode([]);
}
exit;
}
function e($s){ return htmlspecialchars($s ?? '', ENT_QUOTES, 'UTF-8'); }
function human_size($bytes){
if ($bytes < 1024) return $bytes . ' B';
$units = ['KB','MB','GB','TB'];
$i = -1;
do { $bytes /= 1024; $i++; } while ($bytes >= 1024 && $i < count($units)-1);
return number_format($bytes, $bytes < 10 ? 2 : 1) . ' ' . $units[$i];
}
$page = $_GET['page'] ?? 'home';
$projectRoot = realpath(__DIR__ . '/..');
$webRoot = realpath(__DIR__);
$target = $projectRoot;
if ($page === 'files') {
$reqPath = isset($_GET['path']) && $_GET['path'] !== '' ? $_GET['path'] : $projectRoot;
$reqPath = str_replace('\\', '/', $reqPath);
$resolved = realpath($reqPath);
if ($resolved === false || !is_readable($resolved)) {
$target = $projectRoot;
} else {
$target = $resolved;
}
if (isset($_GET['dl']) && is_file($target)) {
$size = filesize($target);
if ($size <= 50 * 1024 * 1024) {
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . basename($target) . '"');
header('Content-Length: ' . $size);
readfile($target);
exit;
}
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $page === 'files' ? 'My Files / ' : ($page === 'admin' ? 'Admin / ' : '') ?>Baseline — A Tennis Club</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Bricolage+Grotesque:opsz,wght@12..96,300;12..96,400;12..96,500;12..96,600;12..96,700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root{
--bg:#0b0c0a;
--bg-2:#111210;
--surface:#161815;
--surface-2:#1d201c;
--surface-3:#252925;
--line:rgba(255,255,255,.07);
--line-2:rgba(255,255,255,.13);
--text:#eceae3;
--dim:#9a9890;
--dimmer:#5e5d56;
--lime:#d4ff3a;
--lime-2:#b6dd24;
--lime-soft:rgba(212,255,58,.12);
--clay:#f06a3a;
--grass:#74c354;
--hard:#5cb6d6;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{background:var(--bg);color:var(--text);font-family:'Bricolage Grotesque',sans-serif;font-size:16px;line-height:1.5;-webkit-font-smoothing:antialiased;font-feature-settings:"ss01","ss02"}
body{overflow-x:hidden;min-height:100vh}
.serif{font-family:'Instrument Serif',serif;letter-spacing:-.01em}
.mono{font-family:'JetBrains Mono',monospace}
::selection{background:var(--lime);color:#000}
body::before{
content:'';position:fixed;inset:0;pointer-events:none;z-index:0;
background:
radial-gradient(800px 500px at 85% -5%, rgba(212,255,58,.06), transparent 60%),
radial-gradient(700px 400px at -5% 100%, rgba(240,106,58,.04), transparent 60%);
}
body::after{
content:'';position:fixed;inset:0;pointer-events:none;z-index:0;opacity:.4;
background-image:radial-gradient(circle at 1px 1px, rgba(255,255,255,.025) 1px, transparent 0);
background-size:24px 24px;
}
.container{max-width:1320px;margin:0 auto;padding:0 32px;position:relative;z-index:1}
nav.top{
position:sticky;top:0;z-index:50;
backdrop-filter:blur(20px) saturate(180%);
background:rgba(11,12,10,.72);
border-bottom:1px solid var(--line);
}
.nav-inner{display:flex;align-items:center;justify-content:space-between;padding:18px 0;gap:24px}
.brand{display:flex;align-items:center;gap:12px;text-decoration:none;color:var(--text)}
.brand-mark{
width:36px;height:36px;border-radius:50%;
background:radial-gradient(circle at 32% 30%, #e8ff5a, var(--lime) 55%, #95b81d 100%);
position:relative;flex-shrink:0;
box-shadow:0 0 0 1px rgba(212,255,58,.4), 0 8px 20px rgba(212,255,58,.25);
}
.brand-mark::before,.brand-mark::after{
content:'';position:absolute;inset:0;border-radius:50%;
border:1px solid rgba(255,255,255,.6);
clip-path:polygon(0 30%, 100% 70%, 100% 75%, 0 35%);
}
.brand-mark::after{clip-path:polygon(0 25%, 100% 65%, 100% 60%, 0 20%);transform:rotate(180deg)}
.brand-text{display:flex;flex-direction:column;line-height:1}
.brand-name{font-family:'Instrument Serif',serif;font-size:22px;letter-spacing:-.01em}
.brand-sub{font-family:'JetBrains Mono',monospace;font-size:9.5px;text-transform:uppercase;letter-spacing:.22em;color:var(--dim);margin-top:3px}
.nav-links{display:flex;align-items:center;gap:4px}
.nav-links a{
color:var(--dim);text-decoration:none;font-weight:500;font-size:13.5px;
padding:8px 14px;border-radius:999px;transition:all .2s;
display:inline-flex;align-items:center;gap:6px;
}
.nav-links a:hover{color:var(--text);background:var(--surface)}
.nav-links a.active{color:var(--lime);background:var(--lime-soft)}
.user-pill{
display:inline-flex;align-items:center;gap:8px;
background:var(--surface-2);border:1px solid var(--line-2);
padding:6px 14px 6px 8px;border-radius:999px;font-size:13px;font-weight:500;
color:var(--text);
}
.user-pill .av{
width:22px;height:22px;border-radius:50%;font-size:11px;font-weight:600;
display:flex;align-items:center;justify-content:center;color:#000;
font-family:'Instrument Serif',serif;
}
.live-dot{width:7px;height:7px;border-radius:50%;background:var(--lime);box-shadow:0 0 8px var(--lime)}
.hero{padding:80px 0 60px;position:relative}
.hero-grid{display:grid;grid-template-columns:1.5fr 1fr;gap:60px;align-items:center}
.eyebrow{
display:inline-flex;align-items:center;gap:10px;
font-family:'JetBrains Mono',monospace;font-size:11.5px;text-transform:uppercase;
letter-spacing:.18em;color:var(--lime);margin-bottom:32px;
padding:6px 14px;border:1px solid rgba(212,255,58,.25);border-radius:999px;
background:var(--lime-soft);
}
.eyebrow .pulse{width:6px;height:6px;border-radius:50%;background:var(--lime);box-shadow:0 0 10px var(--lime);animation:pulse 1.8s ease-in-out infinite}
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.7)}}
.hero h1{
font-family:'Instrument Serif',serif;font-weight:400;
font-size:clamp(54px, 8.5vw, 120px);line-height:.92;
letter-spacing:-.03em;margin-bottom:32px;
}
.hero h1 em{font-style:italic;color:var(--lime)}
.hero h1 .strike{position:relative;display:inline-block}
.hero h1 .strike::after{
content:'';position:absolute;left:-2%;right:-2%;top:53%;height:3px;
background:var(--clay);transform:rotate(-3deg);
}
.hero p.lead{font-size:18px;color:var(--dim);max-width:520px;margin-bottom:40px;line-height:1.55}
.hero-cta{display:flex;gap:12px;flex-wrap:wrap;align-items:center}
.btn{
display:inline-flex;align-items:center;gap:10px;padding:14px 24px;
border-radius:999px;font-weight:600;font-size:14.5px;text-decoration:none;
border:none;cursor:pointer;transition:all .2s;font-family:inherit;
white-space:nowrap;
}
.btn-primary{background:var(--lime);color:#0b0c0a;box-shadow:0 0 0 1px var(--lime), 0 12px 32px rgba(212,255,58,.25)}
.btn-primary:hover{background:#e1ff5a;transform:translateY(-2px);box-shadow:0 0 0 1px var(--lime), 0 16px 38px rgba(212,255,58,.35)}
.btn-ghost{background:transparent;color:var(--text);border:1px solid var(--line-2)}
.btn-ghost:hover{background:var(--surface);border-color:var(--text)}
.btn-arrow{display:inline-flex;width:24px;height:24px;border-radius:50%;background:rgba(0,0,0,.12);align-items:center;justify-content:center;transition:transform .2s}
.btn-primary:hover .btn-arrow{transform:translateX(4px)}
.court-illus{position:relative;aspect-ratio:3/4;max-width:380px;margin-left:auto}
.court-illus svg{width:100%;height:100%;filter:drop-shadow(0 30px 60px rgba(212,255,58,.15))}
.marquee{
margin-top:60px;padding:24px 0;
border-top:1px solid var(--line);border-bottom:1px solid var(--line);
overflow:hidden;white-space:nowrap;
}
.marquee-track{display:inline-flex;gap:48px;animation:scroll 40s linear infinite}
.marquee-item{display:inline-flex;align-items:center;gap:14px;font-family:'Instrument Serif',serif;font-size:24px;color:var(--dim);font-style:italic}
.marquee-item::before{content:'●';color:var(--lime);font-size:10px;font-style:normal}
@keyframes scroll{from{transform:translateX(0)}to{transform:translateX(-50%)}}
.bento{
display:grid;grid-template-columns:repeat(4,1fr);gap:1px;
background:var(--line);border-radius:24px;overflow:hidden;
margin-top:60px;border:1px solid var(--line);
}
.bento-cell{background:var(--bg-2);padding:28px;position:relative}
.bento-cell .num{font-family:'Instrument Serif',serif;font-size:64px;line-height:1;letter-spacing:-.03em;color:var(--text);font-weight:400}
.bento-cell .num em{color:var(--lime);font-style:italic}
.bento-cell .lab{font-family:'JetBrains Mono',monospace;font-size:10.5px;text-transform:uppercase;letter-spacing:.14em;color:var(--dim);margin-top:12px;max-width:160px}
.bento-cell .corner{position:absolute;top:16px;right:16px;font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--dimmer)}
section{padding:120px 0;position:relative}
.section-head{display:flex;justify-content:space-between;align-items:flex-end;margin-bottom:48px;flex-wrap:wrap;gap:24px}
.section-head .tag{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--lime);letter-spacing:.18em;text-transform:uppercase;margin-bottom:14px;display:block}
.section-head h2{font-family:'Instrument Serif',serif;font-weight:400;font-size:clamp(40px,5.5vw,76px);line-height:1;letter-spacing:-.025em}
.section-head h2 em{color:var(--lime);font-style:italic}
.section-head p{color:var(--dim);max-width:380px;font-size:15px}
.name-gate{
background:linear-gradient(135deg, var(--surface-2) 0%, var(--surface) 100%);
border:1px solid var(--line-2);
padding:36px 40px;border-radius:24px;margin-bottom:32px;
display:flex;align-items:center;justify-content:space-between;gap:30px;flex-wrap:wrap;
position:relative;overflow:hidden;
}
.name-gate::before{
content:'';position:absolute;right:-80px;top:-80px;width:280px;height:280px;border-radius:50%;
background:radial-gradient(circle at 35% 35%, var(--lime), transparent 70%);opacity:.08;
}
.name-gate > *{position:relative;z-index:1}
.name-gate h3{font-family:'Instrument Serif',serif;font-weight:400;font-size:30px;margin-bottom:6px}
.name-gate h3 em{color:var(--lime);font-style:italic}
.name-gate p{color:var(--dim);font-size:14px}
.name-gate form{display:flex;gap:8px;flex-wrap:wrap}
.name-gate input{
padding:13px 20px;border-radius:999px;border:1px solid var(--line-2);
font-family:inherit;font-size:14.5px;min-width:240px;
background:var(--bg);color:var(--text);
}
.name-gate input:focus{outline:none;border-color:var(--lime);box-shadow:0 0 0 3px var(--lime-soft)}
.name-gate button{
background:var(--lime);color:#000;padding:13px 26px;border-radius:999px;
border:none;font-weight:600;font-family:inherit;cursor:pointer;transition:transform .15s;font-size:14.5px;
}
.name-gate button:hover{transform:translateY(-2px)}
.booking-wrap{
background:var(--surface);border:1px solid var(--line);border-radius:28px;padding:36px;
position:relative;overflow:hidden;
}
.day-tabs{display:flex;gap:6px;margin-bottom:28px;overflow-x:auto;padding-bottom:6px}
.day-tab{
flex-shrink:0;background:var(--surface-2);border:1px solid var(--line);
border-radius:14px;padding:14px 18px;cursor:pointer;text-align:left;
min-width:90px;transition:all .2s;font-family:inherit;color:var(--text);
}
.day-tab:hover{border-color:var(--line-2);background:var(--surface-3)}
.day-tab.active{background:var(--lime);color:#000;border-color:var(--lime)}
.day-tab .lab{font-family:'JetBrains Mono',monospace;font-size:9.5px;text-transform:uppercase;letter-spacing:.12em;opacity:.6}
.day-tab .num{font-family:'Instrument Serif',serif;font-size:30px;font-weight:400;line-height:1.05;margin-top:2px}
.day-tab .mon{font-size:11px;opacity:.6;font-family:'JetBrains Mono',monospace}
.court-legend{display:flex;gap:20px;margin-bottom:16px;flex-wrap:wrap;font-size:12px;color:var(--dim);font-family:'JetBrains Mono',monospace;text-transform:uppercase;letter-spacing:.1em}
.swatch{display:inline-flex;align-items:center;gap:8px}
.swatch i{width:10px;height:10px;border-radius:3px;display:inline-block}
.sw-clay{background:var(--clay)}
.sw-grass{background:var(--grass)}
.sw-hard{background:var(--hard)}
.sw-mine{background:var(--lime)}
.slots-grid{display:grid;grid-template-columns:84px repeat(3,1fr);gap:6px}
.slots-grid .header-cell{
padding:14px 10px;text-align:center;font-family:'JetBrains Mono',monospace;
font-size:10.5px;text-transform:uppercase;letter-spacing:.14em;color:var(--dim);
}
.slots-grid .time-label{
padding:18px 8px;font-family:'Instrument Serif',serif;font-size:20px;
display:flex;align-items:center;justify-content:center;color:var(--dim);
}
.slot{
background:var(--surface-2);border:1px solid var(--line);border-radius:14px;
padding:14px 14px;min-height:68px;display:flex;flex-direction:column;justify-content:center;gap:3px;
transition:all .15s;cursor:pointer;font-family:inherit;color:var(--text);text-align:left;position:relative;overflow:hidden;
}
.slot:hover{transform:translateY(-1px);border-color:var(--line-2)}
.slot.available{border-style:dashed;background:transparent;border-color:rgba(255,255,255,.08)}
.slot.available:hover{border-style:solid;border-color:var(--lime);background:var(--lime-soft)}
.slot.available .lab-book{font-size:12px;color:var(--lime);font-weight:600;font-family:'JetBrains Mono',monospace;text-transform:uppercase;letter-spacing:.1em}
.slot.available .ct{font-family:'JetBrains Mono',monospace;font-size:10px;text-transform:uppercase;letter-spacing:.1em;color:var(--dimmer)}
.slot.booked{cursor:default}
.slot.booked .who{font-weight:600;font-size:14px;color:var(--text)}
.slot.booked .ct{font-family:'JetBrains Mono',monospace;font-size:10px;text-transform:uppercase;letter-spacing:.1em;color:var(--dim)}
.slot.mine{background:linear-gradient(135deg, rgba(212,255,58,.18), rgba(212,255,58,.06));border-color:rgba(212,255,58,.4)}
.slot.mine .who{color:var(--lime)}
.slot.mine .who::after{content:' (you)';color:var(--dim);font-weight:400;font-size:11.5px}
.slot.mine::before{content:'';position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--lime)}
.slot .cancel{font-size:11px;color:var(--clay);text-decoration:underline;background:none;border:none;cursor:pointer;padding:0;font-family:'JetBrains Mono',monospace;align-self:flex-start;margin-top:2px;text-transform:uppercase;letter-spacing:.08em}
.modal-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.7);backdrop-filter:blur(10px);z-index:100;display:none;align-items:center;justify-content:center;padding:20px}
.modal-backdrop.open{display:flex;animation:fade .2s}
@keyframes fade{from{opacity:0}to{opacity:1}}
.modal{background:var(--surface);border:1px solid var(--line-2);border-radius:24px;padding:36px;max-width:460px;width:100%;position:relative;animation:pop .25s}
@keyframes pop{from{transform:scale(.94) translateY(10px);opacity:0}to{transform:scale(1) translateY(0);opacity:1}}
.modal h3{font-family:'Instrument Serif',serif;font-weight:400;font-size:32px;letter-spacing:-.02em;margin-bottom:8px}
.modal h3 em{color:var(--lime);font-style:italic}
.modal .sub{color:var(--dim);margin-bottom:28px;font-family:'JetBrains Mono',monospace;font-size:12px;text-transform:uppercase;letter-spacing:.12em}
.modal label{display:block;font-family:'JetBrains Mono',monospace;font-size:10.5px;text-transform:uppercase;letter-spacing:.14em;color:var(--dim);margin-bottom:10px}
.court-picker{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:28px}
.court-opt{padding:18px 10px;border:1px solid var(--line-2);border-radius:14px;cursor:pointer;text-align:center;transition:all .2s;background:var(--surface-2);color:var(--text)}
.court-opt:hover{border-color:var(--text)}
.court-opt.selected{background:var(--lime);color:#000;border-color:var(--lime)}
.court-opt .nm{font-family:'Instrument Serif',serif;font-size:18px}
.modal-actions{display:flex;gap:10px;justify-content:flex-end}
.modal .close{position:absolute;right:18px;top:18px;background:none;border:none;cursor:pointer;font-size:18px;color:var(--dim);width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:background .2s}
.modal .close:hover{background:var(--surface-2);color:var(--text)}
.chat-wrap{
background:var(--surface);border:1px solid var(--line);border-radius:28px;overflow:hidden;
}
.chat-header{padding:24px 30px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:center}
.chat-header h3{font-family:'Instrument Serif',serif;font-weight:400;font-size:24px}
.chat-header h3 em{color:var(--lime);font-style:italic}
.chat-header .live{display:flex;align-items:center;gap:8px;font-family:'JetBrains Mono',monospace;font-size:10.5px;text-transform:uppercase;letter-spacing:.14em;color:var(--lime)}
.chat-body{height:480px;overflow-y:auto;padding:28px 30px;display:flex;flex-direction:column;gap:20px}
.chat-body::-webkit-scrollbar{width:6px}
.chat-body::-webkit-scrollbar-thumb{background:var(--line-2);border-radius:3px}
.msg{display:flex;gap:14px;align-items:flex-start;max-width:78%}
.msg.mine{flex-direction:row-reverse;align-self:flex-end}
.avatar{width:38px;height:38px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:600;font-size:14px;color:#000;flex-shrink:0;font-family:'Instrument Serif',serif}
.msg-bubble{background:var(--surface-2);border:1px solid var(--line);padding:12px 18px;border-radius:18px;border-top-left-radius:4px;font-size:14.5px;color:var(--text)}
.msg.mine .msg-bubble{background:var(--lime);color:#000;border-color:var(--lime);border-top-left-radius:18px;border-top-right-radius:4px}
.msg-meta{font-family:'JetBrains Mono',monospace;font-size:10px;text-transform:uppercase;letter-spacing:.12em;color:var(--dim);margin-bottom:6px}
.msg.mine .msg-meta{text-align:right}
.msg-body{word-break:break-word;line-height:1.5}
.chat-empty{text-align:center;padding:50px 20px;color:var(--dim);font-family:'Instrument Serif',serif;font-style:italic;font-size:20px}
.chat-form{display:flex;gap:10px;padding:18px 22px;border-top:1px solid var(--line);background:var(--bg-2)}
.chat-form input{flex:1;border:1px solid var(--line-2);background:var(--surface);padding:14px 20px;border-radius:999px;font-family:inherit;font-size:14.5px;color:var(--text)}
.chat-form input:focus{outline:none;border-color:var(--lime);box-shadow:0 0 0 3px var(--lime-soft)}
.chat-form button{background:var(--lime);color:#000;border:none;padding:0 26px;border-radius:999px;font-weight:600;cursor:pointer;font-family:inherit;font-size:14px;transition:transform .15s}
.chat-form button:hover{transform:translateY(-1px)}
.chat-form.locked input{opacity:.5}
.rules-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px}
.rule-card{background:var(--surface);border:1px solid var(--line);border-radius:20px;padding:28px;position:relative;overflow:hidden;transition:all .25s}
.rule-card:hover{border-color:var(--line-2);transform:translateY(-3px)}
.rule-card .ix{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--lime);letter-spacing:.16em;margin-bottom:14px;display:block}
.rule-card h4{font-family:'Instrument Serif',serif;font-weight:400;font-size:24px;letter-spacing:-.01em;margin-bottom:10px}
.rule-card p{color:var(--dim);font-size:14.5px;line-height:1.55}
footer{padding:80px 0 50px;border-top:1px solid var(--line);margin-top:80px;color:var(--dim);font-size:13.5px}
.foot-grid{display:flex;justify-content:space-between;align-items:flex-end;gap:30px;flex-wrap:wrap}
.foot-grid h4{font-family:'Instrument Serif',serif;font-weight:400;font-size:48px;color:var(--text);letter-spacing:-.02em;margin-bottom:14px;line-height:1}
.foot-grid h4 em{color:var(--lime);font-style:italic}
.foot-right{text-align:right;font-family:'JetBrains Mono',monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.12em}
.foot-right .signout{background:none;border:none;color:var(--clay);cursor:pointer;font-family:inherit;font-size:11.5px;text-transform:uppercase;letter-spacing:.12em;text-decoration:underline}
.subpage-hero{padding:80px 0 40px}
.subpage-hero h1{font-family:'Instrument Serif',serif;font-weight:400;font-size:clamp(48px,7vw,96px);letter-spacing:-.03em;line-height:.95;margin-bottom:18px}
.subpage-hero h1 em{color:var(--lime);font-style:italic}
.subpage-hero p{color:var(--dim);font-size:17px;max-width:580px;line-height:1.55}
.crumbs{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:36px 0 22px;font-family:'JetBrains Mono',monospace;font-size:12.5px}
.crumbs a{color:var(--dim);text-decoration:none;padding:7px 12px;border-radius:8px;background:var(--surface);border:1px solid var(--line);transition:all .15s}
.crumbs a:hover{color:var(--text);border-color:var(--line-2)}
.crumbs a.up{background:var(--lime);color:#000;border-color:var(--lime)}
.crumbs a.up:hover{background:#e1ff5a}
.crumbs a.project{background:var(--surface-2);color:var(--lime);border-color:var(--line-2)}
.crumbs .sep{color:var(--dimmer)}
.crumbs .here{color:var(--text);padding:7px 12px;background:var(--surface-2);border:1px solid var(--line-2);border-radius:8px}
.file-panel{background:var(--surface);border:1px solid var(--line);border-radius:20px;overflow:hidden}
.file-panel-head{padding:20px 28px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px;background:var(--bg-2)}
.file-panel-head h3{font-family:'Instrument Serif',serif;font-weight:400;font-size:22px}
.file-panel-head .meta{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.12em}
.file-list{list-style:none}
.file-row{display:grid;grid-template-columns:24px 1fr 110px 180px;align-items:center;gap:16px;padding:14px 28px;border-bottom:1px solid var(--line);transition:background .12s;text-decoration:none;color:var(--text)}
.file-row:last-child{border-bottom:none}
.file-row:hover{background:var(--surface-2)}
.file-row .ico{font-family:'JetBrains Mono',monospace;color:var(--lime);text-align:center;font-size:14px}
.file-row .nm{font-weight:500;word-break:break-all;font-size:14.5px}
.file-row .nm.dir{color:var(--lime)}
.file-row .sz{font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--dim);text-align:right}
.file-row .mt{font-family:'JetBrains Mono',monospace;font-size:11.5px;color:var(--dimmer);text-align:right}
.parent-row{background:var(--surface-2)}
.parent-row .nm{color:var(--lime);font-weight:600}
.empty-dir{padding:60px 26px;text-align:center;color:var(--dim);font-family:'Instrument Serif',serif;font-style:italic;font-size:19px}
.file-view{background:var(--surface);border:1px solid var(--line);border-radius:20px;overflow:hidden}
.file-view-head{padding:20px 28px;background:var(--bg-2);border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:14px}
.file-view-head .nm{font-family:'JetBrains Mono',monospace;font-size:14px;word-break:break-all;color:var(--text)}
.file-view-head .actions{display:flex;gap:8px}
.file-view-head .actions a{background:var(--lime);color:#000;padding:8px 16px;border-radius:999px;text-decoration:none;font-size:12.5px;font-weight:600;transition:transform .15s}
.file-view-head .actions a:hover{transform:translateY(-1px)}
.file-view-head .actions a.ghost{background:var(--surface-2);color:var(--text);border:1px solid var(--line-2)}
.file-view pre{margin:0;padding:28px;font-family:'JetBrains Mono',monospace;font-size:12.5px;line-height:1.65;color:var(--text);overflow:auto;max-height:75vh;background:var(--bg);white-space:pre;tab-size:4}
.file-view .img-wrap{padding:40px;text-align:center;background:repeating-conic-gradient(var(--bg-2) 0% 25%, var(--surface) 0% 50%) 50%/24px 24px}
.file-view .img-wrap img{max-width:100%;max-height:75vh;border-radius:8px;box-shadow:0 10px 40px rgba(0,0,0,.5)}
.file-view .binary-note{padding:60px 26px;text-align:center;color:var(--dim)}
.file-view .binary-note .big{font-size:36px;margin-bottom:14px;color:var(--lime)}
.admin-hero{padding:80px 0 30px;position:relative}
.admin-hero .badge{display:inline-flex;align-items:center;gap:8px;background:var(--lime);color:#000;padding:8px 16px;border-radius:999px;font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.16em;margin-bottom:24px}
.admin-hero h1{font-family:'Instrument Serif',serif;font-weight:400;font-size:clamp(48px,7vw,96px);letter-spacing:-.03em;line-height:.95;margin-bottom:18px}
.admin-hero h1 em{color:var(--lime);font-style:italic}
.admin-hero p{color:var(--dim);font-size:17px;max-width:580px}
.login-card{max-width:460px;margin:50px auto 0;background:var(--surface);border:1px solid var(--line-2);border-radius:20px;padding:40px;box-shadow:0 40px 80px -30px rgba(0,0,0,.6)}
.login-card h3{font-family:'Instrument Serif',serif;font-weight:400;font-size:30px;letter-spacing:-.02em;margin-bottom:8px}
.login-card h3 em{color:var(--lime);font-style:italic}
.login-card .sub{color:var(--dim);font-size:14px;margin-bottom:26px}
.login-card label{display:block;font-family:'JetBrains Mono',monospace;font-size:10.5px;text-transform:uppercase;letter-spacing:.14em;color:var(--dim);margin-bottom:10px}
.login-card input{width:100%;padding:14px 18px;border:1px solid var(--line-2);border-radius:12px;font-family:inherit;font-size:15px;color:var(--text);background:var(--bg);margin-bottom:18px}
.login-card input:focus{outline:none;border-color:var(--lime);box-shadow:0 0 0 3px var(--lime-soft)}
.login-card button{width:100%;background:var(--lime);color:#000;border:none;padding:15px;border-radius:12px;font-weight:600;cursor:pointer;font-family:inherit;font-size:15px;transition:transform .15s}
.login-card button:hover{transform:translateY(-1px)}
.login-err{background:rgba(240,106,58,.12);color:var(--clay);padding:12px 16px;border-radius:10px;font-size:13.5px;margin-bottom:18px;border-left:3px solid var(--clay)}
.login-hint{font-size:11px;color:var(--dimmer);text-align:center;margin-top:14px;font-family:'JetBrains Mono',monospace;letter-spacing:.12em;text-transform:uppercase}
.admin-toolbar{display:flex;gap:4px;flex-wrap:wrap;margin-bottom:30px;padding:8px;background:var(--surface);border:1px solid var(--line);border-radius:16px;align-items:center}
.admin-toolbar a{padding:10px 18px;border-radius:10px;text-decoration:none;color:var(--dim);font-weight:500;font-size:13.5px;transition:all .15s}
.admin-toolbar a:hover{background:var(--surface-2);color:var(--text)}
.admin-toolbar a.active{background:var(--lime);color:#000}
.admin-toolbar .spacer{flex:1}
.admin-toolbar form{margin:0}
.admin-toolbar button.logout{background:transparent;color:var(--clay);border:1px solid var(--line-2);padding:9px 16px;border-radius:10px;font-weight:500;cursor:pointer;font-family:inherit;font-size:12.5px;transition:all .15s}
.admin-toolbar button.logout:hover{background:var(--clay);color:#fff;border-color:var(--clay)}
.admin-section{background:var(--surface);border:1px solid var(--line);border-radius:20px;overflow:hidden;margin-bottom:24px}
.admin-section-head{padding:22px 30px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px;background:var(--bg-2)}
.admin-section-head h3{font-family:'Instrument Serif',serif;font-weight:400;font-size:24px;letter-spacing:-.01em}
.admin-section-head h3 em{color:var(--lime);font-style:italic}
.admin-section-head .meta{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.12em}
.admin-overview-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:1px;background:var(--line)}
.admin-overview-cell{background:var(--surface);padding:28px}
.admin-overview-cell .num{font-family:'Instrument Serif',serif;font-size:52px;line-height:1;letter-spacing:-.02em}
.admin-overview-cell .num em{color:var(--lime);font-style:italic}
.admin-overview-cell .lab{font-family:'JetBrains Mono',monospace;font-size:10.5px;text-transform:uppercase;letter-spacing:.14em;color:var(--dim);margin-top:12px}
.admin-table{width:100%;border-collapse:collapse}
.admin-table th{text-align:left;padding:14px 22px;font-family:'JetBrains Mono',monospace;font-size:10.5px;text-transform:uppercase;letter-spacing:.14em;color:var(--dim);background:var(--bg-2);border-bottom:1px solid var(--line)}
.admin-table td{padding:16px 22px;border-bottom:1px solid var(--line);vertical-align:top;font-size:14px;color:var(--text)}
.admin-table tr:last-child td{border-bottom:none}
.admin-table tr:hover td{background:var(--surface-2)}
.admin-table .num-cell{font-family:'JetBrains Mono',monospace;color:var(--dim);font-size:12px}
.admin-table .name-cell{font-weight:600}
.admin-table .body-cell{color:var(--text);word-break:break-word;max-width:420px}
.admin-table .actions-cell{white-space:nowrap;text-align:right}
.admin-btn{display:inline-block;padding:6px 12px;border-radius:8px;font-family:inherit;font-size:11.5px;font-weight:500;cursor:pointer;border:1px solid var(--line-2);background:var(--surface-2);color:var(--text);text-decoration:none;margin-left:4px;transition:all .15s}
.admin-btn:hover{background:var(--surface-3);border-color:var(--text)}
.admin-btn.danger{color:var(--clay);border-color:rgba(240,106,58,.3)}
.admin-btn.danger:hover{background:var(--clay);color:#fff;border-color:var(--clay)}
.admin-btn.primary{background:var(--lime);color:#000;border-color:var(--lime)}
.admin-btn.primary:hover{background:#e1ff5a;border-color:#e1ff5a}
.admin-empty{padding:50px 20px;text-align:center;color:var(--dim);font-family:'Instrument Serif',serif;font-style:italic;font-size:19px}
.edit-form{display:flex;gap:6px;align-items:center}
.edit-form input{flex:1;padding:8px 12px;border:1px solid var(--line-2);border-radius:8px;font-family:inherit;font-size:13px;background:var(--bg);color:var(--text)}
.edit-form input:focus{outline:none;border-color:var(--lime)}
.user-card{display:flex;align-items:center;gap:16px;padding:18px 24px;border-bottom:1px solid var(--line);flex-wrap:wrap}
.user-card:last-child{border-bottom:none}
.user-card:hover{background:var(--surface-2)}
.user-card .av{width:44px;height:44px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:600;color:#000;font-family:'Instrument Serif',serif;font-size:17px;flex-shrink:0}
.user-card .uinfo{flex:1;min-width:180px}
.user-card .uinfo .nm{font-weight:600;font-size:15.5px;color:var(--text)}
.user-card .uinfo .st{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;margin-top:4px}
.user-card .uactions{display:flex;gap:6px;flex-wrap:wrap}
.user-card .uactions form{display:flex;gap:6px;align-items:center;margin:0}
.user-card .uactions input{padding:8px 12px;border:1px solid var(--line-2);border-radius:8px;font-family:inherit;font-size:13px;background:var(--bg);color:var(--text);width:140px}
.user-card .uactions input:focus{outline:none;border-color:var(--lime)}
@media (max-width: 900px){
.hero-grid{grid-template-columns:1fr;gap:40px}
.court-illus{max-width:300px;margin:0 auto}
.bento{grid-template-columns:repeat(2,1fr)}
.slots-grid{grid-template-columns:60px repeat(3,1fr);gap:5px}
.slot{padding:10px 8px;min-height:62px}
.slots-grid .time-label{font-size:16px;padding:14px 4px}
.nav-links a:not(.user-pill){font-size:12.5px;padding:7px 10px}
.name-gate{padding:28px}
.file-row{grid-template-columns:24px 1fr 80px;gap:10px;padding:12px 18px}
.file-row .mt{display:none}
.admin-table th:nth-child(1),.admin-table td:nth-child(1){display:none}
.admin-table td,.admin-table th{padding:10px 14px}
.admin-overview-grid{grid-template-columns:repeat(2,1fr)}
}
@media (max-width: 560px){
.container{padding:0 20px}
section{padding:70px 0}
.hero{padding:50px 0 30px}
.slot .ct{display:none}
.slot.booked .who{font-size:12.5px}
.nav-links{gap:2px}
.brand-sub{display:none}
}
</style>
</head>
<body>
<nav class="top">
<div class="container nav-inner">
<a href="/" class="brand">
<span class="brand-mark"></span>
<span class="brand-text">
<span class="brand-name">Baseline</span>
<span class="brand-sub">Tennis · Est. 1998</span>
</span>
</a>
<div class="nav-links">
<a href="/#courts" class="<?= $page==='home'?'active':'' ?>">Courts</a>
<a href="/#clubhouse">Lounge</a>
<a href="?page=files" class="<?= $page==='files'?'active':'' ?>">Files</a>
<a href="?page=admin" class="<?= $page==='admin'?'active':'' ?>">Admin</a>
<?php if ($name):
$hue = crc32($name) % 360;
$initial = strtoupper(mb_substr($name, 0, 1));
?>
<div class="user-pill"><span class="av" style="background:hsl(<?= $hue ?>,70%,60%)"><?= e($initial) ?></span><?= e($name) ?></div>
<?php endif; ?>
</div>
</div>
</nav>
<?php if ($page === 'admin'): ?>
<header class="admin-hero">
<div class="container">
<div class="badge">● Restricted · Staff Only</div>
<h1>Club <em>Control</em></h1>
<p>Backstage at Baseline. Manage members, moderate the lounge, and tidy up the booking ledger.</p>
</div>
</header>
<section style="padding:20px 0 90px">
<div class="container">
<?php if (!$isAdmin): ?>
<div class="login-card">
<h3>Admin <em>sign in</em></h3>
<div class="sub">Enter the admin password to continue.</div>
<?php if (isset($_GET['err'])): ?>
<div class="login-err">Incorrect password. Try again.</div>
<?php endif; ?>
<form method="post">
<label>Password</label>
<input type="password" name="admin_password" placeholder="••••••" required autofocus>
<button type="submit" name="admin_login" value="1">Unlock Admin</button>
</form>
<div class="login-hint">authorized personnel only</div>
</div>
<?php else:
$allMessages = [];
$allBookings = [];
$userStats = [];
if ($pdo) {
try {
$allMessages = $pdo->query("SELECT * FROM messages ORDER BY id DESC")->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) {}
try {
$allBookings = $pdo->query("SELECT * FROM bookings ORDER BY slot_date DESC, slot_time DESC")->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) {}
}
foreach ($allMessages as $m) {
$n = $m['name'];
if (!isset($userStats[$n])) $userStats[$n] = ['msgs'=>0,'books'=>0,'last'=>0];
$userStats[$n]['msgs']++;
if ((int)$m['created_at'] > $userStats[$n]['last']) $userStats[$n]['last'] = (int)$m['created_at'];
}
foreach ($allBookings as $b) {
$n = $b['name'];
if (!isset($userStats[$n])) $userStats[$n] = ['msgs'=>0,'books'=>0,'last'=>0];
$userStats[$n]['books']++;
if ((int)$b['created_at'] > $userStats[$n]['last']) $userStats[$n]['last'] = (int)$b['created_at'];
}
uksort($userStats, 'strcasecmp');
$editId = isset($_GET['edit']) ? (int)$_GET['edit'] : 0;
?>
<div class="admin-toolbar">
<a href="#overview" class="active">Overview</a>
<a href="#users">Users · <?= count($userStats) ?></a>
<a href="#chats">Chats · <?= count($allMessages) ?></a>
<a href="#bookings">Bookings · <?= count($allBookings) ?></a>
<div class="spacer"></div>
<form method="post"><button class="logout" name="admin_logout" value="1">Sign out admin</button></form>
</div>
<div class="admin-section" id="overview">
<div class="admin-section-head">
<h3>At a <em>glance</em></h3>
<div class="meta">Welcome back, admin</div>
</div>
<div class="admin-overview-grid">
<div class="admin-overview-cell"><div class="num"><em><?= count($userStats) ?></em></div><div class="lab">Members on file</div></div>
<div class="admin-overview-cell"><div class="num"><?= count($allMessages) ?></div><div class="lab">Messages posted</div></div>
<div class="admin-overview-cell"><div class="num"><?= count($allBookings) ?></div><div class="lab">Court bookings</div></div>
<div class="admin-overview-cell"><div class="num">
<?php
$upcoming = 0; $today = date('Y-m-d');
foreach ($allBookings as $b) if ($b['slot_date'] >= $today) $upcoming++;
echo $upcoming;
?>
</div><div class="lab">Upcoming slots</div></div>
</div>
</div>
<div class="admin-section" id="users">
<div class="admin-section-head">
<h3><em>Members</em></h3>
<div class="meta"><?= count($userStats) ?> on file</div>
</div>
<?php if (empty($userStats)): ?>
<div class="admin-empty">No members have signed in yet.</div>
<?php else: foreach ($userStats as $uname => $st):
$hue = crc32($uname) % 360;
$initial = strtoupper(mb_substr($uname, 0, 1));
?>
<div class="user-card">
<div class="av" style="background:hsl(<?= $hue ?>,70%,60%)"><?= e($initial) ?></div>
<div class="uinfo">
<div class="nm"><?= e($uname) ?></div>
<div class="st"><?= $st['msgs'] ?> msg<?= $st['msgs']===1?'':'s' ?> · <?= $st['books'] ?> booking<?= $st['books']===1?'':'s' ?><?= $st['last'] ? ' · last seen ' . date('M j, Y g:ia', $st['last']) : '' ?></div>
</div>
<div class="uactions">
<form method="post" onsubmit="return confirm('Rename this user — this updates all their messages and bookings.');">
<input type="hidden" name="admin_rename_from" value="<?= e($uname) ?>">
<input type="text" name="admin_rename_to" placeholder="New name" maxlength="30" required>
<button class="admin-btn" name="admin_rename_user" value="1">Rename</button>
</form>
<form method="post" onsubmit="return confirm('Purge all data for this user?');">
<button class="admin-btn danger" name="admin_purge_user" value="<?= e($uname) ?>">Purge</button>
</form>
</div>
</div>
<?php endforeach; endif; ?>
</div>
<div class="admin-section" id="chats">
<div class="admin-section-head">
<h3>Chat <em>messages</em></h3>
<div class="meta"><?= count($allMessages) ?> total</div>
</div>
<?php if (empty($allMessages)): ?>
<div class="admin-empty">No chat messages yet.</div>
<?php else: ?>
<table class="admin-table">
<thead>
<tr>
<th style="width:60px">#</th>
<th style="width:140px">Member</th>
<th>Message</th>
<th style="width:140px">When</th>
<th style="width:200px;text-align:right">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($allMessages as $m): ?>
<tr>
<td class="num-cell">#<?= (int)$m['id'] ?></td>
<td class="name-cell"><?= e($m['name']) ?></td>
<td class="body-cell">
<?php if ($editId === (int)$m['id']): ?>
<form method="post" class="edit-form">
<input type="hidden" name="admin_edit_msg_id" value="<?= (int)$m['id'] ?>">
<input type="text" name="admin_edit_msg_body" value="<?= e($m['body']) ?>" maxlength="500" autofocus>
<button class="admin-btn primary" type="submit">Save</button>
<a class="admin-btn" href="?page=admin#chats">Cancel</a>
</form>
<?php else: ?>
<?= e($m['body']) ?>
<?php endif; ?>
</td>
<td class="num-cell"><?= date('M j · g:ia', (int)$m['created_at']) ?></td>
<td class="actions-cell">
<?php if ($editId !== (int)$m['id']): ?>
<a class="admin-btn" href="?page=admin&edit=<?= (int)$m['id'] ?>#chats">Edit</a>
<?php endif; ?>
<form method="post" style="display:inline" onsubmit="return confirm('Delete this message?');">
<input type="hidden" name="admin_delete_msg_id" value="<?= (int)$m['id'] ?>">
<button class="admin-btn danger" type="submit">Delete</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<div class="admin-section" id="bookings">
<div class="admin-section-head">
<h3>Court <em>bookings</em></h3>
<div class="meta"><?= count($allBookings) ?> total</div>
</div>
<?php if (empty($allBookings)): ?>
<div class="admin-empty">No bookings on the ledger.</div>
<?php else: ?>
<table class="admin-table">
<thead>
<tr>
<th style="width:60px">#</th>
<th style="width:140px">Member</th>
<th style="width:110px">Court</th>
<th style="width:130px">Date</th>
<th style="width:90px">Time</th>
<th style="width:140px">Booked</th>
<th style="width:120px;text-align:right">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($allBookings as $b):
$past = $b['slot_date'] < date('Y-m-d');
?>
<tr style="<?= $past?'opacity:.45':'' ?>">
<td class="num-cell">#<?= (int)$b['id'] ?></td>
<td class="name-cell"><?= e($b['name']) ?></td>
<td><?= e($b['court']) ?></td>
<td class="num-cell"><?= e($b['slot_date']) ?></td>
<td class="num-cell"><?= e($b['slot_time']) ?></td>
<td class="num-cell"><?= date('M j · g:ia', (int)$b['created_at']) ?></td>
<td class="actions-cell">
<form method="post" style="display:inline" onsubmit="return confirm('Cancel this booking?');">
<input type="hidden" name="admin_delete_booking_id" value="<?= (int)$b['id'] ?>">
<button class="admin-btn danger" type="submit">Cancel</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</section>
<?php elseif ($page === 'files'): ?>
<header class="subpage-hero">
<div class="container">
<div class="eyebrow"><span class="pulse"></span>Server / File Browser</div>
<h1>My <em>Files</em></h1>
<p>A peek behind the curtain — every file the club keeps on the server, from the front-of-house code to the trophy ledger in the database.</p>
</div>
</header>
<section style="padding:20px 0 90px">
<div class="container">
<?php
$parentPath = dirname($target);
$hasParent = ($parentPath !== $target);
$segments = array_values(array_filter(explode('/', $target), function($s){ return $s !== ''; }));
?>
<div class="crumbs">
<?php if ($hasParent): ?>
<a class="up" href="?page=files&path=<?= e(urlencode($parentPath)) ?>" title="Go to parent directory">↑ Up</a>
<span class="sep">/</span>
<?php endif; ?>
<a class="project" href="?page=files" title="Project root">◆ project</a>
<span class="sep">/</span>
<a href="?page=files&path=<?= e(urlencode('/')) ?>" title="Filesystem root">/</a>
<?php
$accum = '';
foreach ($segments as $i => $part):
$accum .= '/' . $part;
$isLast = ($i === count($segments) - 1);
?>
<span class="sep">/</span>
<?php if ($isLast): ?>
<span class="here"><?= e($part) ?></span>
<?php else: ?>
<a href="?page=files&path=<?= e(urlencode($accum)) ?>"><?= e($part) ?></a>
<?php endif; ?>
<?php endforeach; ?>
</div>
<?php if (is_file($target)):
$ext = strtolower(pathinfo($target, PATHINFO_EXTENSION));
$size = filesize($target);
$imgExt = ['png','jpg','jpeg','gif','svg','webp','ico'];
$textExt = ['php','html','htm','css','js','json','md','txt','log','xml','yml','yaml','ini','env','sql','sh','conf','gitignore','lock'];
$basename = basename($target);
$treatAsText = in_array($ext, $textExt) || ($ext === '' && $size < 200000);
$backDir = dirname($target);
?>
<div class="file-view">
<div class="file-view-head">
<div>
<div class="nm"><?= e($basename) ?></div>
<div style="font-size:11px;color:var(--dim);margin-top:6px;font-family:'JetBrains Mono',monospace;letter-spacing:.06em"><?= e(human_size($size)) ?> · modified <?= date('M j, Y g:ia', filemtime($target)) ?></div>
</div>
<div class="actions">
<?php if ($size <= 50*1024*1024): ?>
<a href="?page=files&path=<?= e(urlencode($target)) ?>&dl=1">↓ Download</a>
<?php endif; ?>
<a class="ghost" href="?page=files&path=<?= e(urlencode($backDir)) ?>">← Back</a>
</div>
</div>
<?php if (in_array($ext, $imgExt) && $size <= 5*1024*1024):
$absTarget = realpath($target);
$isWebFile = $webRoot && strpos($absTarget, $webRoot) === 0;
?>
<?php if ($isWebFile):
$webRel = ltrim(substr($absTarget, strlen($webRoot)), '/\\');
?>
<div class="img-wrap"><img src="/<?= e($webRel) ?>" alt=""></div>
<?php else: ?>
<div class="binary-note"><div class="big">◇</div>Image preview is only available for files inside the web root. Use download to view.</div>
<?php endif; ?>
<?php elseif ($treatAsText && $size <= 1024*1024):
$content = @file_get_contents($target);
$isBinary = false;
if ($content !== false && $content !== '') {
$sample = substr($content, 0, 8000);
if (strpos($sample, "\0") !== false) $isBinary = true;
}
?>
<?php if ($isBinary): ?>
<div class="binary-note"><div class="big">◇</div>Binary file — preview not available. Use download to grab it.</div>
<?php else: ?>
<pre><?= e($content) ?></pre>
<?php endif; ?>
<?php else: ?>
<div class="binary-note"><div class="big">◇</div>
<?= $size > 1024*1024 ? 'File is too large to preview here.' : 'No preview available for this file type.' ?>
</div>
<?php endif; ?>
</div>
<?php elseif (is_dir($target)):
$items = [];
$handle = @opendir($target);
if ($handle) {
while (($entry = readdir($handle)) !== false) {
if ($entry === '.' || $entry === '..') continue;
$full = $target . '/' . $entry;
$items[] = [
'name' => $entry,
'full' => $full,
'is_dir' => is_dir($full),
'size' => is_file($full) ? @filesize($full) : 0,
'mtime' => @filemtime($full) ?: 0,
];
}
closedir($handle);
usort($items, function($a, $b){
if ($a['is_dir'] !== $b['is_dir']) return $a['is_dir'] ? -1 : 1;
return strcasecmp($a['name'], $b['name']);
});
}
$dirCount = count(array_filter($items, function($i){ return $i['is_dir']; }));
$fileCount = count($items) - $dirCount;
$isProjectRoot = ($target === $projectRoot);
$headerLabel = $isProjectRoot ? 'Project Root' : ($target === '/' ? 'Filesystem Root' : basename($target));
?>
<div class="file-panel">
<div class="file-panel-head">
<h3><?= e($headerLabel) ?></h3>
<div class="meta"><?= $dirCount ?> folder<?= $dirCount===1?'':'s' ?> · <?= $fileCount ?> file<?= $fileCount===1?'':'s' ?></div>
</div>
<ul class="file-list">
<?php if ($hasParent): ?>
<li>
<a class="file-row parent-row" href="?page=files&path=<?= e(urlencode($parentPath)) ?>">
<div class="ico">↑</div>
<div class="nm">.. (parent directory)</div>
<div class="sz">—</div>
<div class="mt"><?= e($parentPath) ?></div>
</a>
</li>
<?php endif; ?>
<?php if (empty($items) && !$hasParent): ?>
<li><div class="empty-dir">Nothing in here but tumbleweed.</div></li>
<?php elseif (empty($items)): ?>
<li><div class="empty-dir">Nothing in here but tumbleweed.</div></li>
<?php else: foreach ($items as $it): ?>
<li>
<a class="file-row" href="?page=files&path=<?= e(urlencode($it['full'])) ?>">
<div class="ico"><?= $it['is_dir'] ? '▸' : '·' ?></div>
<div class="nm <?= $it['is_dir']?'dir':'' ?>"><?= e($it['name']) ?><?= $it['is_dir']?'/':'' ?></div>
<div class="sz"><?= $it['is_dir'] ? '—' : e(human_size($it['size'])) ?></div>
<div class="mt"><?= $it['mtime'] ? date('M j, Y · g:ia', $it['mtime']) : '' ?></div>
</a>
</li>
<?php endforeach; endif; ?>
</ul>
</div>
<?php else: ?>
<div class="file-panel">
<div class="empty-dir">Path not found. The ball's out of bounds.</div>
</div>
<?php endif; ?>
</div>
</section>
<?php else:
$bookings = [];
if ($pdo) {
try {
$today = date('Y-m-d');
$week = date('Y-m-d', strtotime('+6 days'));
$stmt = $pdo->prepare("SELECT * FROM bookings WHERE slot_date >= ? AND slot_date <= ? ORDER BY slot_date, slot_time");
$stmt->execute([$today, $week]);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $b) {
$bookings[$b['slot_date'] . '|' . $b['slot_time'] . '|' . $b['court']] = $b;
}
} catch (Exception $e) {}
}
$messages = [];
if ($pdo) {
try {
$rows = $pdo->query("SELECT * FROM messages ORDER BY id DESC LIMIT 80")->fetchAll(PDO::FETCH_ASSOC);
$messages = array_reverse($rows);
} catch (Exception $e) {}
}
$courts = ['Clay', 'Grass', 'Hard'];
$times = ['08:00','10:00','12:00','14:00','16:00','18:00'];
$days = [];
for ($i = 0; $i < 7; $i++) {
$d = strtotime("+$i days");
$days[] = ['date' => date('Y-m-d', $d), 'label' => date('D', $d), 'day' => date('j', $d), 'mon' => date('M', $d)];
}
?>
<header class="hero">
<div class="container hero-grid">
<div>
<div class="eyebrow"><span class="pulse"></span>Est. 1998 · Members Society</div>
<h1>Love means <em>nothing</em>. The <span class="strike">match</span> game means <em>everything</em>.</h1>
<p class="lead">A neighborhood tennis club for early birds, weekend warriors, and anyone who's ever yelled at a net cord. Book a court. Find a hit. Talk smack in the lounge.</p>
<div class="hero-cta">
<a href="#courts" class="btn btn-primary">
Reserve a Court
<span class="btn-arrow">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
</span>
</a>
<a href="#clubhouse" class="btn btn-ghost">Visit the Lounge</a>
</div>
</div>
<div class="court-illus" aria-hidden="true">
<svg viewBox="0 0 300 400">
<defs>
<linearGradient id="courtg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#1d201c"/>
<stop offset="100%" stop-color="#0b0c0a"/>
</linearGradient>
<radialGradient id="ballg" cx="35%" cy="30%">
<stop offset="0%" stop-color="#eaff5a"/>
<stop offset="60%" stop-color="#d4ff3a"/>
<stop offset="100%" stop-color="#95b81d"/>
</radialGradient>
</defs>
<rect x="20" y="20" width="260" height="360" rx="8" fill="url(#courtg)" stroke="rgba(255,255,255,.08)"/>
<g stroke="rgba(212,255,58,.35)" stroke-width="1.5" fill="none">
<rect x="50" y="50" width="200" height="300"/>
<line x1="50" y1="200" x2="250" y2="200" stroke="rgba(212,255,58,.6)"/>
<line x1="150" y1="50" x2="150" y2="350"/>
<rect x="90" y="130" width="120" height="140"/>
<line x1="90" y1="200" x2="210" y2="200"/>
</g>
<g>
<line x1="50" y1="200" x2="250" y2="200" stroke="rgba(255,255,255,.5)" stroke-width="2"/>
<pattern id="netp" x="0" y="0" width="5" height="5" patternUnits="userSpaceOnUse">
<path d="M0 0 L5 5 M5 0 L0 5" stroke="rgba(255,255,255,.18)" stroke-width=".5"/>
</pattern>
<rect x="50" y="194" width="200" height="14" fill="url(#netp)"/>
</g>
<g transform="translate(195,105)">
<circle r="28" fill="url(#ballg)"/>
<path d="M-28 0 Q-10 10 0 -28" stroke="rgba(255,255,255,.6)" stroke-width="1.5" fill="none"/>
<path d="M28 0 Q10 -10 0 28" stroke="rgba(255,255,255,.6)" stroke-width="1.5" fill="none"/>
</g>
<g transform="translate(95,290) rotate(-20)">
<ellipse cx="0" cy="0" rx="30" ry="38" fill="none" stroke="rgba(255,255,255,.7)" stroke-width="2"/>
<ellipse cx="0" cy="0" rx="26" ry="34" fill="rgba(212,255,58,.05)"/>
<g stroke="rgba(255,255,255,.4)" stroke-width=".7">
<line x1="-24" y1="-14" x2="24" y2="-14"/>
<line x1="-26" y1="-4" x2="26" y2="-4"/>
<line x1="-26" y1="6" x2="26" y2="6"/>
<line x1="-24" y1="16" x2="24" y2="16"/>
<line x1="-10" y1="-32" x2="-10" y2="32"/>
<line x1="0" y1="-34" x2="0" y2="34"/>
<line x1="10" y1="-32" x2="10" y2="32"/>
</g>
<rect x="-3" y="36" width="6" height="38" fill="rgba(255,255,255,.7)" rx="2"/>
<rect x="-4" y="56" width="8" height="20" fill="var(--lime)"/>
</g>
</svg>
</div>
</div>
<div class="container">
<div class="marquee">
<div class="marquee-track">
<span class="marquee-item">Clay</span>
<span class="marquee-item">Grass</span>
<span class="marquee-item">Hard</span>
<span class="marquee-item">Singles</span>
<span class="marquee-item">Doubles</span>
<span class="marquee-item">Dawn till Dusk</span>
<span class="marquee-item">Whites preferred</span>
<span class="marquee-item">No double faults</span>
<span class="marquee-item">Clay</span>
<span class="marquee-item">Grass</span>
<span class="marquee-item">Hard</span>
<span class="marquee-item">Singles</span>
<span class="marquee-item">Doubles</span>
<span class="marquee-item">Dawn till Dusk</span>
<span class="marquee-item">Whites preferred</span>
<span class="marquee-item">No double faults</span>
</div>
</div>
<div class="bento">
<div class="bento-cell"><div class="corner">01</div><div class="num"><em>3</em></div><div class="lab">Surfaces — Clay, Grass, Hard</div></div>
<div class="bento-cell"><div class="corner">02</div><div class="num">214</div><div class="lab">Active members</div></div>
<div class="bento-cell"><div class="corner">03</div><div class="num">26<em>y</em></div><div class="lab">Since founding</div></div>
<div class="bento-cell"><div class="corner">04</div><div class="num">7<em>am</em></div><div class="lab">Earliest slot</div></div>
</div>
</div>
</header>
<section id="courts">
<div class="container">
<?php if (!$name): ?>
<div class="name-gate">
<div>
<h3>Sign in to the <em>club</em></h3>
<p>Just your name — no passwords, no fuss. We trust you.</p>
</div>
<form method="post" action="">
<input type="text" name="set_name" placeholder="Your name (e.g. Serena W.)" required maxlength="30" autocomplete="off">
<input type="hidden" name="anchor" value="courts">
<button type="submit">Enter Court →</button>
</form>
</div>
<?php endif; ?>
<div class="section-head">
<div>
<span class="tag">/ 01 — Reserve</span>
<h2>Court <em>bookings</em></h2>
</div>
<p>Reserve a one-hour slot. Three surfaces, six daily windows, seven days out. Members only — be kind, cancel early.</p>
</div>
<div class="booking-wrap">
<div class="day-tabs" id="day-tabs">
<?php foreach($days as $i => $d): ?>
<button class="day-tab <?= $i===0?'active':'' ?>" data-day="<?= $d['date'] ?>">
<div class="lab"><?= $d['label'] ?></div>
<div class="num"><?= $d['day'] ?></div>
<div class="mon"><?= $d['mon'] ?></div>
</button>
<?php endforeach; ?>
</div>
<div class="court-legend">
<span class="swatch"><i class="sw-clay"></i> Clay</span>
<span class="swatch"><i class="sw-grass"></i> Grass</span>
<span class="swatch"><i class="sw-hard"></i> Hard</span>
<span class="swatch" style="margin-left:auto"><i class="sw-mine"></i> Your booking</span>
</div>
<?php foreach($days as $i => $d): ?>
<div class="day-panel" data-panel="<?= $d['date'] ?>" style="<?= $i===0?'':'display:none' ?>">
<div class="slots-grid">
<div class="header-cell">Time</div>
<?php foreach($courts as $c): ?>
<div class="header-cell"><?= $c ?></div>
<?php endforeach; ?>
<?php foreach($times as $t): ?>
<div class="time-label"><?= $t ?></div>
<?php foreach($courts as $c):
$key = $d['date'].'|'.$t.'|'.$c;
$b = $bookings[$key] ?? null;
if ($b):
$mine = ($name !== '' && $b['name'] === $name);
?>
<div class="slot booked <?= $mine?'mine':'' ?>">
<div class="who"><?= e($b['name']) ?></div>
<div class="ct"><?= e($c) ?></div>
<?php if ($mine): ?>
<form method="post" style="margin:0">
<input type="hidden" name="cancel_id" value="<?= (int)$b['id'] ?>">
<input type="hidden" name="anchor" value="courts">
<button class="cancel" type="submit">cancel</button>
</form>
<?php endif; ?>
</div>
<?php else: ?>
<button class="slot available"
data-date="<?= $d['date'] ?>"
data-time="<?= $t ?>"
data-court="<?= $c ?>"
<?= $name?'':'disabled style="opacity:.4;cursor:not-allowed"' ?>>
<div class="lab-book">+ Book</div>
<div class="ct"><?= $c ?></div>
</button>
<?php endif; endforeach; ?>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</section>
<section id="clubhouse">
<div class="container">
<div class="section-head">
<div>
<span class="tag">/ 02 — Talk</span>
<h2>Members' <em>lounge</em></h2>
</div>
<p>The watercooler between sets. Brag, complain, find a doubles partner, debate the GOAT (it's Federer).</p>
</div>
<div class="chat-wrap">
<div class="chat-header">
<h3>The <em>lounge</em></h3>
<div class="live"><span class="live-dot"></span>Live</div>
</div>
<div class="chat-body" id="chat-body">
<?php if (empty($messages)): ?>
<div class="chat-empty">Quiet courts today — be the first to break the silence.</div>
<?php else: foreach($messages as $m):
$mine = ($name !== '' && $m['name'] === $name);
$initial = strtoupper(mb_substr($m['name'], 0, 1));
$hue = crc32($m['name']) % 360;
?>
<div class="msg <?= $mine?'mine':'' ?>">
<div class="avatar" style="background:hsl(<?= $hue ?>,70%,60%)"><?= e($initial) ?></div>
<div>
<div class="msg-meta"><?= e($m['name']) ?> · <?= date('g:ia', (int)$m['created_at']) ?></div>
<div class="msg-bubble"><div class="msg-body"><?= e($m['body']) ?></div></div>
</div>
</div>
<?php endforeach; endif; ?>
</div>
<form class="chat-form <?= $name?'':'locked' ?>" method="post">
<input type="text" name="message" placeholder="<?= $name?'Say something to the club…':'Sign in above to chat' ?>" maxlength="500" <?= $name?'':'disabled' ?> required>
<input type="hidden" name="anchor" value="clubhouse">
<button type="submit" <?= $name?'':'disabled' ?>>Send</button>
</form>
</div>
</div>
</section>
<section id="about" style="padding-top:30px">
<div class="container">
<div class="section-head">
<div>
<span class="tag">/ 03 — Etiquette</span>
<h2>House <em>rules</em></h2>
</div>
<p>A few traditions that keep us honest.</p>
</div>
<div class="rules-grid">
<?php
$rules = [
['Whites preferred', 'Clay stains everything. Wear what you don\'t mind retiring.'],
['Cancel early', 'No-shows steal courts. Free your slot if you can\'t make it.'],
['Net cord apologies', 'Raise the hand. It\'s tradition, even on match point.'],
['Quiet on serve', 'Hold conversations between points, not during the toss.'],
['Sweep your clay', 'Brush the lines and drag the surface after every match.'],
['Be a good loser', 'Shake hands at the net. Mean it.'],
];
foreach ($rules as $i => $r): ?>
<div class="rule-card">
<span class="ix">RULE / <?= str_pad($i+1, 2, '0', STR_PAD_LEFT) ?></span>
<h4><?= e($r[0]) ?></h4>
<p><?= e($r[1]) ?></p>
</div>
<?php endforeach; ?>
</div>
</div>
</section>
<!-- Booking modal -->
<div class="modal-backdrop" id="modal">
<div class="modal">
<button class="close" id="modal-close" aria-label="Close">✕</button>
<h3>Confirm your <em>booking</em></h3>
<div class="sub" id="modal-sub">—</div>
<form method="post" id="book-form">
<label>Court Surface</label>
<div class="court-picker" id="court-picker">
<div class="court-opt" data-court="Clay"><div class="nm">Clay</div></div>
<div class="court-opt" data-court="Grass"><div class="nm">Grass</div></div>
<div class="court-opt" data-court="Hard"><div class="nm">Hard</div></div>
</div>
<input type="hidden" name="slot_date" id="f-date">
<input type="hidden" name="slot_time" id="f-time">
<input type="hidden" name="court" id="f-court">
<input type="hidden" name="book" value="1">
<input type="hidden" name="anchor" value="courts">
<div class="modal-actions">
<button type="button" class="btn btn-ghost" id="modal-cancel">Cancel</button>
<button type="submit" class="btn btn-primary">Confirm Booking</button>
</div>
</form>
</div>
</div>
<footer>
<div class="container foot-grid">
<div>
<h4>See you on <em>court</em>.</h4>
<div>Baseline Tennis Club · 14 Racquet Lane · Open dawn to dusk · Members only</div>
</div>
<div class="foot-right">
© <?= date('Y') ?> Baseline TC<br>
<?php if ($name): ?>
Signed in as <?= e($name) ?> ·
<form method="post" style="display:inline">
<button type="submit" name="logout" value="1" class="signout">sign out</button>
</form>
<?php else: ?>
not signed in
<?php endif; ?>
</div>
</div>
</footer>
<?php endif; ?>
<script>
(function(){
// Day tabs
var tabs = document.querySelectorAll('.day-tab');
var panels = document.querySelectorAll('.day-panel');
tabs.forEach(function(t){
t.addEventListener('click', function(){
tabs.forEach(function(x){ x.classList.remove('active'); });
t.classList.add('active');
var d = t.getAttribute('data-day');
panels.forEach(function(p){
p.style.display = (p.getAttribute('data-panel') === d) ? '' : 'none';
});
});
});
// Booking modal
var modal = document.getElementById('modal');
if (modal) {
var fDate = document.getElementById('f-date');
var fTime = document.getElementById('f-time');
var fCourt = document.getElementById('f-court');
var sub = document.getElementById('modal-sub');
var picker = document.getElementById('court-picker');
document.querySelectorAll('.slot.available').forEach(function(s){
s.addEventListener('click', function(e){
if (s.hasAttribute('disabled')) return;
e.preventDefault();
var date = s.getAttribute('data-date');
var time = s.getAttribute('data-time');
var court = s.getAttribute('data-court');
fDate.value = date;
fTime.value = time;
fCourt.value = court;
sub.textContent = date + ' · ' + time + ' · ' + court + ' Court';
picker.querySelectorAll('.court-opt').forEach(function(o){
o.classList.toggle('selected', o.getAttribute('data-court') === court);
});
modal.classList.add('open');
});
});
if (picker) {
picker.querySelectorAll('.court-opt