Visual design reference for SB Sommar. Inspired by sbsommar.se, the live production site. This document is the single source of truth for design decisions.
All CSS must use the custom properties defined in §7. Do not hardcode colors, spacing, or typography values.
| ID | Name | Hex | Use |
|---|---|---|---|
07-§2.1 |
Terracotta | #C76D48 |
Primary accent, buttons, links, highlights |
07-§2.2 |
Sage green | #ADBF77 |
Secondary accent, section headers, tags |
07-§2.3 |
Cream | #F5EEDF |
Page background, warm neutral base |
07-§2.4 |
Navy | #192A3D |
Main headings, strong contrast |
07-§2.5 |
Charcoal | #3B3A38 |
Body text, muted dark |
07-§2.6 |
White | #FFFFFF |
Cards, content blocks, contrast surfaces |
07-§2.8 |
Cream light | #FAF7EF |
Countdown background, lighter cream variant |
Avoid bright or saturated colors outside this palette. The warmth comes from restraint.
system-ui, -apple-system, sans-serif | ID | Element | Size | Weight | Color | Notes |
|---|---|---|---|---|---|
07-§3.4 |
H1 | 40px | 700 | #C76D48 |
Page titles, hero heading |
07-§3.5 |
H2 | 35px | 700 | #C76D48 |
Section headings |
07-§3.6 |
H3 | 30px | 700 | #C76D48 |
Sub-section headings |
07-§3.7 |
Body | 16px | 400 | #3B3A38 |
All running text |
07-§3.8 |
Small / meta | 14px | 400 | #3B3A38 |
Dates, labels, captions |
07-§3.9 |
Pull quote | 25px | 600 | #3B3A38 |
Georgia serif, italic |
07-§3.10 |
Nav links | 12px | 700 | varies | Uppercase, spaced out |
Line height for body text: 1.65. Generous — makes Swedish long-form text comfortable to read.
| ID | Type | Max-width | Use |
|---|---|---|---|
07-§4.1 |
Wide | 1290px |
Full layout, header, hero |
07-§4.2 |
Narrow | 750px |
Reading sections, articles |
Container is centered with margin: 0 auto and horizontal padding on small screens.
Base unit: 8px. Spacing values are multiples of this.
| ID | Token name | Value | Use |
|---|---|---|---|
07-§4.5 |
space-xs |
8px | Tight gaps, icon padding |
07-§4.6 |
space-sm |
16px | Between inline elements |
07-§4.7 |
space-md |
24px | Card padding, form fields |
07-§4.8 |
space-lg |
40px | Between sections within a page |
07-§4.9 |
space-xl |
64px | Between major page sections |
07-§4.10 |
space-xxl |
96px | Hero padding, page top/bottom |
| ID | Breakpoint | Width | Description |
|---|---|---|---|
07-§5.1 |
Desktop | > 1000px | Full layout, side-by-side columns |
07-§5.2 |
Tablet | 690–999px | 2-column grids, condensed header |
07-§5.3 |
Mobile | < 690px | Single column, stacked layout |
120px desktop, 70px mobile. 12px, 700 weight, spaced with letter-spacing: 0.08em. var(--color-sage-hover)) color shift. 13px, 700, letter-spacing: 0.06em,
color: var(--color-terracotta), sage-hover green on active/hover). 11px, 700,
color: var(--color-terracotta), sage-hover green on hover). 1px rule in rgba(0,0,0,0.06). background: var(--color-terracotta), white text,
z-index: 100. Fully rounded corners (--radius-lg), horizontal inset
margins so it appears as a floating card. background: var(--color-terracotta), white icon
bars, border-radius: var(--radius-md). 15px / 700 / opacity: 0.9;
section links at 12px / uppercase / opacity: 0.6. Separated by a
2px solid rgba(255,255,255,0.3) rule. max-height animation, 250ms ease. position: sticky; top: 0 on all viewports so it
remains visible when scrolling. A negative margin-top equal to body
padding-top pulls the bar up, and an equal padding-top compensates, so
the bar content stays at the same vertical position in both normal flow
and stuck mode — no visible jump. html has scroll-padding-top set to account for the sticky navigation
height, so anchor-link targets are not hidden behind the bar. --radius-lg (16px) and uses
object-fit: cover. #FAF7EF (near-white cream), solid — not
semi-transparent. schema.html, idag.html, lagg-till.html. var(--color-terracotta) (#C76D48), text white, 700
weight, border-radius: 999px, padding: 10px 24px. #b35e3a, 200ms ease
transition. display: flex;
justify-content: center; gap: var(--space-sm). data-opens / data-closes attributes and toggles the hidden
attribute. start_date ascending (closest camp
on top). <a> card with a terracotta left border
accent (border-left: 4px solid var(--color-terracotta)), background
var(--color-cream-light), border-radius: var(--radius-md),
padding: var(--space-sm) var(--space-md). 700 weight, color: var(--color-terracotta),
font-size: var(--font-size-base). font-size: var(--font-size-small),
color: var(--color-charcoal), displayed on its own line beneath the
title. .hero-registration-banners): display: flex;
flex-direction: column; gap: var(--space-sm), centred within the hero
container width. color-mix(in srgb, var(--color-terracotta) 6%, var(--color-cream-light)),
200ms ease, no scale transform. 2px solid var(--color-terracotta); outline-offset: 2px). hidden outside the window defined by
data-opens / data-closes. .btn-primary styling (terracotta background, white
text). .registration-cta — display: block; margin: 0 0
var(--space-md) 0: the button sits on its own line directly under
the “Hur anmäler jag oss?” heading. .registration-cta-btn — display: inline-block; text-decoration:
none: the button is sized to its own content. Same layout on
desktop and mobile — no float, no breakpoint-dependent width
change. target="_blank", rel="noopener noreferrer"). 40px. 10px 24px. 4px (subtle, not fully rounded). #C76D48, text white, no border. #C76D48, text #C76D48, transparent background. 200ms ease. 700 weight, 14–16px. var(--color-error, #b91c1c), text
var(--color-error, #b91c1c), transparent background. Same sizing
and font rules as secondary. Hover: background
var(--color-error, #b91c1c), text white. #FFFFFF. 6px. 0 4px 12px rgba(0, 0, 0, 0.04). 24px. border-radius: 50%, ~60px). .testimonial-card wraps each testimonial, constrained to
--container-narrow. #ADBF77 background, dark text. #F5EEDF or white background. + / − or a chevron. max-height transition. 48px × 3px) underneath,
via ::after pseudo-element. 12px uppercase. --color-cream-light background that stretches edge-to-edge across the
viewport, creating horizontal colour bands. Uses the full-bleed technique
(margin-left: calc(-50vw + 50%) etc.) to break out of the body
container. html has overflow-x: hidden to prevent scrollbar from the
100vw calculation. Class: .section-alt. section-first) is excluded from alternation. border-top divider. /live.html is optimised for portrait-orientation screens (e.g. 1080 × 1920 px). The heading moves into the sidebar so events use the full available height from the top of the page.
flex: 3; sidebar flex: 1 (~75 % / 25 %). h1#today-heading) sits at the top of the sidebar. It shows only the current day and date; no page-title prefix. var(--color-sage)), 22px, 700, line-height 1.2, margin-bottom var(--space-sm). font-size: 13px, reduced vertical padding (6px top/bottom). The sidebar of /live.html shows a compact status widget above the descriptive text and QR code.
var(--color-sage)), 48px, 700 weight, tabular-nums, line-height 1. 12px, muted white (rgba(255,255,255,0.4)), block, margin-top var(--space-xs). var(--space-md) margin-bottom. Per-field validation errors appear directly below the input they relate to.
.field div may contain a <span class="field-error"> after the input element. 14px, var(--color-terracotta). 2px solid var(--color-terracotta)) via aria-invalid="true". aria-describedby so screen readers announce the error in context. aria-invalid is cleared, and the border returns to normal. Per-field informational messages (non-error) appear below the input, reusing
the same <span> element as errors but with a different class.
.field-info class provides non-error informational feedback. var(--color-charcoal). color-mix(in srgb, var(--color-sage) 20%, var(--color-cream)). 3px solid var(--color-sage). aria-invalid on the input — the input border remains normal. span.className between field-error and field-info). The submit modal overlays the page during form submission and shows progress, success, or error states.
--radius-lg (16 px) border-radius, generous shadow (0 8px 32px rgba(0,0,0,0.16)). --space-lg top/bottom, --space-md left/right — extra vertical breathing room. tabindex="-1") but must not display a visible focus outline since it is not interactive. ease-out. Within the 300 ms ceiling in 07-§10.2. color-mix(in srgb, var(--color-navy) 60%, transparent). A row of small icon buttons directly above the description <textarea>.
.md-toolbar): display: flex, gap: 4px, padding: 4px, background var(--color-cream-light), border 1px solid rgba(0,0,0,0.1), border-radius: var(--radius-sm) var(--radius-sm) 0 0 (top corners only). The textarea below it gets border-radius: 0 0 var(--radius-sm) var(--radius-sm) and border-top: none so they feel like one component. background: transparent, no border, padding: 6px, border-radius: var(--radius-sm), color: var(--color-charcoal), cursor: pointer. background: rgba(0,0,0,0.06). 2px solid var(--color-terracotta), outline-offset: 2px). 16px × 16px inline SVGs, stroke: currentColor, fill: none, stroke-width: 2. 1px solid rgba(0,0,0,0.1) separator between the toolbar and textarea is achieved by the toolbar’s bottom border matching the textarea’s side borders. A read-only preview area below the description <textarea> that shows live-rendered Markdown.
.md-preview): background: var(--color-cream-light), border: 1px solid rgba(0,0,0,0.10), border-radius: var(--radius-sm), padding: var(--space-sm), margin-top: var(--space-xs), color: var(--color-charcoal). .md-preview-label) above the rendered content: font-size: var(--font-size-small), font-weight: 700, text-transform: uppercase, letter-spacing: 0.04em, color: var(--color-charcoal), opacity: 0.5, margin: 0 0 var(--space-xs), padding-bottom: var(--space-xs), border-bottom: 1px solid rgba(0,0,0,0.07) — mirrors the .event-section-label pattern with a thin separator line below. .event-description rules: paragraphs get margin: 0 0 var(--space-xs), last paragraph margin-bottom: 0. hidden attribute) when the textarea is empty. pointer-events: none on inner content to enforce read-only behaviour. Headings inside event descriptions (.event-desc, .event-description) and
the Markdown preview (.md-preview) must be scaled down from the page-level
heading sizes so they fit the smaller context and follow a strictly decreasing
hierarchy.
1.4em, 700, color: var(--color-terracotta), margin: 0 0 var(--space-xs). 1.2em, 700, color: var(--color-terracotta), margin: 0 0 var(--space-xs). 1.1em, 700, color: var(--color-terracotta), margin: 0 0 var(--space-xs). 1em, 700, color: inherit, margin: 0 0 var(--space-xs). Using em makes the headings proportional to the container’s font-size
(e.g. 13 px in .event-extra, 16 px in .md-preview).
A small counter below text input fields showing how many characters have been typed relative to the maximum.
.char-counter): font-size: var(--font-size-small),
color: var(--color-charcoal), opacity: 0.6, text-align: right,
margin-top: 2px. .char-counter.warn): color: var(--color-terracotta),
opacity: 1. Applied when field value reaches or exceeds 90 % of the maximum
length. hidden attribute) when the field value is below 70 %
of the maximum length. A grid of day buttons replaces the native date picker on the add-activity form. The grid is always multi-select — there is no toggle.
.day-grid): display: flex, flex-wrap: wrap,
gap: var(--space-xs). .day-btn): min-width: 4.5em, padding: var(--space-xs)
var(--space-sm), border: 2px solid rgba(0,0,0,0.12),
border-radius: var(--radius-sm), background: var(--color-white),
color: var(--color-charcoal), font-size: var(--font-size-small),
text-align: center, cursor: pointer. border-color: var(--color-terracotta),
background: color-mix(in srgb, var(--color-terracotta) 10%, var(--color-white)). .day-btn.selected): border-color: var(--color-sage),
background: color-mix(in srgb, var(--color-sage) 12%, var(--color-cream)).
No font-weight change to prevent button size shifts. 2px solid var(--color-terracotta), outline-offset: 2px). .day-grid-nav): display: flex, centered, with ← / →
buttons when the camp has more than 8 days. Page size is set via
data-page-size attribute on the day grid container. .confirm-summary): table layout inside the submit
confirmation modal, with emoji icons in the first column and values in the
second. Compact padding (2px), description row has extra top
padding. .field-info hint with the text
Återkommande aktivitet — välj flera dagar. is rendered between the
Datum * label and the .day-grid container on the add-activity
page. It is always visible and shares the styling defined in
07-§6.44a–6.44g. The /lokaler.html page shows every locale (from source/data/local.yaml)
as a row, with the active camp’s dates forming the horizontal axis and
existing bookings rendered as time-blocks inside each row. It is a
read-only visualisation aimed at spotting free time slots at a glance.
.lokaler-grid-wrapper): overflow-x: auto,
width: fit-content, max-width: 100%. The grid’s visible frame
lives on the wrapper (1px solid color-mix(in srgb, var(--color-charcoal) 22%, transparent)),
so the frame stays put when the inner grid scrolls horizontally. .lokaler-grid): CSS Grid with grid-template-columns:
var(--lokaler-label-col) repeat(var(--lokaler-day-count), var(--lokaler-day-col)).
A 1px gap with a charcoal-25%-over-transparent background shows as
row and column divider lines. No radius — inner cells are rectangular,
and any radius would leave empty triangles at the outer corners. .lokaler-grid-corner): top-left cell labelled
“Lokaler \ Dag” — the backslash reads as a diagonal separator between
the row axis (locales) and the column axis (days). Shares the warm
tan background with the data cells so the label chrome is visually
just the row/column rubrics. position: sticky; left: 0; z-index: 2
so it stays parked above the sticky label column. .lokal-label): flex-direction: column; justify-content: center,
font-weight: 700, color: var(--color-charcoal),
background: var(--color-cream-light), padding: var(--space-xs) var(--space-sm),
position: sticky; left: 0; z-index: 1. Empty locales render
“Inga bokningar” as a small italic sub-label inside the same cell
(.lokal-empty). .day-band-label): weekday abbreviation and date
(e.g. “mån 22/6”), font-size: var(--font-size-small),
background: var(--color-cream-light), font-weight: 700,
center-aligned. .day-band): warm tan background
(color-mix(in srgb, var(--color-cream) 90%, var(--color-charcoal) 10%))
with a repeating-linear-gradient drawing one 1px vertical line per
hour — so 12:00, 15:00, 18:00 … are visible even when a booking
block is only a few pixels wide. position: relative; min-height: 3.5em
so event blocks can be absolutely positioned inside. Bands with
stacked lanes scale min-height via .day-band--lanes-N modifiers
(5em for 2, 6.5em for 3, 8em for 4, 9.5em for 5). .event-block, rendered as <a>): absolute-positioned
within its day band using inline-generated [data-lb="…"] rules for
left/width (percent of the hour band) and custom properties
--lane and --group. Vertical placement is top: calc((var(--lane)
/ var(--group)) * 100% + 2px), height: calc(100% / var(--group) - 4px)
— so non-overlapping events keep full band height even on days where
other events stack. Default colour family signals “OK booking”:
background: color-mix(in srgb, var(--color-sage) 25%, var(--color-white)),
border-left: 3px solid var(--color-sage),
border-radius: var(--radius-sm),
min-width: 8px (enough to stay clickable, small enough that
1-hour events don’t swell and distort the visible time gap between
neighbouring bookings). width: auto; min-width: max-content) so the full title is readable
without a native tooltip, z-index: 2 to lift above neighbours,
background: color-mix(in srgb, var(--color-sage) 45%, var(--color-white)),
plus a soft charcoal drop shadow. Focus ring: 2px solid var(--color-sage-hover),
outline-offset: 2px. .event-block--clash): applied to any event that
overlaps at least one other event in the same locale+day. Overrides
the green palette with --color-error:
background: color-mix(in srgb, var(--color-error) 35%, var(--color-white)),
border-left-color: var(--color-error),
and box-shadow: 0 0 0 1.5px var(--color-error) as a red outline
around the whole block. Hover/focus strengthens to a 55% error mix
and adds a red drop shadow. The clash rules live after the general
.event-block:hover rules in the stylesheet so they win at equal
specificity. .lokal-empty): small italic
“Inga bokningar” text inside .lokal-label when the locale has no
events in the visible range. font-style: italic; font-size:
calc(var(--font-size-small) - 1px),
color: color-mix(in srgb, var(--color-charcoal) 55%, transparent). .lokaler-legend): placed above the grid (the grid is
often taller than the viewport so a below-grid note would be easy to
miss). font-size: var(--font-size-small),
color: var(--color-charcoal),
max-width: 750px so it wraps at the same reading width as
.intro rather than stretching to the wider grid. .lokaler-grid-wrapper retains its
overflow-x: auto, and the locale label column shrinks via
--lokaler-label-col: 6em and --lokaler-day-col: 10em to keep the
day columns readable on phone screens. The add-activity form, the edit-activity form, and per-event detail
pages at /schema/<slug>/ all render the same conflict-warning
banner when the activity’s date/time/place overlaps another booking.
The banner deliberately reuses the clash palette from the Locale
Overview (.event-block--clash) so the two signals feel like the
same thing.
.conflict-warning):
background: color-mix(in srgb, var(--color-error) 35%, var(--color-white)),
border-left: 3px solid var(--color-error),
box-shadow: 0 0 0 1.5px var(--color-error),
border-radius: var(--radius-sm),
padding: var(--space-md). The body text uses
color: var(--color-text) (not red) — only the accent is red, so
the banner reads as “heads up” rather than as a blocking error.
The banner does not prevent submit. .conflict-warning__lead): plain paragraph,
margin: 0 0 var(--space-xs) 0, font-weight: 600. The sentence
uses singular (“en annan aktivitet”) or plural (“flera aktiviteter”)
depending on how many conflicts are in that date group. .conflict-warning__list): unbulleted list with
time, title, and responsible person per row. Time is monospace or
tabular-nums so the columns line up when several conflicts are
listed. .conflict-warning__date, multi-date
add-form only): small <h3> per conflicting date, e.g.
Måndag 4/5. Dates without conflicts do not appear. .conflict-warning__footer a): points to
/lokaler.html with the text Se lokalöversikt →. color:
var(--color-error); underline on hover/focus. Same focus-ring
treatment as other content links. role="status" and
aria-live="polite" so assistive tech announces new conflicts as
the user types, without stealing focus. On the static per-event
page the banner does not need aria-live (the HTML is fixed at
build time). .event-detail after the
place/responsible row and before the description, so readers see
the clash alongside the rest of the booking’s metadata. Write CSS for a component only once its HTML structure exists. Speculative CSS — written before the markup is settled — creates waste and drift.
When CSS is written, start with these at :root:
:root {
/* Colors */
--color-terracotta: #C76D48;
--color-sage: #ADBF77;
--color-cream: #F5EEDF;
--color-navy: #192A3D;
--color-charcoal: #3B3A38;
--color-white: #FFFFFF;
--color-cream-light: #FAF7EF;
/* Typography */
--font-sans: system-ui, -apple-system, sans-serif;
--font-serif: Georgia, serif;
--font-size-base: 16px;
--font-size-h1: 40px;
--font-size-h2: 35px;
--font-size-h3: 30px;
--font-size-pullquote: 25px;
--font-size-small: 14px;
--font-size-nav: 12px;
--line-height-body: 1.65;
/* Spacing */
--space-xs: 8px;
--space-sm: 16px;
--space-md: 24px;
--space-lg: 40px;
--space-xl: 64px;
--space-xxl: 96px;
/* Layout */
--container-wide: 1290px;
--container-narrow: 750px;
/* Shadows */
--shadow-card: 0 4px 12px rgba(0, 0, 0, 0.04);
/* Borders */
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 16px;
--radius-full: 50%;
}
These variables make it trivial to adjust the design globally later.
4.5:1 for body text). WCAG is the Web Content Accessibility Guidelines — the international standard for accessible web design. :focus-visible states: outline: 2px solid var(--color-terracotta); outline-offset: 2px. This applies to buttons, navigation links, form inputs, accordion summaries, content links, and any other focusable element. alt text. aria-expanded, aria-controls). Native <details>/<summary> elements satisfy this requirement — browsers expose expanded/collapsed state to assistive technology without explicit ARIA attributes. Custom accordion components (e.g. the archive timeline) must use explicit aria-expanded and aria-controls. <main> landmark element wrapping the primary content (between navigation and footer). This lets screen readers skip past navigation directly to content. 200–300ms).