<title>Residency Go-Live Hub</title>
<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=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,700;12..96,800&family=Karla:ital,wght@0,400;0,500;0,700;1,400&display=swap">
<style>
:root{
--bg:#F5F8F7; --surface:#FFFFFF; --surface-2:#FAFCFB;
--ink:#0F1D19; --ink-2:#2E403A; --muted:#5F736D;
--line:#DFE8E4; --line-strong:#C8D6D1;
--brand:#48CBA0; --brand-deep:#15795A; --brand-soft:#E7F8F1;
--host-bg:#E3ECFB; --host-fg:#1D4ED8;
--vendor-bg:#FCEAD7; --vendor-fg:#A9530A;
--both-bg:#E0F4E7; --both-fg:#15803D;
--either-bg:#EAE4FA; --either-fg:#6234C9;
--field:#FFFDF2; --field-line:#E7DFC4;
--danger:#B42318;
--shadow:0 1px 2px rgba(15,29,25,.05), 0 8px 24px -12px rgba(15,29,25,.14);
--radius:12px;
}
@media (prefers-color-scheme: dark){
:root:not([data-theme="light"]){
color-scheme: dark;
--bg:#0C1512; --surface:#121E1A; --surface-2:#16241F;
--ink:#E9F0ED; --ink-2:#C3D2CC; --muted:#93A8A1;
--line:#22332D; --line-strong:#31463E;
--brand:#48CBA0; --brand-deep:#6FE0BC; --brand-soft:#153027;
--host-bg:#1B2A47; --host-fg:#A8C5F6;
--vendor-bg:#3A2915; --vendor-fg:#F2BA76;
--both-bg:#16301F; --both-fg:#7ED79E;
--either-bg:#28214A; --either-fg:#C3B2F7;
--field:#1B2620; --field-line:#33463C;
--danger:#F0837A;
--shadow:0 1px 2px rgba(0,0,0,.3), 0 10px 26px -14px rgba(0,0,0,.6);
}
}
:root[data-theme="dark"]{
color-scheme: dark;
--bg:#0C1512; --surface:#121E1A; --surface-2:#16241F;
--ink:#E9F0ED; --ink-2:#C3D2CC; --muted:#93A8A1;
--line:#22332D; --line-strong:#31463E;
--brand:#48CBA0; --brand-deep:#6FE0BC; --brand-soft:#153027;
--host-bg:#1B2A47; --host-fg:#A8C5F6;
--vendor-bg:#3A2915; --vendor-fg:#F2BA76;
--both-bg:#16301F; --both-fg:#7ED79E;
--either-bg:#28214A; --either-fg:#C3B2F7;
--field:#1B2620; --field-line:#33463C;
--danger:#F0837A;
--shadow:0 1px 2px rgba(0,0,0,.3), 0 10px 26px -14px rgba(0,0,0,.6);
}
*{box-sizing:border-box}
body{
margin:0; background:var(--bg); color:var(--ink);
font-family:Karla, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
font-size:15px; line-height:1.55;
-webkit-font-smoothing:antialiased;
}
h1,h2,h3{font-family:"Bricolage Grotesque", Karla, ui-sans-serif, system-ui, sans-serif; text-wrap:balance; margin:0}
a{color:var(--brand-deep)}
button{font:inherit}
::selection{background:var(--brand); color:#06231A}
/* ---------- top bar ---------- */
.topbar{
position:sticky; top:env(safe-area-inset-top, 0px); z-index:40;
background:color-mix(in srgb, var(--surface) 88%, transparent);
backdrop-filter:saturate(1.4) blur(10px);
border-bottom:1px solid var(--line);
}
.topbar-in{
max-width:1180px; margin:0 auto; padding:10px 16px;
display:flex; align-items:center; gap:14px; flex-wrap:wrap;
}
.mark{display:flex; align-items:center; gap:9px; margin-right:auto; min-width:0}
.mark-logo{
font-family:"Bricolage Grotesque", sans-serif; font-weight:800; font-size:21px;
color:var(--brand); letter-spacing:-.03em; line-height:1;
}
.mark-txt{font-size:12.5px; color:var(--muted); letter-spacing:.02em; white-space:nowrap; overflow:hidden; text-overflow:ellipsis}
.meter{display:flex; align-items:center; gap:10px}
.meter-track{
width:130px; height:7px; border-radius:99px; background:var(--line); overflow:hidden;
}
.meter-fill{height:100%; width:0%; background:var(--brand); border-radius:99px; transition:width .35s ease}
.meter-num{font-size:12.5px; color:var(--muted); font-variant-numeric:tabular-nums; white-space:nowrap}
.iconbtn{
background:var(--surface); border:1px solid var(--line); color:var(--ink-2);
width:34px; height:34px; border-radius:9px; cursor:pointer; display:grid; place-items:center;
}
.iconbtn:hover{border-color:var(--line-strong)}
/* ---------- hero ---------- */
.wrap{max-width:1180px; margin:0 auto; padding-inline:16px}
.hero{padding-block:34px 22px}
.eyebrow{
font-size:11.5px; font-weight:700; letter-spacing:.16em; text-transform:uppercase;
color:var(--brand-deep); margin-bottom:9px;
}
.hero h1{font-size:clamp(30px, 5.4vw, 46px); font-weight:800; letter-spacing:-.025em; line-height:1.04}
.hero-sub{margin-top:10px; color:var(--muted); font-size:16px; max-width:60ch}
.hero-chips{display:flex; gap:8px; flex-wrap:wrap; margin-top:16px}
.chip{
display:inline-flex; align-items:center; gap:7px; padding:6px 12px; border-radius:99px;
background:var(--surface); border:1px solid var(--line); font-size:13px; color:var(--ink-2);
}
.chip b{font-weight:700; color:var(--ink)}
.chip.live{background:var(--brand-soft); border-color:color-mix(in srgb, var(--brand) 40%, transparent); color:var(--brand-deep)}
/* ---------- controls ---------- */
.controls{
display:flex; gap:10px; align-items:center; flex-wrap:wrap;
padding:12px 14px; background:var(--surface); border:1px solid var(--line);
border-radius:var(--radius); box-shadow:var(--shadow); margin-bottom:26px;
}
.ctl-label{font-size:11.5px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--muted)}
.segs{display:flex; gap:4px; flex-wrap:wrap}
.seg{
padding:5px 11px; border-radius:8px; border:1px solid var(--line); background:var(--surface-2);
color:var(--ink-2); font-size:13px; cursor:pointer;
}
.seg[aria-pressed="true"]{background:var(--ink); border-color:var(--ink); color:var(--surface)}
.spacer{flex:1 1 auto}
.toggle{display:inline-flex; align-items:center; gap:7px; font-size:13px; color:var(--ink-2); cursor:pointer}
.toggle input{accent-color:var(--brand); width:15px; height:15px}
.linkbtn{background:none; border:none; color:var(--muted); font-size:13px; cursor:pointer; text-decoration:underline; text-underline-offset:3px; padding:4px}
.linkbtn:hover{color:var(--danger)}
/* ---------- layout ---------- */
.cols{display:grid; grid-template-columns:212px minmax(0,1fr); gap:34px; align-items:start; padding-bottom:70px}
@media (max-width:900px){ .cols{grid-template-columns:1fr; gap:0} .rail{display:none} }
.rail{position:sticky; top:calc(env(safe-area-inset-top, 0px) + 66px)}
.rail ol{list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:1px}
.rail a{
display:flex; align-items:center; gap:9px; padding:6px 9px; border-radius:8px;
color:var(--muted); text-decoration:none; font-size:13.2px; line-height:1.3;
}
.rail a:hover{background:var(--surface); color:var(--ink)}
.rail a.done{color:var(--brand-deep)}
.dot{
width:7px; height:7px; border-radius:99px; background:var(--line-strong); flex:none;
}
.rail a.done .dot{background:var(--brand)}
.rail a.part .dot{background:color-mix(in srgb, var(--brand) 50%, var(--line-strong))}
/* ---------- sections ---------- */
.sec{margin-bottom:30px; scroll-margin-top:86px}
.sec-head{display:flex; align-items:baseline; gap:12px; flex-wrap:wrap; margin-bottom:6px}
.sec-head h2{
font-size:22px; font-weight:700; letter-spacing:-.015em;
border-left:4px solid var(--brand); padding-left:11px;
}
.sec-count{font-size:12.5px; color:var(--muted); font-variant-numeric:tabular-nums}
.sec-note{color:var(--muted); font-size:14.2px; margin:0 0 14px; padding-left:15px; max-width:70ch}
.sub{
font-size:12px; font-weight:700; letter-spacing:.11em; text-transform:uppercase;
color:var(--ink-2); margin:20px 0 8px; padding-left:15px;
}
.sub:first-of-type{margin-top:6px}
.list{
background:var(--surface); border:1px solid var(--line); border-radius:var(--radius);
overflow:hidden; box-shadow:var(--shadow);
}
.item{
display:grid; grid-template-columns:auto minmax(0,1fr) auto; gap:12px; align-items:start;
padding:11px 14px; border-top:1px solid var(--line); cursor:pointer;
}
.item:first-child{border-top:none}
.item:hover{background:var(--surface-2)}
.item input[type=checkbox]{
width:18px; height:18px; margin:1px 0 0; accent-color:var(--brand); cursor:pointer; flex:none;
}
.item .txt{font-size:14.4px; line-height:1.5; color:var(--ink)}
.item.checked .txt{color:var(--muted); text-decoration:line-through; text-decoration-color:var(--line-strong)}
.tag{
font-size:10.5px; font-weight:700; letter-spacing:.09em; text-transform:uppercase;
padding:3px 9px; border-radius:99px; white-space:nowrap; margin-top:2px;
}
.tag.host{background:var(--host-bg); color:var(--host-fg)}
.tag.vendor{background:var(--vendor-bg); color:var(--vendor-fg)}
.tag.both{background:var(--both-bg); color:var(--both-fg)}
.tag.either{background:var(--either-bg); color:var(--either-fg)}
/* ---------- callout ---------- */
.callout{
background:var(--brand-soft); border-left:4px solid var(--brand);
border-radius:0 10px 10px 0; padding:13px 16px; margin:14px 0; font-size:14.3px; color:var(--ink-2);
}
.callout b{color:var(--brand-deep)}
/* ---------- fields ---------- */
.fieldgrid{
background:var(--surface); border:1px solid var(--line); border-radius:var(--radius);
overflow:hidden; box-shadow:var(--shadow);
}
.frow{display:grid; grid-template-columns:minmax(150px, 38%) minmax(0,1fr); border-top:1px solid var(--line)}
.frow:first-child{border-top:none}
@media (max-width:620px){ .frow{grid-template-columns:1fr} .frow .fval{border-left:none; border-top:1px solid var(--line)} }
.fkey{padding:10px 14px; font-size:13.8px; color:var(--ink-2); display:flex; align-items:center}
.fval{border-left:1px solid var(--line); background:var(--field)}
.fval input, .fval textarea{
width:100%; border:none; background:transparent; padding:10px 14px; color:var(--ink);
font:inherit; font-size:14px; resize:vertical;
}
.fval input:focus, .fval textarea:focus{outline:2px solid var(--brand); outline-offset:-2px}
.fval textarea{min-height:82px; line-height:1.55}
.blurbbox{
background:var(--surface); border:1px solid var(--line); border-radius:var(--radius);
padding:14px; box-shadow:var(--shadow); margin-bottom:12px;
}
.blurbbox .bhead{display:flex; align-items:center; gap:10px; margin-bottom:8px; flex-wrap:wrap}
.blurbbox h3{font-size:14.5px; font-weight:700}
.bwhere{font-size:12.5px; color:var(--muted); flex:1 1 200px}
.copybtn{
border:1px solid var(--line); background:var(--surface-2); color:var(--ink-2);
padding:4px 11px; border-radius:7px; font-size:12.5px; cursor:pointer;
}
.copybtn:hover{border-color:var(--brand); color:var(--brand-deep)}
.blurbbox textarea{
width:100%; min-height:80px; border:1px solid var(--field-line); background:var(--field);
border-radius:9px; padding:10px 12px; font:inherit; font-size:14px; color:var(--ink); resize:vertical;
}
.blurbbox textarea:focus{outline:2px solid var(--brand); outline-offset:-1px}
.shape{
font-size:13.6px; color:var(--muted); font-style:italic; background:var(--surface-2);
border:1px dashed var(--line-strong); border-radius:9px; padding:11px 13px; margin-bottom:14px;
}
/* ---------- tables ---------- */
.tblwrap{overflow-x:auto; border:1px solid var(--line); border-radius:var(--radius); background:var(--surface); box-shadow:var(--shadow)}
table{border-collapse:collapse; width:100%; min-width:520px; font-size:13.8px}
th{
text-align:left; font-weight:700; font-size:11.5px; letter-spacing:.08em; text-transform:uppercase;
color:var(--muted); padding:10px 14px; background:var(--surface-2); border-bottom:1px solid var(--line);
white-space:nowrap;
}
td{padding:10px 14px; border-bottom:1px solid var(--line); color:var(--ink-2); vertical-align:top}
tr:last-child td{border-bottom:none}
td input{width:100%; border:none; background:transparent; font:inherit; font-size:13.8px; color:var(--ink); padding:2px 0}
td input:focus{outline:2px solid var(--brand); outline-offset:2px; border-radius:3px}
td.fill{background:var(--field)}
/* ---------- rules grid ---------- */
.rules{display:grid; grid-template-columns:1fr 1fr; gap:14px; margin-top:14px}
@media (max-width:700px){ .rules{grid-template-columns:1fr} }
.rulecard{border-radius:var(--radius); padding:14px 16px; border:1px solid var(--line); box-shadow:var(--shadow)}
.rulecard.yes{background:var(--both-bg); border-color:color-mix(in srgb, var(--both-fg) 22%, transparent)}
.rulecard.no{background:color-mix(in srgb, var(--danger) 9%, var(--surface)); border-color:color-mix(in srgb, var(--danger) 22%, transparent)}
.rulecard h3{font-size:12px; letter-spacing:.12em; text-transform:uppercase; margin-bottom:9px}
.rulecard.yes h3{color:var(--both-fg)}
.rulecard.no h3{color:var(--danger)}
.rulecard ul{margin:0; padding:0; list-style:none; display:flex; flex-direction:column; gap:8px}
.rulecard li{font-size:13.8px; color:var(--ink-2); display:grid; grid-template-columns:auto 1fr; gap:8px; align-items:start}
.glyph{font-weight:700; line-height:1.4}
.rulecard.yes .glyph{color:var(--both-fg)}
.rulecard.no .glyph{color:var(--danger)}
/* ---------- morning check ---------- */
.morning{
background:var(--ink); border-radius:var(--radius); padding:18px 18px 8px; color:#EAF3F0; margin-top:4px;
}
.morning .item{background:transparent; border-top-color:rgba(255,255,255,.1)}
.morning .item:hover{background:rgba(255,255,255,.04)}
.morning .item .txt{color:#EAF3F0}
.morning .item.checked .txt{color:#8FA39D}
.morning .list{background:transparent; border:none; box-shadow:none}
.morning-head{display:flex; align-items:baseline; gap:12px; flex-wrap:wrap; margin-bottom:4px}
.morning-head h2{font-size:21px; font-weight:700; color:#fff; border:none; padding:0}
.morning-note{color:#9FB5AE; font-size:14px; margin:4px 0 10px; max-width:68ch}
.foot{
border-top:1px solid var(--line); margin-top:16px; padding-block:22px 40px;
color:var(--muted); font-size:13px; display:flex; gap:12px; justify-content:space-between; flex-wrap:wrap;
}
.toastwrap{position:fixed; bottom:18px; left:50%; transform:translateX(-50%); z-index:60; pointer-events:none}
.toast{
background:var(--ink); color:var(--surface); padding:9px 17px; border-radius:99px; font-size:13.5px;
opacity:0; transition:opacity .2s ease, transform .2s ease; transform:translateY(8px);
}
.toast.show{opacity:1; transform:translateY(0)}
@media (prefers-reduced-motion: reduce){ *{transition:none !important; animation:none !important} }
</style>
<div class="topbar">
<div class="topbar-in">
<div class="mark">
<span class="mark-logo">oya</span>
<span class="mark-txt">Residency go-live</span>
</div>
<div class="meter">
<div class="meter-track"><div class="meter-fill" id="meterFill"></div></div>
<span class="meter-num" id="meterNum">0 / 0</span>
</div>
<button class="iconbtn" id="themeBtn" title="Switch theme" aria-label="Switch theme">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
</button>
</div>
</div>
<div class="wrap">
<header class="hero">
<div class="eyebrow">Pub residency</div>
<h1>Launch marketing checklist</h1>
<p class="hero-sub">Everything the pub and the food brand need live before the first service. Work through it together in launch week — tick as you go and your progress saves in this browser.</p>
<div class="hero-chips">
<span class="chip" id="chipPair">Add the names in Launch details</span>
<span class="chip live" id="chipDays" hidden></span>
</div>
</header>
<div class="controls">
<span class="ctl-label">Show</span>
<div class="segs" id="segs">
<button class="seg" data-filter="all" aria-pressed="true">Everything</button>
<button class="seg" data-filter="host" aria-pressed="false">Host only</button>
<button class="seg" data-filter="vendor" aria-pressed="false">Vendor only</button>
</div>
<span class="spacer"></span>
<label class="toggle"><input type="checkbox" id="hideDone"> Hide completed</label>
<button class="linkbtn" id="resetBtn">Reset</button>
</div>
<div class="cols">
<nav class="rail" aria-label="Sections"><ol id="rail"></ol></nav>
<main id="main"></main>
</div>
<div class="foot">
<span>The golden rule: the same name, hours, menu and link everywhere.</span>
<span>Progress is saved on this device only.</span>
</div>
</div>
<div class="toastwrap"><div class="toast" id="toast"></div></div>
<script>
const H='host', V='vendor', B='both', E='either';
const TAGLABEL={host:'Host', vendor:'Vendor', both:'Both', either:'Host or vendor'};
const SECTIONS=[
{id:'details', title:'Launch details', note:'Agree this before touching any channel. Every listing below copies from here, so it is the single source of truth.', custom:'details',
groups:[{items:[
['Pub, food brand and Oya have signed off the details and the residency blurb', B],
['Menu hosted at one permanent link that can be updated without changing the URL', V]
]}]},
{id:'assets', title:'Brand assets', note:'Swap these into a shared folder at the start of launch week. Every channel below needs them.',
groups:[
{items:[
['Logos in PNG and vector, light and dark versions', B],
['Brand colours and fonts', B],
['Food photography: at least 10 high-resolution images of the hero dishes, shot in the pub where possible', V],
['Interior and exterior photos of the pub, including the bar and seating', H],
['Two or three short vertical videos, under 30 seconds, for Instagram and stories', V],
['A cover image sized for Google, OpenTable and DesignMyNight', B],
['Short brand description of the food business, 50 words and 150 words', V],
["Written permission for each side to use the other's name, logo and images", B]
]},
{sub:'Co-branded pack — suggested, optional', note:'Not required, but it makes the residency look like one joined-up launch. Whoever has the design resource picks it up.', items:[
['A "[Brand] at [Pub]" lockup combining both logos', E],
["A menu template in the brand's style, for print and QR", E],
['A-board or window poster artwork', E],
['Launch post and story templates both accounts can use', E]
]}
]},
{id:'google', title:'Google Business Profile', note:'This is where most people check hours and the menu before they visit, so it comes first.',
groups:[
{sub:"Pub's profile", items:[
['Description updated to mention the food brand and cuisine', H],
['Menu link added, pointing to the agreed menu URL', H],
['Menu section filled in with dishes and prices, or menu photos uploaded', H],
['Food images added: at least 5 dish photos plus the brand logo', H],
['Hours updated; use "More hours" to add kitchen hours separately from bar hours', H],
['Categories and attributes checked, e.g. "Serves food", "Dine-in", "Takeaway", "Delivery"', H],
['Order and booking links added', H],
['Launch-day update post scheduled with a photo and the one-line description', H],
['Old food photos and old menu removed', H]
]},
{sub:"Food brand's profile, if it has one", items:[
["Address or service area updated to the pub, if this is the brand's only site", V],
["Hours and menu link match the pub's profile exactly", V],
['Launch update posted, naming the pub', V]
]}
]},
{id:'web', title:'Websites',
groups:[
{sub:'Pub website', items:[
['Food page rebuilt around the food brand: its logo, colours, photos and short description', H],
['Current menu on the page or linked, with no old menus left anywhere on the site', H],
['Kitchen hours shown alongside bar hours', H],
['Homepage banner or pop-up announcing the residency and launch date', H],
['Booking and order buttons working', H],
["Link to the food brand's Instagram and website", H],
['Page title and meta description mention the cuisine and area, so it shows in search', H]
]},
{sub:'Food brand website', items:[
['Locations or "Find us" page updated with the pub’s name, address, map and hours', V],
['Homepage announcement of the new site', V],
['Menu for this site linked', V],
["Link to the pub's website and booking page", V]
]}
]},
{id:'social', title:'Instagram and socials',
groups:[
{sub:'Bios', items:[
['Pub bio mentions the food brand and tags its handle, e.g. "Kitchen by @brand"', H],
['Food brand bio names the pub, its area and tags its handle, e.g. "Now cooking at @pub, Soho"', V],
["Link in bio on both accounts goes to the menu, website and booking page", B],
['Location, contact and action buttons (Book, Order, Menu) updated on both profiles', B]
]},
{sub:'Profile set-up', items:[
['Pinned launch post ready on both accounts', B],
['Story highlight for the menu and the residency on both accounts', B],
['Launch content scheduled: teaser 7 days out, reveal 3 days out, collab post on launch day', B],
['Collab post set up so it appears on both feeds', B],
['Facebook page and TikTok updated with the same info, if used', B],
['Local food accounts and press invited to the soft launch', B]
]}
]},
{id:'booking', title:'Booking and listing profiles', note:'Covers OpenTable, DesignMyNight, ResDiary, SevenRooms or whichever the pub uses, plus TripAdvisor and Apple Maps.',
groups:[{items:[
['Description rewritten to lead with the food brand and cuisine', H],
['Cuisine type and tags updated', H],
['Menu uploaded or linked to the agreed menu URL', H],
['Kitchen hours added; food bookings not offered outside them', H],
['Food images and the brand logo added; cover image changed to a food shot', H],
['Launch offer or event listed, if running', H],
['Old menus and old food photos removed', H],
['Test booking made to check confirmation emails show the right info', H]
]}]},
{id:'delivery', title:'Delivery platforms', note:'Only if delivering.', custom:'delivery',
groups:[{items:[
["Listing live at the pub's address on each platform", V],
['Listing name follows the agreed pairing, e.g. "[Brand] at [Pub]"', V],
['Hero image, logo and a photo for every dish uploaded', V],
['Menu, prices and descriptions match the in-pub menu', V],
['Hours match the kitchen hours', V],
['Launch promotion set up, if agreed', V],
['Test order placed and received', V]
]}]},
{id:'email', title:'Email', note:"Send to every list either side holds: newsletter subscribers, booking-platform guests who opted in, loyalty or Wi-Fi sign-ups, and the brand's own customers.", custom:'email',
groups:[
{sub:'Set-up', items:[
['List every database each side has and who can send to it', B],
['Check each list has marketing consent before sending', B],
["Agree subject lines and copy for both sends; each side approves the other's draft", B]
]},
{sub:'Sends', afterTable:true, items:[
['Warm-up email sent from the pub', H],
['Warm-up email sent from the food brand', V],
['Launch email sent from the pub', H],
['Launch email sent from the food brand', V],
['Every link in both emails tested: menu, booking, order, Instagram', B],
['Email signatures updated to mention the residency', B]
]}
]},
{id:'venue', title:'In-venue',
groups:[{items:[
["Printed menus on every table, carrying the food brand's logo", H],
['QR codes on tables linking to the agreed menu URL, tested on a phone', H],
['Outside signage: A-board, window poster or vinyl announcing the residency', H],
['Screens (if any) showing food content between sport', H],
['Bar staff briefed with the one-line description and three dishes to recommend', H]
]}]},
{id:'rules', title:'Brand management rules', note:'These stop the two brands pulling against each other. Agree them alongside the launch details and keep them for the life of the residency.', custom:'rules', groups:[]},
{id:'signoff', title:'Go-live sign-off', note:'Two days before launch, host, vendor and Oya run through these together. Anything unfinished goes in the table with an owner and a fix-by date.', custom:'signoff',
groups:[{items:[
["Search the pub's name on Google: menu, hours and food photos are correct", H],
['Both websites show the residency, menu and hours', B],
['Both Instagram bios updated, links working', B],
['OpenTable, DesignMyNight and other booking profiles updated', H],
['Delivery listings live, if delivering', V],
['Warm-up emails sent; launch emails scheduled', B],
['Launch posts scheduled on both accounts', B],
['In-venue menus, QR codes and signage in place', H]
]}]},
{id:'morning', title:'Launch morning check', note:'Ten minutes on a phone, on the morning of the first service, before the doors open. Anything wrong gets fixed before lunch.', dark:true,
groups:[{items:[
["Search the pub's name on Google and Maps: food hours, menu link and food photos show correctly", H],
["Search the food brand's name: it points to the pub", V],
['Scan a table QR code: it opens the current menu', H],
['Tap the link in bio on both Instagram accounts: menu and booking pages load', B],
['Open the pub website food page and the brand’s "Find us" page', B],
['Open OpenTable or DesignMyNight and start a booking: kitchen hours and menu are right', H],
['Open each delivery app: the listing is live and taking orders', V],
['Launch posts and launch emails have gone out, or are scheduled', B]
]}]}
];
const FIELDS=[
['pub','Pub name and address','text'],
['brand','Food brand name','text'],
['pairing','How the pairing is written','text','e.g. Nonna’s at The Eagle'],
['oneline','One-line description','text','e.g. Nonna’s is now cooking at The Eagle'],
['cuisine','Cuisine','text'],
['date','Launch date','date'],
['hours','Kitchen hours, day by day','area'],
['service','Service: dine-in, takeaway, delivery','text'],
['menuurl','Menu link (one URL used everywhere)','text','https://'],
['bookurl','Booking link','text','https://'],
['hashtag','Shared hashtag','text','#'],
['offer','Launch offer and end date, if any','text'],
['hostcontact','Host marketing contact','text'],
['vendcontact','Vendor marketing contact','text']
];
const KEY='oya-residency-golive-v1';
let store={checks:{}, fields:{}};
try{ const raw=localStorage.getItem(KEY); if(raw) store=Object.assign({checks:{},fields:{}}, JSON.parse(raw)); }catch(e){}
function save(){ try{ localStorage.setItem(KEY, JSON.stringify(store)); }catch(e){} }
function esc(s){ return String(s).replace(/[&<>"]/g, c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); }
function itemRow(id, text, owner){
const on = !!store.checks[id];
return `<label class="item${on?' checked':''}" data-owner="${owner}" data-id="${id}">
<input type="checkbox" id="${id}"${on?' checked':''}>
<span class="txt">${esc(text)}</span>
<span class="tag ${owner}">${TAGLABEL[owner]}</span>
</label>`;
}
function fieldsBlock(){
return `<div class="fieldgrid">` + FIELDS.map(f=>{
const [k,label,type,ph]=f;
const v = esc(store.fields[k]||'');
const ctrl = type==='area'
? `<textarea id="f-${k}" data-field="${k}" placeholder="${esc(ph||'')}">${v}</textarea>`
: `<input id="f-${k}" data-field="${k}" type="${type}" value="${v}" placeholder="${esc(ph||'')}">`;
return `<div class="frow"><div class="fkey">${esc(label)}</div><div class="fval">${ctrl}</div></div>`;
}).join('') + `</div>`;
}
function blurbBlock(){
return `<div class="sub">Residency blurb</div>
<p class="sec-note">One short paragraph agreed by the pub, the food brand and Oya. It goes word for word on Google, both websites, booking profiles, delivery listings and emails, so the story is the same wherever people find it.</p>
<div class="shape">[Brand] has taken over the kitchen at [Pub], [area]. Expect [cuisine or two hero dishes], served [days and hours] alongside [the pub’s drinks, sport, garden or whatever it is known for]. Eat in, take away or order on delivery.</div>
${['short','long'].map(k=>{
const meta = k==='short'
? ['Short, about 30 words','Instagram captions, Google update posts, delivery listing descriptions']
: ['Long, about 80 words','Google description, websites, OpenTable, DesignMyNight, emails'];
return `<div class="blurbbox">
<div class="bhead"><h3>${meta[0]}</h3><span class="bwhere">${meta[1]}</span>
<button class="copybtn" data-copy="blurb-${k}">Copy</button></div>
<textarea id="f-blurb-${k}" data-field="blurb-${k}" placeholder="Write it here once, then paste it everywhere.">${esc(store.fields['blurb-'+k]||'')}</textarea>
</div>`;
}).join('')}`;
}
function emailTable(){
return `<div class="tblwrap"><table>
<thead><tr><th>Send</th><th>Timing</th><th>Content</th><th>Sent by</th></tr></thead>
<tbody>
<tr><td><strong>Warm-up</strong></td><td>7 to 10 days before launch</td><td>Something new is coming; tease the brand and a hero dish</td><td>Both, to their own lists</td></tr>
<tr><td><strong>Launch</strong></td><td>Launch day or the day before</td><td>It’s open: menu link, hours, booking link, launch offer</td><td>Both, to their own lists</td></tr>
</tbody></table></div>`;
}
function rulesBlock(){
const rows=[
["Posts using the other side’s logo or name",'The other side','24 hours'],
['Printed menus, signage, window vinyl','Both','48 hours'],
['Price changes or menu changes','Both','Before going live anywhere'],
['Promotions and discounts','Both','48 hours'],
['Press or influencer invites','Both','48 hours'],
['Everyday stories and reshares','No approval needed','—']
];
return `<div class="sub">Approvals</div>
<div class="tblwrap"><table>
<thead><tr><th>Item</th><th>Needs approval from</th><th>Turnaround</th></tr></thead>
<tbody>${rows.map(r=>`<tr><td>${r[0]}</td><td>${r[1]}</td><td>${r[2]}</td></tr>`).join('')}</tbody>
</table></div>
<div class="rules">
<div class="rulecard yes"><h3>Always</h3><ul>
<li><span class="glyph">✓</span><span>Use the agreed naming convention and tag both accounts</span></li>
<li><span class="glyph">✓</span><span>Keep hours, menu and prices identical across every listing</span></li>
<li><span class="glyph">✓</span><span>Credit the other side when resharing their content</span></li>
</ul></div>
<div class="rulecard no"><h3>Never</h3><ul>
<li><span class="glyph">✕</span><span>Post an offer the other side hasn’t agreed</span></li>
<li><span class="glyph">✕</span><span>Use the other side’s logo in a way that changes its colours or proportions</span></li>
<li><span class="glyph">✕</span><span>Reply publicly to a complaint about the other side’s part of the experience; pass it on privately within 24 hours</span></li>
</ul></div>
</div>
<div class="sub">When something goes wrong</div>
<p class="sec-note">A bad review, a food safety issue, an unplanned kitchen closure or a public complaint goes to the other side and to Oya the same day. The side responsible drafts the public reply; the other side sees it before it goes out. If the two sides can’t agree within 48 hours, Oya decides.</p>`;
}
function signoffTables(){
const row=i=>`<tr>
<td class="fill"><input data-field="out-item-${i}" value="${esc(store.fields['out-item-'+i]||'')}" placeholder="What’s outstanding"></td>
<td class="fill"><input data-field="out-risk-${i}" value="${esc(store.fields['out-risk-'+i]||'')}" placeholder="Blocker or risk"></td>
<td class="fill"><input data-field="out-owner-${i}" value="${esc(store.fields['out-owner-'+i]||'')}" placeholder="Owner"></td>
<td class="fill"><input data-field="out-fix-${i}" type="date" value="${esc(store.fields['out-fix-'+i]||'')}"></td>
</tr>`;
const sign=(k,label)=>`<tr><td><strong>${label}</strong></td>
<td class="fill"><input data-field="sign-${k}-name" value="${esc(store.fields['sign-'+k+'-name']||'')}" placeholder="Name"></td>
<td class="fill"><input data-field="sign-${k}-date" type="date" value="${esc(store.fields['sign-'+k+'-date']||'')}"></td></tr>`;
return `<div class="sub">Outstanding items</div>
<div class="tblwrap"><table>
<thead><tr><th>Item</th><th>Blocker or risk</th><th>Owner</th><th>Fix by</th></tr></thead>
<tbody>${[1,2,3].map(row).join('')}</tbody></table></div>
<div class="sub">Signed</div>
<div class="tblwrap"><table style="min-width:420px">
<thead><tr><th></th><th>Name</th><th>Date</th></tr></thead>
<tbody>${sign('host','Host')}${sign('vendor','Vendor')}${sign('oya','Oya')}</tbody></table></div>`;
}
function render(){
const main=document.getElementById('main');
const rail=document.getElementById('rail');
let html='', railHtml='';
SECTIONS.forEach((sec, si)=>{
const total = sec.groups.reduce((n,g)=>n+g.items.length,0);
const body = sec.groups.map((g, gi)=>{
let out='';
if(g.sub) out+=`<div class="sub">${esc(g.sub)}</div>`;
if(g.note) out+=`<p class="sec-note">${esc(g.note)}</p>`;
if(g.afterTable) out+=emailTable()+'<div style="height:14px"></div>';
out+=`<div class="list">`+g.items.map((it,ii)=>itemRow(`s${si}-g${gi}-i${ii}`, it[0], it[1])).join('')+`</div>`;
return out;
}).join('');
let extra='';
if(sec.custom==='details') extra = fieldsBlock() + blurbBlock() + '<div class="sub">Before you start</div>';
if(sec.custom==='delivery') extra = `<div class="callout"><b>Start early.</b> Begin at least 10 working days out — Deliveroo, Uber Eats and Just Eat all need time to approve a new site.</div>`;
if(sec.custom==='rules') extra = rulesBlock();
const cls = sec.dark ? 'sec morning' : 'sec';
html += `<section class="${cls}" id="${sec.id}">
<div class="${sec.dark?'morning-head':'sec-head'}">
<h2>${esc(sec.title)}</h2>
${total?`<span class="sec-count" data-count="${si}">0 / ${total}</span>`:''}
</div>
${sec.note?`<p class="${sec.dark?'morning-note':'sec-note'}">${esc(sec.note)}</p>`:''}
${extra}
${body}
${sec.custom==='signoff'?signoffTables():''}
</section>`;
railHtml += `<li><a href="#${sec.id}" data-rail="${si}"><span class="dot"></span><span>${esc(sec.title)}</span></a></li>`;
});
main.innerHTML=html;
rail.innerHTML=railHtml;
refresh();
}
function refresh(){
let done=0, total=0;
SECTIONS.forEach((sec, si)=>{
let sd=0, st=0;
sec.groups.forEach((g,gi)=>g.items.forEach((it,ii)=>{
st++; if(store.checks[`s${si}-g${gi}-i${ii}`]) sd++;
}));
done+=sd; total+=st;
const c=document.querySelector(`[data-count="${si}"]`);
if(c) c.textContent=`${sd} / ${st}`;
const r=document.querySelector(`[data-rail="${si}"]`);
if(r){ r.classList.toggle('done', st>0 && sd===st); r.classList.toggle('part', sd>0 && sd<st); }
});
document.getElementById('meterFill').style.width = total? (done/total*100)+'%' : '0%';
document.getElementById('meterNum').textContent = `${done} / ${total} done`;
applyFilter();
updateChips();
}
let filter='all', hideDone=false;
function applyFilter(){
document.querySelectorAll('.item').forEach(el=>{
const o=el.dataset.owner;
let show = filter==='all' || o===B || o===E || o===filter;
if(show && hideDone && el.classList.contains('checked')) show=false;
el.hidden = !show;
});
document.querySelectorAll('.list').forEach(l=>{
const any=[...l.querySelectorAll('.item')].some(i=>!i.hidden);
l.hidden=!any;
});
}
function updateChips(){
const brand=(store.fields.brand||'').trim(), pub=(store.fields.pub||'').trim().split(',')[0];
const chip=document.getElementById('chipPair');
chip.innerHTML = (brand||pub)
? `<b>${esc(brand||'[Brand]')}</b> at <b>${esc(pub||'[Pub]')}</b>`
: 'Add the names in Launch details';
const d=(store.fields.date||'').trim();
const dc=document.getElementById('chipDays');
if(d){
const target=new Date(d+'T00:00:00'); const now=new Date(); now.setHours(0,0,0,0);
const days=Math.round((target-now)/86400000);
dc.hidden=false;
dc.textContent = days>1 ? `${days} days to launch` : days===1 ? 'Launches tomorrow' : days===0 ? 'Launches today' : `Live for ${Math.abs(days)} days`;
} else dc.hidden=true;
}
function toast(msg){
const t=document.getElementById('toast');
t.textContent=msg; t.classList.add('show');
clearTimeout(t._t); t._t=setTimeout(()=>t.classList.remove('show'), 1600);
}
document.addEventListener('change', e=>{
const cb=e.target.closest('.item input[type=checkbox]');
if(cb){
const row=cb.closest('.item');
store.checks[row.dataset.id]=cb.checked;
if(!cb.checked) delete store.checks[row.dataset.id];
row.classList.toggle('checked', cb.checked);
save(); refresh(); return;
}
const f=e.target.closest('[data-field]');
if(f){ store.fields[f.dataset.field]=f.value; save(); updateChips(); }
});
document.addEventListener('input', e=>{
const f=e.target.closest('[data-field]');
if(f){ store.fields[f.dataset.field]=f.value; save(); updateChips(); }
});
document.getElementById('segs').addEventListener('click', e=>{
const b=e.target.closest('.seg'); if(!b) return;
filter=b.dataset.filter;
document.querySelectorAll('.seg').forEach(s=>s.setAttribute('aria-pressed', String(s===b)));
applyFilter();
});
document.getElementById('hideDone').addEventListener('change', e=>{ hideDone=e.target.checked; applyFilter(); });
document.getElementById('resetBtn').addEventListener('click', ()=>{
const btn=document.getElementById('resetBtn');
if(btn.dataset.armed!=='1'){
btn.dataset.armed='1'; btn.textContent='Tap again to clear everything';
setTimeout(()=>{ btn.dataset.armed='0'; btn.textContent='Reset'; }, 4000);
return;
}
store={checks:{},fields:{}}; save(); render(); toast('Checklist cleared');
btn.dataset.armed='0'; btn.textContent='Reset';
});
document.addEventListener('click', e=>{
const c=e.target.closest('[data-copy]'); if(!c) return;
const ta=document.getElementById('f-'+c.dataset.copy);
if(!ta || !ta.value.trim()){ toast('Nothing to copy yet'); return; }
const fallback=()=>{ ta.select(); toast('Selected — press Ctrl/Cmd+C'); };
try{
navigator.clipboard.writeText(ta.value).then(()=>toast('Blurb copied'), fallback);
}catch(err){ fallback(); }
});
const tb=document.getElementById('themeBtn');
tb.addEventListener('click', ()=>{
const root=document.documentElement;
const dark = root.getAttribute('data-theme')==='dark' ||
(!root.getAttribute('data-theme') && matchMedia('(prefers-color-scheme: dark)').matches);
root.setAttribute('data-theme', dark?'light':'dark');
try{ localStorage.setItem(KEY+'-theme', dark?'light':'dark'); }catch(e){}
});
try{ const t=localStorage.getItem(KEY+'-theme'); if(t) document.documentElement.setAttribute('data-theme', t); }catch(e){}
render();
</script>