GRiDFLOW
Example project · read-only

Restaurant / local business

Menu, hours, gallery and table reservations for a restaurant, cafe or neighborhood shop. This is GridFlow's Restaurant starter, a complete project the builder starts from. Ember & Oak is a made-up kitchen. Click around: the menu filters, gallery and reservation form all work. Dashed boxes mark where a photo goes: in the builder you upload yours or have GridFlow make one.

Build your own — sign up

Describe your own in a sentence and GridFlow builds it. No card to start.

See the files (5) — read-only
package.json
{
  "name": "ember-and-oak",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "devDependencies": {
    "vite": "^5.4.0"
  }
}
index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="Ember & Oak — a wood-fired neighborhood kitchen. Menu, hours, gallery and table reservations." />
    <title>Ember &amp; Oak — Wood-fired kitchen</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=Fraunces:opsz,wght@9..144,500;9..144,600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
  </head>
  <body>
    <a class="skip-link" href="#menu">Skip to menu</a>

    <header class="site-header" id="top">
      <div class="wrap header-row">
        <a class="brand" href="#top" aria-label="Ember and Oak, home">
          <span class="brand-mark" aria-hidden="true"></span>
          <span class="brand-name">Ember <em>&amp;</em> Oak</span>
        </a>
        <nav class="site-nav" aria-label="Primary">
          <a href="#menu">Menu</a>
          <a href="#gallery">Gallery</a>
          <a href="#reserve">Reserve</a>
          <a href="#visit">Visit</a>
        </nav>
        <a class="btn btn-primary btn-sm" href="#reserve">Book a table</a>
      </div>
    </header>

    <main>
      <section class="hero" aria-labelledby="hero-title">
        <div class="hero-photo gf-placeholder" data-placeholder="1" role="img" aria-label="Placeholder 1 — the hero photo behind the headline">
          <span class="gf-ph-badge">Placeholder 1 · upload a photo, or ask Grid: “create an image for placeholder 1”</span>
        </div>
        <div class="wrap hero-grid">
          <div class="hero-copy">
            <p class="eyebrow">Wood-fired · Seasonal · Neighborhood</p>
            <h1 id="hero-title">Slow fire.<br />Honest plates.</h1>
            <p class="lede">A forty-seat kitchen built around one oak-burning oven. Sourdough, whole roasted vegetables, and cuts we break down ourselves.</p>
            <div class="hero-actions">
              <a class="btn btn-primary" href="#reserve">Reserve a table</a>
              <a class="btn btn-ghost" href="#menu">See the menu</a>
            </div>
          </div>
          <aside class="hero-card" aria-label="Hours and address">
            <div class="status-row">
              <span class="status-dot" id="status-dot" aria-hidden="true"></span>
              <span id="status-text">Checking hours…</span>
            </div>
            <dl class="hero-meta">
              <div>
                <dt>Today</dt>
                <dd id="today-hours">—</dd>
              </div>
              <div>
                <dt>Address</dt>
                <dd>412 Larkspur Street<br />Old Mill District, Portland</dd>
              </div>
              <div>
                <dt>Phone</dt>
                <dd><a href="tel:+15035550148">(503) 555-0148</a></dd>
              </div>
            </dl>
          </aside>
        </div>
      </section>

      <section class="section" id="menu" aria-labelledby="menu-title">
        <div class="wrap">
          <div class="section-head">
            <p class="eyebrow">The menu</p>
            <h2 id="menu-title">Cooked over oak, changed with the season</h2>
          </div>
          <div class="menu-controls">
            <div class="tabs" role="tablist" aria-label="Menu categories" id="menu-tabs"></div>
            <fieldset class="diet-filter" id="diet-filter">
              <legend>Show only</legend>
            </fieldset>
          </div>
          <div id="menu-panels"></div>
          <p class="legend" id="diet-legend"></p>
        </div>
      </section>

      <section class="section section-alt" id="gallery" aria-labelledby="gallery-title">
        <div class="wrap">
          <div class="section-head">
            <p class="eyebrow">The room</p>
            <h2 id="gallery-title">Forty seats, one fire</h2>
          </div>
          <ul class="gallery-grid" id="gallery-grid"></ul>
        </div>
      </section>

      <section class="section" id="reserve" aria-labelledby="reserve-title">
        <div class="wrap reserve-grid">
          <div class="reserve-copy">
            <p class="eyebrow">Reservations</p>
            <h2 id="reserve-title">Save a seat by the oven</h2>
            <p>Tables are held for 15 minutes past the booking time. Parties of nine or more, call us and we will set the long table.</p>
            <ul class="reserve-notes">
              <li>Walk-ins welcome at the counter</li>
              <li>Full menu until close</li>
              <li>Wheelchair accessible, step-free entry</li>
            </ul>
          </div>
          <form class="card reserve-form" id="reserve-form" novalidate>
            <div class="field-grid">
              <div class="field">
                <label for="r-name">Name</label>
                <input id="r-name" name="name" type="text" autocomplete="name" required />
                <p class="error" id="err-name" aria-live="polite"></p>
              </div>
              <div class="field">
                <label for="r-email">Email</label>
                <input id="r-email" name="email" type="email" autocomplete="email" required />
                <p class="error" id="err-email" aria-live="polite"></p>
              </div>
              <div class="field">
                <label for="r-phone">Phone</label>
                <input id="r-phone" name="phone" type="tel" autocomplete="tel" inputmode="tel" placeholder="(503) 555-0100" />
                <p class="error" id="err-phone" aria-live="polite"></p>
              </div>
              <div class="field">
                <label for="r-date">Date</label>
                <input id="r-date" name="date" type="date" required />
                <p class="error" id="err-date" aria-live="polite"></p>
              </div>
              <div class="field">
                <label for="r-time">Time</label>
                <select id="r-time" name="time" required>
                  <option value="">Choose a date first</option>
                </select>
                <p class="error" id="err-time" aria-live="polite"></p>
              </div>
              <div class="field">
                <label for="r-party">Party size</label>
                <select id="r-party" name="party" required>
                  <option value="">Select</option>
                  <option value="1">1 guest</option>
                  <option value="2" selected>2 guests</option>
                  <option value="3">3 guests</option>
                  <option value="4">4 guests</option>
                  <option value="5">5 guests</option>
                  <option value="6">6 guests</option>
                  <option value="7">7 guests</option>
                  <option value="8">8 guests</option>
                  <option value="9">9+ (we will call you)</option>
                </select>
                <p class="error" id="err-party" aria-live="polite"></p>
              </div>
              <div class="field field-full">
                <label for="r-notes">Anything we should know? <span class="optional">Optional</span></label>
                <textarea id="r-notes" name="notes" rows="3" placeholder="Allergies, a birthday, a high chair…"></textarea>
              </div>
            </div>
            <div class="form-actions">
              <button class="btn btn-primary" type="submit">Request reservation</button>
              <p class="form-hint">No payment needed. Saved on this device only.</p>
            </div>
            <p class="error form-error" id="form-error" role="alert"></p>
          </form>
          <div class="card confirmation" id="confirmation" hidden>
            <div class="confirm-check" aria-hidden="true">✓</div>
            <h3>You are on the list</h3>
            <p class="confirm-lead" id="confirm-lead"></p>
            <dl class="confirm-details" id="confirm-details"></dl>
            <p class="confirm-ref">Reference <strong id="confirm-ref"></strong></p>
            <div class="confirm-actions">
              <button class="btn btn-ghost" type="button" id="confirm-new">Make another booking</button>
              <button class="btn btn-danger" type="button" id="confirm-cancel">Cancel this booking</button>
            </div>
          </div>
        </div>
      </section>

      <section class="section section-alt" id="visit" aria-labelledby="visit-title">
        <div class="wrap visit-grid">
          <div class="map-placeholder" id="map" role="img" aria-label="Map placeholder showing the restaurant location in the Old Mill District">
            <div class="map-roads" aria-hidden="true"></div>
            <div class="map-river" aria-hidden="true"></div>
            <div class="map-pin" aria-hidden="true"><span></span></div>
            <div class="map-label" aria-hidden="true">Ember &amp; Oak</div>
          </div>
          <div class="visit-copy">
            <p class="eyebrow">Find us</p>
            <h2 id="visit-title">Two blocks from the river</h2>
            <address>
              412 Larkspur Street<br />
              Old Mill District, Portland, OR 97209
            </address>
            <p>Street parking after 6pm. The Larkspur streetcar stop is at our corner. Bike racks under the awning.</p>
            <a class="btn btn-ghost" href="https://www.google.com/maps/search/?api=1&query=412+Larkspur+Street+Portland" target="_blank" rel="noopener">Get directions</a>
          </div>
        </div>
      </section>
    </main>

    <footer class="site-footer">
      <div class="wrap footer-grid">
        <div>
          <p class="brand-name footer-brand">Ember <em>&amp;</em> Oak</p>
          <p class="muted">A wood-fired neighborhood kitchen. Est. 2019.</p>
          <p class="muted"><a href="mailto:hello@emberandoak.example">hello@emberandoak.example</a><br /><a href="tel:+15035550148">(503) 555-0148</a></p>
        </div>
        <div>
          <h3 class="footer-title">Hours</h3>
          <table class="hours-table" id="hours-table">
            <caption class="sr-only">Opening hours by day</caption>
            <tbody></tbody>
          </table>
        </div>
        <div>
          <h3 class="footer-title">Visit</h3>
          <p class="muted">412 Larkspur Street<br />Old Mill District<br />Portland, OR 97209</p>
          <p class="muted">Kitchen closes 30 minutes before the room.</p>
        </div>
      </div>
      <div class="wrap footer-bottom">
        <p class="muted">Sample content for a template — menu, hours and contact details are placeholders.</p>
        <a class="muted" href="#top">Back to top ↑</a>
      </div>
    </footer>

    <div class="lightbox" id="lightbox" hidden>
      <div class="lightbox-backdrop" data-close></div>
      <div class="lightbox-dialog" role="dialog" aria-modal="true" aria-labelledby="lightbox-caption">
        <button class="lightbox-close" type="button" data-close aria-label="Close gallery">×</button>
        <div class="lightbox-photo" id="lightbox-photo"></div>
        <div class="lightbox-bar">
          <button class="lightbox-nav" type="button" id="lightbox-prev" aria-label="Previous photo">←</button>
          <p id="lightbox-caption"></p>
          <button class="lightbox-nav" type="button" id="lightbox-next" aria-label="Next photo">→</button>
        </div>
      </div>
    </div>

    <script type="module" src="/src/main.js"></script>
  </body>
