Food truck websiteOne-line
Kimi K3.1 Pro
by MoonshotAIreasoning defaultClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 4 min 56 s
- Cost
- $0.450
- Output tokens
- 48,451
- Spent thinking
- 16,936 tokens
- Speed
- 164.6 tok/s
- Console errors
- 0
- First token after
- 1.5 s
- First visible text after
- 2 min 50 s
- Prompt tokens
- 450
- Code size
- 105.7 KB
- Finish reason
- stop
- Served by
- Morph
- Model id
moonshotai/kimi-k3.1-pro- Reasoning sent
model default- Max output
- 943,718 tokens
- Generated
- Nov 12, 2026
What it was asked
The prompt
“i'm opening a korean taco food truck called "Kimchi Loco", make me a website that makes people really want to come”
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 3924 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#ff5338">
<meta name="description" content="Kimchi Loco serves Korean BBQ tacos, kimchi fried rice and loaded street fries around Los Angeles. Find the truck and come hungry.">
<title>Kimchi Loco — Seoul Food. Taco Truck. Full Send.</title>
<style>
:root {
--ink: #171713;
--cream: #f7f1e5;
--paper: #fffaf1;
--red: #ff5338;
--red-dark: #dc361f;
--lime: #dcff45;
--yellow: #ffc94f;
--pink: #ff9eb1;
--green: #5f8e3b;
--muted: #706d63;
--border: 2px solid var(--ink);
--display: Impact, "Arial Black", "Helvetica Neue", Arial, sans-serif;
--body: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
scroll-padding-top: 90px;
}
body {
margin: 0;
min-width: 320px;
overflow-x: hidden;
color: var(--ink);
background: var(--cream);
font-family: var(--body);
-webkit-font-smoothing: antialiased;
}
body.drawer-open {
overflow: hidden;
}
::selection {
color: var(--ink);
background: var(--lime);
}
button,
input,
select {
font: inherit;
}
button,
a {
-webkit-tap-highlight-color: transparent;
}
button {
color: inherit;
cursor: pointer;
}
a {
color: inherit;
text-decoration: none;
}
img,
svg {
display: block;
max-width: 100%;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 3px solid var(--lime);
outline-offset: 4px;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.skip-link {
position: fixed;
top: 10px;
left: 10px;
z-index: 200;
padding: 12px 18px;
color: var(--cream);
background: var(--ink);
transform: translateY(-160%);
}
.skip-link:focus {
transform: translateY(0);
}
.container {
width: min(calc(100% - 48px), 1240px);
margin-inline: auto;
}
.topbar {
height: 34px;
overflow: hidden;
color: var(--cream);
background: var(--ink);
border-bottom: 1px solid #383830;
font-size: 11px;
font-weight: 900;
letter-spacing: .16em;
text-transform: uppercase;
}
.ticker-track,
.marquee-track {
display: flex;
width: max-content;
height: 100%;
align-items: center;
animation: ticker 28s linear infinite;
will-change: transform;
}
.ticker-group,
.marquee-group {
display: flex;
flex-shrink: 0;
align-items: center;
white-space: nowrap;
}
.ticker-group span {
display: inline-flex;
align-items: center;
gap: 28px;
padding-right: 28px;
}
.ticker-group span::after {
color: var(--lime);
content: "✺";
font-size: 14px;
}
@keyframes ticker {
to { transform: translateX(-50%); }
}
.site-header {
position: sticky;
top: 0;
z-index: 40;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
height: 80px;
padding-inline: max(24px, calc((100vw - 1400px) / 2));
background: rgba(247, 241, 229, .97);
border-bottom: var(--border);
}
.logo {
display: inline-flex;
width: fit-content;
align-items: center;
gap: 10px;
}
.logo-mark {
display: grid;
width: 43px;
height: 43px;
place-items: center;
background: var(--red);
border: var(--border);
box-shadow: 3px 3px 0 var(--ink);
font-size: 15px;
font-weight: 950;
letter-spacing: -.08em;
transform: rotate(-5deg);
transition: transform .2s ease;
}
.logo:hover .logo-mark {
transform: rotate(5deg) scale(1.04);
}
.logo-word {
display: flex;
flex-direction: column;
font-family: var(--display);
font-size: 24px;
font-weight: 950;
letter-spacing: -.045em;
line-height: .86;
}
.logo-loco {
color: var(--red);
font-size: 29px;
}
.logo-loco i {
display: inline-block;
color: var(--ink);
font-family: var(--body);
font-size: 15px;
font-style: normal;
vertical-align: 4px;
}
.desktop-nav {
display: flex;
align-items: center;
gap: 32px;
font-size: 12px;
font-weight: 900;
letter-spacing: .1em;
text-transform: uppercase;
}
.desktop-nav a {
position: relative;
padding-block: 10px;
}
.desktop-nav a::after {
position: absolute;
right: 0;
bottom: 3px;
left: 0;
height: 3px;
background: var(--red);
content: "";
transform: scaleX(0);
transform-origin: right;
transition: transform .2s ease;
}
.desktop-nav a:hover::after {
transform: scaleX(1);
transform-origin: left;
}
.nav-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
}
.order-trigger {
display: inline-flex;
min-height: 43px;
align-items: center;
justify-content: center;
gap: 11px;
padding: 0 15px;
background: var(--lime);
border: var(--border);
box-shadow: 3px 3px 0 var(--ink);
font-size: 11px;
font-weight: 950;
letter-spacing: .08em;
text-transform: uppercase;
transition: transform .15s ease, box-shadow .15s ease;
}
.order-trigger:hover {
box-shadow: 1px 1px 0 var(--ink);
transform: translate(2px, 2px);
}
.cart-count {
display: inline-grid;
min-width: 23px;
height: 23px;
place-items: center;
padding-inline: 5px;
color: var(--cream);
background: var(--ink);
border-radius: 50%;
font-size: 11px;
letter-spacing: 0;
}
.cart-count.pop {
animation: countPop .3s ease;
}
@keyframes countPop {
50% { transform: scale(1.35) rotate(8deg); }
}
.mobile-toggle {
display: none;
width: 43px;
height: 43px;
padding: 10px;
background: transparent;
border: var(--border);
}
.mobile-toggle span {
display: block;
width: 100%;
height: 2px;
margin-block: 4px;
background: var(--ink);
transition: transform .2s ease, opacity .2s ease;
}
.mobile-toggle[aria-expanded="true"] span:first-child {
transform: translateY(6px) rotate(45deg);
}
.mobile-toggle[aria-expanded="true"] span:nth-child(2) {
opacity: 0;
}
.mobile-toggle[aria-expanded="true"] span:last-child {
transform: translateY(-6px) rotate(-45deg);
}
.mobile-nav {
position: absolute;
top: 100%;
right: 0;
left: 0;
padding: 12px 24px 24px;
background: var(--cream);
border-bottom: var(--border);
box-shadow: 0 15px 25px rgba(0, 0, 0, .12);
}
.mobile-nav a {
display: flex;
align-items: center;
justify-content: space-between;
padding: 17px 0;
border-bottom: 1px solid rgba(23, 23, 19, .2);
font-family: var(--display);
font-size: 28px;
font-weight: 900;
letter-spacing: -.02em;
text-transform: uppercase;
}
.hero {
display: grid;
grid-template-columns: minmax(0, 56%) minmax(0, 44%);
height: calc(100svh - 150px);
min-height: 610px;
max-height: 780px;
border-bottom: var(--border);
}
.hero-copy {
display: flex;
min-width: 0;
flex-direction: column;
justify-content: center;
padding: 38px clamp(30px, 4vw, 64px);
background:
radial-gradient(circle at 8% 92%, rgba(255, 83, 56, .10) 0 2px, transparent 2.5px) 0 0 / 18px 18px,
var(--cream);
}
.hero-copy > * {
animation: riseIn .65s both;
}
.hero-copy > *:nth-child(2) { animation-delay: .06s; }
.hero-copy > *:nth-child(3) { animation-delay: .12s; }
.hero-copy > *:nth-child(4) { animation-delay: .18s; }
.hero-copy > *:nth-child(5) { animation-delay: .24s; }
.hero-copy > *:nth-child(6) { animation-delay: .3s; }
@keyframes riseIn {
from { opacity: 0; transform: translateY(14px); }
to { opacity: 1; transform: translateY(0); }
}
.eyebrow {
display: flex;
align-items: center;
gap: 13px;
font-size: 11px;
font-weight: 900;
letter-spacing: .1em;
text-transform: uppercase;
}
.eyebrow-chip {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 7px 10px;
background: var(--yellow);
border: 1.5px solid var(--ink);
}
.eyebrow-chip::before {
width: 7px;
height: 7px;
background: var(--red);
border: 1px solid var(--ink);
border-radius: 50%;
content: "";
}
.eyebrow-korean {
color: var(--muted);
font-size: 13px;
letter-spacing: .02em;
}
.hero h1 {
margin: 20px 0 18px;
font-family: var(--display);
font-size: clamp(64px, 6.7vw, 104px);
font-weight: 950;
letter-spacing: -.065em;
line-height: .87;
text-transform: uppercase;
}
.hero h1 span {
display: block;
white-space: nowrap;
}
.hero h1 .headline-red {
position: relative;
width: fit-content;
color: var(--red);
}
.hero h1 .headline-red::after {
position: absolute;
right: 1%;
bottom: -5px;
left: 2%;
height: 7px;
background: var(--ink);
clip-path: polygon(0 40%, 18% 10%, 37% 45%, 58% 0, 77% 40%, 100% 10%, 98% 75%, 77% 60%, 52% 100%, 29% 65%, 2% 100%);
content: "";
}
.hero-description {
max-width: 530px;
margin: 0;
color: #3d3b34;
font-size: clamp(16px, 1.45vw, 19px);
line-height: 1.48;
}
.hero-description strong {
color: var(--ink);
}
.hero-actions {
display: flex;
flex-wrap: wrap;
gap: 13px;
margin-top: 25px;
}
.button {
display: inline-flex;
min-height: 51px;
align-items: center;
justify-content: center;
gap: 18px;
padding: 0 21px;
border: var(--border);
font-size: 12px;
font-weight: 950;
letter-spacing: .075em;
line-height: 1;
text-transform: uppercase;
transition: transform .15s ease, box-shadow .15s ease, background .15s ease;
}
.button-lime {
background: var(--lime);
box-shadow: 4px 4px 0 var(--ink);
}
.button-lime:hover {
box-shadow: 1px 1px 0 var(--ink);
transform: translate(3px, 3px);
}
.button-outline {
background: transparent;
}
.button-outline:hover {
color: var(--cream);
background: var(--ink);
}
.button-dark {
color: var(--cream);
background: var(--ink);
border-color: var(--ink);
}
.button-dark:hover {
color: var(--ink);
background: var(--lime);
}
.button-red {
background: var(--red);
box-shadow: 4px 4px 0 var(--ink);
}
.button-arrow {
font-size: 20px;
font-weight: 500;
line-height: 0;
}
.hero-proof {
display: flex;
align-items: center;
gap: 14px;
margin-top: 25px;
}
.proof-faces {
display: flex;
padding-left: 7px;
}
.proof-face {
display: grid;
width: 34px;
height: 34px;
place-items: center;
margin-left: -7px;
background: var(--pink);
border: 2px solid var(--ink);
border-radius: 50%;
font-size: 10px;
font-weight: 950;
}
.proof-face:nth-child(2) { background: var(--yellow); }
.proof-face:nth-child(3) { background: var(--lime); }
.proof-face:nth-child(4) { background: var(--red); }
.proof-copy {
font-size: 11px;
line-height: 1.35;
}
.proof-stars {
display: block;
margin-bottom: 2px;
color: var(--red);
font-size: 13px;
letter-spacing: .08em;
}
.proof-copy strong {
font-weight: 950;
}
.truck-status {
display: flex;
max-width: 530px;
align-items: center;
gap: 9px;
margin-top: 19px;
padding-top: 15px;
border-top: 1px solid rgba(23, 23, 19, .25);
font-size: 10px;
font-weight: 900;
letter-spacing: .08em;
line-height: 1.4;
text-transform: uppercase;
}
.live-dot {
position: relative;
width: 9px;
height: 9px;
flex: 0 0 auto;
background: #5eac36;
border: 1px solid var(--ink);
border-radius: 50%;
}
.live-dot::after {
position: absolute;
inset: -4px;
border: 1px solid #5eac36;
border-radius: 50%;
content: "";
animation: livePulse 2s ease-out infinite;
}
@keyframes livePulse {
0% { opacity: 1; transform: scale(.6); }
80%, 100% { opacity: 0; transform: scale(1.4); }
}
.hero-art {
position: relative;
min-width: 0;
overflow: hidden;
background: var(--red);
border-left: var(--border);
isolation: isolate;
}
.hero-art::before {
position: absolute;
z-index: -1;
width: min(38vw, 490px);
height: min(38vw, 490px);
top: 48%;
left: 50%;
background: var(--lime);
border: var(--border);
border-radius: 50%;
content: "";
transform: translate(-50%, -50%);
}
.hero-art::after {
position: absolute;
z-index: 4;
inset: 0;
pointer-events: none;
background-image: radial-gradient(rgba(23, 23, 19, .25) .7px, transparent .8px);
background-size: 8px 8px;
content: "";
mix-blend-mode: multiply;
opacity: .25;
}
.art-topline {
position: absolute;
z-index: 6;
top: 20px;
right: 22px;
left: 22px;
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 10px;
border-bottom: 1.5px solid var(--ink);
font-size: 9px;
font-weight: 950;
letter-spacing: .13em;
text-transform: uppercase;
}
.art-topline span:last-child {
font-size: 12px;
letter-spacing: .03em;
}
.food-float {
--mx: 0px;
--my: 0px;
position: absolute;
z-index: 2;
top: 3%;
right: -7%;
bottom: 7%;
left: -7%;
display: grid;
place-items: center;
transform: translate(var(--mx), var(--my));
transition: transform .25s ease-out;
}
.hero-taco {
width: 100%;
height: 100%;
overflow: visible;
animation: foodBob 5s ease-in-out infinite;
filter: saturate(1.04);
}
@keyframes foodBob {
0%, 100% { transform: translateY(3px) rotate(-1deg); }
50% { transform: translateY(-8px) rotate(1deg); }
}
.hero-seal {
position: absolute;
z-index: 7;
top: 66px;
right: 20px;
display: grid;
width: 105px;
height: 105px;
place-items: center;
padding: 15px;
background: var(--cream);
border: var(--border);
border-radius: 50%;
box-shadow: 4px 4px 0 var(--ink);
text-align: center;
transform: rotate(9deg);
animation: sealWiggle 6s ease-in-out infinite;
}
.hero-seal::before {
position: absolute;
inset: 6px;
border: 1.5px dashed var(--ink);
border-radius: 50%;
content: "";
}
.hero-seal strong {
position: relative;
font-family: var(--display);
font-size: 28px;
letter-spacing: -.04em;
line-height: .8;
}
.hero-seal small {
position: relative;
display: block;
margin-top: 7px;
font-size: 8px;
font-weight: 950;
letter-spacing: .12em;
text-transform: uppercase;
}
@keyframes sealWiggle {
0%, 100% { transform: rotate(9deg); }
50% { transform: rotate(2deg); }
}
.food-caption {
position: absolute;
z-index: 8;
right: 23px;
bottom: 23px;
left: 23px;
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 2px 12px;
padding: 15px 18px;
background: var(--cream);
border: var(--border);
box-shadow: 5px 5px 0 var(--ink);
transform: rotate(-1.5deg);
}
.food-caption small {
grid-column: 1;
font-size: 8px;
font-weight: 950;
letter-spacing: .14em;
text-transform: uppercase;
}
.food-caption strong {
grid-column: 1;
font-family: var(--display);
font-size: 28px;
letter-spacing: -.025em;
line-height: 1;
text-transform: uppercase;
}
.food-caption .caption-price {
grid-column: 2;
grid-row: 1 / 3;
font-family: var(--display);
font-size: 27px;
letter-spacing: -.03em;
}
.marquee {
height: 42px;
overflow: hidden;
background: var(--lime);
border-bottom: var(--border);
font-family: var(--display);
font-size: 20px;
font-weight: 900;
letter-spacing: .015em;
text-transform: uppercase;
}
.marquee-track {
animation-duration: 24s;
animation-direction: reverse;
}
.marquee-group span {
display: inline-flex;
align-items: center;
gap: 25px;
padding-right: 25px;
}
.marquee-group span::after {
color: var(--red);
content: "✦";
font-family: var(--body);
font-size: 15px;
}
.section {
padding: 100px 0;
}
.section-kicker {
display: inline-flex;
align-items: center;
gap: 9px;
margin-bottom: 17px;
font-size: 10px;
font-weight: 950;
letter-spacing: .15em;
text-transform: uppercase;
}
.section-kicker::before {
color: var(--red);
content: "✺";
font-size: 18px;
line-height: 1;
}
.section h2 {
margin: 0;
font-family: var(--display);
font-size: clamp(54px, 6.6vw, 90px);
font-weight: 950;
letter-spacing: -.055em;
line-height: .89;
text-transform: uppercase;
}
.section h2 .accent {
color: var(--red);
}
.menu {
background:
linear-gradient(rgba(23, 23, 19, .035) 1px, transparent 1px),
linear-gradient(90deg, rgba(23, 23, 19, .035) 1px, transparent 1px),
var(--cream);
background-size: 28px 28px;
}
.menu-heading {
display: grid;
grid-template-columns: 1.15fr .85fr;
align-items: end;
gap: 50px;
}
.menu-heading-copy {
max-width: 410px;
padding-bottom: 4px;
}
.menu-heading-copy p {
margin: 0 0 22px;
color: #4d4a42;
font-size: 17px;
line-height: 1.55;
}
.filters {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.filter-btn {
min-height: 36px;
padding: 0 14px;
background: var(--paper);
border: 1.5px solid var(--ink);
border-radius: 999px;
font-size: 10px;
font-weight: 950;
letter-spacing: .09em;
text-transform: uppercase;
transition: color .15s ease, background .15s ease;
}
.filter-btn:hover,
.filter-btn.active {
color: var(--cream);
background: var(--ink);
}
.menu-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 27px;
margin-top: 45px;
}
.menu-card {
display: flex;
min-width: 0;
flex-direction: column;
overflow: hidden;
background: var(--paper);
border: var(--border);
box-shadow: 6px 6px 0 var(--ink);
animation: cardIn .45s both;
animation-delay: var(--delay, 0ms);
transition: transform .2s ease, box-shadow .2s ease;
}
.menu-card:hover {
box-shadow: 9px 9px 0 var(--ink);
transform: translate(-3px, -3px);
}
@keyframes cardIn {
from { opacity: 0; transform: translateY(15px); }
to { opacity: 1; transform: translateY(0); }
}
.menu-visual {
position: relative;
height: 235px;
overflow: hidden;
background: var(--card-bg, var(--yellow));
border-bottom: var(--border);
isolation: isolate;
}
.menu-visual::before {
position: absolute;
z-index: -1;
width: 210px;
height: 210px;
top: 50%;
left: 50%;
background: rgba(255, 250, 241, .48);
border: 1.5px solid rgba(23, 23, 19, .6);
border-radius: 50%;
content: "";
transform: translate(-50%, -46%);
}
.menu-visual::after {
position: absolute;
z-index: -1;
inset: 0;
background-image: radial-gradient(rgba(23, 23, 19, .18) .7px, transparent .8px);
background-size: 9px 9px;
content: "";
opacity: .35;
}
.menu-visual svg {
width: 100%;
height: 100%;
transition: transform .3s cubic-bezier(.2, .8, .2, 1);
}
.menu-card:hover .menu-visual svg {
transform: rotate(-2deg) scale(1.055);
}
.food-tag {
position: absolute;
z-index: 3;
top: 13px;
left: 13px;
padding: 6px 9px;
background: var(--cream);
border: 1.5px solid var(--ink);
font-size: 8px;
font-weight: 950;
letter-spacing: .1em;
text-transform: uppercase;
}
.menu-info {
display: flex;
flex: 1;
flex-direction: column;
padding: 21px;
}
.menu-title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
}
.menu-title-row h3 {
margin: 0;
font-family: var(--display);
font-size: 27px;
font-weight: 950;
letter-spacing: -.035em;
line-height: .95;
text-transform: uppercase;
}
.price {
flex: 0 0 auto;
font-family: var(--display);
font-size: 22px;
letter-spacing: -.03em;
}
.menu-description {
min-height: 47px;
margin: 13px 0 17px;
color: #5b584f;
font-size: 13px;
line-height: 1.5;
}
.menu-meta {
display: flex;
min-height: 25px;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 17px;
}
.heat {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 8px;
font-weight: 950;
letter-spacing: .11em;
text-transform: uppercase;
}
.heat-dots {
display: inline-flex;
gap: 3px;
}
.heat-dots i {
width: 8px;
height: 8px;
background: transparent;
border: 1px solid var(--ink);
border-radius: 50%;
}
.heat-dots i.on {
background: var(--red);
}
.diet-tag {
font-size: 8px;
font-weight: 900;
letter-spacing: .08em;
text-transform: uppercase;
}
.add-btn {
display: flex;
width: 100%;
min-height: 43px;
align-items: center;
justify-content: space-between;
margin-top: auto;
padding: 0 12px 0 15px;
color: var(--cream);
background: var(--ink);
border: var(--border);
font-size: 10px;
font-weight: 950;
letter-spacing: .1em;
text-transform: uppercase;
transition: color .15s ease, background .15s ease;
}
.add-btn b {
color: var(--lime);
font-size: 24px;
font-weight: 500;
line-height: 1;
}
.add-btn:hover,
.add-btn.added {
color: var(--ink);
background: var(--lime);
}
.add-btn:hover b,
.add-btn.added b {
color: var(--ink);
}
.combo-banner {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 24px;
margin-top: 45px;
padding: 24px 27px;
background: var(--red);
border: var(--border);
box-shadow: 6px 6px 0 var(--ink);
}
.combo-burst {
display: grid;
width: 73px;
height: 73px;
place-items: center;
background: var(--lime);
border: var(--border);
border-radius: 50%;
font-family: var(--display);
font-size: 25px;
line-height: .8;
text-align: center;
transform: rotate(-8deg);
}
.combo-burst small {
display: block;
font-family: var(--body);
font-size: 7px;
font-weight: 950;
letter-spacing: .08em;
}
.combo-copy small {
display: block;
margin-bottom: 5px;
font-size: 9px;
font-weight: 950;
letter-spacing: .13em;
text-transform: uppercase;
}
.combo-copy h3 {
margin: 0 0 5px;
font-family: var(--display);
font-size: clamp(28px, 3vw, 40px);
letter-spacing: -.035em;
line-height: .95;
text-transform: uppercase;
}
.combo-copy p {
margin: 0;
font-size: 12px;
font-weight: 750;
}
.combo-add {
white-space: nowrap;
}
.menu-footnote {
margin: 24px 0 0;
color: var(--muted);
font-size: 10px;
font-weight: 750;
letter-spacing: .05em;
text-align: center;
text-transform: uppercase;
}
.find {
color: var(--cream);
background: var(--ink);
border-top: var(--border);
}
.find .section-kicker::before {
color: var(--lime);
}
.find h2 .accent {
color: var(--lime);
}
.find-heading {
display: flex;
align-items: end;
justify-content: space-between;
gap: 40px;
margin-bottom: 45px;
}
.find-heading p {
max-width: 330px;
margin: 0 0 5px;
color: #bbb8ad;
font-size: 15px;
line-height: 1.55;
}
.find-grid {
display: grid;
grid-template-columns: minmax(0, .95fr) minmax(0, 1.05fr);
gap: 28px;
}
.next-stop-card {
position: relative;
overflow: hidden;
padding: 28px;
color: var(--ink);
background: var(--lime);
border: 2px solid var(--cream);
}
.next-stop-card::after {
position: absolute;
top: -45px;
right: -35px;
width: 150px;
height: 150px;
border: 2px solid var(--ink);
border-radius: 50%;
content: "";
opacity: .16;
}
.next-label {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
margin-bottom: 19px;
font-size: 9px;
font-weight: 950;
letter-spacing: .12em;
text-transform: uppercase;
}
.next-label span:first-child {
padding: 6px 8px;
color: var(--cream);
background: var(--ink);
}
.next-stop-card h3 {
margin: 0;
font-family: var(--display);
font-size: clamp(44px, 4.5vw, 65px);
letter-spacing: -.045em;
line-height: .88;
text-transform: uppercase;
}
.next-time {
margin: 13px 0 22px;
color: var(--red-dark);
font-family: var(--display);
font-size: 27px;
letter-spacing: -.02em;
text-transform: uppercase;
}
.next-address {
display: flex;
align-items: flex-start;
gap: 11px;
padding-top: 17px;
border-top: 1.5px solid var(--ink);
}
.pin-icon {
display: grid;
width: 27px;
height: 27px;
flex: 0 0 auto;
place-items: center;
background: var(--red);
border: 1.5px solid var(--ink);
border-radius: 50% 50% 50% 0;
font-size: 12px;
transform: rotate(-45deg);
}
.pin-icon span {
transform: rotate(45deg);
}
.next-address strong {
display: block;
font-size: 13px;
line-height: 1.4;
}
.next-address small {
display: block;
margin-top: 4px;
font-size: 11px;
line-height: 1.4;
}
.schedule-heading {
display: flex;
align-items: center;
justify-content: space-between;
margin: 26px 0 12px;
color: #bbb8ad;
font-size: 9px;
font-weight: 950;
letter-spacing: .13em;
text-transform: uppercase;
}
.schedule-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.schedule-card {
display: flex;
min-width: 0;
min-height: 108px;
flex-direction: column;
align-items: flex-start;
padding: 14px 15px;
color: var(--cream);
background: #24241f;
border: 1px solid #626157;
text-align: left;
transition: color .15s ease, background .15s ease, transform .15s ease;
}
.schedule-card:hover {
border-color: var(--lime);
transform: translateY(-2px);
}
.schedule-card.active {
color: var(--ink);
background: var(--cream);
border-color: var(--cream);
}
.schedule-day {
margin-bottom: 10px;
color: var(--lime);
font-size: 8px;
font-weight: 950;
letter-spacing: .13em;
text-transform: uppercase;
}
.schedule-card.active .schedule-day {
color: var(--red-dark);
}
.schedule-card strong {
overflow: hidden;
max-width: 100%;
font-family: var(--display);
font-size: 22px;
letter-spacing: -.025em;
line-height: 1;
text-overflow: ellipsis;
text-transform: uppercase;
white-space: nowrap;
}
.schedule-card > span:last-child {
margin-top: 7px;
color: #bbb8ad;
font-size: 10px;
font-weight: 750;
}
.schedule-card.active > span:last-child {
color: #555247;
}
.map-card {
position: relative;
min-height: 100%;
overflow: hidden;
background-color: #31332b;
background-image:
linear-gradient(rgba(247, 241, 229, .08) 1px, transparent 1px),
linear-gradient(90deg, rgba(247, 241, 229, .08) 1px, transparent 1px);
background-size: 35px 35px;
border: 2px solid var(--cream);
isolation: isolate;
}
.map-card::before {
position: absolute;
width: 480px;
height: 480px;
top: -110px;
right: -180px;
background: #3f4632;
border: 2px solid rgba(247, 241, 229, .1);
border-radius: 50%;
content: "";
}
.road {
position: absolute;
z-index: -1;
height: 29px;
background: #5b5c51;
border-top: 1px solid #88887b;
border-bottom: 1px solid #88887b;
}
.road::after {
position: absolute;
top: 13px;
right: 0;
left: 0;
border-top: 1px dashed rgba(247, 241, 229, .5);
content: "";
}
.road-a {
width: 130%;
top: 29%;
left: -15%;
transform: rotate(-15deg);
}
.road-b {
width: 130%;
top: 57%;
left: -12%;
transform: rotate(24deg);
}
.road-c {
width: 110%;
top: 44%;
left: -5%;
transform: rotate(82deg);
}
.map-label {
position: absolute;
color: rgba(247, 241, 229, .55);
font-size: 8px;
font-weight: 900;
letter-spacing: .2em;
text-transform: uppercase;
}
.map-label-a { top: 18%; left: 10%; transform: rotate(-15deg); }
.map-label-b { top: 68%; right: 10%; transform: rotate(24deg); }
.map-label-c { top: 12%; right: 24%; transform: rotate(82deg); }
.map-area-label {
position: absolute;
top: 24px;
left: 25px;
max-width: calc(100% - 50px);
color: rgba(247, 241, 229, .16);
font-family: var(--display);
font-size: clamp(52px, 6vw, 80px);
letter-spacing: -.05em;
line-height: .9;
text-transform: uppercase;
overflow-wrap: anywhere;
}
.map-marker {
position: absolute;
top: 40%;
left: 51%;
display: flex;
flex-direction: column;
align-items: center;
transform: translate(-50%, -50%);
}
.marker-truck {
position: relative;
display: grid;
width: 102px;
height: 66px;
place-items: center;
color: var(--ink);
background: var(--red);
border: var(--border);
box-shadow: 5px 5px 0 rgba(0, 0, 0, .4);
font-family: var(--display);
font-size: 18px;
letter-spacing: -.04em;
line-height: .85;
text-align: center;
animation: truckBounce 3s ease-in-out infinite;
}
.marker-truck::before,
.marker-truck::after {
position: absolute;
bottom: -10px;
width: 18px;
height: 18px;
background: var(--ink);
border: 3px solid #77776b;
border-radius: 50%;
content: "";
}
.marker-truck::before { left: 12px; }
.marker-truck::after { right: 12px; }
.marker-truck span {
position: relative;
z-index: 1;
padding: 5px;
background: var(--cream);
border: 1.5px solid var(--ink);
}
@keyframes truckBounce {
0%, 100% { transform: translateY(0) rotate(-2deg); }
50% { transform: translateY(-7px) rotate(1deg); }
}
.marker-stem {
width: 2px;
height: 35px;
margin-top: 13px;
background: var(--cream);
}
.marker-dot {
width: 14px;
height: 7px;
background: var(--lime);
border: 1px solid var(--ink);
border-radius: 50%;
box-shadow: 0 0 0 9px rgba(220, 255, 69, .12);
}
.map-info {
position: absolute;
right: 20px;
bottom: 20px;
left: 20px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
padding: 18px;
color: var(--ink);
background: var(--cream);
border: var(--border);
box-shadow: 5px 5px 0 rgba(0, 0, 0, .4);
}
.map-info small {
display: block;
margin-bottom: 5px;
color: var(--red-dark);
font-size: 8px;
font-weight: 950;
letter-spacing: .13em;
text-transform: uppercase;
}
.map-info strong {
display: block;
font-size: 13px;
line-height: 1.35;
}
.save-stop {
min-height: 42px;
flex: 0 0 auto;
padding: 0 13px;
background: var(--lime);
border: var(--border);
font-size: 9px;
font-weight: 950;
letter-spacing: .08em;
text-transform: uppercase;
}
.save-stop[aria-pressed="true"] {
color: var(--cream);
background: var(--ink);
}
.story {
position: relative;
overflow: hidden;
padding-bottom: 0;
background: var(--red);
border-top: var(--border);
}
.story::before {
position: absolute;
top: 30px;
right: -30px;
color: rgba(23, 23, 19, .09);
content: "김치";
font-family: var(--display);
font-size: 270px;
font-weight: 950;
line-height: 1;
transform: rotate(8deg);
}
.story-grid {
position: relative;
display: grid;
grid-template-columns: 1.1fr .9fr;
gap: 80px;
padding-bottom: 90px;
}
.story .section-kicker::before {
color: var(--ink);
}
.story h2 {
font-size: clamp(54px, 6vw, 84px);
}
.story h2 .outline {
color: var(--cream);
}
.story-copy {
align-self: end;
padding-bottom: 5px;
}
.story-lead {
margin: 0 0 18px;
font-size: 20px;
font-weight: 800;
letter-spacing: -.02em;
line-height: 1.4;
}
.story-copy > p:not(.story-lead) {
margin: 0;
font-size: 14px;
line-height: 1.65;
}
.values {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 25px;
}
.values span {
padding: 9px 11px;
background: var(--cream);
border: 1.5px solid var(--ink);
font-size: 8px;
font-weight: 950;
letter-spacing: .09em;
text-transform: uppercase;
}
.story-ribbon {
position: relative;
padding: 22px 24px;
color: var(--cream);
background: var(--ink);
font-family: var(--display);
font-size: clamp(26px, 3vw, 42px);
letter-spacing: -.025em;
line-height: 1;
text-align: center;
text-transform: uppercase;
}
.story-ribbon span {
color: var(--lime);
}
.join {
background: var(--yellow);
border-top: var(--border);
}
.join-inner {
display: grid;
grid-template-columns: .95fr 1.05fr;
align-items: center;
gap: 70px;
}
.join h2 {
font-size: clamp(52px, 5.8vw, 80px);
}
.join-copy p {
max-width: 490px;
margin: 0 0 21px;
font-size: 15px;
line-height: 1.55;
}
.signup-fields {
display: flex;
gap: 10px;
}
.signup-fields input {
min-width: 0;
min-height: 55px;
flex: 1;
padding: 0 17px;
color: var(--ink);
background: var(--paper);
border: var(--border);
border-radius: 0;
font-size: 13px;
font-weight: 750;
outline: none;
}
.signup-fields input:focus {
box-shadow: 0 0 0 3px var(--red);
}
.signup-fields .button {
flex: 0 0 auto;
min-height: 55px;
}
.form-note {
min-height: 18px;
margin: 12px 0 0 !important;
font-size: 10px !important;
font-weight: 750;
letter-spacing: .04em;
}
.signup-form.success .signup-fields {
display: none;
}
.signup-form.success .form-note {
padding: 18px;
background: var(--lime);
border: var(--border);
font-size: 14px !important;
font-weight: 900;
}
.site-footer {
padding: 58px 0 26px;
color: var(--cream);
background: var(--ink);
border-top: 2px solid var(--ink);
}
.footer-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 40px;
padding-bottom: 50px;
}
.footer-callout {
max-width: 400px;
font-size: 14px;
line-height: 1.55;
}
.footer-callout strong {
display: block;
margin-bottom: 8px;
color: var(--lime);
font-size: 11px;
letter-spacing: .12em;
text-transform: uppercase;
}
.footer-links {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 12px 27px;
font-size: 10px;
font-weight: 900;
letter-spacing: .1em;
text-transform: uppercase;
}
.footer-links a:hover {
color: var(--lime);
}
.footer-brand {
overflow: hidden;
padding: 12px 0 20px;
border-top: 1px solid #4b4b42;
border-bottom: 1px solid #4b4b42;
font-family: var(--display);
font-size: clamp(48px, 13vw, 180px);
font-weight: 950;
letter-spacing: -.075em;
line-height: .8;
text-align: center;
text-transform: uppercase;
white-space: nowrap;
}
.footer-brand span {
color: var(--red);
}
.footer-bottom {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
padding-top: 23px;
color: #9d9b91;
font-size: 9px;
font-weight: 750;
letter-spacing: .08em;
text-transform: uppercase;
}
.footer-bottom strong {
color: var(--lime);
}
.drawer-backdrop {
position: fixed;
z-index: 90;
inset: 0;
pointer-events: none;
background: rgba(10, 10, 8, .66);
opacity: 0;
transition: opacity .3s ease;
backdrop-filter: blur(3px);
}
.drawer-backdrop.show {
pointer-events: auto;
opacity: 1;
}
.cart-drawer {
position: fixed;
z-index: 100;
top: 0;
right: 0;
display: flex;
visibility: hidden;
width: min(440px, 100%);
height: 100dvh;
flex-direction: column;
color: var(--ink);
background: var(--cream);
border-left: var(--border);
box-shadow: -15px 0 40px rgba(0, 0, 0, .25);
transform: translateX(105%);
transition: transform .34s cubic-bezier(.2, .8, .2, 1), visibility 0s .34s;
}
.cart-drawer[aria-hidden="false"] {
visibility: visible;
transform: translateX(0);
transition: transform .34s cubic-bezier(.2, .8, .2, 1), visibility 0s;
}
.drawer-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
padding: 22px;
background: var(--lime);
border-bottom: var(--border);
}
.drawer-header small {
display: block;
margin-bottom: 4px;
font-size: 8px;
font-weight: 950;
letter-spacing: .13em;
text-transform: uppercase;
}
.drawer-header h2 {
margin: 0;
font-family: var(--display);
font-size: 31px;
letter-spacing: -.04em;
line-height: 1;
text-transform: uppercase;
}
.drawer-close {
display: grid;
width: 41px;
height: 41px;
flex: 0 0 auto;
place-items: center;
background: var(--cream);
border: var(--border);
font-size: 25px;
line-height: 1;
}
.drawer-content {
flex: 1;
overflow-y: auto;
overscroll-behavior: contain;
padding: 22px;
}
.empty-cart {
display: flex;
min-height: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.empty-cart svg {
width: 120px;
margin-bottom: 20px;
}
.empty-cart h3 {
margin: 0 0 10px;
font-family: var(--display);
font-size: 35px;
letter-spacing: -.035em;
line-height: .95;
text-transform: uppercase;
}
.empty-cart p {
max-width: 280px;
margin: 0 0 22px;
color: var(--muted);
font-size: 13px;
line-height: 1.55;
}
.empty-suggestions {
display: grid;
width: 100%;
gap: 9px;
margin-top: 25px;
}
.empty-suggestions button {
display: flex;
min-height: 51px;
align-items: center;
justify-content: space-between;
padding: 0 14px;
background: var(--paper);
border: 1.5px solid var(--ink);
font-size: 11px;
font-weight: 900;
text-align: left;
}
.empty-suggestions button:hover {
background: var(--yellow);
}
.cart-row {
display: grid;
grid-template-columns: 1fr auto;
gap: 12px;
padding: 17px 0;
border-bottom: 1px solid #cbc5b8;
}
.cart-row:first-child {
padding-top: 0;
}
.cart-row h3 {
margin: 0 0 5px;
font-family: var(--display);
font-size: 22px;
letter-spacing: -.025em;
line-height: 1;
text-transform: uppercase;
}
.cart-row-price {
font-size: 13px;
font-weight: 900;
}
.quantity-control {
display: inline-flex;
align-items: center;
margin-top: 12px;
border: 1.5px solid var(--ink);
}
.quantity-control button {
display: grid;
width: 30px;
height: 29px;
place-items: center;
padding: 0;
background: var(--paper);
border: 0;
font-size: 18px;
line-height: 1;
}
.quantity-control button:hover {
background: var(--lime);
}
.quantity-control span {
display: grid;
min-width: 29px;
height: 29px;
place-items: center;
border-right: 1.5px solid var(--ink);
border-left: 1.5px solid var(--ink);
font-size: 11px;
font-weight: 950;
}
.cart-note {
margin: 18px 0 0;
padding: 12px;
background: #ebe4d6;
font-size: 10px;
font-weight: 700;
line-height: 1.5;
text-align: center;
}
.drawer-footer {
padding: 20px 22px 22px;
background: var(--paper);
border-top: var(--border);
}
.pickup-label {
display: block;
margin-bottom: 7px;
font-size: 8px;
font-weight: 950;
letter-spacing: .12em;
text-transform: uppercase;
}
.pickup-select {
width: 100%;
height: 43px;
margin-bottom: 15px;
padding: 0 12px;
color: var(--ink);
background: var(--cream);
border: 1.5px solid var(--ink);
border-radius: 0;
font-size: 11px;
font-weight: 800;
}
.cart-total {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
font-size: 12px;
font-weight: 950;
letter-spacing: .07em;
text-transform: uppercase;
}
.cart-total strong {
font-family: var(--display);
font-size: 28px;
letter-spacing: -.025em;
}
.full {
width: 100%;
}
.payment-note {
margin: 11px 0 0;
color: var(--muted);
font-size: 9px;
font-weight: 700;
text-align: center;
}
.order-success {
display: flex;
min-height: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.success-check {
display: grid;
width: 86px;
height: 86px;
place-items: center;
margin-bottom: 23px;
background: var(--lime);
border: var(--border);
border-radius: 50%;
box-shadow: 5px 5px 0 var(--ink);
font-size: 42px;
transform: rotate(-6deg);
}
.order-success h3 {
margin: 0;
font-family: var(--display);
font-size: 45px;
letter-spacing: -.045em;
line-height: .9;
text-transform: uppercase;
}
.order-number {
margin: 17px 0;
padding: 9px 13px;
color: var(--cream);
background: var(--ink);
font-size: 11px;
font-weight: 950;
letter-spacing: .12em;
}
.order-success p {
max-width: 300px;
margin: 0;
color: var(--muted);
font-size: 13px;
line-height: 1.55;
}
.toast {
position: fixed;
z-index: 120;
right: 22px;
bottom: 22px;
max-width: min(380px, calc(100% - 32px));
padding: 14px 18px;
pointer-events: none;
color: var(--ink);
background: var(--lime);
border: var(--border);
box-shadow: 4px 4px 0 var(--ink);
font-size: 11px;
font-weight: 900;
line-height: 1.4;
opacity: 0;
transform: translateY(20px);
transition: opacity .2s ease, transform .2s ease;
}
.toast.show {
opacity: 1;
transform: translateY(0);
}
@media (max-width: 1080px) {
.site-header {
padding-inline: 24px;
}
.desktop-nav {
gap: 20px;
}
.hero-copy {
padding-inline: 38px;
}
.hero-seal {
width: 91px;
height: 91px;
}
.menu-grid {
gap: 20px;
}
.menu-info {
padding: 18px;
}
.menu-title-row h3 {
font-size: 24px;
}
.combo-banner {
grid-template-columns: auto 1fr;
}
.combo-add {
grid-column: 1 / -1;
width: 100%;
}
.map-info {
align-items: flex-start;
flex-direction: column;
gap: 12px;
}
.save-stop {
width: 100%;
}
}
@media (max-width: 880px) {
.site-header {
display: flex;
justify-content: space-between;
}
.desktop-nav {
display: none;
}
.mobile-toggle {
display: block;
}
.hero {
grid-template-columns: minmax(0, 54%) minmax(0, 46%);
}
.hero-copy {
padding-inline: 28px;
}
.hero h1 {
font-size: clamp(58px, 7.5vw, 72px);
}
.eyebrow-korean {
display: none;
}
.food-caption {
right: 15px;
bottom: 17px;
left: 15px;
}
.food-caption strong {
font-size: 23px;
}
.food-caption .caption-price {
font-size: 23px;
}
.hero-seal {
top: 70px;
right: 13px;
width: 79px;
height: 79px;
}
.hero-seal strong {
font-size: 22px;
}
.menu-heading,
.join-inner {
grid-template-columns: 1fr;
gap: 30px;
}
.menu-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.find-grid {
grid-template-columns: 1fr;
}
.map-card {
min-height: 480px;
}
.map-info {
align-items: center;
flex-direction: row;
}
.save-stop {
width: auto;
}
.story-grid {
grid-template-columns: 1fr;
gap: 35px;
}
.story-copy {
max-width: 620px;
}
}
@media (max-width: 720px) {
.container {
width: min(calc(100% - 32px), 1240px);
}
.section {
padding: 75px 0;
}
.hero {
display: block;
height: auto;
min-height: 0;
max-height: none;
}
.hero-copy {
padding: 39px 24px 35px;
}
.hero h1 {
font-size: clamp(48px, 12.7vw, 78px);
}
.hero-description {
max-width: 560px;
}
.hero-art {
height: 460px;
border-top: var(--border);
border-left: 0;
}
.hero-art::before {
width: 360px;
height: 360px;
}
.food-float {
top: 2%;
right: -3%;
bottom: 4%;
left: -3%;
}
.hero-seal {
top: 62px;
right: 22px;
width: 94px;
height: 94px;
}
.food-caption {
right: 22px;
bottom: 21px;
left: 22px;
}
.find-heading {
align-items: flex-start;
flex-direction: column;
gap: 20px;
}
.story-grid {
padding-bottom: 70px;
}
.join-inner {
gap: 25px;
}
}
@media (max-width: 520px) {
.topbar {
height: 31px;
font-size: 9px;
}
.site-header {
height: 70px;
padding-inline: 16px;
}
.logo {
gap: 8px;
}
.logo-mark {
width: 35px;
height: 35px;
box-shadow: 2px 2px 0 var(--ink);
font-size: 12px;
}
.logo-word {
font-size: 19px;
}
.logo-loco {
font-size: 23px;
}
.logo-loco i {
font-size: 11px;
vertical-align: 3px;
}
.nav-actions {
gap: 7px;
}
.order-trigger {
min-height: 39px;
gap: 7px;
padding-inline: 9px;
box-shadow: 2px 2px 0 var(--ink);
font-size: 9px;
}
.wide-only {
display: none;
}
.cart-count {
min-width: 20px;
height: 20px;
font-size: 9px;
}
.mobile-toggle {
width: 39px;
height: 39px;
padding: 8px;
}
.hero-copy {
padding: 32px 22px 31px;
}
.eyebrow {
font-size: 9px;
}
.eyebrow-chip {
padding: 6px 8px;
}
.hero h1 {
margin-top: 18px;
font-size: clamp(44px, 13vw, 64px);
}
.hero-description {
font-size: 16px;
}
.hero-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 9px;
margin-top: 22px;
}
.hero-actions .button {
min-height: 48px;
gap: 7px;
padding-inline: 10px;
font-size: 9px;
}
.hero-proof {
gap: 10px;
margin-top: 23px;
}
.proof-face {
width: 30px;
height: 30px;
font-size: 8px;
}
.proof-copy {
font-size: 9px;
}
.proof-stars {
font-size: 11px;
}
.truck-status {
align-items: flex-start;
font-size: 8px;
}
.live-dot {
margin-top: 2px;
}
.hero-art {
height: 375px;
}
.hero-art::before {
width: 285px;
height: 285px;
}
.art-topline {
top: 15px;
right: 16px;
left: 16px;
font-size: 7px;
}
.art-topline span:last-child {
font-size: 10px;
}
.hero-seal {
top: 53px;
right: 13px;
width: 77px;
height: 77px;
padding: 11px;
}
.hero-seal strong {
font-size: 21px;
}
.hero-seal small {
font-size: 6px;
}
.food-caption {
right: 16px;
bottom: 17px;
left: 16px;
padding: 12px 13px;
}
.food-caption small {
font-size: 6px;
}
.food-caption strong {
font-size: 22px;
}
.food-caption .caption-price {
font-size: 21px;
}
.marquee {
height: 38px;
font-size: 18px;
}
.section h2 {
font-size: clamp(48px, 15vw, 68px);
}
.section-kicker {
font-size: 8px;
}
.menu-heading {
gap: 23px;
}
.menu-heading-copy p {
font-size: 15px;
}
.filters {
gap: 6px;
}
.filter-btn {
min-height: 34px;
padding-inline: 12px;
font-size: 8px;
}
.menu-grid {
grid-template-columns: 1fr;
gap: 24px;
margin-top: 32px;
}
.menu-visual {
height: 230px;
}
.menu-title-row h3 {
font-size: 28px;
}
.menu-description {
min-height: 0;
}
.combo-banner {
grid-template-columns: 1fr;
gap: 17px;
margin-top: 33px;
padding: 23px;
}
.combo-burst {
width: 66px;
height: 66px;
}
.combo-copy h3 {
font-size: 34px;
}
.combo-add {
grid-column: auto;
}
.menu-footnote {
line-height: 1.6;
}
.find-heading {
margin-bottom: 30px;
}
.next-stop-card {
padding: 22px;
}
.next-label {
align-items: flex-start;
flex-direction: column;
gap: 10px;
}
.next-stop-card h3 {
font-size: 48px;
}
.schedule-list {
gap: 8px;
}
.schedule-card {
min-height: 103px;
padding: 12px;
}
.schedule-card strong {
font-size: 18px;
}
.map-card {
min-height: 450px;
}
.map-info {
right: 13px;
bottom: 13px;
left: 13px;
align-items: stretch;
flex-direction: column;
padding: 15px;
}
.save-stop {
width: 100%;
}
.marker-truck {
width: 90px;
height: 59px;
}
.story::before {
font-size: 170px;
}
.story-lead {
font-size: 18px;
}
.story-ribbon {
padding: 19px 16px;
font-size: 25px;
line-height: 1.05;
}
.join h2 {
font-size: 53px;
}
.signup-fields {
flex-direction: column;
}
.signup-fields .button {
width: 100%;
}
.footer-top {
flex-direction: column;
padding-bottom: 35px;
}
.footer-links {
justify-content: flex-start;
}
.footer-brand {
padding-block: 17px;
}
.footer-bottom {
align-items: flex-start;
flex-direction: column;
gap: 10px;
line-height: 1.5;
}
.drawer-header,
.drawer-content,
.drawer-footer {
padding-right: 18px;
padding-left: 18px;
}
.toast {
right: 16px;
bottom: 16px;
left: 16px;
max-width: none;
}
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
*,
*::before,
*::after {
scroll-behavior: auto !important;
animation-duration: .01ms !important;
animation-iteration-count: 1 !important;
transition-duration: .01ms !important;
}
}
</style>
</head>
<body>
<a class="skip-link" href="#main">Skip to the good stuff</a>
<div class="topbar" aria-label="Kimchi Loco announcements">
<div class="ticker-track">
<div class="ticker-group">
<span>House-fermented kimchi</span>
<span>Hot tortillas, always</span>
<span>Los Angeles, come hungry</span>
<span>Good food. Loud flavor.</span>
<span>Vegetarian options, big energy</span>
</div>
<div class="ticker-group" aria-hidden="true">
<span>House-fermented kimchi</span>
<span>Hot tortillas, always</span>
<span>Los Angeles, come hungry</span>
<span>Good food. Loud flavor.</span>
<span>Vegetarian options, big energy</span>
</div>
</div>
</div>
<header class="site-header">
<a class="logo" href="#top" aria-label="Kimchi Loco home">
<span class="logo-mark" aria-hidden="true">김치</span>
<span class="logo-word">
<span>KIMCHI</span>
<span class="logo-loco">LOCO <i>✺</i></span>
</span>
</a>
<nav class="desktop-nav" aria-label="Main navigation">
<a href="#menu">The menu</a>
<a href="#find">Find the truck</a>
<a href="#story">Our story</a>
<a href="#join">Get the drop</a>
</nav>
<div class="nav-actions">
<button class="order-trigger" type="button" data-open-cart aria-label="Open your pickup order">
<span>ORDER <span class="wide-only">AHEAD</span></span>
<span class="cart-count">0</span>
</button>
<button class="mobile-toggle" id="mobileToggle" type="button" aria-label="Open navigation" aria-expanded="false" aria-controls="mobileNav">
<span></span><span></span><span></span>
</button>
</div>
<nav class="mobile-nav" id="mobileNav" aria-label="Mobile navigation" hidden>
<a href="#menu">The menu <span>↗</span></a>
<a href="#find">Find the truck <span>↗</span></a>
<a href="#story">Our story <span>↗</span></a>
<a href="#join">Get the drop <span>↗</span></a>
</nav>
</header>
<main id="main">
<section class="hero" id="top">
<div class="hero-copy">
<div class="eyebrow">
<span class="eyebrow-chip">LA's Korean taco truck</span>
<span class="eyebrow-korean">서울 맛 × LA 사랑</span>
</div>
<h1>
<span>Seoul food.</span>
<span>Taco truck.</span>
<span class="headline-red">Full send.</span>
</h1>
<p class="hero-description">
Charred bulgogi. Punchy house kimchi. Hot tortillas, crunchy slaw and a scandalous drizzle of loco sauce. <strong>One bite changes dinner forever.</strong>
</p>
<div class="hero-actions">
<a class="button button-lime" href="#find">
Find the truck <span class="button-arrow" aria-hidden="true">↗</span>
</a>
<a class="button button-outline" href="#menu">
See the menu <span class="button-arrow" aria-hidden="true">↓</span>
</a>
</div>
<div class="hero-proof">
<div class="proof-faces" aria-hidden="true">
<span class="proof-face">JK</span>
<span class="proof-face">MJ</span>
<span class="proof-face">AL</span>
<span class="proof-face">+1K</span>
</div>
<div class="proof-copy">
<span class="proof-stars" aria-label="Five stars">★★★★★</span>
<strong>“Worth crossing town for.”</strong><br>
— the late-night taco people
</div>
</div>
<div class="truck-status">
<span class="live-dot" aria-hidden="true"></span>
<span id="liveStop">Finding our next curbside stop…</span>
</div>
</div>
<div class="hero-art" id="heroArt">
<div class="art-topline">
<span>Handheld happiness · made to order</span>
<span>대박!</span>
</div>
<div class="food-float" id="foodFloat">
<svg class="hero-taco" viewBox="0 0 640 540" role="img" aria-labelledby="heroFoodTitle heroFoodDesc">
<title id="heroFoodTitle">The OG Bulgogi taco</title>
<desc id="heroFoodDesc">A colorful illustrated corn tortilla loaded with Korean barbecue beef, kimchi, crunchy greens, crema and sesame, with a fresh lime wedge.</desc>
<defs>
<linearGradient id="heroShellBack" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#ffe58a"/>
<stop offset=".55" stop-color="#f6bd45"/>
<stop offset="1" stop-color="#d88a22"/>
</linearGradient>
<linearGradient id="heroShellFront" x1=".15" y1="0" x2=".8" y2="1">
<stop offset="0" stop-color="#ffdf70"/>
<stop offset=".48" stop-color="#f5b63b"/>
<stop offset="1" stop-color="#df8c24"/>
</linearGradient>
<linearGradient id="heroBeef" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#a65327"/>
<stop offset=".55" stop-color="#71331e"/>
<stop offset="1" stop-color="#3d2118"/>
</linearGradient>
<linearGradient id="heroKimchi" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#ff7250"/>
<stop offset=".5" stop-color="#e83d24"/>
<stop offset="1" stop-color="#aa251a"/>
</linearGradient>
<filter id="heroShadow" x="-30%" y="-30%" width="160%" height="180%">
<feDropShadow dx="0" dy="16" stdDeviation="12" flood-color="#171713" flood-opacity=".35"/>
</filter>
</defs>
<ellipse cx="324" cy="454" rx="211" ry="29" fill="#171713" opacity=".22"/>
<g filter="url(#heroShadow)" transform="rotate(-4 320 290)">
<path d="M112 333C128 181 239 91 382 107C487 119 548 212 530 340L482 351C489 251 449 184 374 163C267 134 174 213 153 351Z" fill="url(#heroShellBack)" stroke="#9f631e" stroke-width="4"/>
<path d="M140 328C160 208 260 130 374 151C462 167 506 238 496 338C391 366 248 364 140 328Z" fill="#29351e"/>
<path d="M137 314C151 294 163 299 174 277C187 254 199 272 216 250C235 225 246 252 263 236C282 217 296 238 315 225C333 211 345 232 364 224C387 213 397 239 416 234C438 228 442 253 459 256C482 260 474 280 494 294L502 339C390 370 249 363 137 329Z" fill="#729c3d" stroke="#3c612a" stroke-width="4" stroke-linejoin="round"/>
<path d="M157 305C184 284 193 302 218 274C238 252 251 275 275 253C301 230 312 257 339 242C361 230 379 250 396 245C419 239 434 266 458 267" fill="none" stroke="#b5ce56" stroke-width="9" stroke-linecap="round"/>
<g fill="url(#heroKimchi)" stroke="#9d2b1c" stroke-width="2.5">
<path d="M174 304C178 269 195 236 217 226L239 240C213 259 213 288 207 320Z"/>
<path d="M222 309C218 277 240 240 261 224L279 244C257 263 257 293 253 325Z"/>
<path d="M289 319C279 280 299 237 323 224L342 240C319 261 322 298 321 328Z"/>
<path d="M353 321C345 283 365 243 388 236L407 256C384 272 388 304 384 333Z"/>
<path d="M418 326C407 297 426 267 448 262L466 281C446 294 449 315 448 337Z"/>
</g>
<g fill="#f9a488" opacity=".7">
<path d="M190 291C190 266 202 249 216 241" fill="none" stroke="#ffb094" stroke-width="5" stroke-linecap="round"/>
<path d="M305 299C300 273 311 251 325 241" fill="none" stroke="#ffb094" stroke-width="5" stroke-linecap="round"/>
<path d="M369 302C365 280 375 259 389 252" fill="none" stroke="#ffb094" stroke-width="5" stroke-linecap="round"/>
</g>
<g fill="url(#heroBeef)" stroke="#442417" stroke-width="3">
<rect x="168" y="281" width="76" height="43" rx="14" transform="rotate(-18 206 302)"/>
<rect x="227" y="255" width="82" height="45" rx="14" transform="rotate(12 268 278)"/>
<rect x="293" y="278" width="86" height="46" rx="15" transform="rotate(-9 336 301)"/>
<rect x="367" y="264" width="77" height="45" rx="14" transform="rotate(15 405 286)"/>
<rect x="425" y="295" width="66" height="40" rx="13" transform="rotate(-13 458 315)"/>
<rect x="248" y="307" width="85" height="40" rx="14" transform="rotate(-4 290 327)"/>
<rect x="343" y="309" width="82" height="41" rx="14" transform="rotate(7 384 330)"/>
</g>
<g fill="none" stroke="#c2733c" stroke-width="4" stroke-linecap="round" opacity=".72">
<path d="M183 291l35-11M243 268l41 8M310 294l43-8M384 277l37 10M439 307l33-7M266 323l43-3M360 324l42 5"/>
</g>
<g fill="none" stroke="#251b15" stroke-width="3" stroke-linecap="round" opacity=".75">
<path d="M188 301l26-8M250 279l27 5M320 305l29-6M391 289l26 7M271 334l30-2M369 336l28 3"/>
</g>
<g fill="#8bb84e" stroke="#365f2b" stroke-width="2">
<path d="M210 259C194 245 194 230 201 220C216 226 224 238 220 255C228 236 240 232 252 235C250 251 235 262 210 259Z"/>
<path d="M348 253C333 237 337 221 345 213C359 223 364 237 357 251C369 234 383 234 394 240C387 255 372 261 348 253Z"/>
<path d="M431 281C417 266 420 252 428 244C442 252 447 266 440 279C451 266 464 266 474 273C466 286 451 290 431 281Z"/>
</g>
<g fill="none" stroke="#fff4d7" stroke-width="8" stroke-linecap="round">
<path d="M177 318C203 302 224 323 248 307S291 316 312 305S351 316 373 306S415 319 441 304S468 313 480 309"/>
</g>
<g fill="none" stroke="#e4d5b8" stroke-width="2" stroke-linecap="round" opacity=".8">
<path d="M177 322C203 306 224 327 248 311S291 320 312 309S351 320 373 310S415 323 441 308S468 317 480 313"/>
</g>
<g fill="#fff0b9" stroke="#a77a36" stroke-width=".8">
<ellipse cx="206" cy="275" rx="5" ry="2.2" transform="rotate(30 206 275)"/>
<ellipse cx="239" cy="294" rx="5" ry="2.2" transform="rotate(-20 239 294)"/>
<ellipse cx="277" cy="247" rx="5" ry="2.2" transform="rotate(40 277 247)"/>
<ellipse cx="313" cy="269" rx="5" ry="2.2" transform="rotate(-35 313 269)"/>
<ellipse cx="351" cy="288" rx="5" ry="2.2" transform="rotate(15 351 288)"/>
<ellipse cx="398" cy="249" rx="5" ry="2.2" transform="rotate(-25 398 249)"/>
<ellipse cx="430" cy="299" rx="5" ry="2.2" transform="rotate(38 430 299)"/>
<ellipse cx="294" cy="322" rx="5" ry="2.2" transform="rotate(-15 294 322)"/>
<ellipse cx="378" cy="329" rx="5" ry="2.2" transform="rotate(30 378 329)"/>
</g>
<g fill="none" stroke="#d9efcf" stroke-width="4">
<ellipse cx="267" cy="281" rx="14" ry="7" transform="rotate(-20 267 281)"/>
<ellipse cx="408" cy="306" rx="13" ry="6" transform="rotate(16 408 306)"/>
</g>
<path d="M109 324C222 359 403 365 534 330C517 405 439 446 324 449C207 450 130 403 109 324Z" fill="url(#heroShellFront)" stroke="#9f631e" stroke-width="4"/>
<path d="M123 337C239 368 400 371 518 343" fill="none" stroke="#ffe79a" stroke-width="7" stroke-linecap="round" opacity=".75"/>
<path d="M142 366C165 411 232 433 320 434" fill="none" stroke="#ffda70" stroke-width="6" stroke-linecap="round" opacity=".5"/>
<g fill="#bd7823" opacity=".58">
<circle cx="156" cy="357" r="4"/>
<circle cx="187" cy="388" r="3.5"/>
<circle cx="221" cy="363" r="4"/>
<circle cx="251" cy="409" r="4.5"/>
<circle cx="286" cy="381" r="3"/>
<circle cx="327" cy="418" r="4"/>
<circle cx="363" cy="377" r="4.5"/>
<circle cx="400" cy="410" r="3.5"/>
<circle cx="437" cy="371" r="4"/>
<circle cx="479" cy="361" r="3.5"/>
<circle cx="207" cy="420" r="2.5"/>
<circle cx="298" cy="350" r="3"/>
<circle cx="389" cy="353" r="2.5"/>
<circle cx="455" cy="401" r="3"/>
</g>
</g>
<g transform="translate(472 393) rotate(-16)" filter="url(#heroShadow)">
<path d="M0 64A76 76 0 0 1 76-12L0 0Z" fill="#e7f68a" stroke="#426529" stroke-width="4"/>
<path d="M8 54A63 63 0 0 1 65-3L8 8Z" fill="#b8d951"/>
<path d="M8 8L55 5M8 8L42 43M8 8L18 55" fill="none" stroke="#f3ffc2" stroke-width="4"/>
<path d="M0 0L76-12" stroke="#568232" stroke-width="8" stroke-linecap="round"/>
</g>
</svg>
</div>
<div class="hero-seal" aria-hidden="true">
<div>
<strong>100%<br>LOCO</strong>
<small>House kimchi</small>
</div>
</div>
<div class="food-caption">
<small>The one everyone orders</small>
<strong>OG Bulgogi</strong>
<span class="caption-price">$5.50</span>
</div>
</div>
</section>
<div class="marquee" aria-hidden="true">
<div class="marquee-track">
<div class="marquee-group">
<span>Bulgogi tacos</span>
<span>Kimchi fried rice</span>
<span>Gochu-queso</span>
<span>Loco street fries</span>
<span>Cold agua fresca</span>
<span>Zero boring bites</span>
</div>
<div class="marquee-group">
<span>Bulgogi tacos</span>
<span>Kimchi fried rice</span>
<span>Gochu-queso</span>
<span>Loco street fries</span>
<span>Cold agua fresca</span>
<span>Zero boring bites</span>
</div>
</div>
</div>
<section class="section menu" id="menu">
<div class="container">
<div class="menu-heading">
<div>
<span class="section-kicker">Built by hand. Devoured in minutes.</span>
<h2>Pick your<br><span class="accent">happy place.</span></h2>
</div>
<div class="menu-heading-copy">
<p>Every taco hits the plancha when you order. Every sauce is made in-house. Every napkin is absolutely necessary.</p>
<div class="filters" aria-label="Filter menu">
<button class="filter-btn active" type="button" data-filter="all" aria-pressed="true">Everything</button>
<button class="filter-btn" type="button" data-filter="tacos" aria-pressed="false">Tacos</button>
<button class="filter-btn" type="button" data-filter="bowls" aria-pressed="false">Bowls</button>
<button class="filter-btn" type="button" data-filter="sides" aria-pressed="false">Sides</button>
</div>
</div>
</div>
<div class="menu-grid" id="menuGrid"></div>
<div class="combo-banner">
<div class="combo-burst" aria-hidden="true">
<div>SAVE<br>$4<small>COMBO</small></div>
</div>
<div class="combo-copy">
<small>When choosing feels impossible</small>
<h3>The Loco Combo — $18</h3>
<p>Any 2 tacos + Loco street fries + a cold agua fresca.</p>
</div>
<button class="button button-lime combo-add" type="button" data-add="combo">
Add the combo <span class="button-arrow" aria-hidden="true">+</span>
</button>
</div>
<p class="menu-footnote">Vegetarian options always · gluten-free tortillas available · extra napkins encouraged</p>
</div>
</section>
<section class="section find" id="find">
<div class="container">
<div class="find-heading">
<div>
<span class="section-kicker">Your next great decision is parked nearby</span>
<h2>Catch us<br><span class="accent">curbside.</span></h2>
</div>
<p>Follow the smell of caramelized bulgogi. Look for the red truck, the lime flag and the very happy line.</p>
</div>
<div class="find-grid">
<div>
<div class="next-stop-card">
<div class="next-label">
<span id="nextLabel">Today's stop</span>
<span id="nextDate">This week</span>
</div>
<h3 id="nextArea">Los Angeles</h3>
<div class="next-time" id="nextTime">5–10 PM</div>
<div class="next-address">
<span class="pin-icon" aria-hidden="true"><span>✦</span></span>
<div>
<strong id="nextAddress">Loading the next stop…</strong>
<small id="nextNote">Fresh tacos are never far away.</small>
</div>
</div>
</div>
<div class="schedule-heading">
<span>This week on the street</span>
<span>Tap a stop ↓</span>
</div>
<div class="schedule-list" id="scheduleList"></div>
</div>
<div class="map-card" id="mapCard">
<div class="road road-a"></div>
<div class="road road-b"></div>
<div class="road road-c"></div>
<span class="map-label map-label-a">Alameda St</span>
<span class="map-label map-label-b">Loco Ave</span>
<span class="map-label map-label-c">Kimchi Blvd</span>
<div class="map-area-label" id="mapArea">Los Angeles</div>
<div class="map-marker" aria-hidden="true">
<div class="marker-truck"><span>KIMCHI<br>LOCO</span></div>
<div class="marker-stem"></div>
<div class="marker-dot"></div>
</div>
<div class="map-info">
<div>
<small>Look for the lime flag</small>
<strong id="mapAddress">Red truck. Big flavor. You found us.</strong>
</div>
<button class="save-stop" id="saveStop" type="button" aria-pressed="false">+ Save this stop</button>
</div>
</div>
</div>
</div>
</section>
<section class="section story" id="story">
<div class="container story-grid">
<div>
<span class="section-kicker">Two family recipes. One very good idea.</span>
<h2>Not fusion.<br><span class="outline">A flavor</span><br>block party.</h2>
</div>
<div class="story-copy">
<p class="story-lead">Kimchi Loco started with two families, one tiny plancha, and the belief that the best food happens between cultures.</p>
<p>Our kimchi ferments for days. Our bulgogi marinade takes its sweet time. Our tortillas get pressed every morning. Then we bring it all to the street and cook it hot, fast and exactly how we’d make it at home.</p>
<div class="values">
<span>Kimchi fermented in-house</span>
<span>Tortillas pressed daily</span>
<span>Sauces from scratch</span>
<span>No shortcuts</span>
</div>
</div>
</div>
<div class="story-ribbon">Come for the taco. Leave planning <span>your next taco.</span></div>
</section>
<section class="section join" id="join">
<div class="container join-inner">
<div>
<span class="section-kicker">Secret menu drops + truck locations</span>
<h2>Get the<br>good gossip.</h2>
</div>
<div class="join-copy">
<p>Be first to know where we’re parking, what we’re testing and when the late-night Loco menu makes its glorious return.</p>
<form class="signup-form" id="signupForm">
<div class="signup-fields">
<label class="sr-only" for="signupEmail">Email address</label>
<input id="signupEmail" type="email" autocomplete="email" placeholder="YOUR@EMAIL.COM" required>
<button class="button button-dark" type="submit">Join the Loco list <span aria-hidden="true">↗</span></button>
</div>
<p class="form-note" id="formNote" role="status">One delicious email a week. Zero spam. Unsubscribe whenever.</p>
</form>
</div>
</div>
</section>
</main>
<footer class="site-footer">
<div class="container">
<div class="footer-top">
<div class="footer-callout">
<strong>Pull up hungry. Leave happy.</strong>
Korean BBQ, Mexican soul, and enough house kimchi to make your jeans feel optimistic.
</div>
<nav class="footer-links" aria-label="Footer navigation">
<a href="#menu">Menu</a>
<a href="#find">Locations</a>
<a href="#story">Our story</a>
<a href="#join">Get the drop</a>
<a href="#top">Back to top ↑</a>
</nav>
</div>
<div class="footer-brand">KIMCHI <span>LOCO</span></div>
<div class="footer-bottom">
<span>© <span id="year"></span> Kimchi Loco · Los Angeles, California</span>
<span><strong>서울 맛.</strong> LA love. Taco napkins required.</span>
</div>
</div>
</footer>
<div class="drawer-backdrop" id="drawerBackdrop" aria-hidden="true"></div>
<aside class="cart-drawer" id="cartDrawer" aria-hidden="true" aria-labelledby="cartTitle">
<div class="drawer-header">
<div>
<small>Skip the line · pay at the window</small>
<h2 id="cartTitle">Your Loco order</h2>
</div>
<button class="drawer-close" type="button" data-close-cart aria-label="Close order">×</button>
</div>
<div class="drawer-content" id="cartContent"></div>
<div class="drawer-footer" id="cartFooter"></div>
</aside>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script>
const $ = (selector, parent = document) => parent.querySelector(selector);
const $$ = (selector, parent = document) => Array.from(parent.querySelectorAll(selector));
const menuItems = [
{
id: "bulgogi",
name: "OG Bulgogi",
price: 5.50,
category: "tacos",
tag: "#1 Best seller",
description: "Charred soy-garlic ribeye, kimchi slaw, scallion, sesame and loco sauce.",
heat: 1,
bg: "#dcff45",
color: "#7d371f",
accent: "#ef4a2e",
art: "taco",
diet: "House favorite"
},
{
id: "pork",
name: "Spicy Pork",
price: 5.25,
category: "tacos",
tag: "Bring the heat",
description: "Gochujang pork, charred pineapple, pickled onion, cilantro and crema.",
heat: 3,
bg: "#ff9eb1",
color: "#a33b26",
accent: "#ffcc4e",
art: "taco",
diet: "Sweet + spicy"
},
{
id: "mushroom",
name: "Seoul Shroom",
price: 5.00,
category: "tacos",
tag: "Plant-powered",
description: "Crispy sesame mushrooms, kimchi slaw, cucumber and vegan gochu-mayo.",
heat: 1,
bg: "#ffc94f",
color: "#70503a",
accent: "#6b963d",
art: "taco",
diet: "Vegan"
},
{
id: "rice",
name: "Kimchi Fried Rice",
price: 11.00,
category: "bowls",
tag: "Comfort in a bowl",
description: "Smoky kimchi rice, crispy edges, fried egg, scallion, nori and sesame.",
heat: 2,
bg: "#ff9e78",
art: "rice",
diet: "Add bulgogi +$4"
},
{
id: "fries",
name: "Loco Street Fries",
price: 10.00,
category: "sides",
tag: "Built for sharing-ish",
description: "Crispy fries, bulgogi, gochu-queso, kimchi, jalapeño, crema and scallion.",
heat: 2,
bg: "#dcff45",
art: "fries",
diet: "A whole situation"
},
{
id: "queso",
name: "Gochu-Queso",
price: 7.00,
category: "sides",
tag: "Dip into trouble",
description: "Velvety gochujang cheese, toasted sesame, scallion and warm corn chips.",
heat: 1,
bg: "#ffc94f",
art: "queso",
diet: "Vegetarian"
},
{
id: "combo",
name: "The Loco Combo",
price: 18.00,
category: "combo",
description: "Any 2 tacos, Loco street fries and a cold agua fresca.",
inMenu: false
}
];
const money = value => `$${value.toFixed(2)}`;
const getItem = id => menuItems.find(item => item.id === id);
function tacoArt(item) {
return `
<svg viewBox="0 0 340 230" aria-hidden="true">
<ellipse cx="172" cy="190" rx="107" ry="15" fill="#171713" opacity=".2"/>
<g transform="rotate(-5 170 120)">
<path d="M63 153C70 78 126 37 194 46C250 53 282 99 274 159L249 166C252 112 226 81 188 76C136 69 96 104 89 166Z" fill="#f6bd48" stroke="#8f5b21" stroke-width="3"/>
<path d="M79 150C94 89 139 61 190 69C236 75 260 111 254 158C202 174 130 174 79 150Z" fill="#456c2f"/>
<path d="M75 145C86 129 95 139 105 119C115 101 127 117 138 101C151 82 162 101 176 91C191 79 201 99 214 94C231 87 237 107 250 111C266 116 259 135 270 142L269 160C202 178 127 172 75 151Z" fill="#7ea846" stroke="#3e662c" stroke-width="3"/>
<g fill="${item.color}" stroke="#45251b" stroke-width="2.5">
<rect x="90" y="125" width="57" height="31" rx="10" transform="rotate(-16 118 140)"/>
<rect x="131" y="105" width="61" height="33" rx="10" transform="rotate(10 161 121)"/>
<rect x="177" y="126" width="65" height="33" rx="10" transform="rotate(-8 209 142)"/>
<rect x="131" y="143" width="66" height="30" rx="10" transform="rotate(4 164 158)"/>
<rect x="207" y="111" width="45" height="29" rx="9" transform="rotate(15 229 125)"/>
</g>
<g fill="${item.accent}" stroke="#a63222" stroke-width="2">
<path d="M97 129C99 104 112 88 126 82L139 95C121 107 121 122 119 140Z"/>
<path d="M154 121C152 98 166 80 181 75L194 90C179 99 179 114 177 131Z"/>
<path d="M214 135C208 111 222 96 238 93L250 108C235 116 237 129 235 142Z"/>
</g>
<g fill="none" stroke="#fff3d8" stroke-width="6" stroke-linecap="round">
<path d="M89 146C108 133 123 151 141 137S174 148 190 137S222 148 249 134"/>
</g>
<g fill="#fff2bd">
<ellipse cx="122" cy="113" rx="4" ry="1.8" transform="rotate(30 122 113)"/>
<ellipse cx="159" cy="133" rx="4" ry="1.8" transform="rotate(-25 159 133)"/>
<ellipse cx="193" cy="105" rx="4" ry="1.8" transform="rotate(20 193 105)"/>
<ellipse cx="222" cy="133" rx="4" ry="1.8" transform="rotate(-30 222 133)"/>
</g>
<path d="M60 148C119 170 216 175 277 153C267 195 224 213 166 211C108 209 72 185 60 148Z" fill="#f3b23d" stroke="#8f5b21" stroke-width="3"/>
<path d="M72 159C126 177 214 181 264 164" fill="none" stroke="#ffe384" stroke-width="5" stroke-linecap="round"/>
<g fill="#bd7725" opacity=".6">
<circle cx="99" cy="175" r="3"/><circle cx="129" cy="192" r="3"/>
<circle cx="159" cy="179" r="2.5"/><circle cx="193" cy="195" r="3"/>
<circle cx="226" cy="178" r="3"/><circle cx="246" cy="188" r="2.5"/>
</g>
</g>
</svg>`;
}
function riceArt() {
return `
<svg viewBox="0 0 340 230" aria-hidden="true">
<ellipse cx="171" cy="196" rx="107" ry="14" fill="#171713" opacity=".2"/>
<path d="M74 103C79 58 120 38 169 39C224 39 262 65 266 108Z" fill="#f2d69a" stroke="#7c502b" stroke-width="3"/>
<g fill="#fff1cf">
<circle cx="102" cy="87" r="18"/><circle cx="127" cy="66" r="20"/>
<circle cx="155" cy="72" r="23"/><circle cx="187" cy="61" r="20"/>
<circle cx="215" cy="75" r="23"/><circle cx="239" cy="92" r="18"/>
</g>
<g fill="#ed4c2f" stroke="#a62e20" stroke-width="2">
<path d="M104 78l19-18 17 13-10 23-22 2Z"/>
<path d="M193 55l22 4 4 23-22 7-15-15Z"/>
<path d="M221 95l20-13 14 16-12 17-23-3Z"/>
<path d="M137 102l19-15 18 11-7 21-25 1Z"/>
</g>
<g fill="#7d371f">
<rect x="89" y="97" width="28" height="13" rx="5" transform="rotate(-14 103 103)"/>
<rect x="177" y="91" width="31" height="14" rx="5" transform="rotate(12 192 98)"/>
<rect x="211" y="62" width="25" height="12" rx="5" transform="rotate(-18 223 68)"/>
</g>
<ellipse cx="165" cy="76" rx="42" ry="35" fill="#fff8df" stroke="#c8aa70" stroke-width="2"/>
<ellipse cx="165" cy="77" rx="22" ry="20" fill="#ffbd24" stroke="#d98b16" stroke-width="2"/>
<ellipse cx="158" cy="70" rx="7" ry="5" fill="#ffe788" opacity=".8"/>
<path d="M59 105H280L255 178C248 198 99 198 87 177Z" fill="#252520" stroke="#171713" stroke-width="4"/>
<path d="M71 119H268" stroke="#f7f1e5" stroke-width="4" opacity=".45"/>
<path d="M104 175C139 184 204 184 239 175" fill="none" stroke="#ff5338" stroke-width="5" stroke-linecap="round"/>
<g fill="#75a84b">
<rect x="111" y="48" width="7" height="24" rx="3" transform="rotate(-35 114 60)"/>
<rect x="230" y="46" width="7" height="25" rx="3" transform="rotate(30 233 58)"/>
<rect x="137" y="45" width="6" height="20" rx="3" transform="rotate(15 140 55)"/>
</g>
</svg>`;
}
function friesArt() {
return `
<svg viewBox="0 0 340 230" aria-hidden="true">
<ellipse cx="170" cy="193" rx="119" ry="14" fill="#171713" opacity=".2"/>
<g fill="#f8c84f" stroke="#a56a21" stroke-width="2.5">
<rect x="87" y="61" width="22" height="103" rx="6" transform="rotate(-24 98 112)"/>
<rect x="116" y="45" width="22" height="119" rx="6" transform="rotate(-12 127 104)"/>
<rect x="146" y="40" width="22" height="124" rx="6" transform="rotate(-3 157 102)"/>
<rect x="177" y="43" width="22" height="122" rx="6" transform="rotate(7 188 104)"/>
<rect x="207" y="51" width="22" height="112" rx="6" transform="rotate(16 218 107)"/>
<rect x="235" y="67" width="22" height="96" rx="6" transform="rotate(27 246 115)"/>
<rect x="135" y="65" width="21" height="100" rx="6" transform="rotate(19 145 115)"/>
<rect x="185" y="65" width="21" height="99" rx="6" transform="rotate(-19 195 114)"/>
</g>
<path d="M47 118L291 118L266 190H72Z" fill="#f7f1e5" stroke="#171713" stroke-width="4"/>
<path d="M61 132H278L263 177H77Z" fill="#ff5338" opacity=".9"/>
<g fill="#74321f" stroke="#3e241a" stroke-width="2">
<rect x="83" y="115" width="48" height="28" rx="8" transform="rotate(-8 107 129)"/>
<rect x="137" y="128" width="51" height="28" rx="8" transform="rotate(9 162 142)"/>
<rect x="193" y="112" width="51" height="29" rx="8" transform="rotate(-7 218 126)"/>
<rect x="220" y="145" width="40" height="25" rx="8" transform="rotate(12 240 157)"/>
</g>
<g fill="#ec4a2d">
<path d="M105 145l18-16 19 13-7 22-25 1Z"/>
<path d="M177 105l18-13 18 12-8 20-23 1Z"/>
</g>
<path d="M69 139C95 122 112 153 137 133S179 151 200 132S239 150 268 128" fill="none" stroke="#ffd66f" stroke-width="10" stroke-linecap="round"/>
<path d="M78 153C104 136 121 165 146 147S188 163 209 145S243 157 262 146" fill="none" stroke="#fff2d9" stroke-width="5" stroke-linecap="round"/>
<g fill="#6b9c43">
<circle cx="112" cy="112" r="7"/><circle cx="155" cy="105" r="7"/>
<circle cx="204" cy="152" r="7"/><circle cx="250" cy="112" r="7"/>
</g>
</svg>`;
}
function quesoArt() {
return `
<svg viewBox="0 0 340 230" aria-hidden="true">
<ellipse cx="168" cy="191" rx="113" ry="15" fill="#171713" opacity=".2"/>
<g fill="#f4c75d" stroke="#9f6b26" stroke-width="3">
<path d="M72 130L85 48L147 114Z"/>
<path d="M116 111L161 37L192 121Z"/>
<path d="M177 117L231 45L249 131Z"/>
<path d="M213 128L277 76L270 155Z"/>
</g>
<g fill="#b67d2c" opacity=".65">
<circle cx="94" cy="82" r="3"/><circle cx="113" cy="105" r="3"/>
<circle cx="157" cy="72" r="3"/><circle cx="170" cy="102" r="3"/>
<circle cx="224" cy="85" r="3"/><circle cx="250" cy="117" r="3"/>
</g>
<path d="M66 108H274L250 176C243 197 99 197 88 176Z" fill="#ff5338" stroke="#171713" stroke-width="4"/>
<ellipse cx="170" cy="108" rx="104" ry="42" fill="#f7f1e5" stroke="#171713" stroke-width="4"/>
<ellipse cx="170" cy="111" rx="88" ry="30" fill="#f6a928" stroke="#b7651d" stroke-width="2"/>
<path d="M99 112C127 94 142 122 167 105S210 120 239 101" fill="none" stroke="#ffd867" stroke-width="8" stroke-linecap="round"/>
<g fill="#e74a2e">
<circle cx="132" cy="110" r="5"/><circle cx="191" cy="98" r="5"/>
<circle cx="215" cy="118" r="5"/><circle cx="159" cy="124" r="4"/>
</g>
<g fill="#6e9c43">
<rect x="123" y="88" width="6" height="22" rx="3" transform="rotate(-40 126 99)"/>
<rect x="199" y="90" width="6" height="22" rx="3" transform="rotate(34 202 101)"/>
<rect x="165" y="91" width="6" height="19" rx="3" transform="rotate(8 168 100)"/>
</g>
<path d="M108 173C141 183 201 183 233 172" fill="none" stroke="#f7f1e5" stroke-width="4" stroke-linecap="round" opacity=".6"/>
</svg>`;
}
function menuArt(item) {
if (item.art === "rice") return riceArt();
if (item.art === "fries") return friesArt();
if (item.art === "queso") return quesoArt();
return tacoArt(item);
}
function heatMarkup(level) {
const names = ["No heat", "Mild", "Medium", "Hot"];
const dots = [1, 2, 3].map(number => `<i class="${number <= level ? "on" : ""}"></i>`).join("");
return `<span class="heat" aria-label="Heat level: ${names[level]}"><span>${names[level]}</span><span class="heat-dots" aria-hidden="true">${dots}</span></span>`;
}
const menuGrid = $("#menuGrid");
function renderMenu(filter = "all") {
const visible = menuItems.filter(item => item.inMenu !== false && (filter === "all" || item.category === filter));
menuGrid.innerHTML = visible.map((item, index) => `
<article class="menu-card" style="--delay:${index * 55}ms">
<div class="menu-visual" style="--card-bg:${item.bg}">
<span class="food-tag">${item.tag}</span>
${menuArt(item)}
</div>
<div class="menu-info">
<div class="menu-title-row">
<h3>${item.name}</h3>
<span class="price">${money(item.price)}</span>
</div>
<p class="menu-description">${item.description}</p>
<div class="menu-meta">
${heatMarkup(item.heat)}
<span class="diet-tag">${item.diet}</span>
</div>
<button class="add-btn" type="button" data-add="${item.id}" aria-label="Add ${item.name} to your order">
<span>Add to order</span><b aria-hidden="true">+</b>
</button>
</div>
</article>
`).join("");
}
renderMenu();
$$(".filter-btn").forEach(button => {
button.addEventListener("click", () => {
$$(".filter-btn").forEach(item => {
const active = item === button;
item.classList.toggle("active", active);
item.setAttribute("aria-pressed", String(active));
});
renderMenu(button.dataset.filter);
});
});
const stopsByDay = [
{ area: "Venice", address: "1605 Abbot Kinney Blvd, Venice", time: "12–8 PM", note: "Two blocks from the beach. Follow the lime flag." },
null,
{ area: "Westwood", address: "10886 Le Conte Ave, Westwood", time: "5–10 PM", note: "Curbside near Westwood Village. Dinner = handled." },
{ area: "Culver City", address: "8800 Venice Blvd, Culver City", time: "5–10 PM", note: "Outside the Helms Bakery District. Bring your crew." },
{ area: "Echo Park", address: "1521 Sunset Blvd, Echo Park", time: "5–11 PM", note: "Late-night taco energy near the stadium." },
{ area: "Arts District", address: "777 S Alameda St, DTLA", time: "5–11 PM", note: "Find us by the neon. Friday night just got better." },
{ area: "Los Feliz", address: "1800 N Vermont Ave, Los Feliz", time: "12–10 PM", note: "Curbside all day. Vermont has never smelled this good." }
];
const shortDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const upcoming = [];
const now = new Date();
for (let offset = 0; offset < 8 && upcoming.length < 4; offset++) {
const date = new Date(now.getFullYear(), now.getMonth(), now.getDate() + offset);
const stop = stopsByDay[date.getDay()];
if (stop) {
upcoming.push({
...stop,
date,
offset,
key: `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`
});
}
}
let selectedStop = 0;
let savedStopKey = null;
const scheduleList = $("#scheduleList");
const saveStopButton = $("#saveStop");
function renderSchedule() {
scheduleList.innerHTML = upcoming.map((stop, index) => {
const day = stop.offset === 0 ? "Today" : stop.offset === 1 ? "Tomorrow" : shortDays[stop.date.getDay()];
const date = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric" }).format(stop.date);
return `
<button class="schedule-card ${index === selectedStop ? "active" : ""}" type="button" data-stop-index="${index}" aria-pressed="${index === selectedStop}">
<span class="schedule-day">${day} · ${date}</span>
<strong>${stop.area}</strong>
<span>${stop.time}</span>
</button>`;
}).join("");
}
function updateSaveButton() {
const saved = upcoming[selectedStop] && savedStopKey === upcoming[selectedStop].key;
saveStopButton.setAttribute("aria-pressed", String(saved));
saveStopButton.textContent = saved ? "✓ Stop saved" : "+ Save this stop";
}
function updateSelectedStop() {
const stop = upcoming[selectedStop];
if (!stop) return;
$("#nextLabel").textContent = stop.offset === 0 ? "Today's stop" : stop.offset === 1 ? "Tomorrow's stop" : "Next selected stop";
$("#nextDate").textContent = new Intl.DateTimeFormat("en-US", {
weekday: "long",
month: "long",
day: "numeric"
}).format(stop.date);
$("#nextArea").textContent = stop.area;
$("#nextTime").textContent = stop.time;
$("#nextAddress").textContent = stop.address;
$("#nextNote").textContent = stop.note;
$("#mapArea").textContent = stop.area;
$("#mapAddress").textContent = stop.address;
updateSaveButton();
}
const firstStop = upcoming[0];
if (firstStop) {
const prefix = firstStop.offset === 0 ? "Today" : firstStop.offset === 1 ? "Tomorrow" : shortDays[firstStop.date.getDay()];
$("#liveStop").textContent = `${prefix} · ${firstStop.area} · ${firstStop.time} · ${firstStop.address.split(",")[0]}`;
}
renderSchedule();
updateSelectedStop();
scheduleList.addEventListener("click", event => {
const card = event.target.closest("[data-stop-index]");
if (!card) return;
selectedStop = Number(card.dataset.stopIndex);
renderSchedule();
updateSelectedStop();
const marker = $(".map-marker");
marker.animate(
[{ transform: "translate(-50%, -50%) scale(.9)" }, { transform: "translate(-50%, -50%) scale(1.06)" }, { transform: "translate(-50%, -50%) scale(1)" }],
{ duration: 350, easing: "ease-out" }
);
});
saveStopButton.addEventListener("click", () => {
const stop = upcoming[selectedStop];
if (!stop) return;
savedStopKey = savedStopKey === stop.key ? null : stop.key;
updateSaveButton();
showToast(savedStopKey ? `${stop.area} saved for this visit. See you curbside!` : "Stop removed from your saved plans.");
});
const cart = new Map();
const cartDrawer = $("#cartDrawer");
const cartContent = $("#cartContent");
const cartFooter = $("#cartFooter");
const drawerBackdrop = $("#drawerBackdrop");
let lastFocused = null;
function updateCartBadges() {
const count = Array.from(cart.values()).reduce((total, quantity) => total + quantity, 0);
$$(".cart-count").forEach(badge => {
badge.textContent = count;
badge.classList.remove("pop");
void badge.offsetWidth;
badge.classList.add("pop");
});
}
function cartTotal() {
return Array.from(cart.entries()).reduce((total, [id, quantity]) => {
const item = getItem(id);
return total + (item ? item.price * quantity : 0);
}, 0);
}
function renderCart() {
if (cart.size === 0) {
cartContent.innerHTML = `
<div class="empty-cart">
<svg viewBox="0 0 140 130" aria-hidden="true">
<path d="M27 45H113L104 116H36Z" fill="#ffc94f" stroke="#171713" stroke-width="4"/>
<path d="M49 49C48 20 92 20 91 49" fill="none" stroke="#171713" stroke-width="5" stroke-linecap="round"/>
<path d="M43 71C58 60 80 60 98 70" fill="none" stroke="#ff5338" stroke-width="7" stroke-linecap="round"/>
<circle cx="52" cy="92" r="4" fill="#171713"/><circle cx="88" cy="92" r="4" fill="#171713"/>
<path d="M60 101C66 107 76 107 81 101" fill="none" stroke="#171713" stroke-width="3" stroke-linecap="round"/>
<path d="M112 22l4 9 10 2-8 6 2 10-8-5-9 5 3-10-8-6 10-1Z" fill="#dcff45" stroke="#171713" stroke-width="2"/>
</svg>
<h3>Your bag needs<br>a little Loco.</h3>
<p>Start with the crowd favorite. Add another taco because future you deserves it.</p>
<a class="button button-dark" href="#menu" data-close-cart>Browse the full menu <span aria-hidden="true">↓</span></a>
<div class="empty-suggestions">
<button type="button" data-add="bulgogi"><span>OG Bulgogi</span><strong>$5.50 +</strong></button>
<button type="button" data-add="combo"><span>The Loco Combo</span><strong>$18.00 +</strong></button>
</div>
</div>`;
cartFooter.innerHTML = `<p class="payment-note">No account. No app. Just really good tacos.</p>`;
return;
}
cartContent.innerHTML = Array.from(cart.entries()).map(([id, quantity]) => {
const item = getItem(id);
if (!item) return "";
return `
<div class="cart-row">
<div>
<h3>${item.name}</h3>
<div class="cart-row-price">${money(item.price)} each</div>
<div class="quantity-control" aria-label="Quantity for ${item.name}">
<button type="button" data-quantity="decrease" data-id="${id}" aria-label="Remove one ${item.name}">−</button>
<span>${quantity}</span>
<button type="button" data-quantity="increase" data-id="${id}" aria-label="Add one more ${item.name}">+</button>
</div>
</div>
<strong>${money(item.price * quantity)}</strong>
</div>`;
}).join("") + `<p class="cart-note">Combo tacos and add-ons can be customized at the window. We’ll have your order hot and ready.</p>`;
cartFooter.innerHTML = `
<label class="pickup-label" for="pickupTime">Pickup time</label>
<select class="pickup-select" id="pickupTime">
<option>ASAP · about 15–20 min</option>
<option>In 30 minutes</option>
<option>In 45 minutes</option>
<option>In 1 hour</option>
</select>
<div class="cart-total"><span>Subtotal</span><strong>${money(cartTotal())}</strong></div>
<button class="button button-dark full" type="button" data-checkout>Request pickup order <span aria-hidden="true">↗</span></button>
<p class="payment-note">Pay at the truck · tax calculated at pickup</p>`;
}
function openCart() {
lastFocused = document.activeElement;
closeMobileNav();
renderCart();
cartDrawer.setAttribute("aria-hidden", "false");
drawerBackdrop.classList.add("show");
document.body.classList.add("drawer-open");
window.setTimeout(() => $(".drawer-close", cartDrawer).focus(), 120);
}
function closeCart() {
cartDrawer.setAttribute("aria-hidden", "true");
drawerBackdrop.classList.remove("show");
document.body.classList.remove("drawer-open");
if (lastFocused && typeof lastFocused.focus === "function") {
lastFocused.focus({ preventScroll: true });
}
}
function addToCart(id) {
const item = getItem(id);
if (!item) return;
cart.set(id, (cart.get(id) || 0) + 1);
updateCartBadges();
renderCart();
showToast(`${item.name} added. Excellent choice.`);
}
function changeQuantity(id, direction) {
if (!cart.has(id)) return;
const next = cart.get(id) + (direction === "increase" ? 1 : -1);
if (next <= 0) cart.delete(id);
else cart.set(id, next);
updateCartBadges();
renderCart();
}
function checkoutOrder() {
if (cart.size === 0) return;
const total = cartTotal();
const pickupSelect = $("#pickupTime");
const pickup = pickupSelect ? pickupSelect.value : "ASAP · about 15–20 min";
const orderNumber = `KL-${Math.floor(1000 + Math.random() * 9000)}`;
cartContent.innerHTML = `
<div class="order-success">
<div class="success-check" aria-hidden="true">✓</div>
<h3>You’re on<br>the Loco list.</h3>
<div class="order-number">${orderNumber}</div>
<p><strong>${pickup}</strong><br>Your ${money(total)} pickup request is ready. Show this number at the window and pay when you pick up.</p>
</div>`;
cartFooter.innerHTML = `<button class="button button-dark full" type="button" data-close-cart>Done — see you curbside</button>`;
cart.clear();
updateCartBadges();
showToast("Pickup request ready. We’ll see you at the window!");
}
document.addEventListener("click", event => {
const addButton = event.target.closest("[data-add]");
if (addButton) {
addToCart(addButton.dataset.add);
if (addButton.classList.contains("add-btn")) {
const original = addButton.innerHTML;
addButton.classList.add("added");
addButton.innerHTML = `<span>Added — nice choice</span><b aria-hidden="true">✓</b>`;
window.setTimeout(() => {
if (addButton.isConnected) {
addButton.classList.remove("added");
addButton.innerHTML = original;
}
}, 900);
}
return;
}
const quantityButton = event.target.closest("[data-quantity]");
if (quantityButton) {
changeQuantity(quantityButton.dataset.id, quantityButton.dataset.quantity);
return;
}
if (event.target.closest("[data-open-cart]")) {
openCart();
return;
}
if (event.target.closest("[data-close-cart]")) {
closeCart();
return;
}
if (event.target.closest("[data-checkout]")) {
checkoutOrder();
}
});
drawerBackdrop.addEventListener("click", closeCart);
renderCart();
let toastTimer;
function showToast(message) {
const toast = $("#toast");
window.clearTimeout(toastTimer);
toast.textContent = message;
toast.classList.add("show");
toastTimer = window.setTimeout(() => toast.classList.remove("show"), 2600);
}
const mobileToggle = $("#mobileToggle");
const mobileNav = $("#mobileNav");
function closeMobileNav() {
mobileToggle.setAttribute("aria-expanded", "false");
mobileToggle.setAttribute("aria-label", "Open navigation");
mobileNav.hidden = true;
}
mobileToggle.addEventListener("click", () => {
const opening = mobileToggle.getAttribute("aria-expanded") !== "true";
mobileToggle.setAttribute("aria-expanded", String(opening));
mobileToggle.setAttribute("aria-label", opening ? "Close navigation" : "Open navigation");
mobileNav.hidden = !opening;
});
$$("a", mobileNav).forEach(link => link.addEventListener("click", closeMobileNav));
document.addEventListener("keydown", event => {
if (event.key === "Escape") {
if (cartDrawer.getAttribute("aria-hidden") === "false") closeCart();
closeMobileNav();
}
if (event.key === "Tab" && cartDrawer.getAttribute("aria-hidden") === "false") {
const focusable = $$("button, a[href], select, input", cartDrawer).filter(element => !element.disabled);
if (!focusable.length) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
});
const signupForm = $("#signupForm");
signupForm.addEventListener("submit", event => {
event.preventDefault();
if (!signupForm.checkValidity()) {
signupForm.reportValidity();
return;
}
signupForm.classList.add("success");
$("#formNote").textContent = "You’re in! Next stop: your inbox. Get ready for secret drops and taco intel. ✺";
showToast("Welcome to the Loco list. Good decisions ahead.");
});
const heroArt = $("#heroArt");
const foodFloat = $("#foodFloat");
const finePointer = window.matchMedia("(pointer: fine)");
heroArt.addEventListener("pointermove", event => {
if (!finePointer.matches) return;
const bounds = heroArt.getBoundingClientRect();
const x = (event.clientX - bounds.left) / bounds.width - 0.5;
const y = (event.clientY - bounds.top) / bounds.height - 0.5;
foodFloat.style.setProperty("--mx", `${x * 12}px`);
foodFloat.style.setProperty("--my", `${y * 10}px`);
});
heroArt.addEventListener("pointerleave", () => {
foodFloat.style.setProperty("--mx", "0px");
foodFloat.style.setProperty("--my", "0px");
});
$("#year").textContent = new Date().getFullYear();
</script>
</body>
</html>Reasoning trace (16,936 tokens)
I need output exactly one html code block, self-contained inline CSS/JS no external assets fonts network requests except allowed task none. Should create visually impressive Korean taco truck website Kimchi Loco enticing. Should meaningful first paint. Should adapt. Should probably have landing page with bold branding, menu, schedule/find truck, interactions perhaps order/cart? It's website makes people really want to come. Should make great 1280x800 and mobile 375 no horiz scrollbar. Could build sophisticated landing with hero interactive/menu carousel/order buttons, ticker, find truck schedule modal or sections, cart drawer. Should have no external images/fonts so CSS/SVG inline allowed. Should use inline SVG illustrations (tacos etc) or gradient. I can use system fonts. Should strong design perhaps cream/neon hot pink/black editorial Korean street aesthetic. Brand "KIMCHI LOCO", bold condensed via Impact, Arial Black, system. Palette red-orange, lime, pale yellow, black. Food truck graphic drawn CSS/SVG to give tangible visual. Could use inline SVG food illustrations. Hero first fold highly visual: nav top, location badge, huge heading "SEOUL / MEETS / STREET." or "GET LOCO." copy "Korean fire. Mexican soul. Wrapped in a tortilla." CTA find the truck/menu. Right big food photography substitute? I cannot external images but can create gorgeous inline SVG tacos with patterns/shading. CSS styled stylized composition, isometric tray with 3 tacos and ingredients. Could also build food-truck illustration in hero background. Should avoid generic AI design. Strong art direction: zine/punk Korean-Mexican fusion. Grain pattern using inline data SVG? "Everything inline no external images" data URI is still inline; but perhaps no external. Use CSS repeating patterns. Inline SVG filter fractal noise could be used but avoid performance. Bold top announcement ticker. Nav bright yellow or offwhite. Hero left h1 KOREAN SOUL. TACO ATTITUDE. Right giant stylized taco with textures. Could have moving fire lines or ingredients orbit. Use menu section immediately lower. Should have screenshot about 3 secs shows animation. I can add marquee with CSS. Hero visible 800 viewport maybe main 620. Nav 74 + marquee 36, hero 620. Headline first fold. At 1280 800 likely hero all visible and top menu section peeking. Better show nav, hero ~610, ticker 42, then menu section. Something meaningful all visible. Should mobile showh1 and art balanced.
Consider website structure:
- top announcement bar bright chartreuse "TODAY • ... • FIND US..." with live badge.
- sticky nav on black? Header warm cream, logo stacked starburst? Logo red/orange "KIMCHI LOCO" with small Korean "김치로코". Brand logo inline svg or styled text. Navigation MENU / FIND THE TRUCK / OUR STORY and CTA "ORDER AHEAD ↗" bright red.
- Hero in near-black (#111) with cream typography and big red irregular background? Could make full cream easier. Header cream. Hero grid 55/45. Left huge black "SEOUL FOOD. / TACO TRUCK / ENERGY." Tag. Korean vertical text. Right bright coral field with illustrated giant tacos. Main header maybe "KOREAN BBQ.\nWRAPPED UP\n& READY TO GO." Might be too long. "GET LOCO / FOR KIMCHI." fun. Product story "Smoky bulgogi. Crackling kimchi. Tortillas hot off the plancha. This is your new obsession." CTA.
- Price from "$4 tacos · no tiny portions". Ratings/social proof "4.9 ★ 2,000+ happy locos".
- SVG art: 3D-ish two tacos on black tray? Could create a large card angled. Main taco shell with layered ingredient shapes and cilantro. Use svg viewBox 0 0 650 500. Background circle/halftone. Main illustration taco shape using path. Shell path M100,340 C110,180 220,85 370,100 C480,110 545,200 540,335 C410,385 240,390 100,340 Z maybe taco half. Filling appears wavy green/red inside upper shell? Should have visual shape proper. Use SVG gradients, drop shadows. Two tacos angled, maybe taco at front and top behind. Shell has spots. Food drawing in layered vectors. Use groups transform rotate.
- Add sticker "100% LOCО MADE / 김치" rotating maybe slow.
- Floating chips "HOT. CRUNCHY. GONE." and "SMALL BATCH KIMCHI". Cursor responsive maybe art tilt with pointer but not needed. I can animate floating and sheen.
- Bottom marquee "BULGOGI • SPICY PORK • KIMCHI FRIED RICE •GOCHU-Queso • ..."
- Menu section white/cream, title "THE LINEUP" with cards using inline SVG thumbnails, names/descriptions/prices/spice. Could have filter tabs "ALL / TACOS / BOWLS / SIDES" functional filtering. Add plus buttons add to order, order drawer with running total, "Pickup at truck" flow perhaps CTA shows toast and drawer. Should functional interactions, not generic dead anchors.
- Find truck section with schedule cards and "Set reminder" toggles. Location "Tonight: Arts District · 5–10PM" with map styled. For demo, date schedule dynamic maybe fixed dates for specificity. Could dynamic current weekday and "Today" schedules. Use actual schedule array and update day. Buttons "Get directions" cannot window.open, could use anchor href to Google maps target? no external requests? links okay maybe but no network? User clicking opens external, but window.open banned. Could use <a href="https://maps.google.com/?q=..." target="_blank"> but this is external navigation, likely allowed but everything no network means site resources. Better no external reliance and use custom modal location. "COPY ADDRESS" uses clipboard possibly available? Could implement clipboard with fallback; sandbox may block? use click changes text, no errors. Could avoid.
- "Our Story" section and instagram look cards maybe nice but not necessary.
- Footer big "SEE YOU AT THE WINDOW." CTA. All self-contained.
Make sure no text outside code. Should have robust JS no console errors. CSS custom. Interaction:
1 nav links smooth scroll.
2 mobile menu toggles; no overflow.
3 menu category filter buttons update visible items with animations. Each card data-category. Should enough in initial view? first paint hero only.
4 add buttons update floating bag count + total, mini toast. Drawer order summary with quantities, remove/increment and "Send pickup order" opens confirmation or fake checkout form. Could implement drawer and simple form. But marketing site main purpose come; order ahead button opens cart or order modal. If empty, drawer shows recommendation + menu. A functional order builder is nice. Could implement complete drawer with menu items, add, count, total. On checkout, choose pickup time then "Place demo order" and display confirmation. But not real transaction; button "Review pickup order". Could be mock, avoid deceptive. Could say "Order at the window" maybe CTA scroll menu.
5 findtruck "TONIGHT'S STOP" button opens schedule modal or scroll. Nav find.
6 email signup maybe use form validates email, success message inline.
7 sound? no.
Should design strong and first paint not hidden. Avoid huge hero taller than viewport? 1280x800: topbar 32, nav 78, hero min-height calc(100svh - 152) =648, bottom marquee 44 -> total 802, okay. Main hero inner grid and footer. At 1280 screenshot shows all hero. Hero starts at 110; h1 86 maybe. Headline 3 lines 220 height. Right art 480. Nav logo.
At 375: topbar 30, nav 70, hero grid one col, h1 62 or clamp; art 310; hero total ~820, bottom not all visible but fine. Hero CTA visible before art maybe. Main image no horizontal overflow due to transformed/absolute. Use overflow clip.
Art direction: "Midnight Seoul meets LA taco stand" black, radioactive lime #d8ff3e, coral #ff4a2a, warm cream #f5f0e5. Header cream. Hero maybe coral left? Better visually striking split: hero background #f3efe4, h1 black, red highlighted LOCO. Right panel red with diagonal. There is a large black food image card. Topnav cream. Use black horizontal strip at hero bottom. Hero left offwhite with little blue? palette 4 colors. Headline "SEOUL / IN EVERY / BITE." with "SEOUL" black, "IN EVERY" outline? Should immediately understand Korean tacos. Brand is Kimchi Loco. Could h1 "KOREAN BBQ.\nTACO TRUCK.\nZERO CHILL." Three line maybe too tall. "SEOUL FOOD.\nTACO TRUCK.\nFULL SEND." Very punchy. Copy.
Perhaps headline "YOUR NEW / TACO / OBSESSION." But Korean identity. "KOREAN BBQ / JUST GOT / LOCO." H1 includes brand concept.
Option:
eyebrow [KOREAN × MEXICAN • LOS ANGELES]
h1 "GET LOCO.\nEAT KIMCHI." Last words highlighted. Hero copy "Bulgogi with a char. Kimchi with a kick. Handmade tortillas with no business being this good." CTA "FINDTHE TRUCK →" and "SEE THE MENU". Social proof.
Right art: large "NOT YOUR / TACO TUESDAY" vertical badge. Strong.
Logo custom: red square border black with 김치 in small, main word KIMCHI / LOCO. Use star symbol ✺? System support okay. Korean glyph system available likely. Font families Arial, sans. Use text "김치" no external font. Headline Impact fallback "Arial Narrow" maybe available. `font-family: Impact, Haettenschweiler, 'Arial Narrow Bold', sans-serif;` On Linux Chrome Impact may not installed; falls sans. Use `font-weight:950; letter-spacing:-.06em` with Arial Black fallback. `font-stretch: condensed` may work. Use system `Arial Black, Arial, sans-serif`. Bold aesthetic. I could inline custom vector letters? no need.
SVG art detailed:
Define gradients `shell` yellow to orange, `shellBack`, `meat` etc, filters drop shadow. Background black radial with pattern circles. Taco group:
- base contact ellipse.
- back shell path M108 325 C125 173 251 82 393 111 C489 131 544 215 534 332 L501 347 C510 245 467 175 388 153 C267 119 166 201 145 342 Z fill.
- fillings inside: green lettuce wavy path at upper edge from...; red kimchi ribbons, brown beef ovals, onion white thin, sesame small.
- front shell path M101 326 C217 359 394 362 535 329 C515 403 444 447 322 449 C198 449 122 400 101 326 Z fill shell. This makes taco open U? Typical taco side: semicircular shell with filling visible above; front covers lower. Good.
Perhaps a second taco rotated behind smaller.
Should realistic tasty illustrated, not childish. Use paths, highlights. Add char marks on beef and small sesame. Add lime wedge, kimchi dish.
At right, large circle lime with halftone. Sticker circular text perhaps SVG textPath "FERMENTED • FIRED UP • ..." with black central "김치". Could CSS `.seal` circle with text simple.
Could also have illustrated food truck small in find section not necessary.
Heroimage alt accessible inline svg role img aria-label.
Card labels "01 THE OG" "BULGOGI + KIMCHI" black pill on art bottom. Top left pill "MADE TO ORDER".
Decorative hand drawn arrows inline CSS/SVG.
Menu cards use CSS art thumbnails maybe inline SVG per card with unique simple taco, fries, rice etc. Could use shared `<svg>` individually. Six cards:
Bulgogi Taco $5.50 category tacos spice 1, "12-hr marinated ribeye, kimchi slaw, scallion, sesame"
Spicy Pork Taco $5.25 spice 2
Crispy Mushroom Taco $5 vegan
Kimchi Fried Rice $11 bowls
Gochu-Queso Fries $9 sides
K-Mule? drinks maybe not.
Perhaps cards 3 visible across at 1280. Section starts after hero, okay.
Could create initial top "CROWD FAVORITES" 3 cards, filters not necessary but add all. Buttons add.
Find section schedule dynamic:
Three day cards:
WED • today? But screenshot date current maybe unknown. Use JS `Intl.DateTimeFormat` actual now. Schedules fixed day-of-week with day index:
0 CLOSED (meal prep)
1 closed
2 "Wed • Westwood • 5–10 PM"
3 "Thu • Culver City • 5–10 PM"
4 "Fri • Arts District • 5–11 PM"
5 "Sat • Smorgasburg LA • 11 AM–4 PM" (actual Smorgasburg Sunday not Saturday; don't need real known maybe fictional)
6 "Sun • Venice Beach • 12–8 PM".
Since business fictional, choose plausible addresses: "ROW DTLA · 777 S Alameda St", "Abbot Kinney Blvd · Venice", "Helms Bakery · Culver City", "The Bloc · Downtown LA". Hero `TODAY · {location} · {time}` dynamic. I can add green dot. Find card `Next stop` with dynamic. "Truck status: firing up at 5 PM" based current time might vary. Simpler "THIS WEEK ON THE STREET". Today card text. If closed Monday, hero says "NEXT UP · TUE ..." maybe complexity but implement getNextService.
Dates schedule generated next 7 days open days, show 4. `TODAY` if open today else next. CTA `FIND US TONIGHT` maybe if closed wrong. Use `FIND THE TRUCK`.
Location section black with lime text; left schedule, right abstract map card with street grid CSS and red truck marker. Could implement CSS map no external. Road lines with absolute divs and labels, marker. Add "PULL UP HUNGRY." address. Button "COPY ADDRESS" writes clipboard may throw if denied; catch. No alerts. Could use click selects and toast. Could use not copy and "PLAN MY STOP" toggles details. Use "SET A REMINDER" button toggles check. That satisfies.
Modal/drawer:
`aside id=orderDrawer` fixed right, backdrop. Cart count in nav. Body scroll lock optional. `aria-hidden`. Buttons with `data-add` from menu. Cart object Map. on add update. If drawer closed toast "Bulgogi Taco added — nice choice." bottom center. Floating mini order bar maybe not needed. Nav order ahead shows bag count. drawer line items +/-. `pickup` select dynamic times. Button "CONFIRM PICKUP" on click transforms to success with order id and summary. This makes demo functional but could seem real ordering; use final "This is a demo — see you at the truck!" Not needed maybe fake company website should be complete. Could say "PAY AT PICKUP • NO APP NEEDED" and confirmation `YOU'RE ON THE LIST` gives order number. Fine. No actual backend but demo judged visually. Could show "Demo order saved for this visit" but no local storage. Avoid mention demo unless clarity.
Perhaps order button on nav label `ORDER AHEAD <span>0</span>`.
Mobile:
- hide desktop nav links, show menu toggle icon.
- nav logo scales.
- hero p width.
- buttons full? two columns with wrap.
- right art at 320 h.
- menu grid 1 col.
- cart drawer width min(100%,420px).
- sticky header maybe topbar not sticky, nav sticky top 0 z50.
- no horizontal scrollbar via `overflow-x: clip` body. Note clip may prevent sticky? body clip okay? use hidden. `html {scroll-behavior:smooth}` `body{overflow-x:hidden}`.
- max widths.
Animations respect reduce motion.
- reveal IntersectionObserver for sections, initial elements visible if JS fails? CSS `.reveal` opacity 0 only when `.js` class added? I can use script at start document.documentElement.classList.add('js') then observer; first paint hero visible no reveal hero. Other content irrelevant. Should have no risk screenshot second sections hidden if scroll no.
- hero art bob subtle 6s.
- ticker animation.
- sticker spin 16s.
- hover cards.
Accessibility:
buttons labels, focus styles, keyboard Esc closes drawer/mobile nav, drawer focus maybe implement basic. Color contrast lime/black good, red black maybe red white? Red #ff4b2b with black good. Cream text on red can be okay #111 instead.
Hero CTA black on lime.
Use clickable anchors no href #? nav anchors.
Perhaps top bar moving text duplicates aria-hidden.
`prefers-reduced-motion` disable.
Could design first fold:
At 1280:
top strip 34.
nav 80.
hero `.hero {min-height: 620px; display:grid}` maybe 625. `hero-grid max-width 1440 margin auto grid 53% 47%`.
Left padding 54 top 38 bottom 38. Eyebrow margin bottom 22. h1 font clamp(64,7.8vw,112) line .85. At 1280 99.8 => 3 lines 255. Copy 55 high. CTA 50. Social proof 32. Total 22+255+20+55+28+52+26+32=490 plus padding 76 =566 good. H1 lines maybe "SEOUL FOOD." (10 chars at 100*0.55 =550) fits left ~620-100=520? Impact average .55; 11 incl dot =605 too wide. Should h1 90 or text width. Left grid 57% 730 minus 100=630. At 1280 53%=678 minus96=582; "SEOUL FOOD." about 550 works. H1 88. `clamp(4.3rem,7vw,7rem)` => 89.6. 3 lines 228. Good.
Perhaps H1 `SEOUL FOOD.\nTACO TRUCK.\nFULL SEND.` "TACO TRUCK." 11 similar. Strong. But "FULL SEND." colloquial. Could be "KOREAN BBQ.\nTACO TRUCK.\nZERO CHILL." Nice. Should get craving more than attitude. `HOT. FRESH.\nA LITTLE\nLOCO.` Generic. "SEOUL FOOD.\nTACO TRUCK.\nDAMN GOOD." Excellent, direct. Last line coral. Underline.
Perhaps brand asks Korean taco; h1 "SEOUL FOOD.\nTACO TRUCK.\nHELL YES." Last line. Better family-friendly "BIG FLAVOR." 3rd. `SEOUL FOOD. / TACO TRUCK. / BIG FLAVOR.` repetitive but concise. `KOREAN BBQ.\nTACO TRUCK.\nNO RULES.` strong fusion. "NO RULES" brand.
Hero left:
eyebrow "[ LOS ANGELES ] KOREAN BBQ × MEXICAN STREET FOOD"
h1 "GET LOCO.\nEAT SEOUL\nFOOD." But no.
I'll settle `SEOUL FOOD.\nTACO TRUCK.\nFULL SEND.` H1. Copy sells food.
Hero right background coral (#ff4e32), border-left 2 black. Art full. Giant words `FRESH OFF THE PLANCHA` at top. Circular lime sun behind. SVG centered. Bottom white/cream product label card rotated -2 degrees. Sticker top right.
Art panel width 550, enough.
Perhaps hero left social proof: avatar circles CSS initials and stars; "4.9 from the late-night crowd" avoids false precise maybe marketing okay. "THE PEOPLE HAVE SPOKEN ★★★★★" no fake numbers. Could use "2,000+ KIMCHI CONVERTS" but fabricated. Doesn't matter fictional.
Perhaps line "HOT TORTILLAS • COLD DRINKS • GOOD DECISIONS".
Menu section background cream.
Heading includes "WORTH THE / DETOUR." and copy "Every taco built to order. Every sauce made in-house. Every napkin necessary."
3 featured cards visible.
Menu cards border 2 black, image backgrounds lime/coral/lavender. Price circle. Add button black with plus. Distinct.
Could include "Heat scale" on cards dots.
Find section bright lime. Story section maybe red with quote and short narrative. Footer black.
Should not overdo length but full website enough.
I have huge token budget but response maybe 30k. Should ensure code correct no external. Could write around 1800 lines? desired not too excessive. Aim ~1200 lines or ~35k tokens? 20k tokens maybe. Should have final quality. I'll plan HTML structure in detail.
Document:
<!doctype html><html lang=en><head><meta...><title>Kimchi Loco — Korean Tacos...</title><meta description><style>...</style></head><body>
<a class=skip ...>
<div class=topbar> <div class=topbar-track> etc dynamic maybe static "TACOS WITH A SEOUL..." </div></div>
<header class=site-header>
<a logo href="#top" aria-label ...><span class=logo-mark>김치</span><span ...>KIMCHI <b>LOCO</b></span></a>
<nav class=desktop>...</nav>
<div class=nav-actions><button data-open-cart ...>ORDER AHEAD <span id=cartCount>0</span></button><button mobile...></button></div>
<div mobile-panel ...> ... </div>
</header>
<main id=main>
<section class=hero id=top>
<div hero-copy> ...
<div live-pill id=todayPill><span></span><span id=nextStopMini>...</span></div> maybe eyebrow and live.
...
<div hero-actions><a href=#find ...>FIND THE TRUCK <svg arrow>...
<a href=#menu>SEE MENU</a>
<div class=proof> circles ...
</div>
<div hero-art>
<div class=art-topline><span>HANDHELD HAPPINESS</span><span>EST. 2025</span></div>
<div class=sun>
<svg class=hero-food ...>
defs...
back small taco group? Use same icon not href external.
</svg>
<div sticker>100%<br>LOCO<br><small>김치로코</small></div>
<div food-caption><small>THE ONE EVERYONE ORDERS</small><strong>OG BULGOGI</strong><span>$5.50 →</span></div>
</div>
</section>
<div class=marquee>...
<section menu>...
Section header + filter.
<div class=menu-grid id=menuGrid>
<article class=menu-card data-category=tacos data-id...>
<div class=menu-visual bg-yellow>
<span class=card-tag>BEST SELLER</span>
<svg viewBox 0 0 320 210>... taco simpler reuse maybe different each.
<button class=quick-add data-add=... aria-label>+</button>
</div>
<div menu-info><div name price><h3>OG BULGOGI</h3><span>$5.50</span></div><p>...</p><div meta> ...</div></div>
</article>
...
6.
</div>
<div menu-foot><p>...vegetarian...</p><button id=buildOrder>BUILD MY ORDER ↗</button></div>
</section>
<section social strip maybe quote>...
<section find id=find>
<div section intro... `CATCH US IF YOU CAN.`
<div find-grid>
<div schedule-panel><div id=nextStopCard class=next-stop> dynamic day/date location...
<div id=scheduleList class=schedule-list> dynamic 4 days</div>
</div>
<div map-card> CSS map roads and marker ...
<button id=reminderBtn>+ REMIND ME</button>
</div>
</div>
</section>
<section story id=story> left quote, right text and small values chips; maybe visual mini collage of kimchi jar SVG and tortilla.
</section>
<section newsletter class=join id=join><h2>TEXT ME WHEN YOU'RE CLOSE.</h2><form id=signup> email etc. Actually phone would require tel; marketing follow "Join the Loco list". Email input. Functional success.
</section>
</main>
<footer>...
Cart drawer, backdrop, toast.
Could place topbar with current stop dynamic rather than generic. `#topStop` text. The screenshot shows meaningful live info.
Dynamic schedule:
const stops = {
0:{name:"Venice Beach", short:"VENICE", address:"1605 Abbot Kinney Blvd", area:"Venice", time:"12–8 PM", note:"Two blocks from the beach"},
1:null,
2:{name:"Westwood", address:"10886 Le Conte Ave", area:"Westwood", time:"5–10 PM", note:"Near the Westwood Village"},
3:{name:"Culver City", address:"8800 Venice Blvd", area:"Culver City", time:"5–10 PM", note:"Outside Helms Bakery District"},
4:{name:"Echo Park", address:"1521 Sunset Blvd", area:"Echo Park", time:"5–11 PM", note:"Late-night taco energy"},
5:{name:"Arts District", address:"777 S Alameda St", area:"DTLA", time:"5–11 PM", note:"Find us by the neon"},
6:{name:"Los Feliz", address:"1800 N Vermont Ave", area:"Los Feliz", time:"12–10 PM", note:"Curbside, all day"}
}
Closed Monday only. Use actual now; if Monday, next Tuesday.
Function nextStops(count=4): iterate i=0..7, skip null, produce date and isToday i=0. current next stop includes today even if time ended; no issue.
Render schedule 4 cards each with day, date, area/time. Hero `TODAY • ${area.toUpperCase()} • ${time}` or `NEXT UP • ${day.toUpperCase()} • area • time`.
Main next card: if today open "TODAY'S STOP" else "NEXT STOP". Address.
Map labels dynamic area; map stylized not accurate but decorative. `aria-label` no claim exact.
Reminder button toggles to "✓ REMINDER SET" (no persistence) and toast. Could offer `Add to calendar` generating ICS data URI? Downloads inline maybe not necessary. "REMIND ME" without notification backend false. Better "SAVE THIS STOP" toggles saved, works session. Tooltip says saved for this visit. Use `SAVE THIS STOP` and toast. No localStorage.
Cart menu data:
const MENU = [
{id:'bulgogi', name:'OG Bulgogi', price:5.5, cat:'tacos', tag:'#1 BEST SELLER', desc:'Charred soy-garlic ribeye, kimchi slaw, scallion & sesame.', heat:1, color:'lime', art:'beef'},
{id:'pork', name:'Spicy Pork', price:5.25,... heat:3},
{id:'mushroom', name:'Seoul Shroom', price:5, heat:1, vegan true},
{id:'rice', name:'Kimchi Fried Rice', price:11,cat:'bowls'},
{id:'fries', name:'Loco Street Fries', price:10,cat:'sides'},
{id:'queso', name:'Gochu-Queso + Chips', price:7,cat:'sides'}
]
Can render menu via JS instead of hardcoded cards. But requirement meaningful visible on first paint at 3 sec JS immediate okay. If JS disabled not needed. Yet easier generate SVG from function using item art. Inline strings. Avoid XSS fixed data. Use `<svg>` built based on type. Hero SVG handcoded. JS templates okay.
Menu first paint after DOM parsed script end. Fine.
Filtering: buttons with data-filter. `renderMenu(filter)` maybe resets DOM and add listeners via delegation. Use menuGrid.addEventListener click `[data-add]`. Animate cards via style delay.
Menu art function returns SVG per type:
tacoSVG(meat color etc) viewBox 0 0 340 220, shell path and ingredients.
riceSVG bowl with rice kimchi fried egg.
friesSVG tray fries, beef, sauce.
quesoSVG bowl.
Could also avoid innerHTML complexity but okay fixed strings.
Use `aria-hidden=true` on decorative card art.
Card button absolute bottom right black circular `+`.
Heat: `<span class=heat aria-label="Heat level...">` with 3 pepper shapes perhaps Unicode `●` or `◆`. Text `MILD`, `MEDIUM`, `HOT`, label and three i classes. Vegan chip.
Menu item add opens? On click add update, button briefly check and toast, cart count animate. Should event delegation render resets. Card has also button entire `ADD` maybe bottom row. Better card info footer contains price and button `.add-btn data-add`. `+ ADD` pill.
Cart:
cart = new Map();
format currency.
openCart() remove hidden from backdrop/drawer with requestAnimationFrame class open; use CSS visibility and transform. Simpler `[aria-hidden=false]`. Backdrop `.show`. set body class drawer-open overflow hidden. `lastFocus`.
closeCart returns focus if desired.
`renderCart`: if empty innerHTML illustration and `<a href="#menu" class="button black" id=emptyBrowse>...`; listener via drawer click handles `[data-close-cart]` on anchor.
Items row with qty controls data-action inc dec data-id. Subtotal. Note taxes maybe "Tax calculated at pickup" no backend. Pickup select with options generated maybe ASAP 15-20 etc.
Order checkout button if cart empty disabled. On click show `.order-success` by setting drawer body innerHTML? Could have `#cartContent` includes dynamic. When click order, render success and clear cart? Better store ordered, clear after confirmation. `#cartContent.innerHTML = success`. `cart.clear()` update nav. `orderNumber = KL-${Math.floor...}`. Footer in success "Show this at the window • Pay when you pick up". Button "BACK TO THE STREET" close. Could include selected pickup time. Since no real backend, claiming order actually placed misleading but standard demo okay. I can phrase "PICKUP LIST (DEMO)" maybe user wanted real site not prototype. Avoid visible demo word. `REQUEST PICKUP` and success `ORDER REQUEST READY`? Without backend no actual. Most generated demos fake forms accepted. I'll implement.
Order summary drawer header "YOUR LOCO ORDER" subtext "Pay at the window. Skip the line."
Close button.
Cart total updates.
When plus/minus set.
Cart count on nav hidden? display 0 still.
Toast role=status bottom fixed. on add set text and show 2.5s. CSS safe area.
Signup: form submit preventDefault, email input built-in validity via `if(!form.checkValidity()) { form.reportValidity(); return;}` reportValidity allowed (not prompt). Then replace section inner? Better status p text "You're in. Watch your inbox for the next stop. ✺"; hide input/button, add success. `form.innerHTML` fixed safe. No network.
Newsletter asks email not phone. "GET THE DROP." copy weekly truck stops + secret menu.
Mobile nav:
button id menuToggle aria-expanded false controls mobileNav.
nav hidden desktop.
click toggles `.open`; links close.
At width >840 hide.
No `window.open`.
Directions link not external maybe `button id=showRoute` toggles `.route-note` with "Look for the red truck + lime flag..." rather than actual directions. Could have `a href="https://www.google.com/maps/search/?api=1&query=..." target=_blank rel=noopener` but instructions "no network requests" probably not click restrictions, but avoid. `SAVE THIS STOP`.
Map card button `SAVE STOP` functional.
Cursor interactions:
Hero art pointermove tilt using CSS vars; pointerleave reset. On mobile no. `matchMedia('(pointer:fine)')`. Keep subtle, zero errors.
Parallax not necessary.
IntersectionObserver with classes `.reveal` on sections menu etc. CSS `opacity` if `.js .reveal`? If reveal elements loaded below fold, screenshot only hero. On scroll, Observer adds. For first paint no weird. Could simply no reveal, use CSS animations only on hero. Better fewer issues. Cards animate on filter via `@keyframes cardIn`.
Visual details:
- Global border #171712, border-radius 0 or 20? Art direction zine uses straight edges, sticker circles, slight 2deg rotations.
- Main hero corners none.
- Cards border 2px black, box-shadow 6px 6px 0 black; hover translate -3, box-shadow 9px. Menu grid.
- Buttons squared pill maybe border-radius 999 for CTA? Use `border-radius: 0` for editorial? Could use buttons thick border black with box shadow 3px 3px 0 black, uppercase. Brand street style. Pills for tags.
- Logo red `LOCO` and lime star.
- topbar black cream with ticker.
- selection lime.
- CSS noise using body `background-image: radial-gradient(#111 0.6px,transparent .6px)` would show all; not needed.
- Hero right `::after` halftone radial pattern overlay opacity .13 pointer-events none.
- menu visual radial pattern.
- Korean vertical lettering in art `김치가 필요해` maybe "I need kimchi" `김치 로코` brand. Use `대박` ("awesome") on sticker. Use `맛있게 매운` (deliciously spicy) maybe. Keep simple `김치 로코` and `서울 × LA`.
- Small Spanish "¡QUÉ RICO!" and Korean "대박!" reinforce fusion.
- Hero sticker text `¡QUÉ RICO!` outer? CSS circle center `대박!` and small "SO GOOD". Could be simple lime circle with scalloped via `clip-path: polygon(...)` starburst. Use starburst `clip-path: polygon(50% 0%,...)` complex. Circle with dashed border and rotation. `.seal:before` border dotted.
- Food caption cream card with black border.
Hero SVG more detailed:
ViewBox 0 0 640 550. Use group transform rotate(-5 320 300).
Contact shadow ellipse.
Rear taco maybe group transform translate(48 -24) rotate(12) scale(.76), opacity? It adds depth. Main front at translate...
Could draw only giant taco, good quality.
Use `defs` linear gradients:
shellFront #ffd85b to #ee9f24.
shellBack #eaaa35 to #ffdc70.
meat #5a2414 etc.
red kimchi #ed3e24.
green #75a93d.
filter shadow.
Path fill lettuce behind etc. Should plausible.
Detailed group coordinates:
shadow ellipse cx318 cy438 rx210 cy34.
back shell `M117 332 C130 184 238 91 381 108 C487 120 544 215 529 337 L481 347 C489 250 449 184 373 164 C267 135 177 213 157 350 Z`.
Inside base dark path `M141 328 C164 201 264 131 374 151 C464 168 503 240 496 338 C390 366 251 363 141 328Z` fill dark green? Then fillings layered.
Lettuce frill path across around y220-320. Could use multiple path leaves.
Beef chunks at x200 y260 etc as rotated rounded rect with gradient. Kimchi red ribbons.
Shell front `M113 323 C216 359 402 365 532 329 C516 403 440 443 325 447 C210 449 134 403 113 323 Z`. Add path highlight border. Add shell texture spots via `<g fill=#c67d22 opacity=.45>` circles.
Sesame white ovals.
Cilantro leaves green groups.
Lime wedge bottom right group transform translate(480 380): circle sector maybe path `M0 0 L90 0 A90 90 0 0 1 0 90 Z` with rotate. But simpler circle wedge path.
Sauce drips on shell front? white crema zigzag across filling above front maybe.
Hero label sits overlay.
Could create art card SVG simpler but enough.
Menu art uses same mini taco variant with shell and colored fillings. Could define `tacoArt(primary, accent)` string includes path, 8 meat pieces, sauce. Avoid duplicate IDs gradients across separate SVG? If same IDs repeated can conflict globally? Inline SVG IDs in separate SVG technically scoped? References may resolve first globally sometimes. Use no gradients menu, flat colors.
Hero ids unique.
Story visual maybe illustration kimchi jar:
section `.story` grid background #ff5337 color black. left large quote `"WE DIDN’T FUSE TWO CUISINES. WE THREW THEM A BLOCK PARTY."` right paragraph. `.story-stamps` circles "FERMENTED IN-HOUSE", "TORTILLAS PRESSED DAILY", "SAUCES FROM SCRATCH". Could be text. Food story authentic: "Kimchi Loco started with two families, one tiny plancha, and the belief that the best food happens between cultures. My kimchi ferments for days. My marinades take their time. Your tacos? Ready in minutes." Fictional. Good.
Use story `id=story`.
Perhaps include a mini image illustration of jar as CSS? Not needed if typography strong.
Join section cream/black.
Footer with current year via JS.
No external fonts. Should custom heading look via `font-family: "Arial Black", "Helvetica Neue", Arial, sans-serif; font-weight:900; letter-spacing:-.075em;` and maybe `transform: scaleX(.87); transform-origin:left; width:114%` could cause overflow. Avoid. `font-stretch: condensed`. Use `font-family: Impact, "Arial Black", sans-serif`, but if Impact missing Arial Black. h1 letter -0.055em. Good.
Logo text use same.
First paint screenshot likely no JS interactions. Should hero art attractive, dynamic top info. CSS animations 3s at mid loop maybe still good. Ticker moving; okay.
Perhaps art enters animation slide .8s; screenshot at 3s fully. Good.
No loading screen.
Responsive:
`@media(max-width:1050px)` h1 font clamp, hero grid 54/46, art sticker scaled. At 900:
desktop nav hide, mobile toggle display.
hero maybe grid columns 1fr 1fr still until 780. At 760 one column. Hero copy padding 38 24; h1 font 76 at width 760. Right art min-height 460. At 375 h1 57, hero art min-height 330, svg width 115% maybe no horiz due hidden. topbar 32; nav logo small.
At 375 hero copy h1 `font-size: clamp(3.4rem,16vw,4.7rem)` =60. h1 line .87 -> 156. Eyebrow 11. live pill wraps. Copy 17. Buttons each maybe full width? `.hero-actions {flex-direction: row}` buttons 1fr; font12.
Proof avatars may fit. Art caption width calc(100%-32px) bottom 18; sticker width84.
Hero right min-height 340, svg absolute height maybe 360 width 100%; caption overlays. Art-topline.
Find grid one col, schedule row grid etc.
Footer brand huge 18vw but no overflow.
Cart max-width100.
Potential horizontal from marquee width max-content intended with parent overflow hidden. Body hidden.
No external resources.
I'll outline CSS exact:
`:root{--ink:#171713;--cream:#f7f2e7;--paper:#fffaf0;--red:#ff5233;--lime:#dfff45;--yellow:#ffc94a;--pink:#ff9cb1;--muted:#6b695f;--border:2px solid var(--ink);--header-h:80px}`
* box sizing. html scroll-padding-top:90. body margin 0 color.
button input select inherit.
a.
button cursor pointer.
focus-visible outline 3px solid etc.
.container width min(100% - 48px,1240px) margin auto.
.topbar height 34 background ink color cream overflow hidden display flex align center; ticker track animation 25s linear infinite; `white-space:nowrap`.
.site-header height80 background cream border-bottom 2 black display grid grid-template-columns 1fr auto 1fr; padding 0 max... position:sticky top0 z-index40. But header sticky over topbar, fine. Header logo left. nav center. actions right justify end. `max-width`? Header width full with padding 32. At 1280.
.logo display flex gap 10. mark 42 square background red border, transform rotate(-4). word font 24 line .82. logo loco red maybe block. Use `<span class=logo-kimchi>KIMCHI</span><span class=logo-loco>LOCO <i>✺</i></span>`.
.desktop-nav display flex gap28 font 12 bold; a underline on hover.
.order-trigger background lime border 2 black height44 padding; box-shadow 3px3px ink.
.cart-count black color white circle.
.mobile-toggle hidden.
.mobile-nav absolute top:100% left0 right0 background cream border-bottom etc display none.
.hero display grid grid-template-columns:minmax(0,1.05fr) minmax(0,.95fr); min-height:calc(100svh - 114px); max-height? At 1280x800 =686, topbar34+80=114, hero 686, total exactly viewport. But marquee below not visible. Could set min-height:620 and `height:calc(100svh - 150px)` =650, leaving marquee 36 in viewport? top 114 + hero 650 +36 =800. Perfect. `min-height:610px; height:calc(100svh - 150px); max-height:760px;` At 800 =650. At taller good. At viewport less 760 total 114+610+36=760, slight scroll. Fine. At mobile `height:auto; min-height:0;`.
hero-copy background cream padding clamp 28,4vw,64. display flex col justify center.
live-pill maybe before eyebrow adds height. Could use eyebrow row top. `.hero-copy>*` etc.
eyebrow flex align gap font 11; line.
h1 margin 20 0; font clamp 64 7vw 106 line .86. At 1280 h1 89.6. left content width 640? grid 672 - 2*50=572. Text "SEOUL FOOD." at bold Arial Black average ~.68 maybe 10 chars *61=610 too wide. Actual uppercase Arial Black letters: SEOUL 5 at average .7*90=315, space 25, FOOD. 5*63=315 total 655? Should measure approximate. Arial Black typical widths: S .667, E .667,O .778,U .722,L .611 =3.445 em*90=310; space .278=25; F .611 O.778 O.778 D.722 dot.278=3.167*90=285 total620. Content 572 too wide, h1 can wrap automatically undesired (line with spans white-space nowrap -> overflow). Should left width or lower font. Grid 56%=717, padding 50*2=100 =>617 exactly. At 89.6 ~620 fits. Good. Hero grid 56/44 => right563. At 1280. h1 7vw=89.6. `font-family: Impact` narrower if available. Arial Black fallback maybe fit. At 1280 content 616. Could set h1 `font-size:clamp(64px,6.7vw,104px)`=85.8 ->594 fits. At 1440 grid806 padding128=678, font96.5 ->670 fits. Good. At 1024 left573 padding80=493, font68.6 ->477 fits. Great.
`h1 .red {color:red; display:inline-block}` each line block nowrap.
Hero desc max width 510 font18 line1.5. Hero actions.
proof margin top 28 border-top maybe width.
hero-art position relative background red overflow hidden border-left 2 black.
art top line absolute top20 left24 right24 z3 display flex justify.
sun absolute width 400 height400 background lime border-radius 50 top50 left50 transform translate; box-shadow 0 0 0 2 black maybe. Halftone.
food-svg absolute inset 55px -10% 45px? width 110% height calc(100%-70) maybe. `width:110%; height:88%; left:-5%; top:4%;`.
food-caption bottom 24 left24 right24 height? display grid grid columns 1fr auto. Maybe sticker overlaps.
hero `.proof` could overflow at smaller but media.
Marquee border top/bottom black background lime height42.
section padding 100 0. Menu header display flex justify space etc.
`.section-kicker`.
h2 clamp 48,6vw,86 line .9.
menu-intro grid columns maybe. menu-tools filters.
menu-grid grid repeat(3,1fr) gap24 margin-top42.
.menu-card background paper border 2 black box-shadow; overflow hidden? box shadow outside. Visual border-bottom. Info.
`.menu-visual height240 position relative overflow hidden background var`.
`.menu-visual svg width100 height100 transition transform`.
`.quick-add` in info not visual to keep accessible.
`.menu-info padding22`.
h3 24.
price font 20.
desc min-height 48.
menu-bottom flex justify.
tag absolute top14 left14.
Heat dots.
Cards filter hidden via rendering.
Menu footer.
Interlude strip red with quote maybe large horizontal.
Find background ink color cream; section border. Find h2 cream; accent lime.
next stop card background cream ink padding 30 border lime? schedule.
find-grid 1fr1fr gap30.
schedule-list 2x2 cards dark border #555; each first active lime.
Map aspect ratio 1.2 minheight450 background #292b24; CSS lines. Could be within find grid with `position:relative; overflow:hidden; border 2 solid cream`. `.map-grid` absolute inset background-image linear-gradients using rgba. roads transforms.
`.road` absolute background #f7f2e7 opacity .2 etc. labels.
`.map-marker` absolute center etc. marker pin with truck small emoji? Emoji style inconsistent. Use CSS black/red rectangle icon and label "KIMCHI LOCO". A pin circle red border ink with `김치`.
`.map-info` bottom left right background lime color ink padding etc.
Schedule buttons? card clickable update map on click; nice functionality. Render each `.schedule-card` as button, clicking sets selected stop, updates nextStop card/map, active. Initial next open. This makes interactive. `selectedStopIndex`.
Could schedule card nested block time okay. Use `<button type=button class=schedule-card data-stop="${date day}">`.
Main card shows dynamic based selected.
Save button toggles and stores selected id variable.
`aria-pressed`.
Map labels can update `mapArea`, `mapAddress`.
Find header maybe button `VIEW ALL STOPS` no, schedule list enough.
Story cream/red.
Join.
Modal/drawer z100; header sticky 40; toast 120. Backdrop opacity.
`drawer` fixed top0 right0 height100dvh width min(440px,100%) transform translateX(105%); transition; visibility hidden; `[aria-hidden=false]{transform:none;visibility:visible}` but visibility hidden prevents transition on close? set visible when open, delay close maybe. Use `.cart-drawer{visibility:hidden; transition:transform .35s, visibility 0s .35s}` open `visibility:visible; transition:transform .35s`. Fine.
`body.drawer-open{overflow:hidden}`.
Cart content overflow auto flex 1.
Cart footer.
Success.
JS at end:
`const $=(s,p=document)=>p.querySelector(s); const $$=...`.
`const stopSchedule...`
`function dateLabel...`
Time no timezone needed.
Menu art functions.
renderMenu(filter) uses `menuItems.filter`.
Menu card button data-add. Category filters update active and aria-pressed. Use text content not injection from user.
Heat markup uses fixed.
Cart drawer event listeners. For empty browse, click `[data-close]` close.
`openCart` set lastFocused, aria etc, backdrop.hidden=false? Use `<div class=drawer-backdrop id=drawerBackdrop hidden>` and JS hidden=false then requestAnimationFrame add show. Closing remove show then set hidden after 350; this can race if open quickly; manage timeout. Simpler no hidden attribute: backdrop always fixed pointer-events none opacity0; aria-hidden true. `.show` pointer-events auto. Drawer visibility as above. Fine.
Focus close after open setTimeout.
Close Esc.
Trap focus optional implement keydown Tab within drawer; good.
Mobile menu close if drawer open.
`document.addEventListener('keydown', ...)`.
Cart checkout: form? `#checkoutBtn`; select id pickupTime. On click if empty return. Order no random? `Math.random` okay. Success text with orderNum. Set `orderPlaced=true`; cart.clear updateCartCount; cart content success. Then when modifications `renderCart` resets. Should close success and reopen later should show empty. `renderCart` called on open always? openCart calls renderCart, would immediately erase success if closed/reopened okay. On checkout, generate success then clear count. If close then reopen, empty.
Cart drawer static `#cartContent` dynamic includes items; footer static `#cartFooter` dynamic maybe easiest `renderCart` writes both content and footer. Checkout listener delegated to drawer for `#checkoutBtn`.
Cart open triggers renderCart unless `.order-success`? Could always, success disappears if clicking count while open impossible.
Pickup options static in footer HTML when items.
`cartContent.add...` event delegation on whole drawer.
Update count `#cartCount`, mobileCartCount if present.
Add button visual feedback:
```
const btn=event.target.closest('[data-add]');
addToCart(id);
const original=btn.innerHTML; btn.classList.add('added'); btn.innerHTML='✓ ADDED'; setTimeout if btn.isConnected ...
```
for + icons. Fine.
Toast message textContent.
Filter handler buttons.
`buildOrder` opens cart and if empty toast "Start with..."? Better scroll to menu? It opens with empty list recommendations; cart empty contains 3 quick-add buttons maybe. `renderCart` empty shows popular adds `data-add=bulgogi`. Good.
Newsletter.
Hero pointer tilt:
`heroArt.addEventListener('pointermove', e=> { if(e.pointerType...); const x...; foodSvg.style.transform = translate.. rotate...;})`; CSS animation on `.hero-food-wrap` instead so not conflict. Could just set CSS variables `--rx` and transform with wrapper. `.hero-food { transform: rotateX(var...) ... }`. SVG position absolute with animation bob uses transform; pointer sets style translate maybe okay. Use `.food-float` wrapper absolute and svg inside; pointer adjusts wrapper style translate. No need.
Reveal observer:
`const observer = new IntersectionObserver(entries => ...)`; `.reveal` default opacity? Add `<html class="">` script? not necessary. Could style `@media(prefers...)` etc. Better simply use CSS `animation` on hero and filter; no scroll reveal to ensure content visible.
Set footer year.
TopStop dynamic text safe.
Schedule render dynamic using Date and fixed strings. `new Date(now.getFullYear(),now.getMonth(),now.getDate()+offset)`.
Get open stops 4:
```
let all=[];
for(let i=0;i<8&&all.length<4;i++){const dow=date.getDay(); if(schedule[dow]) all.push(...)}
```
selected index0. `isToday=offset===0`.
next headline hero if today `TODAY • ...`; else `NEXT • TUE ...`.
Dates in topbar include `TODAY'S STOP` even Monday no, use next.
Main schedule heading `THIS WEEK`.
`renderSchedule` uses actual selected key `${offset}-${dow}` but data-stop index array. If rerender on click, preserve.
Save stop variable resets on selected change; button text.
Map marker text.
`document.querySelectorAll('.schedule-card')` generated and listener delegation.
Topbar can static message plus dynamic `#topStop` in initial content, but ticker duplicate dynamic IDs not duplicate. Use topbar static only "KOREAN BBQ • MEXICAN SOUL • ...", live pill dynamic. Good.
Hero live pill location near eyebrow. top bar text `NOW SERVING • LOS ANGELES • ...` maybe not dynamic.
Nav menu `href="#menu"`, find, story. Order button.
On mobile nav order maybe separate button data-open-cart event delegation all `[data-open-cart]`.
Cart count all `.cart-count` update.
Mobile toggle text icon CSS spans.
Menu open mobile covers below header. `mobileNav.hidden` can't animate if hidden; simple `classList`. Use `[hidden]{display:none!important}` global maybe. JS toggle hidden.
Click outside? mobile links close, toggle. Esc.
`aria-label`.
Skip link.
Potential issue use `inert`? no need.
No alerts etc.
I should ensure emojis? Use stars text ★, arrows ↗, Korean. Fonts system support.
Could include custom "spice" peppers using small CSS icons not emoji.
Logo no SVG external.
No external image requirement: all illustrations inline SVG generated by JS. Good.
I'll start composing the code. Should be exact one code block. Make sure no syntax errors. I'll think through menuArt JS strings in `<script>`: HTML inline JS containing `</script>` none. Template literals contain SVG, okay. Avoid `${}` in CSS? CSS in style unaffected. Use unique colors sanitized fixed. Data values no injection.
`tacoArt(item)`:
```
return `<svg viewBox="0 0 340 230" ...>
<ellipse .../>
<path d=... fill="#efad34" stroke.../>
<path d=... fill="${item.artColor}" .../>
...
</svg>`
```
Beef chunks using `fill=item.artColor`, accent etc. Different vegetarian green.
`riceArt`: bowl, rice dome, kimchi red bits, fried egg.
`friesArt`: tray polygon and fries lines; generous.
`quesoArt`: chips bowl orange.
Could also use `bowl` item category bowl.
For cards background from item `bg` var maybe class inline `style="--card-bg:${item.bg}"`, safe fixed.
Each article:
```
<article class="menu-card" data-category="${item.cat}" style="--delay:${index*60}ms">
<div class=menu-visual style=...>
<span class=food-tag>${item.tag}</span>
${menuArt(item)}
</div>
<div class=menu-info>
<div class=menu-title-row><h3>...</h3><span class=price>...</span></div>
<p>...</p>
<div class=menu-meta><span class=heat>...</span>${vegan...}</div>
<button class=add-btn data-add="${item.id}"><span>ADD TO ORDER</span><b>+</b></button>
</div>
</article>
```
Add button full width at bottom black cream with plus lime. Good. Filter cards all 6.
`heatLabel` if heat0 "NO HEAT" else ["","MILD","MEDIUM","HOT"][heat]. Dots 3 with on <= heat. For fries heat2, queso1.
Cards categories filters all,tacos,bowls,sides. There is one bowl so filter grid maybe one card left; fine.
Vegan chip if true, other "MADE TO ORDER". Menu footer "Gluten-free tortillas available • ...".
Find schedule cards 2x2, button colors active. CSS black card with cream border; active lime.
Main next stop:
kicker, h3 area, time, address, note. `NEXT SERVICE` if selected offset not0. `TODAY` label.
Find map `mapInfo`.
Save button update.
Story maybe CSS grid and quote line. Add small sticker `NO SHORTCUTS. JUST SEOUL.` Nice.
Newsletter form:
`<label class=sr-only for=email>Email address</label><input type=email required placeholder=YOUR@EMAIL.COM autocomplete=email><button>JOIN THE LOCO LIST ↗</button>`.
No action.
`#formNote` live region.
After success form elements hidden and note visible. Keep section height. Do not use innerHTML email.
`form.classList.add('success')`; inputs container hide. Put `.signup-fields`. status text.
Button after submission hidden via `.success .signup-fields{display:none}`.
Form note initial "One tasty text a week. Zero spam." if email say email. "One delicious email a week. Unsubscribe whenever."
On submit note text success.
CTA top "FIND THE TRUCK" scroll native. Order ahead cart.
Perhaps CTA "SEE THE MENU" black outlined.
Current year.
Header sticky affects drawer? drawer z100.
Hero art ARIA: `<svg role=img aria-labelledby=heroFoodTitle heroFoodDesc>` with title desc. JS card art aria hidden.
No console error if clipboard no. No network.
No localStorage.
Potential `scroll-behavior` and click start not game irrelevant.
Check first paint size:
1280 width topnav:
header grid 1fr auto 1fr. Logo ~160, nav~330, actions~160. fine.
Hero left height650. Copy padding 42. Content:
live pill 28 + eyebrow 14+ margin? Could have both too much.
I can put eyebrow only with dot and live location below h1? Structure:
`<div class=eyebrow><span class=eyebrow-chip>LA'S KOREAN TACO TRUCK</span><span>김치 로코</span></div>`
h1 3 lines ~225
desc 54
actions 52
proof 45
live pill 30 maybe at bottom. Total 28? Sum eyebrow22+20 margin+h1225+18+desc54+26+actions52+26+proof45+18+live32=538 plus84=622 fits 650. Good.
Perhaps proof and live too much. Put live pill at top below eyebrow? Should have current stop meaningful. `.truck-status` at bottom with green dot.
Hero h1 margin 20 0 18. Desc margin0. actions margin 26. proof margin 26. status margin-top 20.
At 650 copy padding 42 top bottom leaves566. Content maybe 536 fits.
H1 at85.8 line.87*3=224. Eyebrow22; h1 margins38 =>262; desc54 =>316; actions52+26=78 =>394; proof36+25=61 =>455; status30+18=48 =>503. Fits 566.
At 1024 hero min610, font68.6 line179; content458 plus64=522 fits.
Right art caption, sticker.
At mobile height auto copy padding 32 22; h1 60 line156, content 430 +64=494. Art360. Total854 plus nav. First screenshot 800 shows most art but bottom caption maybe below. User can scroll. CTA visible first ~550. Good.
At 375 h1 width 331, "SEOUL FOOD." at60 approximate 413? Wait approximate 6.9 em*60=414, too wide. Impact narrower but fallback may overflow. Should mobile h1 size 50 maybe 6.9*50=345 still >331; actual 6.6*50=330. So use `font-size: clamp(48px,14vw,64px)` at375=52.5 -> ~350 maybe slight. Could set 12.8vw=48 ->317. At 375 good. At 430=55 ->363 vs content386 good. At 600=76 but max64 ->422 vs content556 good. Use mobile `font-size:clamp(47px,13vw,72px); letter-spacing:-.065em`. At375 48.75. Arial Black width estimated after letter-spacing negative -.065*10 chars =-.65em reducing total from6.9 to6.25*48.75=305. fits. Desktop with letter-spacing negative reduces width significantly too. Good. h1 nowrap safe.
At 320 width (not required but down to375) 47*6.25=294 content276 (320-44) slight. can font44 under400 via clamp 44,13vw. 375 48.75. content331; good. 320 44*6.25=275 content276 exactly. Good.
Hero status text can wrap.
Proof avatars 4*28 overlap and text may fit 331.
CTA two buttons with padding 16 maybe first text 16 chars at11=176 plus 30=206, two cannot fit. Use flex column at max480 or full width. `.hero-actions {display:grid;grid-template-columns:1fr 1fr}` at mobile with font11, find truck 15*7=105+padding24=129 each width 163 good. okay. At 375 second 12*7=84. good.
`.btn padding 0 20`, at desktop.
Mobile logo + actions order ahead maybe too wide: 375 header padding16, logo150 + order135 + menu44 + gaps16 =345 >343. Hide text `ORDER AHEAD` at <=600 and show `ORDER` or bag. Could markup `<span class=order-label>ORDER <span class=wide-only>AHEAD</span></span>` CSS wide-only hidden mobile. Button width auto ~105 incl count. Logo140 +105+42+gaps16 +padding32=335 fits 375. At320 tight but okay hide mark maybe. Header grid on mobile `display:flex; justify-content:space-between; gap10`, nav center hidden. Logo ~135, nav actions order105 menu42 gap8 total155, plus logo=290 +32 padding =322 fits375, at320 barely; logo mark 34 + text100 =144, total 331. Could hide `.logo-mark` at max390? Keep brand visual; reduce font. `logo-word font 19` width90, mark34 gap8 total132. actions order94+button40+gap6=140; +132+32=304. fits320. Good.
At mobile `.order-trigger height40 padding0 10; font10; box-shadow2`. count20.
Header height70.
Hero total adjusted topbar34+70.
Topbar ticker speed etc.
Desktop `.mobile-toggle display none`, `.nav-actions` flex.
Menu at mobile h2 font50. cards grid one.
Section container width calc100%-32 (16 gutters).
Find card h3 42.
Story h2 etc.
Footer huge brand font clamp64,13vw,180; line .75; no nowrap? "KIMCHI LOCO" one line at1280 166px width approximate 11*.62*166=1130 fits. At375 48.75 but min64 =>437 too wide. use font `clamp(48px,13vw,180px)`, at375 48.75 width333 fits343. good. white-space nowrap. letter-spacing -.07.
Footer top flex, links.
Make sure body no horizontal from box-shadow within grid container (shadow extends 6px beyond width but container 24 gutters so okay).
Sticker rotation within overflow art hidden.
Marquee width max-content body hidden.
I can enhance food appeal with copy:
Hero desc: "Smoky bulgogi, punchy house kimchi and hot tortillas, stacked with crunchy slaw and our secret loco sauce. One bite and dinner plans change forever."
Perhaps long 3 lines, 80h. Keep 2-3 lines: "Charred bulgogi. Punchy house kimchi. Hot tortillas, crunchy slaw and a scandalous drizzle of loco sauce." This is 3 lines max 510 at18 ~50 char/line, 112 chars ~3 lines=81. At left 570 ~58 chars line =>2 lines 54. Good. Add "Made fresh. Gone fast." bold maybe.
Proof "★★★★★ / “Worth crossing town for.” / — literally everyone at the truck" fake quote. Use `★★★★★ <strong>4.9</strong> from 1,200+ taco people` okay. Fictional marketing.
Status live next location.
Menu header "YOUR NEW / USUAL." maybe strong. `THE LINEUP` simple.
Section kicker `BUILT BY HAND. DEVOURED IN MINUTES.` h2 `PICK YOUR / HAPPY PLACE.` Cards. Good.
Find h2 `YOUR FEAST / HAS A PIN ON IT.` maybe.
Story quote.
Join `DON'T JUST / CRUSH ON US.` followed "Get the drop."
No need too much.
Perhaps add order strip after menu: `CAN'T DECIDE? / THE LOCO COMBO: ANY 2 TACOS + FRIES + DRINK $18` with add combo. This is enticing and functional. Could add banner `.combo-banner` between menu grid and footer with button `ADD THE COMBO — $18`. Add menu item combo id combo category special not shown in grid? In data add `hidden:true`; cart can add. Price18. Add button data-add=combo. Great. Makes food enticing and interactive. Banner red/lime. `2 TACOS + LOCO FRIES + AGUA FRESCA` $18.00. Add. Menu items includes combo cat:'combo', exclude render via `item.combo` maybe `item.inMenu !== false`.
On filter all show 6 only excluding combo. cart lookup all.
Add agua fresca card? Drinks not filter. Combo includes. Fine.
`cart name:The Loco Combo`.
Empty cart quick add OG, combo.
Hero social proof no fake claims maybe okay.
Map dynamic with no real map but aesthetic. Use `road road-a` etc. CSS grid + line labels "ALAMEDA", "7TH", etc. If selected area differs road labels static but map stylized generic. Dynamic map neighborhood title enough. Use map decorative `aria-hidden=true` but address outside.
Could make route card marker wobble animation.
Now consider whether to use full inline HTML menu for SEO rather than JS. Not necessary, but semantic crawlers perhaps. Could generate via JS quickly. I can have `<noscript>` message but no need.
One concern user requested website, not app; don't over-engineer ordering so focus visual. Fine.
No need ask questions.
I'll formulate response code around maybe 50k chars. I have token budget huge. Must be exactly one code block no extra. I should validate mental JS.
Detailed JS:
```
const menuItems = [...];
const byId = id => menuItems.find(item => item.id === id);
const money = value => `$${value.toFixed(2)}`;
const heatMarkup = heat => {
const names=['NO HEAT','MILD','MEDIUM','HOT'];
return `<span class="heat" aria-label="${names[heat]}"><span>${names[heat]}</span><span class=heat-dots aria-hidden=true>${[1,2,3].map(...).join('')}</span></span>`;
}
```
Nested spans fine.
`menuArt(item)` switch item.art.
SVG can use double quotes within template. Use `stroke-linecap="round"`.
`renderMenu(filter='all')`:
```
const visible=menuItems.filter(i=>i.inMenu!==false && (...));
menuGrid.innerHTML = visible.map((item,index)=>...).join('');
```
Add `aria-live=polite` on grid? Dynamic filter okay, could cause whole read, no.
Filter buttons static.
`document.querySelector('.filters').addEventListener('click',e=>{...})`.
Set aria-pressed for each.
Cart:
```
const cart = new Map();
function cartQuantity(){...}
function updateCartBadges(){ document.querySelectorAll('.cart-count').forEach(...); }
function addToCart(id){
const item=byId(id); if(!item)return;
cart.set(id,(cart.get(id)||0)+1);
updateCartBadges(); renderCart(); showToast...
}
```
If order success currently and user add via cart empty quick add, render resets.
`renderCart()`:
if size0:
cartContent empty HTML includes button class `empty-add` data-add etc.
cartFooter.innerHTML `<button ... data-close-cart>KEEP EXPLORING</button>` maybe disabled checkout? Use no.
else:
rows map. qty accessible labels.
`const total=[...cart].reduce...`
footer pickup select and button data-checkout.
When `addToCart` invoked from menu while drawer closed, renderCart updates hidden drawer no issue. Button feedback and toast.
Order count badge class pop reset via animation:
```
badge.classList.remove('pop'); void badge.offsetWidth; badge.classList.add('pop');
```
No console.
Drawer delegation:
```
cartDrawer.addEventListener('click', e => {
const add=e.target.closest('[data-add]'); if(add){addToCart(...);return;}
const qty=e.target.closest('[data-qty]'); ...
const close=e.target.closest('[data-close-cart]'); if(close){closeCart();return;}
const checkout=e.target.closest('[data-checkout]'); if(checkout){ checkoutOrder(); }
});
```
But document also click delegation for `[data-add]` on menu and drawer if both leads double if add listener both. Use one global document listener for add, and separate for qty/close/checkout. open triggers global.
```
document.addEventListener('click', e=>{
const add=e.target.closest('[data-add]'); if(add)...
const open=e.target.closest('[data-open-cart]')...
})
```
Drawer close handled global as well. Schedule separate. Fine.
Button feedback only for `.add-btn`, `.combo-add`, not cart popular? any button can.
When renderCart inside add handler, original target removed if drawer; then timeout checks `isConnected`.
`addToCart` shows toast with item name; drawer open toast okay.
Decrease:
```
if(action==='increase') set+1 else if current<=1 delete...
renderCart update badges
```
`change` pickup select not needed; checkout finds `$('#pickupTime')?.value || 'ASAP · 15–20 MIN'`.
Checkout order:
capture total before clear, items count. random id. render content success, footer reset close button.
`updateCartBadges`.
`showToast('Pickup order locked in. See you at the window!')`.
No form submission.
`cartContent.innerHTML` success with check SVG CSS.
Footer `<button class="button button-dark full" data-close-cart>DONE — BACK TO THE SITE</button>`.
closeCart no render; next open calls renderCart empty.
`openCart` renderCart always means if clicking open after success while open not.
Cart open on any order.
Focus last. Backdrop click close.
`lastFocused` can be body; check `lastFocused && typeof focus...`.
Trap:
```
if(e.key==='Tab' && open) {
const focusables=...
if none return;
if shift && active===first... etc
}
```
`getComputedStyle` not needed hidden drawer only when open.
Mobile nav hidden attribute; if open drawer close nav.
Toast:
`let toastTimer; function showToast(msg){ clearTimeout; textContent; classList.add show; toastTimer=setTimeout...}`.
No alert.
Schedule:
```
const weekdays = ['SUNDAY',...]
const shortDays...
const stopsByDay = [obj,null,...]
const now=new Date();
const upcoming=[];
for(let i=0;i<8 && upcoming.length<4;i++){
const date=new Date(now.getFullYear(),now.getMonth(),now.getDate()+i);
const stop=stopsByDay[date.getDay()];
if(stop) upcoming.push({...stop,date,offset:i,key:`${date...}`});
}
let selected=0, savedKey=null;
function renderSchedule(){...}
function updateStop(){
const stop=upcoming[selected];
const prefix=stop.offset===0?'TODAY':stop.offset===1?'TOMORROW':shortDays...
...
const dayDate=new Intl.DateTimeFormat('en-US',{weekday:'long',month:'long',day:'numeric'}).format(stop.date)
...
savedKey...
}
```
Should top mini text initial upcoming[0].
`topStopMini` and `truckStatusText` maybe same. Hero `#liveStop` text.
`nextLabel`, `nextDate`, `nextArea`, `nextAddress`, `nextTime`, `nextNote`, `mapArea`.
`nextStopCard` maybe time separate.
renderSchedule includes each card:
```
<button class="schedule-card ${i===selected?'active':''}" data-stop-index="${i}" aria-pressed=...>
<span class=schedule-day>${offset0?'TODAY':...}</span>
<strong>${area}</strong>
<span>${time}</span>
</button>
```
Date small.
After `innerHTML` active. updateStop called separately.
Schedule click selected=Number... updateStop/renderSchedule; map card maybe brief pulse.
Save button click:
if savedKey===current key then null else current; updateSaveButton (but updateStop resets text); toast.
`updateStop` call updateSaveButton at end.
No dynamic actual directions.
Live pill if no upcoming improbable because 6 open days. Good.
Use `escape` not needed fixed data.
Newsletter no external.
Hero tilt:
```
const art=$('.hero-art'), float=$('.food-float');
art.addEventListener('pointermove',e=>{ if(!matchMedia...)return; ...; float.style.setProperty('--mx', `${x*8}px`); ...})
art.addEventListener('pointerleave', reset)
```
CSS `.food-float {transform:translate(var(--mx,0),var(--my,0)); transition:transform .2s}` and inner svg animation `foodBob`. position. Good.
No need requestAnimationFrame but pointer events could update often simple.
At pointer fine only.
Intersection observer not needed.
Year.
`document.documentElement.classList.add('js')` not needed.
Art SVG has title IDs unique.
CSS hidden attr: `[hidden]{display:none!important}`.
Use `100svh` supported latest Chrome. Fine.
Map save button inside `.map-info` maybe map-info bottom contains title etc, save. On mobile content fits.
Story section:
```
<section class=story id=story>
<div class=container story-grid>
<div><span section-kicker>... </span><h2>Two cultures.<br><span>One loud</span><br>little truck.</h2></div>
<div class=story-copy><p class=story-lead>...</p><p>...</p><div class=values>... </div></div>
</div>
<div class=story-ribbon>...</div>
</section>
```
Could be bright red background. Use black text. Story values cream border black.
Join black? Find black then story red then join cream good.
Footer.
Add floating order mobile? Header order always accessible sticky. Great.
Art menu CSS:
`.menu-card:nth-child` no.
`.menu-card:hover .menu-visual svg {transform:rotate(-2deg) scale(1.05)}`.
Some cards contain art bowl all center.
`@keyframes cardIn` opacity from0 transform 12. initial render animation delay via style `--delay`.
First screenshot at menu below fold irrelevant.
Filter rendering cards reanimate.
Hero animation:
`.hero-copy > * { animation:riseIn .65s both; }` apply nth delays maybe at screenshot done. `.hero-art .food-float` etc.
Reduced motion disable.
No flash hidden.
Top ticker at 3sec mid content still shows repeated enough.
Marquee `aria-label` maybe duplicates hidden.
`.marquee-track span` repeated 2 groups.
Ticker top content max-content 2 groups exactly 50% translation works if group same. Structure:
```
<div class=ticker-track><div class=ticker-group>...</div><div aria-hidden...>...</div></div>
```
CSS flex width max; @keyframes translateX(-50%); gap? If track gap 40 and groups padding-right 40, exact enough.
Marquee similar.
Focus outlines.
One thing `<main id=main>` skip target not focus but okay.
I'll think brand copy and final aesthetic robust. Use black outline + electric colors + illustration. Very distinct. Good.
Potential title h1 apostrophe none.
Art SVG main uses filters and complex paths. Should have no error malformed paths. Browsers forgiving.
Hero SVG:
```
<svg class=hero-taco viewBox="0 0 640 540">
<defs>...</defs>
<ellipse... />
<g filter="url(#tacoShadow)" transform="rotate(-4 320 300)">
...
</g>
<g transform="translate(485 388) rotate(-16)"> lime ...</g>
...
</svg>
```
Filter drop-shadow supported. Include scallion rings `<ellipse fill=none ...>`.
White crema path over front? at y315.
Front shell covers portions.
`path d="M112 322 C226..."`
Texture small circles front y360.
Back meat ingredients at x...
Some paths fill none etc.
`linearGradient id=heroShell` etc.
Art caption overlaps lime? okay.
Card art JS:
Taco mini:
- shell back path
- lettuce path
- meat chunk 5 groups.
- shell front
- sauce path.
`item.color` meat and `item.accent` sauce.
For mushroom color #71543e etc.
Pork red-brown.
No unique IDs.
Rice art:
`path bowl` etc.
Fries art 10 rectangles rotated manually; good.
Queso art chips triangles and cheese bowl.
Cart empty tiny svg use simple bag icon.
Topbar dynamic maybe static "FRESH KIMCHI • HOT TORTILLAS • GOOD CHOICES". Good.
No actual image requests.
Now respond. Carefully output no extra. I can include around 1600 lines. Token budget enough. I'll draft the response in one go but maintain syntax.