A sample Coop, built by hand to show what Roost makes for restaurants. The name and details are invented. See Roost →
Anchor & Oak
the Coop · everything under one roof
your hub

Anchor and Oak, today.

A landmark surf and turf house, and now the only place its whole operation lives. Tonight's oysters, the next hire, the training that sticks, the brand in one voice. One roof.

happy hour
4 to 6
daily · dollar off oysters
on the ice
0
varieties tonight
inquiries
0
awaiting a reply
site flags
0
open in the audit
On the Ice Tonight
tap any oyster to edit · full menu in Feather 02

A note from Lynsie, who built this

Hi. Before I built Coops, I spent twenty years in food and beverage. I know what it is to hold the whole restaurant in your head at once: the specials, the new hire who starts Thursday, the press email you keep meaning to answer, the vendor order, the eighty-six. I know the specific exhaustion of being the only one who knows where everything is.

This is what I make now: one calm place for all of it, built by hand, so a busy owner can finally hand pieces off without them falling through the cracks. This demo is invented, but it is built exactly the way I build the real ones.

If you run a restaurant and this feels like your life, I would love to build you one of your own.

Start with a free intake →
what the tide brought in

Daily Specials Manager

The raw bar rotates daily and the website should too. Update tonight's oysters and specials here once. They flow to the home rail, and this becomes the single source for the site's menu page, chalkboards, and social posts.

Tonight's oysters
Specials & standing offers
Why this matters: the menus on anchorandoak.com display for visitors but are invisible to search engines and link previews, so stale third-party sites win the "Anchor and Oak menu" search. This Feather is built to become the crawlable, always-current source: the site, social, the TVs someday, everywhere at once.
who we are, in words and pixels

Brand Kit

The single source of truth. When anyone writes a post, prints a card, or answers a review, this is the voice and the look they pull from.

Palette
Harbor Navy
#16303F
Oyster Slate
#56707D
Ice
#E4EDF0
Linen
#F5F2EC
Brass
#B08D3E

Tap any label to copy the hex. Drawn from the intake (gray, dark blue, neutrals) and the room itself: navy water, shell gray, linen napkins, brass rail.

Typography

Display: Archivo. Modern and confident, close to the site's current look. Used for headers and the wordmark treatment.

Body: Inter. Clean and quiet, for everything readable.

Utility: IBM Plex Mono. Prices, oyster origins, labels. The chalkboard energy, digitized.

Voice

Friendly. Warm. Welcoming. The server who remembers your order, in writing. Proof it already exists on the menu: the whiskey list annotates Willett Family Estate as "Sam's Favorite" and dessert sells "Nina's Tiramisu" and Rivermont's own Maple Hill ice cream. Personal, named, zero pretension. Every piece of copy should sound like that.

warm hospitalityhonest sourcingzero pretension
Boilerplate (50 words)

Anchor and Oak is an upscale surf and turf house, steaks and raw bar, on the Harbor Row waterfront in Rivermont, Virginia. Since 2015, Anchor and Oak has served fresh, dry-aged steaks, responsibly sourced East Coast seafood, a proper raw bar, and a warm, energetic room where everyone is welcome. Regulars just call it the Anchor.

The facts
Address210 Harbor Row, Rivermont, VA 24011
Phone(540) 555-0182
Emailinfo@anchorandoak.com
Dinner, Sun to Thu4:00 to 9:00
Dinner, Fri to Sat4:00 to 9:30
Happy Hour, daily4:00 to 6:00
Instagram@anchorandoak
Sister restaurantThe Copper Cellar
Send to Canva

Setting up your brand in Canva by hand, color by color, font by font, is the worst. This makes it one step instead.

Generate a clean brand sheet with your colors, fonts, and boilerplate, then let Canva\u2019s Brand Kit Builder read it in. In Canva: Brand → Brand Kit → Create → upload the sheet (or paste your Roost brand page URL) and Canva extracts the palette and details for you.

Tip: Canva\u2019s importer reads a PDF or a webpage. Print this to PDF, or point Canva at your roost-studio.com brand page.

every asset, one shelf

Media Kit

No more hunting through Drive, camera rolls, and old emails. Every logo, photo, and template linked from one place.

Photo Library

Upload once from any phone, tagged so the right shot is findable in seconds. Same system running Brownsville Market's library.

Uploaded. It now lives in the shared library on every device.
Asset links
Kickoff asks: the vector logo files (the anchor-and-oak crest badge) from whoever holds the original design source, so we have a true transparent SVG instead of the PNG. Then the photoshoot.
ready when they call

Press Kit

Everything a journalist, festival organizer, or Best-Of voter might ask for, ready to send in one reply instead of a scramble.

Fact sheet

Tap any row to edit it. Download opens your print dialog: choose Save as PDF for a branded one-pager ready to send to any inquiry.

Bios

Marcus Ellery

Owner & Founder

Marcus opened Anchor and Oak in 2015 after two decades in hospitality, including nearly ten years leading the dining room at Ashford Estate. A Certified Sommelier, he curates the wine program at Anchor and Oak and co-owns The Copper Cellar across the street.

Nina Okafor

Executive Chef

A Rivermont native, Nina trained in the kitchens at The Jefferson Hotel and the Linden Row Inn, led Meridian 44, opened Salt Cellar with Marcus, and served as Chef de Cuisine at Osteria Nove before returning to Anchor and Oak.

Diego Ramos

Sous Chef

Diego joins from the Willow Bend Club in Norfolk, with a love of butchery, Mediterranean cuisine, and close relationships with area farms and producers.