</html>
src/main.js
import "./style.css";
import { business, hours, dietTags, menu, gallery } from "./data.js";

const STORAGE_KEY = "ember-oak.reservation";
const $ = (sel, root) => (root || document).querySelector(sel);

function el(tag, attrs, children) {
  const node = document.createElement(tag);
  if (attrs) {
    for (const key of Object.keys(attrs)) {
      const val = attrs[key];
      if (val == null || val === false) continue;
      if (key === "class") node.className = val;
      else if (key === "text") node.textContent = val;
      else if (key.startsWith("on")) node.addEventListener(key.slice(2), val);
      else node.setAttribute(key, val === true ? "" : val);
    }
  }
  (children || []).forEach((c) => node.append(c));
  return node;
}

/* ---------- Hours + open status ---------- */

function toMinutes(hhmm) {
  const parts = hhmm.split(":");
  return Number(parts[0]) * 60 + Number(parts[1]);
}

function fmtTime(hhmm) {
  const m = toMinutes(hhmm);
  const h24 = Math.floor(m / 60);
  const mins = m % 60;
  const h12 = h24 % 12 === 0 ? 12 : h24 % 12;
  const suffix = h24 < 12 ? "am" : "pm";
  return h12 + (mins ? ":" + String(mins).padStart(2, "0") : "") + suffix;
}

function hoursLabel(day) {
  return day.open ? fmtTime(day.open) + " – " + fmtTime(day.close) : "Closed";
}

function renderHours() {
  const now = new Date();
  const todayIdx = now.getDay();
  const today = hours[todayIdx];
  $("#today-hours").textContent = hoursLabel(today);

  const dot = $("#status-dot");
  const text = $("#status-text");
  const nowMin = now.getHours() * 60 + now.getMinutes();
  if (today.open && nowMin >= toMinutes(today.open) && nowMin < toMinutes(today.close)) {
    dot.className = "status-dot is-open";
    text.textContent = "Open now · closes " + fmtTime(today.close);
  } else if (today.open && nowMin < toMinutes(today.open)) {
    dot.className = "status-dot is-closed";
    text.textContent = "Opens today at " + fmtTime(today.open);
  } else {
    let next = (todayIdx + 1) % 7;
    while (!hours[next].open) next = (next + 1) % 7;
    dot.className = "status-dot is-closed";
    text.textContent = "Closed · opens " + hours[next].day + " " + fmtTime(hours[next].open);
  }

  // Footer table starts on Monday for readability.
  const tbody = $("#hours-table tbody");
  tbody.replaceChildren();
  for (let i = 1; i <= 7; i++) {
    const idx = i % 7;
    const row = el("tr", { class: idx === todayIdx ? "is-today" : null }, [
      el("td", { text: hours[idx].day }),
      el("td", { text: hoursLabel(hours[idx]) }),
    ]);
    tbody.append(row);
  }
}

