← back to Fleet Tv
public/today.html
289 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Fleet TV — Today</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #07080a;
--fg: #e8e6e0;
--dim: #6a6862;
--rule: rgba(255,255,255,0.08);
--pane: #11141a;
--accent: #c9a14b;
}
html, body { background: var(--bg); color: var(--fg); font-family: -apple-system, "Helvetica Neue", "Segoe UI", system-ui, sans-serif; min-height: 100vh; }
.head {
position: sticky; top: 0; z-index: 5;
display: flex; align-items: baseline; gap: 1.5rem;
padding: 1rem 1.5rem;
background: linear-gradient(180deg, rgba(7,8,10,0.94), rgba(7,8,10,0.6));
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--rule);
}
.head h1 { font-size: 0.95rem; letter-spacing: 0.18em; text-transform: uppercase; font-weight: 500; }
.head .crumb { color: var(--dim); font-size: 0.78rem; letter-spacing: 0.16em; text-transform: uppercase; }
.head .crumb a { color: var(--dim); text-decoration: none; border-bottom: 1px solid transparent; }
.head .crumb a:hover { color: var(--fg); border-color: var(--rule); }
.head .stamp { margin-left: auto; color: var(--dim); font-size: 0.78rem; letter-spacing: 0.18em; text-transform: uppercase; font-variant-numeric: tabular-nums; }
/* Hero */
.hero {
position: relative;
padding: 4rem 1.5rem 2.5rem;
background:
linear-gradient(180deg, var(--theme-accent, #c9a14b) 0%, transparent 70%),
var(--bg);
border-bottom: 1px solid var(--rule);
overflow: hidden;
}
.hero::before {
content: ''; position: absolute; inset: 0;
background: radial-gradient(ellipse at 30% 30%, rgba(255,255,255,0.06) 0%, transparent 50%);
pointer-events: none;
}
.hero-content {
position: relative; z-index: 1;
max-width: 1100px; margin: 0 auto;
display: grid; grid-template-columns: auto 1fr; gap: 2.5rem; align-items: center;
}
.day-card {
width: 140px; aspect-ratio: 1;
background: rgba(0,0,0,0.45);
border: 1px solid rgba(255,255,255,0.18);
border-radius: 6px;
display: grid; place-content: center;
text-align: center;
backdrop-filter: blur(6px);
}
.day-card .dow {
font-size: 0.78rem; letter-spacing: 0.32em; text-transform: uppercase; color: rgba(255,255,255,0.7);
}
.day-card .day {
font-size: 3.4rem; line-height: 1; font-weight: 200; margin: 0.4rem 0;
color: #fff; letter-spacing: -0.02em;
}
.day-card .month {
font-size: 0.7rem; letter-spacing: 0.32em; text-transform: uppercase; color: rgba(255,255,255,0.55);
}
.theme-name {
font-size: 3rem; line-height: 1.05; letter-spacing: -0.015em; font-weight: 300;
color: #fff;
text-shadow: 0 2px 30px rgba(0,0,0,0.4);
}
.theme-sub {
margin-top: 0.85rem;
font-size: 1.05rem; line-height: 1.5;
color: rgba(255,255,255,0.75);
max-width: 38rem;
font-weight: 300;
}
.theme-keys {
margin-top: 1.25rem;
display: flex; flex-wrap: wrap; gap: 0.4rem;
}
.theme-keys span {
padding: 0.32rem 0.8rem 0.34rem;
font-size: 0.65rem; letter-spacing: 0.2em; text-transform: uppercase;
background: rgba(0,0,0,0.4);
border: 1px solid rgba(255,255,255,0.18);
border-radius: 999px;
color: rgba(255,255,255,0.85);
backdrop-filter: blur(4px);
}
/* Editorial grid below the hero */
.editorial {
max-width: 1100px;
margin: 0 auto;
padding: 2.5rem 1.5rem 4rem;
display: grid; gap: 0.6rem;
/* asymmetric editorial layout: 2 large + small grid */
grid-template-columns: repeat(6, 1fr);
grid-auto-rows: 200px;
}
.editorial figure {
position: relative; overflow: hidden; border-radius: 4px;
background: var(--pane);
cursor: pointer;
transition: transform 0.3s ease;
grid-column: span 2;
grid-row: span 1;
transform: translateY(8px); opacity: 0;
animation: fadeIn 0.55s ease forwards;
}
.editorial figure:hover { transform: translateY(-2px); }
/* Make every 7th and 11th tile big to create rhythm */
.editorial figure:nth-child(7n+1) { grid-column: span 3; grid-row: span 2; }
.editorial figure:nth-child(11n+5) { grid-column: span 4; grid-row: span 1; }
@keyframes fadeIn { to { transform: translateY(0); opacity: 1; } }
.editorial figure img {
width: 100%; height: 100%; object-fit: cover; display: block;
transition: transform 0.6s ease;
}
.editorial figure:hover img { transform: scale(1.04); }
.editorial figure figcaption {
position: absolute; left: 0; right: 0; bottom: 0;
padding: 0.65rem 0.8rem 0.55rem;
background: linear-gradient(0deg, rgba(0,0,0,0.85), rgba(0,0,0,0));
color: #fff;
transform: translateY(105%); transition: transform 0.3s ease;
}
.editorial figure:hover figcaption { transform: translateY(0); }
.editorial figcaption .t { font-size: 0.78rem; line-height: 1.3; }
.editorial figcaption .v { display: block; margin-top: 0.2rem; font-size: 0.62rem; letter-spacing: 0.18em; text-transform: uppercase; opacity: 0.65; }
.pill {
position: absolute; top: 0.5rem; left: 0.5rem;
padding: 0.18rem 0.55rem 0.2rem;
font-size: 0.6rem; letter-spacing: 0.18em; text-transform: uppercase;
background: rgba(0,0,0,0.7);
border: 1px solid currentColor;
border-radius: 999px;
color: #fff;
backdrop-filter: blur(4px);
}
.splash {
position: fixed; inset: 0; display: grid; place-items: center;
background: var(--bg); z-index: 10;
transition: opacity 0.5s ease;
}
.splash.hide { opacity: 0; pointer-events: none; }
.splash .mark {
font-size: 1rem; letter-spacing: 0.42em; text-transform: uppercase;
color: var(--accent);
animation: pulse 1.6s ease-in-out infinite;
}
@keyframes pulse { 0%,100% { opacity: 0.4; } 50% { opacity: 1; } }
.lightbox { position: fixed; inset: 0; z-index: 50; display: none; align-items: center; justify-content: center; background: rgba(0,0,0,0.92); padding: 2rem; }
.lightbox.open { display: flex; }
.lightbox img { max-width: 92vw; max-height: 80vh; border-radius: 4px; box-shadow: 0 30px 100px rgba(0,0,0,0.7); }
.lightbox .info { position: absolute; bottom: 2rem; left: 50%; transform: translateX(-50%); text-align: center; color: var(--fg); background: rgba(20,22,26,0.92); padding: 1rem 1.5rem; border-radius: 4px; backdrop-filter: blur(6px); max-width: 80vw; }
.lightbox .info .t { font-size: 1rem; line-height: 1.3; }
.lightbox .info .v { font-size: 0.7rem; letter-spacing: 0.18em; text-transform: uppercase; margin-top: 0.4rem; opacity: 0.7; }
.lightbox .info a { display: inline-block; margin-top: 0.6rem; color: #c9a14b; text-decoration: none; font-size: 0.75rem; letter-spacing: 0.16em; text-transform: uppercase; border-bottom: 1px solid currentColor; padding-bottom: 1px; }
.lightbox .close { position: absolute; top: 1.5rem; right: 1.5rem; color: var(--fg); font-size: 1rem; letter-spacing: 0.18em; text-transform: uppercase; cursor: pointer; }
</style>
</head>
<body>
<div class="splash" id="splash"><div class="mark">Today · curating</div></div>
<div class="head">
<h1>Fleet TV</h1>
<span class="crumb"><a href="/">wall</a> · <a href="/mosaic.html">mosaic</a> · <a href="/stats.html">stats</a> · <a href="/search.html">search</a> · today</span>
<div class="stamp" id="stamp">—</div>
</div>
<section class="hero" id="hero">
<div class="hero-content">
<div class="day-card">
<div class="dow" id="dow">—</div>
<div class="day" id="day">—</div>
<div class="month" id="month">—</div>
</div>
<div>
<div class="theme-name" id="theme-name">—</div>
<div class="theme-sub" id="theme-sub">—</div>
<div class="theme-keys" id="theme-keys"></div>
</div>
</div>
</section>
<div class="editorial" id="editorial"></div>
<div class="lightbox" id="lightbox">
<span class="close" id="lb-close">close ✕</span>
<img id="lb-img" alt="">
<div class="info">
<div class="t" id="lb-title">—</div>
<div class="v" id="lb-vendor">—</div>
<a id="lb-link" target="_blank" rel="noopener noreferrer">view on site →</a>
</div>
</div>
<script>
(() => {
const $ = (id) => document.getElementById(id);
const lightbox = $('lightbox'), lbImg = $('lb-img'), lbTitle = $('lb-title'),
lbVendor = $('lb-vendor'), lbLink = $('lb-link'), lbClose = $('lb-close');
async function load() {
$('splash').classList.remove('hide');
try {
const r = await fetch('/api/today', { cache: 'no-store' });
const j = await r.json();
const t = j.theme;
const now = new Date();
$('dow').textContent = t.weekday;
$('day').textContent = String(now.getDate());
$('month').textContent = now.toLocaleString(undefined, { month: 'short' });
$('theme-name').textContent = t.name;
$('theme-sub').textContent = t.subtitle;
$('theme-keys').innerHTML = t.keywords.map(k => `<span>${escape(k)}</span>`).join('');
$('hero').style.setProperty('--theme-accent', t.accent);
$('stamp').textContent = j.total + ' picks · ' + new Date(j.generatedAt).toTimeString().slice(0,5);
const ed = $('editorial');
ed.innerHTML = '';
const frag = document.createDocumentFragment();
j.items.forEach((p, i) => {
const fig = document.createElement('figure');
fig.style.animationDelay = (Math.min(i, 30) * 35) + 'ms';
fig.innerHTML = `
<span class="pill" style="color:${p.accent}">${p.site.replace(/wallpaper$|wallcoverings?$|walls$/i, m => '·' + m)}</span>
<img loading="lazy" referrerpolicy="no-referrer" src="${p.image}" alt="${escape(p.title)}" onerror="this.parentNode.style.display='none'">
<figcaption><span class="t">${escape(p.title)}</span><span class="v">${escape(p.vendor || p.site)} · ${escape(p.matchedKeyword)}</span></figcaption>
`;
fig.addEventListener('click', () => openLightbox(p));
frag.appendChild(fig);
});
ed.appendChild(frag);
} catch (e) {
console.error(e);
} finally {
setTimeout(() => $('splash').classList.add('hide'), 300);
}
}
function openLightbox(p) {
lbImg.src = p.image;
lbTitle.textContent = p.title || '(untitled)';
lbVendor.textContent = (p.vendor || '') + (p.vendor && p.site ? ' · ' : '') + p.site;
lbLink.href = p.productUrl;
lightbox.classList.add('open');
}
lbClose.addEventListener('click', () => lightbox.classList.remove('open'));
lightbox.addEventListener('click', (e) => { if (e.target === lightbox) lightbox.classList.remove('open'); });
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') lightbox.classList.remove('open');
if (e.key.toLowerCase() === 'r') load();
if (e.key.toLowerCase() === 'w') location.href = '/';
if (e.key.toLowerCase() === 'm') location.href = '/mosaic.html';
if (e.key.toLowerCase() === 's') location.href = '/stats.html';
if (e.key === '/') { e.preventDefault(); location.href = '/search.html'; }
if (e.key.toLowerCase() === 'a') location.href = '/atlas.html';
if (e.key.toLowerCase() === 'x') location.href = '/matrix.html';
});
function escape(s) {
return String(s).replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c]));
}
load();
// Re-check at top of every hour in case the day rolled over while the tab was open
setInterval(load, 60 * 60 * 1000);
})();
</script>
</body>
</html>