Sam Whitfield

Bar Manager

One of Anchor and Oak's longest-serving team members and many guests' favorite bartender. House cordials, fresh juices, and one of the best whisky selections in Virginia.

Press mentions
Release templates

Full press releases, ready to copy. Fill anything in [BRACKETS], delete what you don't need, send. Copy grabs the whole release.

welcoming new hands

Onboarding Hub

Hospitality turnover is real. This is the welcome that gets a new hire from first day to fully solo without slowing the floor.

First-day welcome

Who we are

A seafood restaurant and raw bar on Harbor Row since 2015. Three principles: warm hospitality, honest sourcing, zero pretension. Read the Brand Kit before your first shift.

Who to ask

Marcus: wine, big decisions, anything ownership.
Nina: kitchen, menu, allergens.
Diego: prep, sourcing, butchery.
Sam: the bar, cocktails, whisky.

Your first week

Two shifts shadowing a senior server, one shift on the floor with a trainer, then solo with the team backing you up. Raw Bar 101 before you touch a shucking knife.

Dress and floor rules

Clean, dark, closed-toe non-slip shoes. Apron provided. No strong fragrance near the raw bar, it fights the oysters.

First-90-days checklist

Tap a row to check it off. Progress syncs across the team.

Compliance

Virginia ABC server training

required · annual

Submit your certificate within 30 days of hire.

required

Food handler card

required · every 3 years

Virginia Department of Health, for anyone handling food including the raw bar.

required

W-4 and I-9

first day

Bring photo ID and your Social Security card on day one.

required
the craft, taught

Training Library

A living library, not a static handbook. The knowledge that currently lives in a few heads, written down so it survives every season of turnover.

the house memory

Internal Wiki

SOPs, FAQs, and decisions, searchable and current instead of scattered across texts and shift-change conversations.

the structure without the software bloat

Manager Toolkit

One-on-ones, ordering rhythm, hiring pipeline, and a decision log so "wait, why did we change that" always has an answer. Built for whoever runs the floor, today and after every future handoff. The role owns this Feather, not any one name.

Frameworks

The 15-minute one-on-one

Three questions, same order every time: What's working on your shifts? What's grinding? What should I know that I don't? Log anything actionable in the decision log below.

Ordering rhythm

Oysters sized to the week's reservations plus happy hour pull. Par sheet lives in the Media Kit. Rule of thumb: order for the Friday you expect, not the Tuesday you had.

Hiring pipeline

Applications land in Form Capture. Screen within 48 hours, trail shift within a week, decision the day after the trail. Speed is the whole advantage in this labor market.

The review frame

Twice a year, twenty minutes: one thing to keep doing, one thing to grow, one path forward. No surprises, nothing in a review that wasn't said in a one-on-one first.

Decision log
the front door, organized

Reservations

Tables route to OpenTable. Everything OpenTable cannot hold lands here: private events, buyouts, and job applications, with a review touch point before anything files itself away.

Table reservations

Table reservations live on OpenTable

The coop does not compete with the book. Guests reserve at OpenTable, and this inbox handles what OpenTable cannot: private events, buyouts, and job applications. Deeper integration is a kickoff question for Marcus (see the note below).

Active inbox
Try the event inquiry form
Got it. This lands in the shared inbox above, on every device, and in your email at the same time.
Archive

A passed event first asks for its debrief: the card highlights at the top of the inbox until post-event notes are written, then files itself here. Every archived event keeps its prep and its post-mortem, so the next buyout starts from what the last one taught.

stopping link rot before it starts

Site & Content Audit

A running check of anchorandoak.com and everywhere the brand appears. Seeded with real findings from a July 2026 review, so this Feather earns its keep on day one.

say it here, not in the walk-in

Feedback Loop

The team's channel for reporting what's broken and requesting what would help, without leaving the Coop or catching a manager mid-rush.

Recent notes
the walk-in, accounted for

Inventory & Ordering

Vendors in one directory, counts the whole team keeps current, low stock flagged before it bites, and reorders drafted from what Anchor and Oak typically buys. Order history stays forever, so any week can answer "what did we get and how much."

Vendors
Counts

Tap minus and plus to set what is actually on the shelf; every touch stamps the item counted today. Items at or below par flag themselves. When every item for a vendor has been counted today, the reorder banner raises on the home screen.

Order history
read me first

How This Coop Works

Five minutes of orientation, then you're home. Come back to this Feather any time, and point every new team member here first.

What a Coop is

A private hub built by hand for Anchor and Oak, by Roost. It replaces the patchwork of Excel sheets, Google Docs, and scattered logins with one warm place. No login, just this address. Keep the link inside the team.

What a Feather is

Each numbered card on the home screen is a Feather, one capability of the Coop. Tap a card to step inside, use the back links at the top and bottom of every Feather to come home.

Editing

Anywhere you see an add button or a small edit control, the content is yours to change. Changes save instantly and sync to everyone with the link within about a minute, so an oyster updated at the bar shows up on every device. Bigger structural changes are a message to Roost away.

The version stamp

The footer carries a version number, currently v12. When Roost ships an update, that number changes. It is the definitive answer to "am I looking at the latest?"

When something needs care

The Request care button in the corner reaches Roost directly. Pick an urgency, say what you need, and it lands with the right priority. Broken things get fixed, ideas get scoped, questions get answered.

What this is not

Not a system of record for sensitive originals. Payroll files, contracts, and financials keep their secure homes, and the Coop points to where they live.

Welcome

more from the Flock

Keep exploring what a Coop can be