/* ---------- Menu tabs + dietary filter ---------- */

const activeDiets = new Set();

function renderMenu() {
  const tabs = $("#menu-tabs");
  const panels = $("#menu-panels");
  tabs.replaceChildren();
  panels.replaceChildren();

  menu.forEach((cat, i) => {
    const tab = el("button", {
      class: "tab",
      role: "tab",
      id: "tab-" + cat.id,
      "aria-selected": i === 0 ? "true" : "false",
      "aria-controls": "panel-" + cat.id,
      tabindex: i === 0 ? "0" : "-1",
      text: cat.label,
      onclick: () => selectTab(cat.id),
      onkeydown: onTabKey,
    });
    tabs.append(tab);

    const panel = el("div", {
      class: "menu-panel",
      role: "tabpanel",
      id: "panel-" + cat.id,
      "aria-labelledby": "tab-" + cat.id,
      tabindex: "0",
      hidden: i !== 0,
    });
    panels.append(panel);
  });

  const filter = $("#diet-filter");
  Object.keys(dietTags).forEach((key) => {
    const input = el("input", { type: "checkbox", value: key, onchange: onDietChange });
    filter.append(el("label", { class: "chip" }, [input, el("span", { text: dietTags[key].label })]));
  });

  $("#diet-legend").textContent =
    "Dietary key: " +
    Object.keys(dietTags).map((k) => dietTags[k].short + " " + dietTags[k].label).join(" · ") +
    ". Prices are sample data.";

  fillPanels();
}

function fillPanels() {
  menu.forEach((cat) => {
    const panel = $("#panel-" + cat.id);
    panel.replaceChildren();
    const list = el("ul", { class: "menu-list" });
    const items = cat.items.filter((item) => [...activeDiets].every((d) => item.tags.includes(d)));
    if (!items.length) {
      list.append(el("li", { class: "menu-empty", text: "Nothing in " + cat.label.toLowerCase() + " matches every selected filter yet — ask us, the kitchen adapts most plates." }));
    }
    items.forEach((item) => {
      const tags = el("div", { class: "tags" }, item.tags.map((t) => el("span", { class: "tag", title: dietTags[t].label, "aria-label": dietTags[t].label, text: dietTags[t].short })));
      list.append(el("li", { class: "menu-item" }, [
        el("span", { class: "name", text: item.name }),
        el("span", { class: "price", text: "$" + item.price }),
        el("span", { class: "desc", text: item.desc }),
        tags,
      ]));
    });
    panel.append(list);
  });
}

function selectTab(id) {
  menu.forEach((cat) => {
    const on = cat.id === id;
    const tab = $("#tab-" + cat.id);
    tab.setAttribute("aria-selected", on ? "true" : "false");
    tab.tabIndex = on ? 0 : -1;
    $("#panel-" + cat.id).hidden = !on;
    if (on) tab.focus();
  });
}

function onTabKey(e) {
  const ids = menu.map((c) => c.id);
  const current = ids.indexOf(e.currentTarget.id.replace("tab-", ""));
  let next = null;
  if (e.key === "ArrowRight") next = (current + 1) % ids.length;
  if (e.key === "ArrowLeft") next = (current - 1 + ids.length) % ids.length;
  if (e.key === "Home") next = 0;
  if (e.key === "End") next = ids.length - 1;
  if (next !== null) {
    e.preventDefault();
    selectTab(ids[next]);
  }
}

function onDietChange(e) {
  if (e.target.checked) activeDiets.add(e.target.value);
  else activeDiets.delete(e.target.value);
  fillPanels();
}

/* ---------- Gallery + lightbox ---------- */

let lightboxIndex = 0;
let lastFocus = null;

function renderGallery() {
  const grid = $("#gallery-grid");
  grid.replaceChildren();
  gallery.forEach((photo, i) => {
    const btn = el("button", { class: "gallery-btn", type: "button", "aria-label": "Open photo: " + photo.caption, onclick: () => openLightbox(i) }, [
      el("div", { class: "photo photo-" + photo.hue, "aria-hidden": "true" }),
      el("div", { class: "cap", text: photo.caption }),
    ]);
    grid.append(el("li", null, [btn]));
  });

  const box = $("#lightbox");
  box.querySelectorAll("[data-close]").forEach((n) => n.addEventListener("click", closeLightbox));
  $("#lightbox-prev").addEventListener("click", () => showPhoto(lightboxIndex - 1));
  $("#lightbox-next").addEventListener("click", () => showPhoto(lightboxIndex + 1));
  box.addEventListener("keydown", (e) => {
    if (e.key === "Escape") closeLightbox();
    if (e.key === "ArrowLeft") showPhoto(lightboxIndex - 1);
    if (e.key === "ArrowRight") showPhoto(lightboxIndex + 1);
    if (e.key === "Tab") trapFocus(e, box);
  });
}

function trapFocus(e, root) {
  const focusables = root.querySelectorAll("button");
  const first = focusables[0];
  const last = focusables[focusables.length - 1];
  if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
  else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
}

function showPhoto(i) {
  lightboxIndex = (i + gallery.length) % gallery.length;
  const photo = gallery[lightboxIndex];
  $("#lightbox-photo").className = "lightbox-photo photo-" + photo.hue;
  $("#lightbox-caption").textContent = (lightboxIndex + 1) + " / " + gallery.length + " — " + photo.caption;
}

function openLightbox(i) {
  lastFocus = document.activeElement;
  showPhoto(i);
  $("#lightbox").hidden = false;
  document.body.style.overflow = "hidden";
  $("#lightbox-next").focus();
}

function closeLightbox() {
  $("#lightbox").hidden = true;
  document.body.style.overflow = "";
  if (lastFocus) lastFocus.focus();
}

/* ---------- Reservation form ---------- */

function isoDate(d) {
  return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
}

function parseDate(str) {
  const p = str.split("-").map(Number);
  return new Date(p[0], p[1] - 1, p[2]);
}

