Add live countdown, Silver Q venue section, real address, OneTouch favicon

This commit is contained in:
Devin Willis 2026-08-26 14:31:24 -04:00
parent cd91fbbb95
commit 7a331d241a
No known key found for this signature in database
GPG key ID: 3776B3C5AFF3FFEA
6 changed files with 329 additions and 30 deletions

View file

@ -28,9 +28,9 @@ DTSTART;TZID=America/New_York:20260827T190000
DTEND;TZID=America/New_York:20260827T210000
RRULE:FREQ=WEEKLY;BYDAY=TH
SUMMARY:The Founder's Break
LOCATION:Silver Q\, Gainesville\, FL
LOCATION:Silver Q Billiards & Sports Bar\, 225 W University Ave\, Gainesville\, FL 32601
URL:https://foundersbreak.onetouch.studio
DESCRIPTION:UF startup pool social. Every Thursday\, 7 to 9 PM at Silver Q.
DESCRIPTION:UF startup pool social. Every Thursday\, 7 to 9 PM at Silver Q\, 225 W University Ave.
Zero pool skills required. Free\, 18+. Bring your ideas and a valid ID.\n\n
Hosted by OneTouch Studio. Details and RSVP: https://foundersbreak.onetouch.
studio

View file

@ -161,6 +161,53 @@ a { color: var(--accent2); }
background: linear-gradient(190deg, transparent 55%, rgba(6, 6, 11, 0.5));
}
/* ── Countdown ────────────────────────────────────────────── */
.countdown-band { max-width: var(--shell); margin: 0 auto; padding: 0 var(--gut) 4.5rem; }
.countdown {
position: relative; isolation: isolate; overflow: hidden;
padding: 2.6rem 2rem;
border: 1px solid var(--border); border-radius: var(--r-surface);
background:
radial-gradient(90% 140% at 50% 0%, rgba(16, 185, 129, 0.12), transparent 68%),
var(--bg2);
text-align: center;
}
.countdown-label {
font-size: 0.72rem; font-weight: 700; letter-spacing: 0.16em;
text-transform: uppercase; color: var(--accent2);
}
.countdown-clock {
display: flex; justify-content: center; align-items: flex-start;
gap: clamp(1.2rem, 5vw, 3.4rem);
margin: 1.5rem 0 1.2rem;
}
.countdown-clock .unit { display: flex; flex-direction: column; align-items: center; gap: 0.35rem; min-width: 3.4ch; }
.countdown-clock .num {
font-family: "Space Grotesk", sans-serif; font-weight: 700;
font-size: clamp(2.4rem, 7vw, 4rem); line-height: 1;
font-variant-numeric: tabular-nums; letter-spacing: -0.03em;
}
.countdown-clock .cap {
font-size: 0.7rem; font-weight: 600; letter-spacing: 0.13em;
text-transform: uppercase; color: var(--muted);
}
/* One-frame lift as a digit changes, so the clock reads as live rather than static. */
.countdown-clock .num.tick { animation: tick 0.42s var(--ease); }
@keyframes tick {
0% { transform: translateY(-0.14em); opacity: 0.45; }
100% { transform: none; opacity: 1; }
}
.countdown-when { color: var(--muted); font-size: 0.95rem; }
.countdown.is-live { border-color: var(--border-hover); }
.countdown.is-live .countdown-label { color: var(--accent2); }
.countdown.is-live .countdown-when { color: var(--text); font-weight: 600; }
/* ── Details: display-value strip ─────────────────────────── */
.details { max-width: var(--shell); margin: 0 auto; padding: 0 var(--gut) 6rem; }
@ -248,6 +295,39 @@ a { color: var(--accent2); }
.cell-text h3 { font-size: 1.16rem; font-weight: 600; margin-bottom: 0.65rem; }
.cell-text p { color: var(--muted); font-size: 0.96rem; max-width: 52ch; }
/* ── Venue ────────────────────────────────────────────────── */
.venue {
max-width: var(--shell); margin: 0 auto;
padding: 0 var(--gut) 7rem;
display: grid; grid-template-columns: 0.85fr 1.15fr; gap: 4rem; align-items: center;
}
.venue-logo { width: auto; height: clamp(96px, 13vw, 132px); margin-bottom: 1.8rem; opacity: 0.95; }
.venue h2 { font-size: clamp(1.6rem, 3.2vw, 2.2rem); font-weight: 700; }
.venue p { margin-top: 1rem; color: var(--muted); max-width: 46ch; }
.venue address { margin-top: 1.4rem; font-style: normal; }
.venue address a {
font-family: "Space Grotesk", sans-serif; font-weight: 600;
font-size: 1.12rem; line-height: 1.45; color: var(--text); text-decoration: none;
border-bottom: 1px solid var(--border-hover);
}
.venue address a:hover { color: var(--accent2); }
.venue-meta {
display: flex; flex-wrap: wrap; gap: 0.5rem 1.5rem;
margin-top: 1.4rem; font-size: 0.9rem;
}
.venue-meta a { color: var(--accent2); text-decoration: none; border-bottom: 1px solid var(--border-hover); }
.venue-meta a:hover { border-bottom-color: var(--accent2); }
.venue-meta span { color: var(--muted); }
.venue-figure img {
width: 100%; aspect-ratio: 3 / 2; object-fit: cover;
border-radius: var(--r-surface); border: 1px solid var(--hairline);
}
.venue-figure figcaption { margin-top: 0.8rem; font-size: 0.82rem; color: var(--fine); }
/* ── RSVP ─────────────────────────────────────────────────── */
.rsvp { padding: 0 var(--gut) 7rem; }
@ -321,9 +401,17 @@ form { margin-top: 2rem; }
.cell-wide { grid-column: span 2; }
}
@media (max-width: 1000px) {
.venue { grid-template-columns: 1fr; gap: 2.4rem; }
.venue-figure { order: -1; }
}
@media (max-width: 760px) {
:root { --gut: 1.25rem; }
.nav-link { display: none; }
.countdown { padding: 2rem 1rem; }
.countdown-clock { gap: 0.9rem; }
.venue { padding-bottom: 5rem; }
.detail-grid { grid-template-columns: 1fr 1fr; }
.detail { padding: 1.4rem 1.1rem; }
.detail:nth-child(odd) { border-left: 0; }

View file

@ -1,9 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<!-- OneTouch Studio's mark, cropped to the cube and ring so it still reads at 16px. -->
<rect width="64" height="64" rx="14" fill="#06060b"/>
<g transform="translate(32 32)" fill="none" stroke="#10b981" stroke-linecap="round" stroke-linejoin="round">
<circle cx="0" cy="0" r="23" stroke-width="2.5"/>
<g transform="scale(0.62)" stroke-width="4.5">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 672 672" width="672" height="672">
<!-- The OneTouch Studio mark exactly as it ships on onetouch.studio, sat on a
dark rounded square so it stays visible in a light browser tab. -->
<rect width="672" height="672" rx="148" fill="#06060b"/>
<g transform="translate(336 336)">
<circle cx="0" cy="0" r="160" fill="none" stroke="#FFFFFF" stroke-width="5.5"/>
<circle cx="0" cy="0" r="223" fill="none" stroke="#FFFFFF" stroke-width="7.5"/>
<circle cx="0" cy="0" r="279" fill="none" stroke="#FFFFFF" stroke-width="5.5"/>
<g transform="scale(0.72)" fill="none" stroke="#FFFFFF" stroke-width="8" stroke-linecap="round" stroke-linejoin="round">
<polygon points="0,-34 30,-14 0,6 -30,-14"/>
<polygon points="0,6 30,-14 30,20 0,40 -30,20 -30,-14"/>
<line x1="0" y1="-34" x2="0" y2="6"/>
@ -11,6 +15,17 @@
<line x1="-30" y1="-14" x2="-30" y2="20"/>
<line x1="30" y1="-14" x2="30" y2="20"/>
</g>
<circle cx="0" cy="-23" r="3.4" fill="#10b981" stroke="none"/>
<circle cx="0" cy="-279" r="9" fill="#FFFFFF"/>
<circle cx="223" cy="0" r="9" fill="#FFFFFF"/>
<circle cx="0" cy="223" r="9" fill="#FFFFFF"/>
<circle cx="-197.28" cy="-197.28" r="6.5" fill="#FFFFFF"/>
<circle cx="113.14" cy="113.14" r="5.5" fill="#FFFFFF"/>
<line x1="-12" y1="-279" x2="12" y2="-279" stroke="#FFFFFF" stroke-width="3" stroke-linecap="round"/>
<line x1="223" y1="-12" x2="223" y2="12" stroke="#FFFFFF" stroke-width="3" stroke-linecap="round"/>
<line x1="-12" y1="223" x2="12" y2="223" stroke="#FFFFFF" stroke-width="3" stroke-linecap="round"/>
<line x1="-205.76" y1="-188.80" x2="-188.80" y2="-205.76" stroke="#FFFFFF" stroke-width="2.5" stroke-linecap="round"/>
<line x1="108.19" y1="118.09" x2="118.09" y2="108.19" stroke="#FFFFFF" stroke-width="2.5" stroke-linecap="round"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 834 B

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -1,9 +1,10 @@
/* The Founder's Break - nav state, scroll reveal, RSVP submit.
/* The Founder's Break - nav state, scroll reveal, live countdown, RSVP submit.
No scroll listeners: nav state and reveal both ride IntersectionObserver. */
(function () {
"use strict";
var TZ = "America/New_York";
var reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
/* ── Nav background once the page has moved off the top ── */
@ -23,14 +24,130 @@
var io = new IntersectionObserver(function (entries, obs) {
entries.forEach(function (entry, i) {
if (!entry.isIntersecting) return;
var delay = Math.min(i, 4) * 70;
setTimeout(function () { entry.target.classList.add("in"); }, delay);
setTimeout(function () { entry.target.classList.add("in"); }, Math.min(i, 4) * 70);
obs.unobserve(entry.target);
});
}, { threshold: 0.15, rootMargin: "0px 0px -8% 0px" });
reveals.forEach(function (el) { io.observe(el); });
}
/* ── Live countdown ──────────────────────────────────────
Mirrors the rule in views/index.ejs so the clock keeps itself honest
without a reload: once an event ends, it rolls to the following week
on its own, skipping any cancelled date. */
var el = document.getElementById("countdown");
if (el) startCountdown(el);
function etParts(date) {
// "2026-08-27" plus the weekday index, both in Eastern time.
var ymd = new Intl.DateTimeFormat("en-CA", {
timeZone: TZ, year: "numeric", month: "2-digit", day: "2-digit",
}).format(date);
var wd = new Intl.DateTimeFormat("en-US", { timeZone: TZ, weekday: "short" }).format(date);
var order = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
return { iso: ymd, day: order[wd] };
}
function etOffsetHours(iso) {
var label = new Intl.DateTimeFormat("en-US", { timeZone: TZ, timeZoneName: "shortOffset" })
.format(new Date(iso + "T12:00:00Z"));
var m = label.match(/GMT([+-]?\d{1,2})/);
return m ? parseInt(m[1], 10) : -5;
}
function instantFor(iso, hour) {
var p = iso.split("-").map(Number);
return new Date(Date.UTC(p[0], p[1] - 1, p[2], hour - etOffsetHours(iso), 0, 0));
}
function startCountdown(root) {
var weekday = Number(root.dataset.weekday);
var startHour = Number(root.dataset.startHour);
var endHour = Number(root.dataset.endHour);
var first = root.dataset.first;
var cancelled = (root.dataset.cancelled || "").split(",").filter(Boolean);
var nums = {};
root.querySelectorAll("[data-unit]").forEach(function (n) { nums[n.dataset.unit] = n; });
var label = document.getElementById("countdown-label");
var when = document.getElementById("countdown-when");
var clock = document.getElementById("countdown-clock");
function nextOccurrence(now) {
var today = etParts(now);
var parts = today.iso.split("-").map(Number);
var cur = new Date(Date.UTC(parts[0], parts[1] - 1, parts[2]));
var firstParts = first.split("-").map(Number);
var firstDate = new Date(Date.UTC(firstParts[0], firstParts[1] - 1, firstParts[2]));
if (cur < firstDate) cur = firstDate;
var iso = function (d) { return d.toISOString().slice(0, 10); };
while (cur.getUTCDay() !== weekday) cur.setUTCDate(cur.getUTCDate() + 1);
// Only roll past today once the night is actually over.
if (iso(cur) === today.iso && now >= instantFor(today.iso, endHour)) {
cur.setUTCDate(cur.getUTCDate() + 7);
}
while (cancelled.indexOf(iso(cur)) !== -1) cur.setUTCDate(cur.getUTCDate() + 7);
return iso(cur);
}
function render() {
var now = new Date();
var iso = nextOccurrence(now);
var start = instantFor(iso, startHour);
var end = instantFor(iso, endHour);
var pretty = new Intl.DateTimeFormat("en-US", {
timeZone: TZ, weekday: "long", month: "long", day: "numeric",
}).format(start);
if (now >= start && now < end) {
root.classList.add("is-live");
label.textContent = "Happening right now";
clock.hidden = true;
when.textContent = "Until " + fmtTime(end) + " tonight. Come find us at the tables.";
return;
}
root.classList.remove("is-live");
clock.hidden = false;
var diff = Math.max(0, start - now);
var total = Math.floor(diff / 1000);
set("days", Math.floor(total / 86400));
set("hours", Math.floor(total / 3600) % 24);
set("minutes", Math.floor(total / 60) % 60);
set("seconds", total % 60);
label.textContent = total < 86400 ? "The Break is tonight" : "Until the next Break";
when.textContent = pretty + ", " + fmtTime(start) + " at Silver Q";
}
function fmtTime(d) {
return new Intl.DateTimeFormat("en-US", { timeZone: TZ, hour: "numeric", minute: "2-digit" }).format(d);
}
function set(unit, value) {
var node = nums[unit];
if (!node) return;
var text = unit === "days" ? String(value) : String(value).padStart(2, "0");
if (node.textContent === text) return;
node.textContent = text;
if (reduceMotion) return;
node.classList.remove("tick");
void node.offsetWidth; // restart the animation
node.classList.add("tick");
}
render();
var timer = setInterval(render, 1000);
// A backgrounded tab throttles the interval; catch up the moment it returns.
document.addEventListener("visibilitychange", function () {
if (!document.hidden) render();
});
window.addEventListener("pagehide", function () { clearInterval(timer); });
}
/* ── RSVP ── */
var form = document.getElementById("rsvp-form");
if (!form) return;

View file

@ -2,14 +2,15 @@
//
// The event recurs weekly, so nothing here is a one-off date. index.ejs computes
// the next occurrence at render time from `weekday` + `firstEvent`, minus anything
// listed in `cancelled`. To skip a week (holiday, venue closed, whatever), add the
// ISO date to `cancelled` and redeploy. Do not hand-edit dates anywhere else.
// listed in `cancelled`, and public/js/main.js runs the same rule client-side so a
// page left open overnight still rolls itself forward. To skip a week, add the ISO
// date to `cancelled` and redeploy. Do not hand-edit dates anywhere else.
module.exports = {
name: "The Founder's Break",
tagline: "UF Startup Pool Social",
description:
"A weekly pool social for the people building things in Gainesville. Every Thursday at Silver Q.",
"A weekly pool social for the people building things in Gainesville. Every Thursday at Silver Q, 225 W University Ave.",
// Schedule
timezone: "America/New_York",
@ -17,15 +18,28 @@ module.exports = {
firstEvent: "2026-08-27",
startTime: "7:00 PM",
endTime: "9:00 PM",
startHour24: 19,
endHour24: 21, // after this hour on event day, the page rolls to next week
cancelled: [
// "2026-11-26", // example: Thanksgiving
],
// Venue
venue: "Silver Q",
city: "Gainesville",
mapUrl: "https://www.google.com/maps/search/?api=1&query=Silver+Q+Gainesville+FL",
// Venue. Every field here is sourced, not guessed. Silver Q Billiards & Sports
// Bar, established 1992, is the host venue; it is not run by OneTouch Studio.
venue: {
name: "Silver Q",
fullName: "Silver Q Billiards & Sports Bar",
street: "225 W University Ave",
city: "Gainesville",
state: "FL",
zip: "32601",
note: "Downtown, about a mile from campus. Doors from 4:00 PM, we take over the tables at 7:00.",
established: "1992",
instagram: "silverqgnv",
instagramUrl: "https://www.instagram.com/silverqgnv/",
mapUrl:
"https://www.google.com/maps/search/?api=1&query=Silver+Q+Billiards+225+W+University+Ave+Gainesville+FL+32601",
},
entry: "Free, 18+",
// Host
@ -45,6 +59,10 @@ module.exports = {
"https://nyc3.digitaloceanspaces.com/corben-sites/images/c23ea202-5442-41b4-9a1b-1586e81c50eb/0b5cd98c-b31a-4d09-980f-d31da5873a53/break-shot.jpg",
napkin:
"https://nyc3.digitaloceanspaces.com/corben-sites/images/c23ea202-5442-41b4-9a1b-1586e81c50eb/eb54a5f7-a30e-4a7a-b3cb-0beb5eff43ff/napkin.jpg",
venue:
"https://nyc3.digitaloceanspaces.com/corben-sites/images/c23ea202-5442-41b4-9a1b-1586e81c50eb/458dab49-e942-4016-a744-127da6f9a754/silverq-venue.jpg",
venueLogo:
"https://nyc3.digitaloceanspaces.com/corben-sites/images/c23ea202-5442-41b4-9a1b-1586e81c50eb/5adbc5b1-ed83-48ae-b067-8ca01d0fde44/silverq-logo.png",
},
expect: [

View file

@ -2,9 +2,12 @@
/* ---- Next occurrence -------------------------------------------------
All arithmetic happens on a UTC Date that carries the *Eastern calendar
date*, never a wall-clock instant. That keeps day-of-week and day-adding
correct across DST without needing a timezone library. */
correct across DST without needing a timezone library. public/js/main.js
runs the same rule so the live countdown agrees with this render and can
roll itself over without a reload. */
const TZ = site.timezone || "America/New_York";
const now = new Date();
const V = site.venue;
const todayISO = new Intl.DateTimeFormat("en-CA", {
timeZone: TZ, year: "numeric", month: "2-digit", day: "2-digit",
@ -24,10 +27,10 @@
while (next.getUTCDay() !== site.weekday) next.setUTCDate(next.getUTCDate() + 1);
if (isoOf(next) === todayISO && hourET >= (site.endHour24 || 21)) next.setUTCDate(next.getUTCDate() + 7);
const cancelled = site.cancelled || [];
let skippedWeek = null;
// Keep the FIRST skipped date: with two cancellations in a row the notice
// should name the soonest one people were expecting, not the last one.
const cancelled = site.cancelled || [];
let skippedWeek = null;
while (cancelled.includes(isoOf(next))) {
if (!skippedWeek) skippedWeek = isoOf(next);
next.setUTCDate(next.getUTCDate() + 7);
@ -40,14 +43,23 @@
const nextShort = fmtDate({ weekday: "short", month: "short", day: "numeric" });
const weekdayName = new Intl.DateTimeFormat("en-US", { timeZone: "UTC", weekday: "long" }).format(next);
// Eastern UTC offset on the event date, so the countdown target is a real instant.
const offLabel = new Intl.DateTimeFormat("en-US", { timeZone: TZ, timeZoneName: "shortOffset" })
.format(new Date(`${nextISO}T12:00:00Z`));
const offHours = Number((offLabel.match(/GMT([+-]?\d{1,2})/) || [, "-5"])[1]);
const pad = (n) => String(n).padStart(2, "0");
const offStr = `${offHours < 0 ? "-" : "+"}${pad(Math.abs(offHours))}:00`;
const startISO = `${nextISO}T${pad(site.startHour24)}:00:00${offStr}`;
const endISO = `${nextISO}T${pad(site.endHour24)}:00:00${offStr}`;
const heroLine = isTonight
? `Tonight at ${site.venue}, ${site.startTime} to ${site.endTime}. Grab a cue and meet the people building things in ${site.city}.`
: `Every ${weekdayName} at ${site.venue}, ${site.startTime} to ${site.endTime}. The next one is ${nextLong}.`;
? `Tonight at ${V.name}, ${site.startTime} to ${site.endTime}. Grab a cue and meet the people building things in ${V.city}.`
: `Every ${weekdayName} at ${V.name}, ${site.startTime} to ${site.endTime}. The next one is ${nextLong}.`;
const details = [
{ label: "When", value: `Every ${weekdayName}` },
{ label: "Time", value: `${site.startTime} to ${site.endTime}` },
{ label: "Where", value: `${site.venue}, ${site.city}`, href: site.mapUrl },
{ label: "Where", value: `${V.name}, ${V.street}`, href: V.mapUrl },
{ label: "Entry", value: site.entry },
];
%><!DOCTYPE html>
@ -67,6 +79,7 @@
<meta name="twitter:card" content="summary_large_image">
<link rel="icon" href="/images/favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/images/favicon.svg">
<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=Inter:wght@400;500;600;700&family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet">
@ -83,12 +96,12 @@
"@type": "Event",
name: site.name,
description: site.description,
startDate: `${nextISO}T19:00:00-04:00`,
endDate: `${nextISO}T21:00:00-04:00`,
startDate: startISO,
endDate: endISO,
eventSchedule: { "@type": "Schedule", repeatFrequency: "P1W", byDay: "https://schema.org/Thursday", startTime: "19:00", endTime: "21:00" },
eventStatus: "https://schema.org/EventScheduled",
eventAttendanceMode: "https://schema.org/OfflineEventAttendanceMode",
location: { "@type": "Place", name: site.venue, address: { "@type": "PostalAddress", addressLocality: site.city, addressRegion: "FL", addressCountry: "US" } },
location: { "@type": "Place", name: V.fullName, address: { "@type": "PostalAddress", streetAddress: V.street, addressLocality: V.city, addressRegion: V.state, postalCode: V.zip, addressCountry: "US" } },
organizer: { "@type": "Organization", name: site.host.name, url: site.host.url },
isAccessibleForFree: true,
image: [site.images.hero],
@ -108,6 +121,7 @@
<nav class="nav-right" aria-label="Primary">
<a class="nav-link" href="#details">Details</a>
<a class="nav-link" href="#expect">What to expect</a>
<a class="nav-link" href="#venue">The venue</a>
<a class="nav-cta" href="#rsvp">RSVP</a>
</nav>
</header>
@ -131,6 +145,31 @@
</figure>
</section>
<!-- Live countdown. Server-rendered values are the no-JS fallback; main.js
takes over, ticks, and recomputes the following Thursday on its own. -->
<section class="countdown-band">
<div class="countdown"
id="countdown"
data-start="<%= startISO %>"
data-end="<%= endISO %>"
data-weekday="<%= site.weekday %>"
data-start-hour="<%= site.startHour24 %>"
data-end-hour="<%= site.endHour24 %>"
data-first="<%= site.firstEvent %>"
data-cancelled="<%= (site.cancelled || []).join(',') %>">
<p class="countdown-label" id="countdown-label">
<%= isTonight ? "The Break is tonight" : "Until the next Break" %>
</p>
<div class="countdown-clock" id="countdown-clock" role="timer" aria-live="off">
<div class="unit"><span class="num" data-unit="days">-</span><span class="cap">days</span></div>
<div class="unit"><span class="num" data-unit="hours">-</span><span class="cap">hours</span></div>
<div class="unit"><span class="num" data-unit="minutes">-</span><span class="cap">min</span></div>
<div class="unit"><span class="num" data-unit="seconds">-</span><span class="cap">sec</span></div>
</div>
<p class="countdown-when" id="countdown-when"><%= nextLong %>, <%= site.startTime %> at <%= V.name %></p>
</div>
</section>
<!-- Details: display-value strip, vertical hairlines only -->
<section class="details" id="details">
<% if (skippedWeek) { %>
@ -176,6 +215,28 @@
</div>
</section>
<!-- Venue -->
<section class="venue" id="venue">
<div class="venue-copy reveal">
<img class="venue-logo" src="<%= site.images.venueLogo %>" width="395" height="736"
alt="<%= V.fullName %>" loading="lazy">
<h2>We play at <%= V.name %>.</h2>
<p><%= V.note %></p>
<address>
<a href="<%= V.mapUrl %>" target="_blank" rel="noopener"><%= V.street %><br><%= V.city %>, <%= V.state %> <%= V.zip %></a>
</address>
<p class="venue-meta">
<a href="<%= V.instagramUrl %>" target="_blank" rel="noopener">@<%= V.instagram %></a>
<span>Pouring since <%= V.established %></span>
</p>
</div>
<figure class="venue-figure reveal">
<img src="<%= site.images.venue %>" width="1400" height="991" loading="lazy"
alt="The Silver Q storefront on West University Avenue at dusk.">
<figcaption>Look for the sign on West University Avenue.</figcaption>
</figure>
</section>
<!-- RSVP -->
<section class="rsvp" id="rsvp">
<div class="rsvp-panel reveal">
@ -213,7 +274,7 @@
<div class="rsvp-done" id="rsvp-done" hidden>
<h3>You are on the list.</h3>
<p>See you <%= isTonight ? "tonight" : nextLong %> at <%= site.venue %>, <%= site.startTime %>. Bring your ideas and a valid ID.</p>
<p>Check your inbox for a confirmation. See you <%= isTonight ? "tonight" : nextLong %> at <%= V.name %>, <%= V.street %>, <%= site.startTime %>. Bring your ideas and a valid ID.</p>
<a class="ics" href="/assets/founders-break.ics">Add the whole series to your calendar</a>
</div>
</div>
@ -229,7 +290,7 @@
<span>Hosted by <%= site.host.name %></span>
</a>
<a href="mailto:<%= site.host.email %>"><%= site.host.email %></a>
<a href="<%= site.mapUrl %>" target="_blank" rel="noopener"><%= site.venue %>, <%= site.city %></a>
<a href="<%= V.mapUrl %>" target="_blank" rel="noopener"><%= V.name %>, <%= V.street %></a>
</div>
</footer>