This is one of several hand-built demos. Tour another like a model home.

See the whole Flock →
'); w.document.close(); w.focus(); setTimeout(function(){ w.print(); }, 300); } function renderMentions(){ document.getElementById('mentionList').innerHTML = DATA.mentions.map(function(mn){ var link = mn.link ? '

Read the article \u2192

' : 'link needed'; return '
' + toolBtns('mention', mn.id) + '

' + esc(mn.name) + '

' + esc(mn.sub) + '

' + esc(mn.desc) + '

' + link + '
'; }).join(''); } function renderReleases(){ document.getElementById('releaseList').innerHTML = DATA.releases.map(function(r){ var preview = (r.desc || '').split('\n').slice(0, 4).join(' ').slice(0, 180); return '
' + toolBtns('release', r.id) + '

' + esc(r.name) + '

' + esc(r.sub) + '
' + '

' + esc(preview) + '\u2026

' + '
' + '
'; }).join(''); } function copyRelease(id){ var r = DATA.releases.filter(function(x){ return x.id === id; })[0]; if(r) copyText(r.desc); } // ── media kit photo library (Netlify Blobs via /photos) ── var PHOTOS_URL = '/.netlify/functions/photos'; var PHOTOS = []; var PHOTO_FILTER = 'all'; var PHOTO_CATS = ['all','specials','oysters','inside','outdoor patio','special events','bar','announcements','staff','other']; function loadPhotos(){ try { fetch(PHOTOS_URL).then(function(r){ if(!r.ok) throw 0; return r.json(); }).then(function(idx){ PHOTOS = Array.isArray(idx) ? idx : []; renderPhotos(); }).catch(function(){ document.getElementById('phNote').style.display = 'block'; }); } catch(e){ document.getElementById('phNote').style.display = 'block'; } } function renderPhotos(){ var fEl = document.getElementById('phFilter'); fEl.innerHTML = PHOTO_CATS.map(function(c){ return ''; }).join(''); var items = PHOTOS.filter(function(p){ return PHOTO_FILTER === 'all' || p.cat === PHOTO_FILTER; }); document.getElementById('phGrid').innerHTML = items.map(function(p){ return '
' + '' + esc(p.name) + '' + '
' + esc(p.name) + '
' + esc(p.cat) + '
'; }).join('') || '

No photos' + (PHOTO_FILTER === 'all' ? ' yet. The first upload starts the library.' : ' tagged ' + esc(PHOTO_FILTER) + ' yet.') + '

'; } function setPhotoFilter(c){ PHOTO_FILTER = c; renderPhotos(); } function uploadPhoto(){ var file = document.getElementById('phFile').files[0]; if(!file){ toast('Choose a photo first'); return; } var name = (document.getElementById('phName').value || file.name).trim(); var cat = document.getElementById('phCat').value; var img = new Image(); var reader = new FileReader(); reader.onload = function(){ img.src = reader.result; }; img.onload = function(){ var max = 1600; var scale = Math.min(1, max / Math.max(img.width, img.height)); var canvas = document.createElement('canvas'); canvas.width = Math.round(img.width * scale); canvas.height = Math.round(img.height * scale); canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height); var dataUrl = canvas.toDataURL('image/jpeg', 0.82); fetch(PHOTOS_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name, cat: cat, data: dataUrl }) }).then(function(r){ if(!r.ok) throw 0; return r.json(); }).then(function(){ document.getElementById('phOk').style.display = 'block'; document.getElementById('phFile').value = ''; document.getElementById('phName').value = ''; loadPhotos(); }).catch(function(){ toast('Upload needs the deployed site'); }); }; reader.readAsDataURL(file); } function deletePhoto(id){ if(!window.confirm('Remove this photo from the library?')) return; fetch(PHOTOS_URL + '?id=' + encodeURIComponent(id), { method: 'DELETE' }) .then(function(){ loadPhotos(); }).catch(function(){}); } // ── welcome tour: first visit only, replayable from Feather 01 ── var TOUR_KEY = 'anchoroak-tour-v1'; var TOUR_STEPS = [ {t:'Welcome to the Coop', b:'One warm place for everything Anchor and Oak: tonight\u2019s specials, training, hiring, brand, and press. No login, just this address. This tour takes under a minute.'}, {t:'The ice comes first', b:'The rail at the top of home is tonight\u2019s raw bar. Tap any oyster to edit it on the spot, and the change reaches every device within a minute.'}, {t:'Twelve Feathers', b:'Each numbered card is one capability. Tap in, use the back links at the top and bottom to come home. Feather 01 explains everything in more depth, any time.'}, {t:'Everything is editable', b:'Add buttons and edit controls are everywhere. Changes save instantly and sync to the whole team, so the Coop is always current, never a stale binder.'}, {t:'Help lives in the header', b:'Feedback sends a note to the manager. Care reaches Roost directly with an urgency you choose. Both are one tap away on every screen. That\u2019s the tour, welcome home.'} ]; var TOUR_AT = 0; function startTour(force){ try { if(!force && localStorage.getItem(TOUR_KEY)) return; } catch(e){} TOUR_AT = 0; renderTourStep(); document.getElementById('tourOverlay').classList.add('open'); } function renderTourStep(){ var s = TOUR_STEPS[TOUR_AT]; document.getElementById('tourStep').textContent = (TOUR_AT + 1) + ' of ' + TOUR_STEPS.length; document.getElementById('tourTitle').textContent = s.t; document.getElementById('tourBody').textContent = s.b; document.getElementById('tourNext').textContent = TOUR_AT === TOUR_STEPS.length - 1 ? 'Done' : 'Next'; } function tourNext(){ if(TOUR_AT < TOUR_STEPS.length - 1){ TOUR_AT++; renderTourStep(); } else { endTour(); } } function endTour(){ document.getElementById('tourOverlay').classList.remove('open'); try { localStorage.setItem(TOUR_KEY, 'done'); } catch(e){} } function renderWiki(){ var q = (document.getElementById('wikiSearch').value || '').toLowerCase().trim(); var items = DATA.wiki.filter(function(w){ if(!q) return true; return (w.name + ' ' + (w.tags||'') + ' ' + w.desc).toLowerCase().indexOf(q) !== -1; }); document.getElementById('wikiList').innerHTML = items.map(function(w){ var tags = (w.tags||'').split(' ').filter(Boolean).map(function(t){ return '' + esc(t) + ''; }).join(''); return '
' + toolBtns('wiki', w.id) + '