function slotsFor(dateStr) {
  const day = hours[parseDate(dateStr).getDay()];
  if (!day.open) return [];
  const slots = [];
  const last = toMinutes(day.close) - business.lastSeatingOffset;
  const isToday = dateStr === isoDate(new Date());
  const now = new Date();
  const nowMin = now.getHours() * 60 + now.getMinutes();
  for (let m = toMinutes(day.open); m <= last; m += business.slotMinutes) {
    if (isToday && m <= nowMin + 30) continue;
    const hh = String(Math.floor(m / 60)).padStart(2, "0");
    const mm = String(m % 60).padStart(2, "0");
    slots.push(hh + ":" + mm);
  }
  return slots;
}

function populateTimes() {
  const dateInput = $("#r-date");
  const timeSelect = $("#r-time");
  timeSelect.replaceChildren();
  const value = dateInput.value;
  if (!value) {
    timeSelect.append(el("option", { value: "", text: "Choose a date first" }));
    return;
  }
  const slots = slotsFor(value);
  if (!slots.length) {
    const day = hours[parseDate(value).getDay()];
    timeSelect.append(el("option", { value: "", text: day.open ? "No seatings left that day" : "Closed on " + day.day + "s" }));
    return;
  }
  timeSelect.append(el("option", { value: "", text: "Select a time" }));
  slots.forEach((s) => timeSelect.append(el("option", { value: s, text: fmtTime(s) })));
}

function setError(field, message) {
  const input = $("#r-" + field);
  const err = $("#err-" + field);
  err.textContent = message || "";
  input.setAttribute("aria-invalid", message ? "true" : "false");
  return !message;
}

function validate(data) {
  let ok = true;
  ok = setError("name", data.name.trim().length >= 2 ? "" : "Please tell us who the table is for.") && ok;
  ok = setError("email", /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email.trim()) ? "" : "Enter a valid email so we can confirm.") && ok;
  const digits = data.phone.replace(/\D/g, "");
  ok = setError("phone", !data.phone.trim() || digits.length >= 10 ? "" : "Phone should have at least 10 digits.") && ok;

  let dateMsg = "";
  if (!data.date) dateMsg = "Pick a date.";
  else if (data.date < isoDate(new Date())) dateMsg = "That date has already passed.";
  else if (!hours[parseDate(data.date).getDay()].open) dateMsg = "We are closed that day. Try Tuesday through Sunday.";
  ok = setError("date", dateMsg) && ok;

  ok = setError("time", data.time ? "" : "Choose a seating time.") && ok;
  let partyMsg = "";
  if (!data.party) partyMsg = "How many guests?";
  else if (Number(data.party) >= 9) partyMsg = "For nine or more, call " + business.phone + " and we will set the long table.";
  ok = setError("party", partyMsg) && ok;
  return ok;
}

function makeRef() {
  const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
  let out = "EO-";
  for (let i = 0; i < 6; i++) out += chars[Math.floor(Math.random() * chars.length)];
  return out;
}

function longDate(dateStr) {
  return parseDate(dateStr).toLocaleDateString(undefined, { weekday: "long", month: "long", day: "numeric" });
}

function showConfirmation(res) {
  $("#reserve-form").hidden = true;
  const card = $("#confirmation");
  card.hidden = false;
  $("#confirm-lead").textContent = "Thanks, " + res.name.split(" ")[0] + ". We will confirm by email at " + res.email + ".";
  const details = $("#confirm-details");
  details.replaceChildren();
  const rows = [
    ["When", longDate(res.date) + " · " + fmtTime(res.time)],
    ["Guests", res.party + (Number(res.party) === 1 ? " guest" : " guests")],
    ["Where", business.address],
    ["Notes", res.notes.trim() || "None"],
  ];
  rows.forEach((r) => details.append(el("div", null, [el("dt", { text: r[0] }), el("dd", { text: r[1] })])));
  $("#confirm-ref").textContent = res.ref;
  card.focus({ preventScroll: true });
}

function loadSaved() {
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (!raw) return null;
    const res = JSON.parse(raw);
    if (res.date < isoDate(new Date())) { localStorage.removeItem(STORAGE_KEY); return null; }
    return res;
  } catch (err) {
    return null;
  }
}

function saveReservation(res) {
  try { localStorage.setItem(STORAGE_KEY, JSON.stringify(res)); } catch (err) { /* storage unavailable */ }
}

function resetForm() {
  const form = $("#reserve-form");
  form.reset();
  form.querySelectorAll("[aria-invalid]").forEach((n) => n.removeAttribute("aria-invalid"));
  form.querySelectorAll(".error").forEach((n) => (n.textContent = ""));
  $("#r-date").value = "";
  populateTimes();
  form.hidden = false;
  $("#confirmation").hidden = true;
  $("#r-name").focus();
}

function initReservation() {
  const form = $("#reserve-form");
  const dateInput = $("#r-date");
  const today = new Date();
  const max = new Date(today.getTime() + 60 * 24 * 60 * 60 * 1000);
  dateInput.min = isoDate(today);
  dateInput.max = isoDate(max);
  dateInput.addEventListener("change", () => { populateTimes(); setError("date", ""); setError("time", ""); });
  populateTimes();

  ["name", "email", "phone", "time", "party"].forEach((f) => {
    $("#r-" + f).addEventListener("input", () => setError(f, ""));
  });

  form.addEventListener("submit", (e) => {
    e.preventDefault();
    const fd = new FormData(form);
    const data = {
      name: String(fd.get("name") || ""),
      email: String(fd.get("email") || ""),
      phone: String(fd.get("phone") || ""),
      date: String(fd.get("date") || ""),
      time: String(fd.get("time") || ""),
      party: String(fd.get("party") || ""),
      notes: String(fd.get("notes") || ""),
    };
    const formError = $("#form-error");
    if (!validate(data)) {
      formError.textContent = "A few fields need attention above.";
      const firstBad = form.querySelector('[aria-invalid="true"]');
      if (firstBad) firstBad.focus();
      return;
    }
    formError.textContent = "";
    const res = Object.assign({ ref: makeRef(), createdAt: new Date().toISOString() }, data);
    saveReservation(res);
    showConfirmation(res);
  });

  $("#confirm-new").addEventListener("click", resetForm);
  $("#confirm-cancel").addEventListener("click", () => {
    if (!window.confirm("Cancel this booking? You can always make a new one.")) return;
    try { localStorage.removeItem(STORAGE_KEY); } catch (err) { /* ignore */ }
    resetForm();
  });

  $("#confirmation").tabIndex = -1;
  const saved = loadSaved();
  if (saved) showConfirmation(saved);
}

/* ---------- Boot ---------- */

