Interior — Portfolio Grid
A warm editorial portfolio grid for an interior design studio: filter projects by residential, commercial, and hospitality, then browse large image cards that name each project and its completion year. Cards lift and zoom on hover, keyboard focus is fully visible, and clicking any card slides open a detail drawer with a hero image, brief, and specifications. Built with semantic HTML, a muted natural palette, and vanilla JavaScript.
MCP
Code
:root {
--bg: #f6f2ec;
--paper: #fbf9f5;
--ink: #2c2620;
--ink-2: #5a5147;
--muted: #8a8175;
--clay: #b08968;
--clay-d: #8c6a4f;
--walnut: #5c4433;
--sage: #9caf88;
--line: rgba(44, 38, 32, 0.12);
--white: #fff;
--r-sm: 4px;
--r-md: 8px;
--r-lg: 14px;
--shadow: 0 1px 2px rgba(44, 38, 32, 0.04), 0 8px 24px rgba(44, 38, 32, 0.06);
--shadow-lg: 0 12px 30px rgba(44, 38, 32, 0.16), 0 2px 6px rgba(44, 38, 32, 0.08);
}
* { box-sizing: border-box; }
html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: "Inter", system-ui, sans-serif;
line-height: 1.5;
}
.page {
max-width: 1160px;
margin: 0 auto;
padding: 0 32px 64px;
}
/* Masthead */
.masthead {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
padding: 28px 0 22px;
border-bottom: 1px solid var(--line);
}
.brand { display: flex; align-items: center; gap: 14px; }
.brand__mark {
display: grid;
place-items: center;
width: 44px;
height: 44px;
border-radius: var(--r-md);
background: var(--walnut);
color: var(--paper);
font-family: "Cormorant Garamond", serif;
font-size: 20px;
font-weight: 600;
letter-spacing: 0.5px;
}
.brand__text { display: flex; flex-direction: column; }
.brand__name {
font-family: "Cormorant Garamond", serif;
font-size: 24px;
font-weight: 600;
line-height: 1.1;
color: var(--ink);
}
.brand__sub {
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
.topnav { display: flex; gap: 26px; }
.topnav__link {
position: relative;
color: var(--ink-2);
text-decoration: none;
font-size: 14px;
font-weight: 500;
padding: 4px 0;
transition: color 0.2s ease;
}
.topnav__link::after {
content: "";
position: absolute;
left: 0;
bottom: -2px;
width: 0;
height: 1px;
background: var(--clay-d);
transition: width 0.25s ease;
}
.topnav__link:hover,
.topnav__link.is-active { color: var(--ink); }
.topnav__link:hover::after,
.topnav__link.is-active::after { width: 100%; }
.topnav__link:focus-visible {
outline: 2px solid var(--clay-d);
outline-offset: 3px;
border-radius: var(--r-sm);
}
/* Intro */
.intro { max-width: 720px; padding: 56px 0 40px; }
.intro__eyebrow {
margin: 0 0 16px;
font-size: 12px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--clay-d);
font-weight: 600;
}
.intro__title {
margin: 0 0 18px;
font-family: "Cormorant Garamond", serif;
font-weight: 500;
font-size: clamp(2.4rem, 5vw, 3.6rem);
line-height: 1.06;
letter-spacing: -0.01em;
color: var(--ink);
}
.intro__lede {
margin: 0;
max-width: 560px;
color: var(--ink-2);
font-size: 1.02rem;
}
/* Controls */
.controls {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
padding: 10px 0 26px;
border-top: 1px solid var(--line);
}
.filters { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
appearance: none;
border: 1px solid var(--line);
background: var(--paper);
color: var(--ink-2);
font-family: inherit;
font-size: 13px;
font-weight: 500;
padding: 8px 18px;
border-radius: 100px;
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease, transform 0.15s ease;
}
.chip:hover { border-color: var(--clay); color: var(--ink); transform: translateY(-1px); }
.chip.is-active {
background: var(--walnut);
border-color: var(--walnut);
color: var(--paper);
}
.chip:focus-visible {
outline: 2px solid var(--clay-d);
outline-offset: 2px;
}
.count {
margin: 0;
font-size: 13px;
color: var(--muted);
letter-spacing: 0.02em;
}
.count #count-num { color: var(--ink); font-weight: 600; }
/* Grid */
.grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 26px;
}
.card {
position: relative;
display: block;
width: 100%;
text-align: left;
border: none;
padding: 0;
background: transparent;
cursor: pointer;
font-family: inherit;
color: inherit;
border-radius: var(--r-lg);
transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
animation: rise 0.4s ease both;
}
@keyframes rise {
from { opacity: 0; transform: translateY(14px); }
to { opacity: 1; transform: translateY(0); }
}
.card__media {
position: relative;
aspect-ratio: 4 / 3;
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: var(--shadow);
transition: box-shadow 0.3s ease;
}
.card__img {
position: absolute;
inset: 0;
transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
.card__img::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(44, 38, 32, 0) 40%, rgba(44, 38, 32, 0.42) 100%);
}
.card__badge {
position: absolute;
top: 14px;
left: 14px;
z-index: 2;
font-size: 11px;
letter-spacing: 0.1em;
text-transform: uppercase;
font-weight: 600;
color: var(--ink);
background: rgba(251, 249, 245, 0.92);
padding: 5px 11px;
border-radius: 100px;
backdrop-filter: blur(4px);
}
.card__year {
position: absolute;
bottom: 14px;
right: 16px;
z-index: 2;
font-family: "Cormorant Garamond", serif;
font-size: 20px;
font-weight: 500;
color: var(--paper);
}
.card__body { padding: 16px 4px 4px; }
.card__name {
margin: 0 0 4px;
font-family: "Cormorant Garamond", serif;
font-size: 1.5rem;
font-weight: 500;
line-height: 1.15;
color: var(--ink);
}
.card__loc {
margin: 0;
font-size: 13px;
color: var(--muted);
letter-spacing: 0.02em;
}
.card:hover { transform: translateY(-4px); }
.card:hover .card__media { box-shadow: var(--shadow-lg); }
.card:hover .card__img { transform: scale(1.07); }
.card:focus-visible {
outline: 2px solid var(--clay-d);
outline-offset: 5px;
border-radius: var(--r-lg);
}
.card:active { transform: translateY(-1px); }
.grid__empty {
grid-column: 1 / -1;
padding: 60px 0;
text-align: center;
color: var(--muted);
font-family: "Cormorant Garamond", serif;
font-size: 1.4rem;
}
/* Footer */
.foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
margin-top: 56px;
padding-top: 24px;
border-top: 1px solid var(--line);
font-size: 12px;
color: var(--muted);
}
.foot__note { font-style: italic; }
/* Drawer */
.drawer { position: fixed; inset: 0; z-index: 50; }
.drawer[hidden] { display: none; }
.drawer__backdrop {
position: absolute;
inset: 0;
background: rgba(44, 38, 32, 0.44);
opacity: 0;
transition: opacity 0.3s ease;
}
.drawer.is-open .drawer__backdrop { opacity: 1; }
.drawer__panel {
position: absolute;
top: 0;
right: 0;
height: 100%;
width: min(460px, 92vw);
background: var(--paper);
box-shadow: -12px 0 40px rgba(44, 38, 32, 0.2);
overflow-y: auto;
transform: translateX(100%);
transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}
.drawer.is-open .drawer__panel { transform: translateX(0); }
.drawer__close {
position: absolute;
top: 14px;
right: 14px;
z-index: 3;
width: 40px;
height: 40px;
border-radius: 50%;
border: none;
background: rgba(251, 249, 245, 0.9);
color: var(--ink);
font-size: 24px;
line-height: 1;
cursor: pointer;
transition: background 0.2s ease, transform 0.2s ease;
}
.drawer__close:hover { background: var(--white); transform: rotate(90deg); }
.drawer__close:focus-visible { outline: 2px solid var(--clay-d); outline-offset: 2px; }
.drawer__hero {
height: 260px;
position: relative;
}
.drawer__hero::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(44, 38, 32, 0) 55%, rgba(44, 38, 32, 0.25) 100%);
}
.drawer__body { padding: 24px 28px 40px; }
.badge {
display: inline-block;
font-size: 11px;
letter-spacing: 0.1em;
text-transform: uppercase;
font-weight: 600;
color: var(--clay-d);
border: 1px solid var(--line);
padding: 4px 11px;
border-radius: 100px;
}
.drawer__title {
margin: 14px 0 6px;
font-family: "Cormorant Garamond", serif;
font-weight: 500;
font-size: 2rem;
line-height: 1.1;
color: var(--ink);
}
.drawer__meta {
margin: 0 0 20px;
font-size: 13px;
color: var(--muted);
letter-spacing: 0.03em;
}
.drawer__brief {
margin: 0 0 24px;
color: var(--ink-2);
font-size: 0.98rem;
}
.specs {
margin: 0 0 28px;
padding: 20px 0;
border-top: 1px solid var(--line);
border-bottom: 1px solid var(--line);
display: grid;
gap: 14px;
}
.specs div { display: flex; justify-content: space-between; gap: 20px; }
.specs dt {
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
margin: 0;
}
.specs dd {
margin: 0;
text-align: right;
font-size: 14px;
color: var(--ink);
font-weight: 500;
}
.btn {
appearance: none;
border: none;
width: 100%;
background: var(--walnut);
color: var(--paper);
font-family: inherit;
font-size: 14px;
font-weight: 600;
letter-spacing: 0.02em;
padding: 14px 20px;
border-radius: var(--r-md);
cursor: pointer;
transition: background 0.2s ease, transform 0.15s ease;
}
.btn:hover { background: var(--clay-d); }
.btn:active { transform: scale(0.98); }
.btn:focus-visible { outline: 2px solid var(--clay-d); outline-offset: 3px; }
/* Toast */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 16px);
z-index: 80;
background: var(--ink);
color: var(--paper);
font-size: 14px;
padding: 12px 22px;
border-radius: 100px;
box-shadow: var(--shadow-lg);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease, transform 0.3s ease;
}
.toast.is-show { opacity: 1; transform: translate(-50%, 0); }
/* Responsive */
@media (max-width: 820px) {
.grid { gap: 20px; }
.intro { padding: 40px 0 30px; }
}
@media (max-width: 520px) {
.page { padding: 0 20px 48px; }
.masthead { flex-wrap: wrap; gap: 14px; }
.topnav { gap: 18px; order: 3; width: 100%; }
.grid { grid-template-columns: 1fr; }
.intro__title { font-size: 2rem; }
.drawer__panel { width: 100%; }
.controls { padding-bottom: 20px; }
}(function () {
"use strict";
// Gradient "photographs" — warm, natural, clearly fictional.
var gradients = {
ashfield: "linear-gradient(135deg,#cdb79e 0%,#a8886a 55%,#6d5138 100%)",
lumen: "linear-gradient(135deg,#e4dccb 0%,#b6a488 60%,#7b6a52 100%)",
marigold: "linear-gradient(135deg,#e6c9a1 0%,#c69b6d 55%,#8c6a4f 100%)",
fernbank: "linear-gradient(135deg,#c3cbaf 0%,#9caf88 55%,#5f6f4c 100%)",
slate: "linear-gradient(135deg,#c8c2b8 0%,#948d80 55%,#524b40 100%)",
tideline: "linear-gradient(135deg,#d5cfc4 0%,#a99f8d 50%,#6f6552 100%)",
ember: "linear-gradient(135deg,#dcb597 0%,#b08968 55%,#7d5a3d 100%)",
quarry: "linear-gradient(135deg,#d8d0c2 0%,#b0a692 55%,#6b5f4b 100%)"
};
var projects = [
{
id: "ashfield",
name: "Ashfield House",
category: "residential",
location: "Sonoma, California",
year: "2025",
scope: "Full renovation",
area: "3,400 sq ft",
materials: "Lime plaster · white oak · travertine",
brief:
"A 1920s ranch reworked around a central courtyard. We opened sightlines to the olive grove, layered lime-washed walls with reclaimed oak, and kept a quiet, sun-warmed palette throughout."
},
{
id: "lumen",
name: "Lumen Offices",
category: "commercial",
location: "Portland, Oregon",
year: "2024",
scope: "Workplace design",
area: "11,200 sq ft",
materials: "Cork · brushed brass · wool felt",
brief:
"Headquarters for a design cooperative. Acoustic wool baffles and cork floors soften an open floorplate, while brass detailing and warm task lighting bring calm to long working days."
},
{
id: "marigold",
name: "Marigold Hotel",
category: "hospitality",
location: "Oaxaca, Mexico",
year: "2026",
scope: "Boutique hotel · 22 rooms",
area: "18,000 sq ft",
materials: "Clay render · barro tile · rattan",
brief:
"A courtyard hotel built from local clay and hand-thrown barro tile. Rattan screens filter the afternoon light, and every room opens to a planted loggia for slow mornings."
},
{
id: "fernbank",
name: "Fernbank Cottage",
category: "residential",
location: "Hudson Valley, New York",
year: "2023",
scope: "Interior architecture",
area: "2,100 sq ft",
materials: "Sage limewash · ash · linen",
brief:
"A weekend cottage tucked into the treeline. Sage-toned limewash and ash joinery echo the forest outside, with deep window seats framing the seasons as they turn."
},
{
id: "slate",
name: "Slate & Co. Studio",
category: "commercial",
location: "Chicago, Illinois",
year: "2024",
scope: "Retail & showroom",
area: "4,600 sq ft",
materials: "Honed stone · blackened steel · oak",
brief:
"A ceramics showroom where the objects lead. Honed stone plinths and blackened steel shelving recede into shadow, letting glazed surfaces catch a single band of north light."
},
{
id: "tideline",
name: "Tideline Residence",
category: "residential",
location: "Cornwall, England",
year: "2025",
scope: "Coastal home",
area: "2,800 sq ft",
materials: "Lime plaster · driftwood oak · wool",
brief:
"A cliffside home shaped by weather. Salt-resistant finishes and driftwood-toned oak keep interiors soft and pale, so the changing sea remains the brightest thing in the room."
},
{
id: "ember",
name: "Ember Wine Bar",
category: "hospitality",
location: "Austin, Texas",
year: "2026",
scope: "Restaurant & bar",
area: "3,900 sq ft",
materials: "Clay tadelakt · walnut · ember glass",
brief:
"An intimate wine bar lit like dusk. Tadelakt walls in clay tones wrap a walnut bar, and low amber pendants keep the whole room at a warm, unhurried glow."
},
{
id: "quarry",
name: "Quarry Loft",
category: "residential",
location: "Antwerp, Belgium",
year: "2022",
scope: "Loft conversion",
area: "1,950 sq ft",
materials: "Micro-cement · patinated brass · oak",
brief:
"A former stone warehouse turned home. Micro-cement floors and patinated brass fittings honour the raw shell, softened by a floating oak volume that holds the sleeping quarters."
}
];
var grid = document.getElementById("grid");
var countNum = document.getElementById("count-num");
var chips = Array.prototype.slice.call(document.querySelectorAll(".chip"));
var drawer = document.getElementById("drawer");
var panel = drawer.querySelector(".drawer__panel");
var toastEl = document.getElementById("toast");
var activeFilter = "all";
var lastFocused = null;
var toastTimer;
function labelFor(cat) {
return cat.charAt(0).toUpperCase() + cat.slice(1);
}
function render(filter) {
var list = projects.filter(function (p) {
return filter === "all" || p.category === filter;
});
countNum.textContent = list.length;
grid.innerHTML = "";
if (!list.length) {
var empty = document.createElement("p");
empty.className = "grid__empty";
empty.textContent = "No projects in this category yet.";
grid.appendChild(empty);
return;
}
list.forEach(function (p, i) {
var card = document.createElement("button");
card.type = "button";
card.className = "card";
card.style.animationDelay = i * 45 + "ms";
card.setAttribute("data-id", p.id);
card.setAttribute(
"aria-label",
p.name + ", " + labelFor(p.category) + " project in " + p.location + ", " + p.year
);
card.innerHTML =
'<div class="card__media">' +
'<div class="card__img" style="background:' + gradients[p.id] + '"></div>' +
'<span class="card__badge">' + labelFor(p.category) + "</span>" +
'<span class="card__year">' + p.year + "</span>" +
"</div>" +
'<div class="card__body">' +
'<h2 class="card__name">' + p.name + "</h2>" +
'<p class="card__loc">' + p.location + "</p>" +
"</div>";
card.addEventListener("click", function () {
openDrawer(p);
});
grid.appendChild(card);
});
}
function openDrawer(p) {
lastFocused = document.activeElement;
drawer.hidden = false;
document.getElementById("drawer-hero").style.background = gradients[p.id];
document.getElementById("drawer-badge").textContent = labelFor(p.category);
document.getElementById("drawer-title").textContent = p.name;
document.getElementById("drawer-meta").textContent = p.location + " · " + p.year;
document.getElementById("drawer-brief").textContent = p.brief;
var specs = document.getElementById("drawer-specs");
specs.innerHTML =
'<div><dt>Scope</dt><dd>' + p.scope + "</dd></div>" +
'<div><dt>Area</dt><dd>' + p.area + "</dd></div>" +
'<div><dt>Materials</dt><dd>' + p.materials + "</dd></div>";
// force reflow so the transition runs
void drawer.offsetWidth;
drawer.classList.add("is-open");
document.body.style.overflow = "hidden";
panel.focus();
}
function closeDrawer() {
drawer.classList.remove("is-open");
document.body.style.overflow = "";
window.setTimeout(function () {
drawer.hidden = true;
}, 350);
if (lastFocused && typeof lastFocused.focus === "function") {
lastFocused.focus();
}
}
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-show");
window.clearTimeout(toastTimer);
toastTimer = window.setTimeout(function () {
toastEl.classList.remove("is-show");
}, 2600);
}
chips.forEach(function (chip) {
chip.addEventListener("click", function () {
if (chip.classList.contains("is-active")) return;
chips.forEach(function (c) {
c.classList.remove("is-active");
c.setAttribute("aria-pressed", "false");
});
chip.classList.add("is-active");
chip.setAttribute("aria-pressed", "true");
activeFilter = chip.getAttribute("data-filter");
render(activeFilter);
});
});
drawer.addEventListener("click", function (e) {
if (e.target.hasAttribute("data-close")) closeDrawer();
});
document.getElementById("inquire").addEventListener("click", function () {
var title = document.getElementById("drawer-title").textContent;
toast("Inquiry sent for " + title + " — the studio will be in touch.");
closeDrawer();
});
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && drawer.classList.contains("is-open")) {
closeDrawer();
}
});
render(activeFilter);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Marlow & Fen — Selected Work</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=Cormorant+Garamond:wght@400;500;600&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="masthead">
<div class="brand">
<span class="brand__mark">M&F</span>
<div class="brand__text">
<span class="brand__name">Marlow & Fen</span>
<span class="brand__sub">Interior Studio · Est. 2011</span>
</div>
</div>
<nav class="topnav" aria-label="Primary">
<a href="#work" class="topnav__link is-active">Work</a>
<a href="#work" class="topnav__link">Studio</a>
<a href="#work" class="topnav__link">Journal</a>
<a href="#work" class="topnav__link">Contact</a>
</nav>
</header>
<section class="intro" id="work">
<p class="intro__eyebrow">Selected Work · 2016 — 2026</p>
<h1 class="intro__title">Interiors made to be lived in, slowly.</h1>
<p class="intro__lede">
A decade of residential, commercial, and hospitality projects across coast and city —
each shaped by natural light, honest materials, and a restrained, warm palette.
</p>
</section>
<div class="controls">
<div class="filters" role="group" aria-label="Filter projects by category">
<button class="chip is-active" type="button" data-filter="all" aria-pressed="true">All</button>
<button class="chip" type="button" data-filter="residential" aria-pressed="false">Residential</button>
<button class="chip" type="button" data-filter="commercial" aria-pressed="false">Commercial</button>
<button class="chip" type="button" data-filter="hospitality" aria-pressed="false">Hospitality</button>
</div>
<p class="count" aria-live="polite"><span id="count-num">8</span> projects</p>
</div>
<main class="grid" id="grid" aria-label="Project portfolio">
<!-- cards injected by script.js -->
</main>
<footer class="foot">
<span>Marlow & Fen — Interior Studio</span>
<span class="foot__note">Fictional showcase · all imagery via CSS gradients</span>
</footer>
</div>
<!-- Detail drawer -->
<div class="drawer" id="drawer" hidden>
<div class="drawer__backdrop" data-close></div>
<aside class="drawer__panel" role="dialog" aria-modal="true" aria-labelledby="drawer-title" tabindex="-1">
<button class="drawer__close" type="button" data-close aria-label="Close project detail">×</button>
<div class="drawer__hero" id="drawer-hero"></div>
<div class="drawer__body">
<span class="badge" id="drawer-badge">Residential</span>
<h2 class="drawer__title" id="drawer-title">Project</h2>
<p class="drawer__meta" id="drawer-meta">Location · Year</p>
<p class="drawer__brief" id="drawer-brief"></p>
<dl class="specs" id="drawer-specs"></dl>
<button class="btn" type="button" id="inquire">Inquire about this project</button>
</div>
</aside>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A calm, editorial portfolio page for a fictional interior design studio, Marlow & Fen. The header sets the tone with a Cormorant Garamond wordmark, a short studio line, and a row of filter chips — All, Residential, Commercial, and Hospitality — that instantly narrow the grid. A live count keeps you oriented as projects animate in and out.
Each project is a large image card with a soft gradient plate, a category badge, the project name, its location, and completion year. Hover lifts the card, deepens its shadow, and slowly zooms the image, while keyboard users get a clear focus ring and can activate any card with Enter or Space. Selecting a card opens a right-side detail drawer with a hero band, a written brief, and a specification list (scope, area, materials), dismissible via the close button, the backdrop, or the Escape key.
Everything is self-contained vanilla JavaScript: the filtering, the count, the drawer, and a small toast helper for the inquiry action. The layout reflows to a single column below 520px, the drawer becomes full width, and thin rules plus generous whitespace keep the natural, print-like feel intact.