' + esc(w.name) + '

' + esc(w.desc) + '

' + tags + '
'; }).join('') || '

No entries match. Try another word, or add the answer so it exists next time.

'; } function renderDecisions(){ document.getElementById('decisionList').innerHTML = DATA.decisions.map(function(d){ return '
' + toolBtns('decision', d.id) + '

' + esc(d.name) + '

' + esc(d.sub) + '

' + esc(d.desc) + '

'; }).join(''); } function renderInbox(){ // Lifecycle: upcoming -> needs review (date passed, no post-mortem) -> archived. // Auto-archive only happens once post-event notes exist; the review state is the touch point. var today = new Date().toISOString().slice(0, 10); var changed = false; DATA.inbox.forEach(function(i){ if(i.date && i.date < today && i.status !== 'archived' && (i.postNotes || '').trim()){ i.status = 'archived'; changed = true; } }); if(changed) saveData(); var isPast = function(i){ return i.date && i.date < today; }; var review = DATA.inbox.filter(function(i){ return i.status !== 'archived' && isPast(i); }); var active = DATA.inbox.filter(function(i){ return i.status !== 'archived' && !isPast(i); }); var archived = DATA.inbox.filter(function(i){ return i.status === 'archived'; }); var reviewHtml = review.map(function(i){ return '
post-event notes needed
' + toolBtns('inquiry', i.id) + '

' + esc(i.name) + '

' + esc(i.sub) + ' \u00b7 ' + esc(i.date) + ' \u00b7 event has passed

' + esc(i.desc) + '

' + (i.preNotes ? '

Prep: ' + esc(i.preNotes) + '

' : '') + '
' + '
' + '
'; }).join(''); var activeHtml = active.map(function(i){ return '
' + esc(i.status) + '' + toolBtns('inquiry', i.id).replace('class="tools"', 'class="tools" style="top:12px;right:86px"') + '

' + esc(i.name) + '

' + esc(i.sub) + (i.date ? ' \u00b7 ' + esc(i.date) : '') + '

' + esc(i.desc) + '

' + (i.preNotes ? '

Prep: ' + esc(i.preNotes) + '

' : '') + '
'; }).join(''); document.getElementById('inboxList').innerHTML = (reviewHtml + activeHtml) || '

Inbox zero. Submissions land here.

'; document.getElementById('archiveList').innerHTML = archived.map(function(i){ return '
' + toolBtns('inquiry', i.id) + '

' + esc(i.name) + '

' + esc(i.sub) + (i.date ? ' \u00b7 ' + esc(i.date) : '') + '

' + esc(i.desc) + '

' + (i.preNotes ? '

Prep: ' + esc(i.preNotes) + '

' : '') + (i.postNotes ? '

Post-mortem: ' + esc(i.postNotes) + '

' : '

Archived without notes. Tap edit to add what the next one should know.

') + '
'; }).join('') || '

Nothing archived yet.

'; } function archiveInquiry(id){ DATA.inbox.forEach(function(i){ if(i.id === id){ i.status = 'archived'; } }); saveData(); renderInbox(); renderChips(); renderGrid(); toast('Archived'); } function cycleInbox(id){ var order = ['new','seen','done']; DATA.inbox.forEach(function(i){ if(i.id === id){ i.status = order[(order.indexOf(i.status)+1) % order.length]; } }); saveData(); renderInbox(); renderChips(); renderGrid(); } function renderAudit(){ document.getElementById('auditList').innerHTML = DATA.audit.map(function(a){ var cls = a.status === 'open' ? 'flag' : 'done'; var label = a.status === 'open' ? 'open' : 'fixed'; return '
' + label + '' + '

' + esc(a.name) + '

' + esc(a.sub) + '

' + esc(a.desc) + '

'; }).join(''); } function toggleAudit(id){ DATA.audit.forEach(function(a){ if(a.id === id){ a.status = a.status === 'open' ? 'fixed' : 'open'; } }); saveData(); renderAudit(); renderChips(); renderGrid(); } function renderFeedback(){ document.getElementById('feedbackList').innerHTML = DATA.feedback.map(function(f){ return '
' + esc(f.status) + '' + '

' + esc(f.who) + '

' + (f.kind === 'bug' ? 'something is broken' : 'a suggestion') + '

' + esc(f.desc) + '

'; }).join('') || '

No notes yet.