renderHours();
renderMenu();
renderGallery();
initReservation();
setInterval(renderHours, 60 * 1000);
src/style.css
:root {
  --bg: #151312;
  --bg-2: #1d1a18;
  --bg-3: #262220;
  --line: rgba(246, 239, 228, 0.12);
  --line-strong: rgba(246, 239, 228, 0.24);
  --ink: #f6efe4;
  --ink-2: rgba(246, 239, 228, 0.72);
  --ink-3: rgba(246, 239, 228, 0.6);
  --accent: #e8a53a;
  --accent-2: #f2bd66;
  --accent-ink: #1a1408;
  --danger: #e0644f;
  --ok: #7fc38a;
  --font-display: "Fraunces", "Iowan Old Style", "Palatino Linotype", Georgia, serif;
  --font-body: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --radius: 12px;
  --radius-lg: 20px;
  --shadow: 0 24px 48px -24px rgba(0, 0, 0, 0.6);
  --wrap: 1120px;
  color-scheme: dark;
}

*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { transition: none !important; animation: none !important; }
}
body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}
img { max-width: 100%; display: block; }
a { color: var(--accent-2); text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 3px; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; }
h1, h2, h3 { font-family: var(--font-display); font-weight: 500; line-height: 1.1; margin: 0; letter-spacing: -0.01em; }
h1 { font-size: clamp(2.5rem, 6vw, 4.5rem); }
h2 { font-size: clamp(1.75rem, 3.5vw, 2.5rem); }
h3 { font-size: 1.375rem; }
p { margin: 0; }
.muted { color: var(--ink-2); }
.eyebrow {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 16px;
}
.wrap { width: min(var(--wrap), calc(100% - 40px)); margin-inline: auto; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.skip-link {
  position: absolute; left: 16px; top: -48px;
  padding: 8px 16px; background: var(--accent); color: var(--accent-ink);
  border-radius: 8px; z-index: 100; font-weight: 600;
}
.skip-link:focus { top: 16px; }

/* Buttons */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: 48px; padding: 0 24px; border-radius: 999px;
  font-weight: 600; font-size: 0.9375rem; border: 1px solid transparent;
  transition: transform 0.15s ease, background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
  text-decoration: none;
}
.btn:hover { text-decoration: none; transform: translateY(-1px); }
.btn:active { transform: translateY(0); }
.btn-sm { min-height: 40px; padding: 0 16px; font-size: 0.875rem; }
.btn-primary { background: var(--accent); color: var(--accent-ink); box-shadow: 0 8px 24px -12px rgba(232, 165, 58, 0.8); }
.btn-primary:hover { background: var(--accent-2); }
.btn-ghost { background: transparent; border-color: var(--line-strong); color: var(--ink); }
.btn-ghost:hover { border-color: var(--ink); background: rgba(246, 239, 228, 0.06); }
.btn-danger { background: transparent; border-color: rgba(224, 100, 79, 0.5); color: #f0907f; }
.btn-danger:hover { background: rgba(224, 100, 79, 0.12); border-color: var(--danger); }

/* Header */
.site-header {
  position: sticky; top: 0; z-index: 50;
  background: rgba(21, 19, 18, 0.82);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line);
}
.header-row { display: flex; align-items: center; gap: 24px; min-height: 72px; }
.brand { display: inline-flex; align-items: center; gap: 12px; color: var(--ink); }
.brand:hover { text-decoration: none; }
.brand-mark {
  width: 32px; height: 32px; border-radius: 50%;
  background: radial-gradient(circle at 40% 35%, #f8d38a 0%, var(--accent) 35%, #8a4a12 75%, #3b1d08 100%);
  box-shadow: 0 0 0 4px rgba(232, 165, 58, 0.15);
}
.brand-name { font-family: var(--font-display); font-size: 1.25rem; font-weight: 600; letter-spacing: -0.01em; }
.brand-name em { font-style: italic; color: var(--accent); font-weight: 500; }
.site-nav { display: flex; gap: 8px; margin-left: auto; }
.site-nav a { color: var(--ink-2); padding: 8px 12px; border-radius: 8px; font-weight: 500; font-size: 0.9375rem; }
.site-nav a:hover { color: var(--ink); background: rgba(246, 239, 228, 0.06); text-decoration: none; }

/* Hero */
.hero { position: relative; padding: 80px 0 96px; overflow: hidden; isolation: isolate; }
/* The hero photo slot. The gradient stands in until a picture is placed; the badge says how. */
.gf-ph-badge {
  position: absolute; top: 20px; right: 20px; z-index: 1; pointer-events: none;
  font-size: 12px; line-height: 1.3; padding: 8px 12px; border-radius: 999px;
  border: 1px dashed rgba(246, 239, 228, 0.38); color: rgba(246, 239, 228, 0.8); background: rgba(21, 19, 18, 0.55);
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); max-width: min(60vw, 420px);
}
.hero-photo {
  position: absolute; inset: 0; z-index: -1;
  background:
    radial-gradient(ellipse 60% 50% at 78% 30%, rgba(232, 165, 58, 0.45), transparent 70%),
    radial-gradient(ellipse 40% 60% at 90% 80%, rgba(138, 74, 18, 0.6), transparent 70%),
    radial-gradient(ellipse 70% 40% at 20% 100%, rgba(59, 29, 8, 0.8), transparent 70%),
    linear-gradient(180deg, #1a1614 0%, var(--bg) 100%);
}
.hero-photo::before {
  content: ""; position: absolute; right: -8%; top: -30%; width: 58vw; max-width: 760px; aspect-ratio: 1; border-radius: 50%;
  background: radial-gradient(circle, transparent 54%, rgba(232, 165, 58, 0.22) 55%, rgba(232, 165, 58, 0.22) 56.5%, transparent 57.5%, transparent 66%, rgba(232, 165, 58, 0.08) 67%, transparent 68.5%);
  animation: drift 18s ease-in-out infinite alternate;
}
@keyframes drift { from { transform: translate3d(0, 0, 0) scale(1); } to { transform: translate3d(-3%, 4%, 0) scale(1.05); } }
.hero-photo::after {
  content: ""; position: absolute; inset: 0;
  background-image: radial-gradient(rgba(246, 239, 228, 0.06) 1px, transparent 1px);
  background-size: 24px 24px; opacity: 0.5;
  mask-image: linear-gradient(180deg, transparent, #000 40%, transparent);
  -webkit-mask-image: linear-gradient(180deg, transparent, #000 40%, transparent);
}
.hero-grid { display: grid; grid-template-columns: 1.4fr 1fr; gap: 48px; align-items: end; }
.hero .lede { font-size: 1.125rem; color: var(--ink-2); max-width: 46ch; margin-top: 24px; }
.hero-actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 32px; }
.hero-card {
  background: rgba(29, 26, 24, 0.85); border: 1px solid var(--line);
  border-radius: var(--radius-lg); padding: 24px; box-shadow: var(--shadow);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
}
.status-row { display: flex; align-items: center; gap: 10px; font-weight: 600; font-size: 0.9375rem; padding-bottom: 16px; border-bottom: 1px solid var(--line); }
.status-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--ink-3); box-shadow: 0 0 0 4px rgba(246, 239, 228, 0.08); }
.status-dot.is-open { background: var(--ok); box-shadow: 0 0 0 4px rgba(127, 195, 138, 0.2); }
.status-dot.is-closed { background: var(--danger); box-shadow: 0 0 0 4px rgba(224, 100, 79, 0.2); }
.hero-meta { margin: 0; display: grid; gap: 16px; padding-top: 16px; }
.hero-meta dt { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); font-weight: 600; }
.hero-meta dd { margin: 4px 0 0; color: var(--ink); }

