Style Guide
This document covers the design tokens, SCSS architecture, and component styling conventions used across the CaterCow frontend. Consistent use of these standards keeps the UI coherent and reduces onboarding friction — it is the canonical reference for color names, mixin signatures, and styling patterns.
Visual Identity (Design Tokens)
Color Palette
All color variables are defined in parallel files — use the one that matches the app you're working in:
- Shared / Nuxt 2 Public & Caterer:
js/nuxt2/shared/assets/stylesheets/common/variables.scss - Nuxt 4 Marketing:
js/nuxt4/assets/stylesheets/common/variables.scss - Admin:
js/nuxt2/admin/assets/stylesheets/_variables.scss
These files are injected globally via each app's all.scss bundle, so you can reference any variable directly in a component's <style> block without importing anything extra.
Core Brand Colors
These are the most frequently used values across the codebase (50+ uses each for the top tier). Reach for these before inventing new values.
| Variable | Hex | Usage |
|---|---|---|
| $white | #ffffff | Backgrounds, cards, modals |
| $black | #1f1f1f | Primary text color |
| $blue | #4878dd | Primary interactive color — buttons, links, active states; also exposed as --brand-color |
| $light-grey | #e9e9e9 | Default border color throughout the UI |
| $grey | #b5b5b5 | Secondary borders, input borders, disabled states |
| $off-white | #f7f7f7 | Subtle background tints, alternating table rows |
| $dark-grey | #6c6c6c | Secondary text, captions, placeholder text |
| $light-blue | #eaf0ff | Feature section backgrounds, highlight tints |
| $red | #fd2c07 | Error states, destructive actions |
Semantic / Status Colors
| Variable | Hex | When to Use |
|---|---|---|
| $green | #2ecc71 | Accepted orders, success states |
| $light-green | #e8faf0 | Success state backgrounds |
| $yellow | #f8b03a | Pending orders, warning states |
| $light-yellow | #faf2e8 | Warning backgrounds, marketing section accents |
| $red | #fd2c07 | Errors, cancellations, destructive actions |
| $light-red | #ffeae8 | Error state backgrounds |
Supporting & Edge-Case Colors
Use sparingly — if a core color would work, prefer it over these.
| Variable | Hex | Notes |
|---|---|---|
| $pink | #f7006c | Accent moments only |
| $peach | #fd867b | Marketing/CTA accents |
| $purple | #706fd3 | Occasional brand accent |
| $cream | #fff2dd | Warm background tints |
| $pale-blue | #f3f6ff | Subtle blue tints |
| $dark-pink | #dba193 | Nuxt 4 only |
| $light-pink | #f3d7cb | Nuxt 4 only |
Admin-Specific Colors
These live exclusively in js/nuxt2/admin/assets/stylesheets/_variables.scss and are not available in the shared or Nuxt 4 apps.
| Variable | Hex | Notes |
|---|---|---|
| $accent-one | #0088cc | Admin primary action color |
| $accent-two | #f9423a | Admin nav and destructive buttons |
| $accent-three | #0b4c6d | Admin secondary button color |
| $accent-three--alt | #5a9fc2 | Lighter variant of accent-three |
| $error-color | #eb101b | Admin-specific error (distinct from shared $red) |
| $success-color | #00ab00 | Admin-specific success (distinct from shared $green) |
| $order-accepted-color | #3fb34f | Order status badge: accepted |
| $order-pending-color | #f9930a | Order status badge: pending |
| $order-canceled-color | #c30a0a | Order status badge: canceled |
| $primary-font-color | rgb(72, 61, 58) | Admin body text |
| $link-color | rgb(0, 136, 204) | Admin link default |
Note on legacy admin variables:
js/nuxt2/admin/assets/stylesheets/_old_variables.scsscontains a set of camelCase variables ($catercowColor,$managedBlue, etc.) that pre-date the current naming conventions. Avoid using them in new code — prefer the current Admin variables above or the shared tokens where possible.
CSS Custom Properties (Theming)
The platform supports runtime theming through CSS custom properties. These are used throughout the shared and Nuxt 4 apps, typically as a fallback pattern alongside SCSS variables:
color: var(--text-color, $black);
background-color: var(--background-color, $white);The full set of documented custom properties:
| Property | Purpose |
|---|---|
--background-color | Page/component background (light mode default: white) |
--background-color-lighter | Slightly lighter surface, used in shadow-box |
--text-color | Primary body text color |
--text-color-lighter | Secondary/muted text color |
--brand-color | Interactive color (maps to $blue by default) |
--brand-color-text | Text on top of brand-colored surfaces (typically white) |
--brand-color-darker | Hover/active state of brand color |
--background-img | Hero background image (desktop) |
--background-img-mobile | Hero background image (mobile) |
When building components that will be used in themed contexts (restaurant profiles, embedded widgets), always use the var(--property, $fallback) pattern rather than hardcoding the SCSS variable alone.
Typography
Font Families
| Context | Font Stack | Source |
|---|---|---|
| Shared / Nuxt 4 (Public, Caterer, Marketing) | "Work Sans", sans-serif | js/nuxt2/shared/assets/stylesheets/common/typography.scss |
| Admin | Lato, sans-serif | js/nuxt2/admin/assets/stylesheets/_variables.scss ($global-font) |
Root Font Size Scale
The entire type system uses rem units anchored to a fluid root size set in typography.scss:
| Viewport | html font-size | Effective 1rem |
|---|---|---|
| Mobile (default) | 59.375% | 9.5px |
≥ 480px (mobile-landscape) | 62.5% | 10px |
≥ 1360px (desktop-large) | 65.625% | 10.5px |
This means 1.4rem evaluates to 13.3px on mobile and 14.7px on large screens — everything scales proportionally without media query overrides on individual elements.
Heading and Element Sizes
These sizes are defined in the $font-sizes SCSS map in js/nuxt2/shared/assets/stylesheets/common/typography_mixins.scss and applied globally to HTML elements by typography.scss. Use the font-size() mixin to reference them by tag name.
| Element | Size |
|---|---|
h1 | 4rem |
h2 | 3rem |
h3 | 2.2rem |
h4 | 1.8rem |
h5 | 1.6rem |
h6 | 1.4rem |
p, a, label, input, button, td, li, small (except) | 1.4rem |
small | 1.28rem |
The Admin app uses its own discrete variables ($h1-font-size through $h6-font-size) defined in js/nuxt2/admin/assets/stylesheets/_variables.scss, with slightly different sizes at h3–h6.
Spacing & Layout
Container Widths
| Variable | Value | Usage |
|---|---|---|
| $content-container-width | 114rem (~1140px) | Standard page content (use content-container mixin) |
| $large-container-width | 144rem (~1440px) | Wide sections and marketing layouts (use large-container mixin) |
| $container-padding | 1.5rem | Left/right padding inside containers |
| $grid-gap | 1.6rem | Default CSS grid gap |
| $grid-gutter-size | 5rem | Desktop spacing between grid columns |
| $grid-gutter-size--mobile | 2rem | Mobile spacing between grid columns |
| $border-radius | 4px | Standard border radius (Admin only; shared uses 0.8rem for inputs, 3.2rem for buttons) |
Responsive Breakpoints
Breakpoints are defined in js/nuxt2/shared/assets/stylesheets/common/responsive.scss (shared by all apps) and js/nuxt4/assets/stylesheets/common/responsive.scss.
| Variable | Value | Typical Use |
|---|---|---|
| $mobile-modern | 375px | Minimum supported phone width |
| $mobile-landscape | 480px | Root font-size bump, landscape phones |
| $tablet | 768px | Tablet portrait and two-column layouts |
| $tablet-large | 960px | Tablet landscape, primary layout shift |
| $desktop | 1200px | Full desktop layout |
| $desktop-large | 1360px | Large desktop, root font-size bump |
| $wide-screen | 1400px | Wide desktop |
| $super-wide | 1536px | Extra-wide screens |
| $ultra-wide-screen | 1660px | Ultra-wide monitors |
| $laptop-height | 900px | Height-based queries (use with taller mixin) |
Admin only:
$breakpoint-mobile: 800px— the Admin app's binary mobile/desktop boundary, used by themobile-onlyanddesktop-onlymixins.
SCSS Architecture & Usage
Global Files
Understanding which files are global and why prevents the most common SCSS mistake: accidentally generating duplicate CSS.
The all.scss Rule
Each app has an all.scss that is injected into every single Vue component by the build system:
| App | Path |
|---|---|
| Admin | js/nuxt2/admin/assets/stylesheets/all.scss |
| Public & Caterer | js/nuxt2/shared/assets/stylesheets/common/all.scss |
| Nuxt 4 Marketing | js/nuxt4/assets/stylesheets/common/all.scss |
These files contain only $variables, @mixin definitions, and @function definitions, no rendered CSS output. Anything that generates actual CSS output here will be copy-pasted into the bundle for every component that uses the file, ballooning bundle size.
Global Rendered Stylesheets
Actual base CSS (resets, type scale, form defaults) lives in separate files imported at the layout level, not via all.scss:
| File | What It Does | Imported By |
|---|---|---|
js/nuxt2/shared/assets/stylesheets/common/typography.scss | HTML/body reset, $font-sizes map applied to elements, link colors, box-sizing | All Nuxt 2 layout .vue files |
js/nuxt2/shared/assets/stylesheets/common/forms.scss | Input, textarea, select base styles using @include input-styles, autofill override, .row grid helper | All Nuxt 2 layout .vue files |
js/nuxt2/shared/assets/stylesheets/common/tables.scss | .records-table and .list-item component styles | Public & Caterer layout .vue files |
js/nuxt4/assets/stylesheets/common/typography.scss | Nuxt 4 equivalent of the shared typography reset | js/nuxt4/app.vue |
js/nuxt2/admin/assets/stylesheets/base.scss | Admin buttons, forms, animations, and icon helpers | Admin Nuxt config |
js/nuxt2/admin/assets/stylesheets/layout.scss | Admin html/body reset with Lato font and heading sizes | Admin Nuxt config |
Mixins & Functions
All mixins are available in any component <style> block without extra imports, because all.scss is injected globally.
Responsive Layout
@include wider($breakpoint) — the primary responsive tool across shared and Nuxt 4 apps.
// js/nuxt2/shared/assets/stylesheets/common/responsive.scss
.my-component {
display: block;
@include wider($tablet) {
display: grid;
grid-template-columns: 1fr 1fr;
}
@include wider($desktop) {
grid-template-columns: repeat(3, 1fr);
}
}@include thinner($breakpoint) — max-width variant, Nuxt 4 only.
// js/nuxt4/assets/stylesheets/common/responsive.scss
.sidebar {
@include thinner($tablet) {
display: none;
}
}@include taller($height) — height-based media query.
.sticky-nav {
@include taller($laptop-height) {
position: sticky;
top: 0;
}
}Admin only — @include mobile-only() / @include desktop-only() — binary breakpoint at $breakpoint-mobile (800px).
// js/nuxt2/admin/assets/stylesheets/_mixins.scss
.admin-panel {
@include mobile-only {
padding: 1rem;
}
@include desktop-only {
padding: 3rem;
display: grid;
grid-template-columns: 240px 1fr;
}
}Grid & Layout
@include grid($columns, $columns--mobile) — CSS grid shorthand with built-in responsive collapse.
.card-grid {
@include grid(repeat(3, 1fr), 1fr);
// Desktop: 3 equal columns, 5rem gap
// Mobile: single column, 2rem gap
}@include content-container — centers content at max 114rem.
.page-section {
@include content-container;
padding-left: $container-padding;
padding-right: $container-padding;
}@include large-container — centers content at max 144rem.
.marketing-band {
@include large-container;
}@include shadow-box — standard card/panel appearance.
.info-card {
@include shadow-box;
padding: 2rem;
border-radius: 0.8rem;
}@include fullWidthHack — breaks out of a container to span the full viewport. Use sparingly — only when a section genuinely needs to ignore its parent's max-width.
.full-bleed-banner {
@include fullWidthHack;
background-color: $light-blue;
}Lists
@include ul-horizontal($spacing, $spacing--mobile) — flex-based horizontal list with responsive spacing.
.tag-list {
@include ul-horizontal(1.5rem, 0.8rem);
list-style: none;
padding: 0;
}@include li-unstyled($spacing) — removes bullets and adds vertical spacing.
.feature-list {
@include li-unstyled(1.2rem);
}Buttons, Links & Inputs
@include button-styles — the standard pill button using --brand-color.
.cta-button {
@include button-styles;
width: 100%;
@include wider($tablet) {
width: auto;
}
}@include link-styles — transparent background link styled with --brand-color.
.inline-action {
@include link-styles;
font-size: font-size(p);
}@include link-button($color) — colored pill-shaped inline-block link. Defaults to $blue.
.secondary-action {
@include link-button($green);
}@include input-styles — full shared input styling with focus behavior.
.custom-input {
@include input-styles;
width: 100%;
}@include input-border — applies only the 1px solid $grey border (subset of input-styles).
Typography
@include font-size($tag) — looks up a size from the $font-sizes map by HTML tag name.
.caption {
@include font-size(small); // => font-size: 1.28rem
}
.body-copy {
@include font-size(p); // => font-size: 1.4rem
}@include text-smoothing — antialiasing, particularly useful on light text over dark backgrounds.
.hero-headline {
@include text-smoothing;
color: $white;
}@include pre-text — pre-formatted text block (notes, multi-line strings).
.order-notes {
@include pre-text;
}Status Colors
@include status-colors() — BEM modifier pattern for order status text colors. Apply to a parent element; modifiers like --pending, --accepted, --canceled are generated automatically.
<template>
<span :class="`order-status order-status--${order.status}`">
{{ order.status }}
</span>
</template>
<style lang="scss" scoped>
.order-status {
@include status-colors;
font-weight: bold;
}
</style>@include orderStatusBackground() (Admin only) — same BEM pattern for background colors on status badges.
Hero Backgrounds
@include hero($url) — static hero with a hardcoded image URL.
.brand-hero {
@include hero('/images/catering-hero.jpg');
height: 40rem;
}@include dynamic-hero (Nuxt 4 only) — hero driven by --background-img and --background-img-mobile CSS custom properties. Use this for restaurant profiles and other pages where the image is determined at runtime.
.restaurant-hero {
@include dynamic-hero;
height: 36rem;
}<template>
<section class="restaurant-hero" :style="heroVars" />
</template>
<script setup>
const heroVars = computed(() => ({
'--background-img': `url(${props.desktopImage})`,
'--background-img-mobile': `url(${props.mobileImage})`
}));
</script>Loading States
@include loader-animation — shimmer/skeleton loading effect using a ::before pseudo-element. Great for content placeholders.
.package-card--loading {
@include loader-animation;
background-color: $off-white;
height: 20rem;
border-radius: 0.8rem;
}Admin Navigation Patterns
@include pills($bg-color, $text-color) — pill/tab navigation with BEM structure. Expects child elements with .pills__tab and .pills__link classes.
.filter-nav {
@include pills($accent-one, $accent-one);
}@include tabs($bg-color, $text-color) — horizontal scrollable tab strip with BEM structure and responsive behavior.
.section-tabs {
@include tabs($accent-one, $accent-one);
}SVG Helpers (Shared)
.my-icon {
@include svg; // 1em × inline-block, inherits color
}
.fill-icon {
@include svg-fill; // fills SVG paths with currentColor
}
.stroke-icon {
@include svg-stroke; // strokes SVG paths with currentColor
}Alert / Notification Colors
.alert {
padding: 1.2rem;
border: 1px solid;
border-radius: 0.8rem;
&--info { @include info-colors; }
&--warning { @include warning-colors; }
&--success { @include success-colors; }
&--error { @include error-colors; }
&--empty { @include empty-colors; }
}Utility Classes
This codebase is not utility-first. There is no Tailwind or utility framework. The only hand-authored utility classes are a handful of margin helpers defined in typography.scss:
| Class | Declaration | Available In |
|---|---|---|
.mt-0 | margin-top: 0 | Nuxt 4 only |
.ml-1 | margin-left: 0.5em | Shared & Nuxt 4 |
.mr-1 | margin-right: 0.5em | Shared & Nuxt 4 |
For layout needs beyond these, use mixins or component-scoped SCSS rather than adding new utility classes. This keeps the global namespace clean and avoids style conflicts in scoped contexts.
Component Styling Standards
Scoped vs. Global Styles
The default is <style lang="scss" scoped>. Scoped styles are appropriate for the vast majority of components. Vue's scoped attribute adds a unique data attribute to the component's elements, preventing styles from leaking out and affecting other components.
<!-- Standard pattern for components and pages -->
<style lang="scss" scoped>
.my-component {
padding: 2rem;
&__title {
@include font-size(h3);
color: $black;
}
}
</style>Use unscoped styles only when intentional global CSS is needed. Layout files (layouts/*.vue) use unscoped styles to inject foundational styles into the app shell. These import the shared typography, forms, and table stylesheets:
<!-- layouts/default.vue (Nuxt 2 shared pattern) -->
<style lang="scss">
@import '~shared/assets/stylesheets/common/typography';
@import '~shared/assets/stylesheets/common/forms';
@import '~shared/assets/stylesheets/common/tables';
</style>Do not add global
@importstatements in non-layout components. If a component needs styles fromtypography.scssorforms.scss, those are already available globally because they're loaded by the layout.
Overriding third-party component styles: To pierce the scoped boundary and override a child component or third-party library, use :deep() (Vue 3 / Nuxt 4) or the ::v-deep combinator (Vue 2 / Nuxt 2):
<!-- Vue 3 / Nuxt 4 -->
<style lang="scss" scoped>
.my-wrapper {
:deep(.formkit-input) {
border-radius: 0.4rem;
}
}
</style>
<!-- Vue 2 / Nuxt 2 -->
<style lang="scss" scoped>
.my-wrapper {
::v-deep .some-library-class {
color: $blue;
}
}
</style>Some Admin pages use two <style> blocks intentionally — one scoped and one global. This is an acceptable escape hatch but should be documented with a comment explaining why the global block is necessary.
Implementation Guide: Styling a New Component
When adding a new component, follow these steps to stay consistent with the system.
1. Use existing tokens, not hardcoded values
Every color, spacing value, and font size has a variable. Avoid hardcoded hex or pixel values — check the token list first.
// Avoid: hardcoded values
.badge {
background-color: #eaf0ff;
color: #4878dd;
font-size: 14px;
padding: 6px 12px;
}
// Preferred
.badge {
background-color: $light-blue;
color: $blue;
@include font-size(p);
padding: 0.6rem 1.2rem;
}2. Prefer mixins over repeated declarations
If the same three or four declarations appear in multiple places, a mixin likely exists. Check mixins.scss before writing new CSS.
// Avoid: repeated declarations
.primary-button {
background-color: var(--brand-color, $blue);
border-radius: 3.2rem;
color: var(--brand-color-text, $white);
font-weight: bold;
padding: 1.2rem 3rem;
}
// Preferred
.primary-button {
@include button-styles;
}3. Build responsiveness mobile-first with wider()
Start with mobile styles, then layer in larger-screen overrides:
<style lang="scss" scoped>
.feature-grid {
display: flex;
flex-direction: column;
gap: $grid-gap;
@include wider($tablet) {
display: grid;
grid-template-columns: 1fr 1fr;
}
@include wider($desktop) {
grid-template-columns: repeat(3, 1fr);
gap: $grid-gutter-size;
}
}
</style>4. Use the CSS custom property fallback pattern
For any property that a themed context could override (background, text color, brand accent), use var() with the SCSS variable as fallback:
.panel {
background-color: var(--background-color, $white);
color: var(--text-color, $black);
border: 1px solid $light-grey;
&__cta {
@include button-styles; // already uses var(--brand-color, $blue) internally
}
}5. Complete component example
<template>
<div class="info-card">
<h3 class="info-card__title">{{ title }}</h3>
<p class="info-card__body">{{ body }}</p>
<button class="info-card__action" @click="$emit('click')">
{{ cta }}
</button>
</div>
</template>
<style lang="scss" scoped>
.info-card {
@include shadow-box;
border-radius: 0.8rem;
padding: 2rem;
@include wider($tablet) {
padding: 3rem;
}
&__title {
@include font-size(h4);
color: var(--text-color, $black);
margin-bottom: 1rem;
}
&__body {
@include font-size(p);
color: $dark-grey;
margin-bottom: 2rem;
}
&__action {
@include button-styles;
}
}
</style>This component uses zero hardcoded values, responds to theming via CSS custom properties, and is responsive without any extra work.
Mixin Quick Reference
| Need | Mixin |
|---|---|
Responsive min-width breakpoint | wider($breakpoint) |
Responsive max-width breakpoint (Nuxt 4) | thinner($breakpoint) |
| Standard page-width container | content-container |
| Wide marketing container | large-container |
| Card / panel visual | shadow-box |
| CSS grid with responsive collapse | grid($columns, $columns--mobile) |
| Horizontal flex list | ul-horizontal($spacing) |
| Pill button | button-styles |
| Text link | link-styles |
| Standard text input | input-styles |
| Font size by element type | font-size($tag) |
| Order status text colors | status-colors |
| Skeleton loading animation | loader-animation |
| Full-viewport-width breakout | fullWidthHack |
| Static hero image | hero($url) |
| Dynamic/themed hero image | dynamic-hero (Nuxt 4) |
| Hide scrollbar cross-browser | hide-scrollbar |
| Absolute centering | absCenter |