Pages Easy
Location & Hours
Restaurant Find Us page — SVG map placeholder with pin, hours table with current-day highlight, transit + parking cards, contact form, and a print-friendly directions card.
Open in Lab
MCP
html css vanilla-js
Targets: JS HTML
Code
:root {
--cream: #f5f0e8;
--cream-2: #ece4d4;
--bone: #faf7f1;
--terracotta: #c1714a;
--terracotta-d: #a05a38;
--forest: #2d4a3e;
--forest-d: #1e3329;
--gold: #c9a84c;
--gold-light: #e6c97a;
--ink: #2c1a0e;
--ink-2: #4a3828;
--warm-gray: #7a6a58;
--success: #4f7a3a;
--danger: #b3432a;
--warning: #d99020;
--font-display: "Playfair Display", Georgia, serif;
--font-body: "Inter", system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
--r-sm: 6px;
--r-md: 10px;
--r-lg: 16px;
--shadow-1: 0 1px 2px rgba(44, 26, 14, 0.08), 0 2px 6px rgba(44, 26, 14, 0.06);
--shadow-2: 0 8px 24px rgba(44, 26, 14, 0.12);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-body);
background: var(--cream);
color: var(--ink);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
/* Nav */
.nav {
padding: 16px 28px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(44, 26, 14, 0.08);
background: var(--bone);
}
.brand {
font-family: var(--font-display);
font-size: 1.15rem;
font-weight: 800;
color: var(--ink);
text-decoration: none;
}
.back {
font-size: 0.84rem;
color: var(--ink-2);
text-decoration: none;
font-weight: 600;
}
.back:hover {
color: var(--terracotta-d);
}
/* Head */
.head {
text-align: center;
padding: 64px 28px 36px;
max-width: 760px;
margin: 0 auto;
}
.kicker {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--terracotta);
font-weight: 700;
margin-bottom: 8px;
}
.head h1 {
font-family: var(--font-display);
font-size: clamp(2rem, 5vw, 3.2rem);
font-weight: 700;
letter-spacing: -0.015em;
}
.sub {
margin-top: 10px;
font-size: 0.98rem;
color: var(--ink-2);
}
.status {
margin-top: 18px;
display: inline-flex;
align-items: center;
gap: 8px;
background: var(--bone);
padding: 8px 14px;
border-radius: 999px;
border: 1px solid rgba(44, 26, 14, 0.08);
font-size: 0.82rem;
font-weight: 600;
color: var(--ink-2);
}
.status .dot {
width: 8px;
height: 8px;
border-radius: 999px;
background: var(--warm-gray);
}
.status.is-open .dot {
background: var(--success);
box-shadow: 0 0 0 4px rgba(79, 122, 58, 0.18);
}
.status.is-closed .dot {
background: var(--danger);
}
/* Map row */
.map-row {
max-width: 1080px;
margin: 0 auto;
padding: 24px 28px;
display: grid;
grid-template-columns: 1.4fr 1fr;
gap: 18px;
align-items: stretch;
}
@media (max-width: 880px) {
.map-row {
grid-template-columns: 1fr;
}
}
.map {
border-radius: var(--r-lg);
overflow: hidden;
background: var(--cream-2);
border: 1px solid rgba(44, 26, 14, 0.08);
position: relative;
display: flex;
flex-direction: column;
}
.map-svg {
display: block;
width: 100%;
height: 100%;
min-height: 280px;
}
.map-cap {
position: absolute;
bottom: 10px;
left: 12px;
background: rgba(250, 247, 241, 0.92);
padding: 5px 10px;
border-radius: var(--r-sm);
font-size: 0.7rem;
color: var(--warm-gray);
backdrop-filter: blur(6px);
}
/* Address card */
.address-card {
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.08);
border-radius: var(--r-lg);
padding: 24px 26px;
display: flex;
flex-direction: column;
gap: 14px;
}
.address-card address {
font-style: normal;
font-size: 1rem;
color: var(--ink-2);
line-height: 1.7;
}
.address-card strong {
font-family: var(--font-display);
font-size: 1.3rem;
color: var(--ink);
}
.address-meta {
display: flex;
flex-direction: column;
gap: 6px;
padding-top: 10px;
border-top: 1px dashed rgba(44, 26, 14, 0.18);
}
.address-meta div {
display: flex;
justify-content: space-between;
font-size: 0.86rem;
}
.address-meta dt {
color: var(--warm-gray);
letter-spacing: 0.04em;
}
.address-meta dd {
font-family: var(--font-mono);
font-weight: 700;
color: var(--ink);
}
.card-actions {
display: flex;
gap: 8px;
margin-top: auto;
padding-top: 8px;
}
.ghost,
.primary {
flex: 1;
border-radius: 999px;
font-family: inherit;
font-size: 0.84rem;
font-weight: 700;
padding: 11px 14px;
cursor: pointer;
border: 1px solid transparent;
transition: background 0.15s, color 0.15s;
}
.ghost {
background: transparent;
border-color: rgba(44, 26, 14, 0.2);
color: var(--ink-2);
}
.ghost:hover {
background: var(--cream-2);
color: var(--ink);
}
.primary {
background: var(--forest);
color: var(--bone);
}
.primary:hover {
background: var(--forest-d);
}
/* Grid: hours + info */
.grid {
max-width: 1080px;
margin: 16px auto 0;
padding: 8px 28px 32px;
display: grid;
grid-template-columns: 1fr 1.4fr;
gap: 16px;
align-items: start;
}
@media (max-width: 880px) {
.grid {
grid-template-columns: 1fr;
}
}
.hours-card {
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.08);
border-radius: var(--r-md);
padding: 22px 24px 20px;
}
.hours-card h2 {
font-family: var(--font-display);
font-size: 1.3rem;
font-weight: 700;
margin-bottom: 12px;
}
.hours-table {
width: 100%;
border-collapse: collapse;
}
.hours-table tr {
border-bottom: 1px dashed rgba(44, 26, 14, 0.12);
}
.hours-table tr:last-child {
border-bottom: none;
}
.hours-table td {
padding: 7px 0;
font-size: 0.9rem;
}
.hours-table td:first-child {
color: var(--ink-2);
font-weight: 600;
}
.hours-table td:last-child {
color: var(--ink);
font-weight: 600;
font-variant-numeric: tabular-nums;
text-align: right;
}
.hours-table tr.is-today td {
color: var(--terracotta-d);
}
.hours-table tr.is-today td:first-child::after {
content: " · today";
font-size: 0.66rem;
letter-spacing: 0.14em;
text-transform: uppercase;
font-weight: 700;
color: var(--terracotta);
}
.hours-table tr.is-closed td:last-child {
color: var(--warm-gray);
font-weight: 500;
}
.hours-note {
margin-top: 14px;
font-size: 0.78rem;
color: var(--warm-gray);
font-style: italic;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
}
.info {
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.08);
border-radius: var(--r-md);
padding: 20px 18px 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.info-icon {
font-size: 1.6rem;
width: 36px;
height: 36px;
display: grid;
place-items: center;
border-radius: 999px;
background: var(--cream-2);
color: var(--forest);
}
.info h3 {
font-family: var(--font-display);
font-size: 1.05rem;
font-weight: 700;
}
.info ul {
list-style: none;
display: flex;
flex-direction: column;
gap: 4px;
font-size: 0.84rem;
color: var(--ink-2);
}
.info ul li strong {
color: var(--ink);
margin-right: 4px;
}
/* Contact */
.contact {
max-width: 760px;
margin: 0 auto;
padding: 56px 28px 80px;
}
.section-head {
text-align: center;
margin-bottom: 24px;
}
.section-head h2 {
font-family: var(--font-display);
font-size: clamp(1.6rem, 4vw, 2.2rem);
font-weight: 700;
letter-spacing: -0.015em;
}
.contact-sub {
margin-top: 6px;
font-size: 0.92rem;
color: var(--warm-gray);
}
.contact-form {
display: flex;
flex-direction: column;
gap: 12px;
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.08);
border-radius: var(--r-md);
padding: 22px 24px;
}
.contact-form .row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.contact-form label {
display: flex;
flex-direction: column;
gap: 6px;
font-size: 0.74rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--ink-2);
font-weight: 700;
}
.contact-form input,
.contact-form select,
.contact-form textarea {
border: 1px solid rgba(44, 26, 14, 0.12);
background: var(--cream);
border-radius: var(--r-md);
padding: 10px 12px;
font-family: inherit;
font-size: 0.92rem;
color: var(--ink);
outline: none;
transition: border-color 0.15s;
resize: vertical;
}
.contact-form input:focus,
.contact-form select:focus,
.contact-form textarea:focus {
border-color: var(--terracotta);
}
.contact-form .primary {
align-self: flex-start;
flex: 0 0 auto;
padding: 12px 22px;
}
.contact-ok {
color: var(--success);
font-weight: 700;
font-size: 0.88rem;
}
/* Footer */
.footer {
border-top: 1px solid rgba(44, 26, 14, 0.08);
background: var(--bone);
text-align: center;
padding: 28px;
}
.footer-brand {
font-family: var(--font-display);
font-size: 1.2rem;
font-weight: 800;
}
.footer-meta {
font-size: 0.82rem;
color: var(--warm-gray);
margin-top: 4px;
}
/* Print */
@media print {
body {
background: white;
}
.no-print {
display: none !important;
}
.map,
.grid,
.contact {
display: none !important;
}
.head {
padding: 16px 0 0;
text-align: left;
}
.map-row {
grid-template-columns: 1fr;
padding: 16px 0;
}
.address-card {
box-shadow: none;
border: 1px solid #999;
}
}// shape: [open] in minutes, [close] in minutes; null = closed
const HOURS = [
{ day: "Monday", windows: null },
{ day: "Tuesday", windows: [[19 * 60, 23 * 60]] },
{
day: "Wednesday",
windows: [[19 * 60, 23 * 60 + 30]],
},
{
day: "Thursday",
windows: [[19 * 60, 23 * 60 + 30]],
},
{
day: "Friday",
windows: [[19 * 60, 23 * 60 + 30]],
},
{
day: "Saturday",
windows: [
[13 * 60, 16 * 60],
[19 * 60, 23 * 60 + 30],
],
},
{
day: "Sunday",
windows: [[13 * 60, 16 * 60]],
},
];
const JS_DOW_TO_INDEX = [6, 0, 1, 2, 3, 4, 5]; // Sunday → 6 (last row), Mon → 0
const tbody = document.getElementById("hoursBody");
const status = document.getElementById("status");
const statusText = document.getElementById("statusText");
function fmt(mins) {
const h = Math.floor(mins / 60);
const m = mins % 60;
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
}
function describeWindows(windows) {
if (!windows) return "Closed";
return windows.map(([a, b]) => `${fmt(a)} – ${fmt(b)}`).join(" · ");
}
function buildHours() {
const todayIdx = JS_DOW_TO_INDEX[new Date().getDay()];
tbody.innerHTML = HOURS.map((row, idx) => {
const cls = [idx === todayIdx ? "is-today" : "", !row.windows ? "is-closed" : ""]
.filter(Boolean)
.join(" ");
return `<tr class="${cls}">
<td>${row.day}</td>
<td>${describeWindows(row.windows)}</td>
</tr>`;
}).join("");
}
function statusNow() {
const now = new Date();
const todayIdx = JS_DOW_TO_INDEX[now.getDay()];
const minutesNow = now.getHours() * 60 + now.getMinutes();
const today = HOURS[todayIdx];
if (today.windows) {
const open = today.windows.find(([a, b]) => minutesNow >= a && minutesNow < b);
if (open) {
status.classList.remove("is-closed");
status.classList.add("is-open");
statusText.textContent = `Open now · closes ${fmt(open[1])}`;
return;
}
const upcoming = today.windows.find(([a]) => minutesNow < a);
if (upcoming) {
status.classList.remove("is-open");
status.classList.add("is-closed");
statusText.textContent = `Closed · opens today ${fmt(upcoming[0])}`;
return;
}
}
// Find the next day that has hours
for (let i = 1; i <= 7; i++) {
const next = HOURS[(todayIdx + i) % 7];
if (next.windows) {
status.classList.remove("is-open");
status.classList.add("is-closed");
statusText.textContent = `Closed · opens ${next.day.slice(0, 3)} ${fmt(next.windows[0][0])}`;
return;
}
}
}
document.getElementById("copyAddr").addEventListener("click", async (e) => {
const btn = e.currentTarget;
const text = "Casa Olivar, 42 Calle del Olivar, 28012 Madrid, Spain";
try {
await navigator.clipboard.writeText(text);
btn.textContent = "Copied ✓";
} catch {
btn.textContent = "Copy failed";
}
setTimeout(() => (btn.textContent = "Copy address"), 1600);
});
document.getElementById("printDir").addEventListener("click", () => {
window.print();
});
const form = document.getElementById("contactForm");
const ok = document.getElementById("contactOk");
form.addEventListener("submit", (e) => {
e.preventDefault();
ok.hidden = false;
form.querySelector("button").disabled = true;
setTimeout(() => {
ok.hidden = true;
form.querySelector("button").disabled = false;
form.reset();
}, 3000);
});
buildHours();
statusNow();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700&display=swap"
/>
<link rel="stylesheet" href="style.css" />
<title>Visit · Casa Olivar</title>
</head>
<body>
<header class="nav no-print">
<a class="brand" href="#">Casa Olivar</a>
<a class="back" href="#">← Back to home</a>
</header>
<main>
<section class="head">
<p class="kicker">Visit</p>
<h1>42 Calle del Olivar.</h1>
<p class="sub">
A 4-minute walk from Metro Antón Martín · 12 minutes from Atocha
station.
</p>
<p class="status" id="status">
<span class="dot"></span>
<span id="statusText">—</span>
</p>
</section>
<!-- Map + address card -->
<section class="map-row">
<figure class="map" aria-label="Map with restaurant location">
<svg
viewBox="0 0 600 360"
preserveAspectRatio="xMidYMid slice"
class="map-svg"
>
<!-- Park / green -->
<rect width="600" height="360" fill="#ece4d4" />
<path
d="M0 240 L130 200 L240 250 L360 220 L480 270 L600 240 L600 360 L0 360 Z"
fill="#c9d6c4"
opacity="0.65"
/>
<!-- River -->
<path
d="M-20 110 Q140 80 260 130 T560 110 L620 130 L620 150 Q460 170 320 150 T-20 130 Z"
fill="#7fa6bd"
opacity="0.5"
/>
<!-- Streets -->
<g
stroke="#bfae93"
stroke-width="6"
stroke-linecap="round"
opacity="0.85"
>
<line x1="0" y1="60" x2="600" y2="80" />
<line x1="80" y1="0" x2="120" y2="360" />
<line x1="40" y1="200" x2="600" y2="220" />
<line x1="280" y1="0" x2="320" y2="360" />
<line x1="0" y1="300" x2="600" y2="320" />
<line x1="460" y1="0" x2="500" y2="360" />
</g>
<!-- Olivar street (highlighted) -->
<line
x1="120"
y1="60"
x2="600"
y2="180"
stroke="#c1714a"
stroke-width="4"
opacity="0.85"
/>
<text
x="330"
y="130"
fill="#a05a38"
font-family="Inter, sans-serif"
font-size="11"
font-weight="700"
transform="rotate(14 330 130)"
>
Calle del Olivar
</text>
<!-- Surrounding labels -->
<text x="60" y="80" fill="#7a6a58" font-size="10" font-family="Inter" font-weight="600">
Metro Antón Martín
</text>
<text x="430" y="60" fill="#7a6a58" font-size="10" font-family="Inter" font-weight="600">
Plaza Tirso
</text>
<text x="520" y="290" fill="#7a6a58" font-size="10" font-family="Inter" font-weight="600">
Atocha
</text>
<text x="40" y="270" fill="#5e7a4a" font-size="10" font-family="Inter" font-weight="600">
Parque del Retiro
</text>
<!-- Pin -->
<g transform="translate(320 158)">
<circle r="22" fill="#2d4a3e" opacity="0.18" />
<circle r="10" fill="#2d4a3e" />
<circle r="4" fill="#faf7f1" />
</g>
<g transform="translate(338 144)">
<rect width="106" height="34" rx="6" fill="#faf7f1" stroke="#c9a84c" />
<text x="10" y="14" fill="#c1714a" font-size="8" font-family="Inter" font-weight="700"
letter-spacing="1.5">
CASA OLIVAR
</text>
<text x="10" y="27" fill="#2c1a0e" font-size="9" font-family="Inter" font-weight="600">
42 Calle del Olivar
</text>
</g>
</svg>
<figcaption class="map-cap">Map is for illustration. Geo-accurate map in production.</figcaption>
</figure>
<aside class="address-card" id="addressCard">
<p class="kicker">Address</p>
<address>
<strong>Casa Olivar</strong><br />
42 Calle del Olivar<br />
28012 Madrid · Spain
</address>
<dl class="address-meta">
<div><dt>Phone</dt><dd>+34 910 555 042</dd></div>
<div><dt>Email</dt><dd>hola@casaolivar.es</dd></div>
<div><dt>Coords</dt><dd>40.412 N · 3.700 W</dd></div>
</dl>
<div class="card-actions no-print">
<button class="ghost" type="button" id="copyAddr">Copy address</button>
<button class="primary" type="button" id="printDir">Print directions</button>
</div>
</aside>
</section>
<!-- Hours + transit/parking -->
<section class="grid">
<article class="hours-card">
<h2>Opening hours</h2>
<table class="hours-table">
<tbody id="hoursBody"></tbody>
</table>
<p class="hours-note">
Last seating 22:00 · Bar stays open until 01:00 Thu–Sat.
</p>
</article>
<div class="info-grid">
<article class="info">
<span class="info-icon">Ⓜ</span>
<h3>Transit</h3>
<ul>
<li><strong>Metro</strong> · Antón Martín (Line 1) · 4 min</li>
<li><strong>Bus</strong> · 6, 26, 32, M1 — Tirso de Molina</li>
<li><strong>Cercanías</strong> · Sol or Atocha · 12 min</li>
</ul>
</article>
<article class="info">
<span class="info-icon">🅿</span>
<h3>Parking</h3>
<ul>
<li><strong>SER zone</strong> · Tue–Fri 9–21 · Sat 9–15</li>
<li><strong>Parking Plaza Tirso</strong> · 2 min · €2.60/h</li>
<li><strong>Free</strong> · Sundays + after 21:00</li>
</ul>
</article>
<article class="info">
<span class="info-icon">♿</span>
<h3>Accessibility</h3>
<ul>
<li>Ground-floor dining room · no steps</li>
<li>Accessible restroom · 90 cm doorway</li>
<li>Service dogs welcome</li>
</ul>
</article>
</div>
</section>
<!-- Contact form -->
<section class="contact">
<header class="section-head">
<p class="kicker">General enquiries</p>
<h2>For everything else, a note.</h2>
<p class="contact-sub">
For reservations, use the booking page — we respond to other
enquiries within 24 hours.
</p>
</header>
<form class="contact-form" id="contactForm" novalidate>
<div class="row">
<label>
<span>Name</span>
<input type="text" name="name" required placeholder="Your name" />
</label>
<label>
<span>Email</span>
<input type="email" name="email" required placeholder="you@example.com" />
</label>
</div>
<label>
<span>Topic</span>
<select name="topic">
<option>Private events</option>
<option>Press & media</option>
<option>Lost & found</option>
<option>Allergy / dietary question</option>
<option>Job application</option>
<option>Other</option>
</select>
</label>
<label>
<span>Message</span>
<textarea name="message" rows="4" maxlength="600"
placeholder="A short note — we'll get back to you."></textarea>
</label>
<button class="primary" type="submit">Send message</button>
<p class="contact-ok" id="contactOk" hidden>
✓ Sent — we'll be in touch within 24 hours.
</p>
</form>
</section>
</main>
<footer class="footer no-print">
<p class="footer-brand">Casa Olivar</p>
<p class="footer-meta">© 2026 · 42 Calle del Olivar · Madrid</p>
</footer>
<script src="script.js"></script>
</body>
</html>Location & Hours
The Find Us page. Stylised SVG map placeholder with a pinned location and a “We’re here” call-out, a 7-row hours table that highlights the current day (and shows “Open now · closes 23:30” / “Closed · opens Tue 19:00”), and side cards for metro/bus, parking, and accessibility.
Bottom of the page hosts a short contact form for general enquiries, and a “Print directions” CTA that triggers window.print() against the address card.