/* Sections */
.section { padding: 96px 0; }
.section-alt { background: var(--bg-2); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.section-head { max-width: 640px; margin-bottom: 40px; }

/* Menu */
.menu-controls { display: flex; flex-wrap: wrap; gap: 16px 32px; align-items: flex-end; justify-content: space-between; margin-bottom: 32px; }
.tabs { display: flex; flex-wrap: wrap; gap: 8px; }
.tab {
  min-height: 44px; padding: 0 20px; border-radius: 999px;
  background: transparent; border: 1px solid var(--line-strong); color: var(--ink-2); font-weight: 500;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.tab:hover { color: var(--ink); border-color: var(--ink); }
.tab[aria-selected="true"] { background: var(--ink); color: var(--bg); border-color: var(--ink); }
.diet-filter { border: 0; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.diet-filter legend { float: left; font-size: 0.8125rem; color: var(--ink-3); margin-right: 8px; padding: 0; }
.chip { position: relative; display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }
.chip input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.chip span {
  display: inline-flex; align-items: center; min-height: 32px; padding: 0 12px; border-radius: 999px;
  border: 1px solid var(--line-strong); font-size: 0.8125rem; font-weight: 600; color: var(--ink-2);
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.chip:hover span { border-color: var(--ink); color: var(--ink); }
.chip input:checked + span { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
.chip input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 3px; }
.menu-panel[hidden] { display: none; }
.menu-list { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(2, 1fr); gap: 0 48px; }
.menu-item { display: grid; grid-template-columns: 1fr auto; gap: 4px 24px; padding: 20px 0; border-bottom: 1px solid var(--line); transition: padding-left 0.15s ease; }
.menu-item:hover { padding-left: 8px; }
.menu-item .name { font-family: var(--font-display); font-size: 1.25rem; font-weight: 500; }
.menu-item .price { font-weight: 600; color: var(--accent-2); font-variant-numeric: tabular-nums; }
.menu-item .desc { grid-column: 1; color: var(--ink-2); font-size: 0.9375rem; }
.menu-item .tags { grid-column: 1 / -1; display: flex; gap: 6px; margin-top: 4px; }
.tag {
  font-size: 0.6875rem; font-weight: 700; letter-spacing: 0.06em; padding: 2px 8px; border-radius: 6px;
  background: rgba(246, 239, 228, 0.08); color: var(--ink-2); border: 1px solid var(--line);
}
.menu-empty { padding: 40px 0; color: var(--ink-2); grid-column: 1 / -1; }
.legend { margin-top: 24px; font-size: 0.8125rem; color: var(--ink-3); }

/* Gallery */
.gallery-grid { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.gallery-grid li:first-child { grid-column: span 2; grid-row: span 2; }
.gallery-btn {
  display: block; width: 100%; padding: 0; border: 0; border-radius: var(--radius); overflow: hidden;
  background: none; text-align: left; position: relative; aspect-ratio: 4 / 3;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.gallery-grid li:first-child .gallery-btn { aspect-ratio: auto; height: 100%; min-height: 280px; }
.gallery-btn:hover { transform: translateY(-2px); box-shadow: var(--shadow); }
.gallery-btn .photo, .lightbox-photo { position: absolute; inset: 0; transition: transform 0.4s ease; }
.gallery-btn:hover .photo { transform: scale(1.04); }
.gallery-btn .cap {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 40px 16px 16px;
  background: linear-gradient(180deg, transparent, rgba(21, 19, 18, 0.85));
  font-size: 0.875rem; font-weight: 500; color: var(--ink);
}
.photo-amber  { background: radial-gradient(circle at 30% 40%, #f6c667, #c9772a 40%, #4a2410 85%); }
.photo-umber  { background: linear-gradient(160deg, #6b4423, #2e1a0e 60%, #1a100a); }
.photo-wheat  { background: radial-gradient(circle at 70% 30%, #f1d9a6, #b98b4a 45%, #5b3d1c 90%); }
.photo-ember  { background: radial-gradient(circle at 50% 80%, #ff9a3c, #9a3b12 45%, #2a120a 85%); }
.photo-olive  { background: linear-gradient(140deg, #8a8f4f, #4c5330 55%, #22261a); }
.photo-rust   { background: radial-gradient(circle at 40% 60%, #d9683d, #8f3a1f 50%, #3a1a10 90%); }
.photo-amber::after, .photo-umber::after, .photo-wheat::after, .photo-ember::after, .photo-olive::after, .photo-rust::after {
  content: ""; position: absolute; inset: 0;
  background-image: radial-gradient(rgba(255, 255, 255, 0.08) 1px, transparent 1px);
  background-size: 6px 6px; mix-blend-mode: overlay;
}

/* Reservation */
.reserve-grid { display: grid; grid-template-columns: 1fr 1.4fr; gap: 48px; align-items: start; }
.reserve-copy p { color: var(--ink-2); margin-top: 16px; }
.reserve-notes { margin: 24px 0 0; padding: 0; list-style: none; display: grid; gap: 12px; color: var(--ink-2); }
.reserve-notes li { padding-left: 24px; position: relative; }
.reserve-notes li::before { content: ""; position: absolute; left: 0; top: 10px; width: 12px; height: 2px; background: var(--accent); }
.card { background: var(--bg-2); border: 1px solid var(--line); border-radius: var(--radius-lg); padding: 32px; box-shadow: var(--shadow); }
.field-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px 16px; }
.field-full { grid-column: 1 / -1; }
.field label { display: block; font-size: 0.875rem; font-weight: 600; margin-bottom: 8px; }
.field .optional { color: var(--ink-3); font-weight: 400; }
.field input, .field select, .field textarea {
  width: 100%; min-height: 48px; padding: 10px 14px;
  background: var(--bg-3); border: 1px solid var(--line-strong); border-radius: 10px;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.field select {
  appearance: none; -webkit-appearance: none; padding-right: 40px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none' stroke='%23f6efe4' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 14px center;
}
.field textarea { resize: vertical; min-height: 88px; }
.field input:hover, .field select:hover, .field textarea:hover { border-color: var(--ink-3); }
.field input:focus, .field select:focus, .field textarea:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(232, 165, 58, 0.25); }
.field input[aria-invalid="true"], .field select[aria-invalid="true"] { border-color: var(--danger); }
.field ::placeholder { color: var(--ink-3); }
.error { color: #f0907f; font-size: 0.8125rem; min-height: 0; margin-top: 6px; }
.error:empty { display: none; }
.form-error { margin-top: 16px; }
.form-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 16px; margin-top: 24px; }
.form-hint { font-size: 0.8125rem; color: var(--ink-3); }
.confirmation { grid-column: 2; text-align: left; animation: rise 0.35s ease; }
@keyframes rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.confirm-check { width: 48px; height: 48px; border-radius: 50%; background: var(--accent); color: var(--accent-ink); display: grid; place-items: center; font-weight: 700; font-size: 1.25rem; margin-bottom: 16px; }
.confirm-lead { color: var(--ink-2); margin-top: 8px; }
.confirm-details { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; margin: 24px 0; padding: 20px 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.confirm-details dt { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); font-weight: 600; }
.confirm-details dd { margin: 4px 0 0; font-weight: 500; }
.confirm-ref { color: var(--ink-2); }
.confirm-ref strong { color: var(--accent-2); font-family: ui-monospace, "SF Mono", Menlo, monospace; letter-spacing: 0.08em; }
.confirm-actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 24px; }

/* Map */
.visit-grid { display: grid; grid-template-columns: 1.2fr 1fr; gap: 48px; align-items: center; }
.map-placeholder {
  position: relative; aspect-ratio: 4 / 3; border-radius: var(--radius-lg); overflow: hidden;
  background: #221e1b; border: 1px solid var(--line); box-shadow: var(--shadow);
}
.map-roads {
  position: absolute; inset: 0;
  background-image:
    linear-gradient(rgba(246, 239, 228, 0.1) 2px, transparent 2px),
    linear-gradient(90deg, rgba(246, 239, 228, 0.1) 2px, transparent 2px),
    linear-gradient(rgba(246, 239, 228, 0.04) 1px, transparent 1px),
    linear-gradient(90deg, rgba(246, 239, 228, 0.04) 1px, transparent 1px);
  background-size: 120px 120px, 120px 120px, 30px 30px, 30px 30px;
  transform: rotate(-8deg) scale(1.3);
}
.map-river {
  position: absolute; left: -10%; right: -10%; top: 62%; height: 18%;
  background: linear-gradient(90deg, rgba(64, 98, 120, 0.7), rgba(64, 98, 120, 0.5));
  border-radius: 50% 40% 60% 50% / 100%; transform: rotate(-6deg);
}
.map-pin { position: absolute; left: 46%; top: 38%; width: 28px; height: 28px; }
.map-pin::before { content: ""; position: absolute; inset: 0; border-radius: 50% 50% 50% 0; transform: rotate(-45deg); background: var(--accent); box-shadow: 0 8px 16px -6px rgba(0, 0, 0, 0.8); }
.map-pin span { position: absolute; left: 8px; top: 8px; width: 12px; height: 12px; border-radius: 50%; background: var(--bg); }
.map-pin::after { content: ""; position: absolute; left: -14px; top: -14px; width: 56px; height: 56px; border-radius: 50%; border: 2px solid rgba(232, 165, 58, 0.5); animation: pulse 2.4s ease-out infinite; }
@keyframes pulse { 0% { transform: scale(0.5); opacity: 1; } 100% { transform: scale(1.6); opacity: 0; } }
.map-label { position: absolute; left: 46%; top: 30%; transform: translateX(-38%); background: var(--ink); color: var(--bg); font-size: 0.75rem; font-weight: 700; padding: 4px 10px; border-radius: 6px; white-space: nowrap; }
.visit-copy address { font-style: normal; font-family: var(--font-display); font-size: 1.25rem; margin: 16px 0; }
.visit-copy p { color: var(--ink-2); margin-bottom: 24px; }

/* Footer */
.site-footer { border-top: 1px solid var(--line); padding: 64px 0 32px; background: var(--bg); }
.footer-grid { display: grid; grid-template-columns: 1.2fr 1fr 1fr; gap: 40px; }
.footer-brand { font-size: 1.5rem; margin-bottom: 8px; }
.footer-title { font-family: var(--font-body); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.14em; color: var(--ink-3); margin-bottom: 16px; }
.hours-table { border-collapse: collapse; width: 100%; font-size: 0.9375rem; }
.hours-table td { padding: 6px 0; border-bottom: 1px solid var(--line); color: var(--ink-2); }
.hours-table td:last-child { text-align: right; font-variant-numeric: tabular-nums; }
.hours-table tr.is-today td { color: var(--ink); font-weight: 600; }
.hours-table tr.is-today td:first-child::after { content: " · today"; color: var(--accent); font-weight: 500; font-size: 0.8125rem; }
.footer-bottom { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 16px; margin-top: 48px; padding-top: 24px; border-top: 1px solid var(--line); font-size: 0.8125rem; }

/* Lightbox */
.lightbox { position: fixed; inset: 0; z-index: 90; display: grid; place-items: center; padding: 24px; }
.lightbox[hidden] { display: none; }
.lightbox-backdrop { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.8); }
.lightbox-dialog { position: relative; width: min(920px, 100%); background: var(--bg-2); border: 1px solid var(--line); border-radius: var(--radius-lg); overflow: hidden; box-shadow: var(--shadow); }
.lightbox-photo { position: relative; aspect-ratio: 16 / 9; }
.lightbox-bar { display: flex; align-items: center; gap: 16px; padding: 16px 20px; }
.lightbox-bar p { flex: 1; text-align: center; font-size: 0.9375rem; }
.lightbox-nav, .lightbox-close {
  width: 40px; height: 40px; border-radius: 50%; border: 1px solid var(--line-strong);
  background: rgba(21, 19, 18, 0.6); color: var(--ink); font-size: 1.125rem; display: grid; place-items: center;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.lightbox-nav:hover, .lightbox-close:hover { background: var(--ink); color: var(--bg); border-color: var(--ink); }
.lightbox-close { position: absolute; top: 12px; right: 12px; z-index: 2; }

/* Responsive */
@media (max-width: 900px) {
  .hero-grid, .reserve-grid, .visit-grid { grid-template-columns: 1fr; }
  .hero { padding: 56px 0 64px; }
  .confirmation { grid-column: auto; }
  .footer-grid { grid-template-columns: 1fr 1fr; }
  .gallery-grid { grid-template-columns: repeat(2, 1fr); }
  .gallery-grid li:first-child { grid-column: span 2; grid-row: auto; }
  .gallery-grid li:first-child .gallery-btn { aspect-ratio: 16 / 9; min-height: 0; }
}
@media (max-width: 640px) {
  .header-row { flex-wrap: wrap; min-height: 56px; padding-top: 8px; gap: 12px; }
  .site-nav {
    order: 3; flex-basis: 100%; margin: -4px -8px 0; padding: 0 8px 8px; gap: 0;
    overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch;
  }
  .site-nav::-webkit-scrollbar { display: none; }
  .site-nav a { flex: 0 0 auto; padding: 6px 10px; font-size: 0.875rem; }
  .header-row .btn { margin-left: auto; }
  .section { padding: 64px 0; }
  .menu-list { grid-template-columns: 1fr; }
  .field-grid { grid-template-columns: 1fr; }
  .card { padding: 24px 20px; }
  .footer-grid { grid-template-columns: 1fr; }
  .confirm-details { grid-template-columns: 1fr; }
  .wrap { width: calc(100% - 32px); }
}
@media (max-width: 400px) {
  .gallery-grid { grid-template-columns: 1fr; }
  .gallery-grid li:first-child { grid-column: auto; }
  .hero-actions .btn { width: 100%; }
}
src/data.js
// Sample data for the template. Replace with the real business details.

export const business = {
  name: "Ember & Oak",
  phone: "(503) 555-0148",
  address: "412 Larkspur Street, Portland, OR 97209",
  // Minutes booked ahead of close that we stop taking reservations.
  lastSeatingOffset: 60,
  slotMinutes: 30,
};

// Hours keyed by JS getDay(): 0 = Sunday … 6 = Saturday. null = closed.
// Times are 24h "HH:MM".
export const hours = [
  { day: "Sunday",    open: "11:00", close: "21:00" },
  { day: "Monday",    open: null,    close: null },
  { day: "Tuesday",   open: "17:00", close: "22:00" },
  { day: "Wednesday", open: "17:00", close: "22:00" },
  { day: "Thursday",  open: "17:00", close: "22:00" },
  { day: "Friday",    open: "12:00", close: "23:00" },
  { day: "Saturday",  open: "12:00", close: "23:00" },
];

export const dietTags = {
  v:  { label: "Vegetarian", short: "V" },
  vg: { label: "Vegan",      short: "VG" },
  gf: { label: "Gluten-free", short: "GF" },
  df: { label: "Dairy-free", short: "DF" },
  sp: { label: "Spicy",      short: "🌶" },
};

export const menu = [
  {
    id: "starters",
    label: "Starters",
    items: [
      { name: "Oven sourdough", desc: "House loaf, cultured butter, flaky salt.", price: 6, tags: ["v"] },
      { name: "Charred leeks", desc: "Romesco, toasted hazelnuts, sherry vinegar.", price: 12, tags: ["vg", "gf", "df"] },
      { name: "Burrata & stone fruit", desc: "Grilled peaches, basil oil, aged balsamic.", price: 15, tags: ["v", "gf"] },
      { name: "Nduja flatbread", desc: "Blistered dough, whipped ricotta, hot honey.", price: 13, tags: ["sp"] },
      { name: "Smoked trout rillette", desc: "Dill, pickled shallot, rye crisps.", price: 14, tags: ["df"] },
    ],
  },
  {
    id: "mains",
    label: "Mains",
    items: [
      { name: "Half chicken", desc: "Brined overnight, salsa verde, oven potatoes.", price: 28, tags: ["gf", "df"] },
      { name: "Whole roasted cauliflower", desc: "Tahini, pomegranate, crispy chickpeas.", price: 22, tags: ["vg", "gf", "df"] },
      { name: "Dry-aged ribeye, 14 oz", desc: "Bone marrow butter, watercress. For sharing.", price: 58, tags: ["gf"] },
      { name: "Ember-roasted salmon", desc: "Brown butter, charred lemon, fennel slaw.", price: 32, tags: ["gf"] },
      { name: "Mushroom & farro", desc: "Maitake, black garlic, pecorino, soft egg.", price: 24, tags: ["v"] },
      { name: "Harissa lamb shoulder", desc: "Slow-roasted, mint yogurt, flatbread. Serves two.", price: 64, tags: ["sp"] },
    ],
  },
  {
    id: "desserts",
    label: "Desserts",
    items: [
      { name: "Burnt Basque cheesecake", desc: "Blackberry, olive oil.", price: 11, tags: ["v", "gf"] },
      { name: "Wood-oven apple tart", desc: "Vanilla cream, brown sugar crumb.", price: 10, tags: ["v"] },
      { name: "Dark chocolate pot", desc: "Sea salt, hazelnut praline.", price: 9, tags: ["vg", "gf", "df"] },
      { name: "Cheese board", desc: "Three local cheeses, quince, walnut bread.", price: 16, tags: ["v"] },
    ],
  },
  {
    id: "drinks",
    label: "Drinks",
    items: [
      { name: "House red / white", desc: "Willamette Valley, by the glass.", price: 12, tags: ["vg", "gf"] },
      { name: "Smoked old fashioned", desc: "Rye, oak smoke, orange peel.", price: 15, tags: ["vg", "gf"] },
      { name: "Local pale ale", desc: "Rotating tap from the neighborhood.", price: 8, tags: ["vg"] },
      { name: "Fire-roasted lemonade", desc: "Charred lemons, thyme, soda. Zero proof.", price: 7, tags: ["vg", "gf", "df"] },
      { name: "Espresso", desc: "Single origin, roasted across the street.", price: 4, tags: ["vg", "gf", "df"] },
    ],
  },
];

export const gallery = [
  { id: "oven",    caption: "The oak-burning oven, lit at 3pm every day",  hue: "amber" },
  { id: "room",    caption: "The dining room just before service",         hue: "umber" },
  { id: "bread",   caption: "Sourdough, scored and ready",                 hue: "wheat" },
  { id: "bar",     caption: "Eight seats at the counter",                  hue: "ember" },
  { id: "garden",  caption: "The back patio in late summer",               hue: "olive" },
  { id: "plate",   caption: "Charred leeks with romesco",                  hue: "rust" },
];