'; } function renderChecklist(){ document.getElementById('hireChecklist').innerHTML = DATA.checklist.map(function(c){ return '
' + '
' + (c.done ? '\u2713' : '') + '
' + '
' + esc(c.label) + '
'; }).join(''); } function toggleCheck(id){ DATA.checklist.forEach(function(c){ if(c.id === id){ c.done = !c.done; } }); saveData(); renderChecklist(); renderGrid(); } // ── generic editor ── var ED = { kind:null, id:null }; var ED_SPECS = { oyster: { title:'Oyster', list:'oysters', fields:[ {k:'name', label:'Name', type:'text'}, {k:'origin', label:'Origin', type:'text'}, {k:'price', label:'Price each (number only)', type:'text'}, {k:'notes', label:'Tasting notes (brine, finish)', type:'text'}, {k:'va', label:'Virginia water? (yes or no)', type:'text'} ]}, special: { title:'Special', list:'specials', fields:[ {k:'name', label:'Name', type:'text'}, {k:'sub', label:'Timing or subtitle', type:'text'}, {k:'desc', label:'Description', type:'textarea'}, {k:'tag', label:'Tag (standing, signature, or tonight)', type:'text'} ]}, media: { title:'Asset', list:'media', fields:[ {k:'name', label:'Asset name', type:'text'}, {k:'sub', label:'Subtitle', type:'text'}, {k:'link', label:'Link (URL)', type:'text'}, {k:'note', label:'Note', type:'textarea'} ]}, module: { title:'Training module', list:'training', fields:[ {k:'name', label:'Module name', type:'text'}, {k:'sub', label:'Length and format', type:'text'}, {k:'desc', label:'What it covers', type:'textarea'}, {k:'video', label:'Video link (YouTube or Drive, optional)', type:'text'}, {k:'req', label:'Required? (yes or no)', type:'text'} ]}, fact: { title:'Fact', list:'facts', fields:[ {k:'name', label:'Label', type:'text'}, {k:'desc', label:'Value', type:'text'} ]}, mention: { title:'Press mention', list:'mentions', fields:[ {k:'name', label:'Outlet', type:'text'}, {k:'sub', label:'What and when', type:'text'}, {k:'desc', label:'Why it matters', type:'textarea'}, {k:'link', label:'Link to the article', type:'text'} ]}, release: { title:'Release template', list:'releases', fields:[ {k:'name', label:'Template name', type:'text'}, {k:'sub', label:'When to use it', type:'text'}, {k:'desc', label:'Full release text (use [BRACKETS] for fills)', type:'textarea'} ]}, inquiry: { title:'Inbox entry', list:'inbox', fields:[ {k:'name', label:'What is it', type:'text'}, {k:'sub', label:'Party size or context', type:'text'}, {k:'date', label:'Event date (YYYY-MM-DD, blank if none)', type:'text'}, {k:'desc', label:'Details', type:'textarea'}, {k:'preNotes', label:'Pre-event notes', type:'textarea'}, {k:'postNotes', label:'Post-mortem (after the event)', type:'textarea'} ]}, vendor: { title:'Vendor', list:'vendors', fields:[ {k:'name', label:'Vendor name', type:'text'}, {k:'rep', label:'Rep\u2019s name', type:'text'}, {k:'orderVia', label:'How orders go in', type:'choice', options:['text','email','phone','portal']}, {k:'phone', label:'Phone', type:'text'}, {k:'email', label:'Email', type:'text'}, {k:'deadlines', label:'Order deadlines (separate with \u00b7)', type:'text'}, {k:'delivery', label:'Delivery days', type:'text'}, {k:'notes', label:'Notes (minimums, quirks, what to confirm)', type:'textarea'} ]}, item: { title:'Inventory item', list:'inventory', fields:[ {k:'name', label:'Item', type:'text'}, {k:'unit', label:'Unit (case, bag, tin)', type:'text'}, {k:'vendorId', label:'Vendor', type:'vendorpick'}, {k:'par', label:'Par (reorder point)', type:'text'}, {k:'count', label:'Current count', type:'text'} ]}, wiki: { title:'Wiki entry', list:'wiki', fields:[ {k:'name', label:'Title', type:'text'}, {k:'tags', label:'Search words (space separated)', type:'text'}, {k:'desc', label:'The answer', type:'textarea'} ]}, decision: { title:'Decision', list:'decisions', fields:[ {k:'name', label:'Decision', type:'text'}, {k:'sub', label:'When', type:'text'}, {k:'desc', label:'The why, for future you', type:'textarea'} ]}, audit: { title:'Audit finding', list:'audit', fields:[ {k:'name', label:'Finding', type:'text'}, {k:'sub', label:'Where and when found', type:'text'}, {k:'desc', label:'Details and the fix', type:'textarea'} ]} }; function openEditor(kind, id){ ED.kind = kind; ED.id = id || null; var spec = ED_SPECS[kind]; var item = id ? DATA[spec.list].filter(function(x){ return x.id === id; })[0] : null; document.getElementById('edTitle').textContent = (id ? 'Edit ' : 'Add ') + spec.title.toLowerCase(); document.getElementById('edFields').innerHTML = spec.fields.map(function(f, idx){ var val = item ? item[f.k] : ''; if(f.k === 'va' || f.k === 'req') val = item ? (item[f.k] ? 'yes' : 'no') : ''; var fid = 'edf' + idx; if(f.type === 'textarea') return ''; if(f.type === 'choice'){ return ''; } if(f.type === 'vendorpick'){ return ''; } return ''; }).join(''); document.getElementById('editorOverlay').classList.add('open'); } function closeEditor(){ document.getElementById('editorOverlay').classList.remove('open'); } function saveEditor(){ var spec = ED_SPECS[ED.kind]; var vals = {}; spec.fields.forEach(function(f, idx){ var raw = document.getElementById('edf' + idx).value.trim(); if(f.k === 'va' || f.k === 'req'){ vals[f.k] = /^y/i.test(raw); } else { vals[f.k] = raw; } }); if(!vals[spec.fields[0].k]){ toast('Give it a name first'); return; } if(ED.kind === 'audit' && !ED.id) vals.status = 'open'; if(ED.kind === 'inquiry' && !ED.id) vals.status = 'new'; if(ED.id){ DATA[spec.list].forEach(function(x){ if(x.id === ED.id){ Object.keys(vals).forEach(function(k){ x[k] = vals[k]; }); } }); } else { vals.id = uid(ED.kind.slice(0,2)); DATA[spec.list].push(vals); } saveData(); closeEditor(); renderAll(); toast('Saved'); } function removeItem(kind, id){ var spec = ED_SPECS[kind]; if(!window.confirm('Remove this from the Coop?')) return; DATA[spec.list] = DATA[spec.list].filter(function(x){ return x.id !== id; }); saveData(); renderAll(); toast('Removed'); } // ── form capture demo ── function submitInquiry(){ var name = document.getElementById('fcName').value.trim(); var type = document.getElementById('fcType').value; var msg = document.getElementById('fcMsg').value.trim(); if(!name || !msg){ toast('Name and a few details, please'); return; } DATA.inbox.unshift({ id: uid('in'), name: type + ' (' + name + ')', sub: 'just now', date: (document.getElementById('fcDate').value || ''), desc: msg, preNotes: '', postNotes: '', status: 'new' }); saveData(); renderInbox(); renderChips(); renderGrid(); document.getElementById('fcOk').style.display = 'block'; document.getElementById('fcName').value = ''; document.getElementById('fcMsg').value = ''; } // ── feedback loop ── // Delivery seam: in the deployed coop this also POSTs to /.netlify/functions/care // with kind 'feedback'. Same seam as the care request below. function submitFeedback(){ var who = document.getElementById('fbWho').value.trim() || 'Someone on the team'; var kind = document.getElementById('fbKind').value; var msg = document.getElementById('fbMsg').value.trim(); if(!msg){ toast('Say a little more first'); return; } DATA.feedback.unshift({ id: uid('fb'), who: who, kind: kind, desc: msg, status: 'new' }); saveData(); renderFeedback(); renderGrid(); postToSeam({ kind: 'feedback', type: kind, name: who, message: msg }); document.getElementById('fbMsg').value = ''; toast('Note sent'); } // ── care request ── var CARE_PRIO = 'Whenever'; function openCare(){ document.getElementById('careOverlay').classList.add('open'); document.getElementById('careOk').style.display = 'none'; } function closeCare(){ document.getElementById('careOverlay').classList.remove('open'); } function pickPrio(btn){ CARE_PRIO = btn.getAttribute('data-p'); var row = document.getElementById('prioRow'); Array.prototype.forEach.call(row.children, function(c){ c.classList.remove('sel'); }); btn.classList.add('sel'); } function sendCare(){ var name = document.getElementById('careName').value.trim(); var email = document.getElementById('careEmail').value.trim(); var msg = document.getElementById('careMsg').value.trim(); if(!name || !msg){ toast('Name and what you need, please'); return; } postToSeam({ kind: 'care', priority: CARE_PRIO, name: name, email: email, message: msg }); document.getElementById('careOk').style.display = 'block'; document.getElementById('careMsg').value = ''; } // ── the delivery seam ── // Email today, care ticketing system later. Swapping the backend never touches this client: // the function at /.netlify/functions/care owns delivery. function postToSeam(payload){ payload.coop = 'Anchor & Oak'; payload.stamp = 'v12'; try { fetch('/.netlify/functions/care', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }).catch(function(){ /* static preview: local only, delivery activates on deploy */ }); } catch(e){ /* same */ } } // ── dispatch ── function showView(viewId){ var views = document.querySelectorAll('.view'); Array.prototype.forEach.call(views, function(v){ v.classList.remove('active'); }); var target = document.getElementById(viewId); if(target) target.classList.add('active'); window.scrollTo(0, 0); if(viewId === 'specials'){ renderOysters(); renderSpecials(); } if(viewId === 'menugen'){ renderMgTemplates(); renderMenuPreview(); } if(viewId === 'media'){ renderMedia(); loadPhotos(); } if(viewId === 'training'){ renderTraining(); } if(viewId === 'wiki'){ renderWiki(); } if(viewId === 'toolkit'){ renderDecisions(); } if(viewId === 'forms'){ renderInbox(); } if(viewId === 'audit'){ renderAudit(); } if(viewId === 'feedback'){ renderFeedback(); } if(viewId === 'onboarding'){ renderChecklist(); } if(viewId === 'press'){ renderFacts(); renderMentions(); renderReleases(); } if(viewId === 'inventory'){ renderVendors(); renderInventory(); renderOrders(); } } function printIce(){ var today = new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }); var rows = DATA.oysters.map(function(o){ return '' + esc(o.name) + (o.va ? ' VA' : '') + '
' + esc(o.origin) + (o.notes ? ' \u00b7 ' + esc(o.notes) : '') + '' + '$' + esc(o.price) + ''; }).join(''); var w = window.open('', '_blank'); if(!w){ toast('Allow pop-ups to print'); return; } w.document.write('On the Ice Tonight' + '' + '
' + '
ANCHOR & OAK FISH & OYSTER
' + '
On the Ice Tonight
' + '
' + esc(today) + ' \u00b7 served over ice with lemon, mignonette, cocktail sauce, horseradish
' + '' + rows + '
' + '
happy hour daily 4 to 6 \u00b7 dollar off oysters
' + '
'); w.document.close(); w.focus(); setTimeout(function(){ w.print(); }, 300); } function renderAll(){ renderChips(); renderIce(); renderGrid(); renderOysters(); renderSpecials(); renderMedia(); renderTraining(); renderWiki(); renderDecisions(); renderInbox(); renderAudit(); renderFeedback(); renderChecklist(); renderFacts(); renderMentions(); renderReleases(); renderVendors(); renderInventory(); renderOrders(); renderBanner(); } renderAll(); loadPhotos(); startTour(false); // ════════════════ MENU GENERATOR ════════════════ var MG_TEMPLATE = 'classic'; var MG_TEMPLATES = [ {id:'classic', name:'Classic', desc:'Elegant serif, centered, price leaders. The timeless steakhouse look.'}, {id:'modern', name:'Modern', desc:'Clean, left-aligned, monospaced prices. Contemporary and crisp.'} ]; function renderMgTemplates(){ var host = document.getElementById('mgTemplates'); if (!host) return; host.innerHTML = MG_TEMPLATES.map(function(t){ var on = t.id === MG_TEMPLATE; return ''; }).join(''); } function setMgTemplate(id){ MG_TEMPLATE = id; renderMgTemplates(); renderMenuPreview(); } var MG_EDIT = false; var MG_WORK = null; // working copy while editing function menuModelFromData(){ var oysters = (DATA.oysters || []).map(function(o){ return { id:o.id, name:o.name, origin:o.origin||'', notes:o.notes||'', price:o.price||'' }; }); var specials = (DATA.specials || []).map(function(sp){ return { id:sp.id, name:sp.name, sub:sp.sub||'', desc:sp.desc||'' }; }); return { oysters:oysters, specials:specials }; } function menuModel(){ if (MG_WORK) return MG_WORK; return menuModelFromData(); } function menuInnerHTML(forPrint){ var m = menuModel(); var cls = MG_TEMPLATE; var editable = MG_EDIT && !forPrint; function esc(t){ return String(t==null?'':t).replace(/&/g,'&').replace(//g,'>'); } function fld(kind, group, idx, key, val, cls2){ if (editable){ return '' + esc(val) + ''; } return '' + esc(val) + ''; } var out = ''; out += ''; return out; } function renderMenuPreview(){ var host = document.getElementById('mgPreview'); if (!host) return; host.innerHTML = menuInnerHTML(false); } function resetMenuPreview(){ // Discard any in-progress edits and reload the preview from the live menu data. if (MG_EDIT){ MG_WORK = menuModelFromData(); // fresh copy, edits dropped, stay in edit mode } else { MG_WORK = null; } renderMenuPreview(); if (typeof toast === 'function') toast('Preview reset to the current menu.'); } function menuPrintCSS(){ return '@page{margin:0.6in;}' + 'body{font-family:Georgia,"Times New Roman",serif;color:#16303F;margin:0;}' + '.menu-doc{max-width:640px;margin:0 auto;padding:20px;}' + '.menu-head{text-align:center;border-bottom:2px solid #B08D3E;padding-bottom:14px;margin-bottom:22px;}' + '.menu-name{font-size:34px;font-weight:800;letter-spacing:1px;}' + '.menu-sub{font-size:13px;letter-spacing:3px;text-transform:uppercase;color:#56707D;margin-top:6px;}' + '.menu-section{margin-bottom:24px;}' + '.menu-sec-title{font-size:16px;letter-spacing:2px;text-transform:uppercase;color:#B08D3E;border-bottom:1px solid #DDD6C9;padding-bottom:6px;margin:0 0 12px;}' + '.menu-item{display:flex;align-items:baseline;gap:8px;margin-bottom:10px;}' + '.menu-item.block{display:block;margin-bottom:14px;}' + '.menu-item-main{flex:1;}' + '.menu-item-name{font-weight:700;font-size:15px;}' + '.menu-item-note{display:block;font-size:12px;color:#56707D;font-style:italic;}' + '.menu-item-desc{display:block;font-size:12px;color:#3C525E;margin-top:2px;}' + '.menu-dots{flex:1;border-bottom:1px dotted #B8AE97;transform:translateY(-3px);min-width:20px;}' + '.menu-item-price{font-weight:700;font-size:14px;white-space:nowrap;}' + '.menu-foot{text-align:center;font-size:11px;color:#8A8271;margin-top:26px;letter-spacing:1px;}' // modern variant overrides + '.menu-modern{font-family:"Helvetica Neue",Arial,sans-serif;}' + '.menu-modern .menu-head{text-align:left;}' + '.menu-modern .menu-sec-title{color:#16303F;}' + '.menu-modern .menu-item-price{font-family:"IBM Plex Mono",monospace;}'; } function printMenu(){ var w = window.open('', '_blank'); if (!w) return; w.document.write('Anchor and Oak Menu' + '' + menuInnerHTML(true) + ''); w.document.close(); setTimeout(function(){ w.print(); }, 300); } // ── Menu Generator: inline editing ── function toggleMenuEdit(){ MG_EDIT = !MG_EDIT; if (MG_EDIT){ MG_WORK = menuModelFromData(); // start a working copy } var tgl = document.getElementById('mgEditToggle'); var save = document.getElementById('mgSaveBtn'); var hint = document.getElementById('mgEditHint'); if (tgl) tgl.textContent = MG_EDIT ? 'Done editing' : 'Edit menu'; if (save) save.style.display = MG_EDIT ? '' : 'none'; if (hint) hint.style.display = MG_EDIT ? '' : 'none'; renderMenuPreview(); } function mgEdit(el){ if (!MG_WORK) return; var g = el.getAttribute('data-g'); var i = parseInt(el.getAttribute('data-i'), 10); var k = el.getAttribute('data-k'); if (!MG_WORK[g] || !MG_WORK[g][i]) return; MG_WORK[g][i][k] = el.textContent; } function saveMenuEdits(){ if (!MG_WORK){ return; } // write the working copy back into DATA (Feather 02 source of truth) MG_WORK.oysters.forEach(function(o){ var t = (DATA.oysters||[]).find(function(x){ return x.id === o.id; }); if (t){ t.name=o.name; t.origin=o.origin; t.notes=o.notes; t.price=o.price; } }); MG_WORK.specials.forEach(function(sp){ var t = (DATA.specials||[]).find(function(x){ return x.id === sp.id; }); if (t){ t.name=sp.name; t.sub=sp.sub; t.desc=sp.desc; } }); if (typeof saveData === 'function') saveData(); // reflect on the home rail + specials feather if present if (typeof renderOysters === 'function') renderOysters(); if (typeof renderSpecials === 'function') renderSpecials(); if (typeof renderIceRail === 'function') renderIceRail(); // toast if available if (typeof toast === 'function') toast('Saved to the menu. Feather 02 is updated.'); MG_EDIT = false; MG_WORK = null; toggleMenuEditReset(); renderMenuPreview(); } function toggleMenuEditReset(){ var tgl = document.getElementById('mgEditToggle'); var save = document.getElementById('mgSaveBtn'); var hint = document.getElementById('mgEditHint'); if (tgl) tgl.textContent = 'Edit menu'; if (save) save.style.display = 'none'; if (hint) hint.style.display = 'none'; } // ── Brand Kit: Canva-ready export ── function exportBrandSheet(){ // Pull colors from the palette swatches in the Brand Kit feather. var swatches = document.querySelectorAll('#brand .swatch'); var colors = []; Array.prototype.forEach.call(swatches, function(sw){ var chip = sw.querySelector('.sw'); var label = sw.querySelector('.sw-l'); if (!chip || !label) return; var bg = getComputedStyle(chip).backgroundColor; // convert rgb() to hex var hex = rgbToHex(bg); var lines = label.textContent.split(/\s{2,}|\n/); var name = (label.textContent.replace(/#[0-9A-Fa-f]{3,6}/,'').trim()) || 'Color'; colors.push({ name:name, hex:hex }); }); var boiler = ''; var bEl = document.getElementById('boiler'); if (bEl) boiler = bEl.textContent; function esc(t){ return String(t==null?'':t).replace(/&/g,'&').replace(//g,'>'); } var colorRows = colors.map(function(c){ return '
' + '
' + esc(c.name) + '
' + '
' + esc(c.hex) + '
'; }).join(''); var css = '@page{margin:0.7in;}' + 'body{font-family:Arial,Helvetica,sans-serif;color:#16303F;margin:0;padding:0;}' + '.bs-doc{max-width:720px;margin:0 auto;padding:30px;}' + '.bs-title{font-size:30px;font-weight:800;letter-spacing:1px;margin:0 0 4px;}' + '.bs-sub{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:#8A8271;margin-bottom:26px;}' + '.bs-h{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:#B08D3E;border-bottom:1px solid #DDD6C9;padding-bottom:6px;margin:26px 0 14px;}' + '.bs-colors{display:flex;flex-wrap:wrap;gap:14px;}' + '.bs-color{display:flex;align-items:center;gap:10px;width:210px;}' + '.bs-chip{width:44px;height:44px;border-radius:8px;border:1px solid rgba(0,0,0,0.1);flex:0 0 auto;}' + '.bs-color-name{font-weight:700;font-size:14px;}' + '.bs-color-hex{font-family:monospace;font-size:13px;color:#56707D;}' + '.bs-font{margin-bottom:10px;font-size:15px;}' + '.bs-font b{font-size:17px;}' + '.bs-boiler{font-size:13px;line-height:1.7;color:#3C525E;}' + '.bs-foot{margin-top:30px;font-size:11px;color:#8A8271;text-align:center;letter-spacing:1px;}'; var html = 'Anchor and Oak, Brand Sheet for Canva' + '
' + '

Anchor & Oak

' + '
Brand Sheet \u00b7 ready for Canva Brand Kit
' + '
Brand Colors
' + colorRows + '
' + '
Typography
' + '
Display / Headers: Archivo (bold). Modern and confident.
' + '
Utility / Detail: IBM Plex Mono. Prices, labels, small print.
' + '
Boilerplate
' + esc(boiler) + '
' + '
Generated by the Anchor & Oak Coop \u00b7 built by Roost \u00b7 roost-studio.com
' + '
'; var w = window.open('', '_blank'); if (!w) return; w.document.write(html); w.document.close(); setTimeout(function(){ w.print(); }, 350); } function rgbToHex(rgb){ if (!rgb) return '#000000'; if (rgb.charAt(0) === '#') return rgb; var m = rgb.match(/\d+/g); if (!m || m.length < 3) return '#000000'; function h(n){ var s = parseInt(n,10).toString(16); return s.length===1 ? '0'+s : s; } return ('#' + h(m[0]) + h(m[1]) + h(m[2])).toUpperCase(); } Restaurant owner? Want one of your own?