Starter template source
The complete, publishable source of the reference template, copy it as the base for your own. This is the exact artifact published as [email protected].
assets/theme.css
Section titled “assets/theme.css”/* starter theme, a deliberately distinct look (midnight/emerald) proving the artifact owns the skin */:root:root { --bg: #0e1614; --bg-tint: #14201d; --surface: #16231f; --ink: #eef6f2; --muted: #93a8a0; --primary: #2dd4a7; --primary-strong: #19b28a; --primary-soft: rgba(45, 212, 167, 0.14); --accent: #f2b23e; --accent-strong: #d99a24; --line: rgba(238, 246, 242, 0.14); --line-strong: rgba(45, 212, 167, 0.4); --shadow-sm: 0 8px 24px rgba(0, 0, 0, 0.35); --shadow: 0 24px 60px rgba(0, 0, 0, 0.5); --radius: 10px; --radius-sm: 7px;}body { background: var(--bg); }h1, h2, h3, h4 { color: var(--ink); font-family: 'IBM Plex Sans Arabic', var(--p60s-siteFont, 'Inter'), sans-serif; letter-spacing: -0.02em; margin: 0 0 1rem; line-height: 1.1; }h1 { font-size: clamp(2.4rem, 5vw, 3.6rem); }h2 { font-size: clamp(1.7rem, 3vw, 2.4rem); }h3 { font-size: 1.15rem; }p { color: var(--muted); line-height: 1.7; margin: 0 0 1rem; }strong { color: var(--ink); }.site-header { background: rgba(14, 22, 20, 0.86); }.lq-hero { padding: 4.5rem 0 2.5rem; border-bottom: 1px solid var(--line); }.lq-kicker { display: inline-block; border: 1px solid var(--line-strong); color: var(--primary); border-radius: 4px; padding: 0.3rem 0.8rem; font-size: 0.75rem; letter-spacing: 0.14em; text-transform: uppercase; font-weight: 700; }.lq-hero h1 { margin-top: 1rem; max-width: 18ch; }.lq-hero-body { max-width: 62ch; }.lq-values { padding: 3.5rem 0; }.lq-value-rail { list-style: none; margin: 0; padding: 0 0 0 1.4rem; border-left: 2px solid var(--line-strong); display: grid; gap: 1.6rem; max-width: 700px; }.lq-value-rail li { position: relative; }.lq-value-dot { position: absolute; left: -1.75rem; top: 0.35rem; width: 0.65rem; height: 0.65rem; border-radius: 50%; background: var(--primary); box-shadow: 0 0 0 4px var(--primary-soft); }.lq-value-rail h3 { margin: 0 0 0.25rem; }.lq-value-rail p { margin: 0; }.lq-cta { padding: 3.5rem 0 4.5rem; }.lq-cta-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 2.5rem; align-items: start; }@media (max-width: 900px) { .lq-cta-grid { grid-template-columns: 1fr; } }
/* The one font slot, headings above already follow it; body text too. */body { font-family: ' IBM Plex Sans Arabic', var(--p60s-siteFont, 'Inter'), ui-sans-serif, system-ui, sans-serif; }
/* ── Home hero (supports.heroImagery, the reference treatment) ───────────────────────────── The no-photo state is a DESIGNED gradient (rule: derive or omit, never a placeholder image). Photos get a scrim built from the palette itself via color-mix, so uploads tone into the template, and any palette change re-tones them for free. Never place a hero photo raw. */.lq-homehero { position: relative; padding: 4.5rem 0; background: radial-gradient(720px 380px at 85% -10%, var(--primary-soft), transparent 60%), linear-gradient(165deg, var(--bg-tint) 0%, var(--bg) 100%);}.lq-homehero--photo { background-size: cover; background-position: center; }.lq-homehero--photo::before { content: ''; position: absolute; inset: 0; background: linear-gradient( 180deg, color-mix(in srgb, var(--bg) 68%, transparent) 0%, color-mix(in srgb, var(--bg) 38%, transparent) 50%, color-mix(in srgb, var(--bg-tint) 72%, transparent) 100% );}.lq-homehero > .container { position: relative; z-index: 1; }.lq-homehero-inner { display: grid; gap: 2.5rem; align-items: center; }@media (min-width: 900px) { .lq-homehero-inner { grid-template-columns: 1.2fr 1fr; }}.lq-homehero-lead { color: var(--muted); max-width: 46ch; margin: 0.9rem 0 1.4rem; }.lq-homehero-actions { display: flex; gap: 0.8rem; flex-wrap: wrap; }
/* The platform hero_carousel island, styled through its class API, the island owns motion (auto-advance, swipe, reduced-motion), this owns the look. Each slide's .hero-slide-scrim is the same palette treatment as a single photo. */.lq-homehero--carousel { overflow: hidden; }.lq-homehero--carousel .hero-carousel { position: absolute; inset: 0; z-index: 0; }.lq-homehero--carousel .hero-slides,.lq-homehero--carousel .hero-slide { position: absolute; inset: 0; }.lq-homehero--carousel .hero-slide { opacity: 0; transition: opacity 1s ease; }.lq-homehero--carousel .hero-slide--active { opacity: 1; }.lq-homehero--carousel .hero-slide-img { width: 100%; height: 100%; object-fit: cover; display: block; }.lq-homehero--carousel .hero-slide-scrim { position: absolute; inset: 0; background: linear-gradient( 180deg, color-mix(in srgb, var(--bg) 68%, transparent) 0%, color-mix(in srgb, var(--bg) 38%, transparent) 50%, color-mix(in srgb, var(--bg-tint) 72%, transparent) 100% );}.lq-homehero--carousel .hero-dots { position: absolute; left: 0; right: 0; bottom: 0.8rem; display: flex; justify-content: center; gap: 0.4rem; z-index: 2;}.lq-homehero--carousel .hero-dot { width: 9px; height: 9px; border-radius: 50%; border: 1px solid var(--accent); background: transparent; padding: 0; cursor: pointer;}.lq-homehero--carousel .hero-dot--active { background: var(--accent); }
/* ── Vista framing (photoFraming 'whole'), the hero adapts to the PHOTO's shape ───────────── */.lq-homehero--vista { padding-top: 0; }.lq-vista { position: relative; overflow: hidden; }.lq-vista-sizer { display: block; width: 100%; height: auto; max-height: 74vh; object-fit: cover; }.lq-vista-scrim,.lq-homehero--vista .hero-slide-scrim { position: absolute; inset: 0; pointer-events: none; background: linear-gradient( 180deg, color-mix(in srgb, var(--bg) 45%, transparent) 0%, color-mix(in srgb, var(--bg) 12%, transparent) 40%, color-mix(in srgb, var(--bg-tint) 60%, transparent) 100% );}.lq-homehero--vista .lq-homehero-inner { padding-top: 2.4rem; }.lq-homehero--vista .hero-carousel { position: absolute; inset: 0; z-index: 0; }
/* Vista 1.5.1: copy ON the photograph (strengthened lower scrim); the widget follows in its own strip; narrow screens flow the copy back underneath. */.lq-vista-copy { position: absolute; inset-inline: 0; bottom: 0; padding: 1.4rem 0 2rem; z-index: 1; }.lq-homehero--vista .lq-vista-scrim,.lq-homehero--vista .hero-slide-scrim { background: linear-gradient( 180deg, color-mix(in srgb, var(--bg) 38%, transparent) 0%, color-mix(in srgb, var(--bg) 10%, transparent) 38%, color-mix(in srgb, var(--bg-tint) 82%, transparent) 100% );}.lq-vista-sizer { min-height: 280px; }.lq-vista-widget { padding: 2.2rem 0 0; display: flex; justify-content: center; }.lq-vista-widget aside { width: min(560px, 100%); }@media (max-width: 720px) { .lq-vista-copy { position: static; padding: 1.8rem 0 0; } .lq-vista-sizer { min-height: 160px; }}
/* Vista + form (1.5.2): the widget rides the seam, grid pulled up over the band (under the 280px floor), gradient filling where the image ends; narrow screens stack back underneath. */.lq-homehero--vista-form { padding-bottom: 2.6rem; }.lq-vista-overlap { position: relative; z-index: 1; margin-top: -240px; align-items: start; }@media (max-width: 899px) { .lq-vista-overlap { margin-top: 0; padding-top: 1.6rem; }}
/* People / team cards, grid to single column on phones. */.lq-people { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1.2rem; text-align: center;}.lq-person { background: var(--surface); border: 1px solid var(--line); border-radius: 12px; padding: 1.4rem 1.1rem;}.lq-person-photo { width: 88px; height: 88px; border-radius: 50%; object-fit: cover; display: block; margin: 0 auto 0.8rem;}.lq-person-role { margin: 0.15rem 0 0; font-size: 0.9rem; color: var(--muted); }.lq-person-statement { margin: 0.7rem 0 0; font-style: italic; color: var(--muted); }@media (max-width: 640px) { .lq-people { grid-template-columns: 1fr; }}
/* Campaigns, the same quiet rail as values: title, summary, one action. */.lq-campaigns { padding: 3.5rem 0; }.lq-campaign-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 1.6rem; max-width: 700px; }.lq-campaign h3 { margin: 0 0 0.25rem; }.lq-campaign p { margin: 0 0 0.6rem; }
/* Behaviours (engine-wired via data-p60-*, docs: Motion and behaviour guide). Everything that hides pre-behaviour content is .p60-js-scoped: the no-JS render stays complete. */
/* Reveal: the values rail rises in with a stagger. */.p60-js .lq-value-rail li { opacity: 0; transform: translateY(0.6rem); transition: opacity 480ms ease, transform 480ms ease; transition-delay: calc(var(--p60-reveal-index, 0) * 90ms);}.p60-js .lq-value-rail.is-revealed li { opacity: 1; transform: none; }@media (prefers-reduced-motion: reduce) { .p60-js .lq-value-rail li { opacity: 1; transform: none; transition: none; }}
/* Behaviour carousel: the standard hero's photo stack (the vista framings use the island). */.lq-hero-slides { position: absolute; inset: 0; overflow: hidden; }.lq-hero-slide { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }.p60-js .lq-hero-slide { opacity: 0; transition: opacity 700ms ease; }.p60-js .lq-hero-slide.is-active { opacity: 1; }.lq-hero-slides .p60-carousel-dots { position: absolute; inset-inline: 0; bottom: 0.9rem; display: flex; justify-content: center; gap: 0.45rem; z-index: 2;}.lq-hero-slides .p60-carousel-dot { width: 0.55rem; height: 0.55rem; border-radius: 50%; border: 1px solid rgba(255, 255, 255, 0.9); background: transparent; padding: 0; cursor: pointer;}.lq-hero-slides .p60-carousel-dot--active { background: rgba(255, 255, 255, 0.95); }layout.liquid
Section titled “layout.liquid”{% comment %} The starter's chrome, header/nav/footer around the {% content %} slot. It reuses the platform chrome classes (.site-header/.site-nav/…) so the base mechanics (sticky header, burger breakpoint, dropdown hover) apply, and emits the documented behaviour attributes (data-nav-burger, #site-nav) so the platform tail script wires the mobile burger. Identity is PLACED, never rebuilt: {% island 'member_menu' %} and {% island 'search' %}. The footer derives everything from brand/nav, no fabricated copy (derive or omit).{% endcomment %}<header class="site-header"> <div class="container header-inner"> <a class="brand-lockup" href="/"> {% if site.brand.logoUrl and site.brand.logoType != 'icon' %} <img class="brand-logo" src="{{ site.brand.logoUrl }}" alt="{{ site.brand.name }}"> {% elsif site.brand.logoUrl %} <img class="brand-logo-icon" src="{{ site.brand.logoUrl }}" alt=""> <strong>{{ site.brand.name }}</strong> {% else %} <span class="brand-mark" aria-hidden="true">♥</span> <strong>{{ site.brand.name }}</strong> {% endif %} </a>
<button type="button" class="nav-burger" data-nav-burger aria-label="Menu" aria-expanded="false" aria-controls="site-nav"> <span aria-hidden="true"></span> <span aria-hidden="true"></span> <span aria-hidden="true"></span> </button>
<nav class="site-nav" id="site-nav" aria-label="Primary"> {% for item in site.nav.items %} {% if item.children %} <span class="nav-dropdown"> <a href="{{ item.href }}" class="nav-dropdown-toggle" aria-haspopup="true">{{ item.label }} ▾</a> <span class="nav-dropdown-menu"> {% for child in item.children %} <a href="{{ child.href }}"{% if child.external %} target="_blank" rel="noopener"{% endif %}>{{ child.label }}</a> {% endfor %} </span> </span> {% elsif item.cta %} <a class="nav-cta" href="{{ item.href }}"{% if item.external %} target="_blank" rel="noopener"{% endif %}>{{ item.label }}</a> {% else %} <a href="{{ item.href }}"{% if item.external %} target="_blank" rel="noopener"{% endif %}>{{ item.label }}</a> {% endif %} {% endfor %} {% island 'search' %} {% island 'member_menu' %} </nav> </div></header>
<main> {% content %}</main>
<footer class="site-footer"> <div class="container footer-grid"> <div> <p class="lq-kicker">{{ site.brand.name }}</p> {% if site.brand.tagline %}<h2>{{ site.brand.tagline }}</h2>{% endif %} </div> <div class="footer-links"> {% comment %} A tenant-curated footer menu when they've built one, else the main site.nav. {% endcomment %} {% if site.nav.footer.size > 0 %} {% for item in site.nav.footer %} <a href="{{ item.href }}"{% if item.external %} target="_blank" rel="noopener"{% endif %}>{{ item.label }}</a> {% for child in item.children %} <a href="{{ child.href }}"{% if child.external %} target="_blank" rel="noopener"{% endif %}>{{ child.label }}</a> {% endfor %} {% endfor %} {% else %} {% for item in site.nav.items %} <a href="{{ item.href }}">{{ item.label }}</a> {% endfor %} {% endif %} </div> </div> <div class="container"> <p class="footer-note"> <span class="footer-legal-links"> <a href="/cookies">Cookies</a> · <button type="button" class="footer-linklike" data-consent-open>Cookie settings</button> </span> </p> </div></footer>manifest.json
Section titled “manifest.json”{ "name": "starter", "version": "1.13.0", "format": "port60-liquid@1", "label": "Starter", "description": "The reference template for the Port60 dialect, the developer docs' worked example and the base to copy when building your own. 1.1.0 adds the layout slot: the template owns the header, navigation and footer chrome.", "supports": { "pages": [ "home", "about" ], "sections": [ "homeHero", "hero", "values", "cta", "people", "campaigns", "impactMap" ], "islands": [ "donation_widget", "member_menu", "hero_carousel", "map", "primary_action_widget", "search" ], "layout": true, "heroImagery": true, "behaviors": [ "reveal", "carousel" ], "focus": [ "donate", "volunteer" ] }, "compositions": { "home": [ { "type": "homeHero", "role": "core" }, { "type": "campaigns", "role": "recommended" }, { "type": "impactMap", "role": "optional" }, { "type": "cta", "role": "recommended" } ], "about": [ { "type": "hero", "role": "core" }, { "type": "values", "role": "recommended" }, { "type": "people", "role": "optional" }, { "type": "cta", "role": "recommended" } ] }, "settings": { "schema": [ { "key": "siteFont", "kind": "font", "label": "Site font", "default": "Inter", "weights": [ 400, 500, 600, 700, 800 ] } ] }, "changelog": "Places the site search box in the header: a typeahead across events, articles, services, campaigns, appeals, volunteering and documents, with the results page at /search.", "imagery": { "hero": { "idealAspect": "16:9", "minWidth": 1600, "note": "Landscape around 16:9 fills the hero cleanly; panoramas suit the whole-photo framing." } }}sections/campaigns.liquid
Section titled “sections/campaigns.liquid”{% comment %} Live site.content.campaigns, the starter's plain rendering of the `site.content.campaigns` context, kept simple on purpose: this template is the worked example template authors copy from. The context is the platform's published site.content.campaigns; hrefs are platform-derived, always link via campaign.href. Derive or omit: nothing renders when no campaign is live.{% endcomment %}{% if site.content.campaigns.size > 0 %}<section class="lq-site.content.campaigns"> <div class="container"> <h2>{{ section.heading | default: campaignsLabel }}</h2> {% if section.text %}<p>{{ section.text }}</p>{% endif %} <ul class="lq-campaign-list"> {% for campaign in site.content.campaigns %} <li class="lq-campaign"> <h3>{{ campaign.title }}</h3> {% if campaign.summary %}<p>{{ campaign.summary }}</p>{% endif %} <a class="button button-primary" href="{{ campaign.href }}">View campaign</a> </li> {% endfor %} </ul> </div></section>{% endif %}sections/cta.liquid
Section titled “sections/cta.liquid”<section class="lq-cta"> <div class="container lq-cta-grid"> <div> <h2>{{ section.heading }}</h2> {% if section.text %}<p>{{ section.text }}</p>{% endif %} <a class="button button-primary" href="{{ section.buttonHref }}">{{ section.buttonLabel }}</a> </div> <div class="lq-cta-widget"> {% island 'donation_widget' %} </div> </div></section>sections/hero.liquid
Section titled “sections/hero.liquid”<section class="lq-hero"> <div class="container"> {% if section.eyebrow %}<p class="lq-kicker">{{ section.eyebrow }}</p>{% endif %} <h1>{{ section.title }}</h1> <div class="lq-hero-body">{{ section.bodyHtml | raw }}</div> </div></section>sections/homeHero.liquid
Section titled “sections/homeHero.liquid”{% comment %} The home opener, and the REFERENCE IMPLEMENTATION of supports.heroImagery. Rules worth copying:
1. EVERY field is optional. Copy derives from brand (| default) or omits; no photos means the designed gradient below, never a placeholder image. 2. Photos are NEVER placed raw. The treatments in theme.css lay scrims built from the palette variables (color-mix over var(--bg)/var(--bg-tint)), so any uploaded photograph is toned into the template. Tune the treatment, don't remove it. 3. Rotation is the PLATFORM's hero_carousel island (templates ship no JavaScript): with two or more photos, place it and style its documented classes. 4. photoFraming is the charity's call, honour BOTH: 'fill' (default) covers your designed hero shape (per-photo focus picks the surviving slice); 'whole' is the VISTA, the hero adapts toward the photo's own shape (height-clamped) so a wide landscape stays entirely visible, with your copy stacked after the band.{% endcomment %}{% assign heroCount = section.images | size %}{% if heroCount == 1 %} {% assign heroPhoto = section.images.first.imageUrl %}{% elsif heroCount == 0 %} {% assign heroPhoto = section.imageUrl %}{% endif %}{% assign heroFocus = 'center' %}{% if heroCount > 0 %} {% case section.images.first.focus %} {% when 'top' %}{% assign heroFocus = 'center top' %} {% when 'bottom' %}{% assign heroFocus = 'center bottom' %} {% when 'left' %}{% assign heroFocus = 'left center' %} {% when 'right' %}{% assign heroFocus = 'right center' %} {% endcase %}{% endif %}{% assign heroFraming = section.photoFraming | default: 'fill' %}{% if heroCount > 0 and heroFraming == 'whole' %}{% if section.actionStyle == 'button' or section.givingStyle == 'button' %}<section class="lq-homehero lq-homehero--vista{% if heroCount > 1 %} lq-homehero--carousel{% endif %}"> <div class="lq-vista"> <img class="lq-vista-sizer" src="{{ section.images.first.imageUrl }}" alt="{{ section.images.first.alt }}" style="object-position: {{ heroFocus }}" fetchpriority="high" decoding="async"> {% if heroCount > 1 %}{% island 'hero_carousel' %}{% else %}<div class="lq-vista-scrim" aria-hidden="true"></div>{% endif %} <div class="lq-vista-copy"> <div class="container"> <p class="lq-kicker">{{ section.eyebrow | default: site.brand.name }}</p> <h1>{{ section.title | default: site.brand.tagline | default: site.brand.name }}</h1> {% if section.lead %}<p class="lq-homehero-lead">{{ section.lead }}</p>{% endif %} <div class="lq-homehero-actions"> <a class="button button-primary" href="{{ section.primaryHref | default: '/donate' }}">{{ section.primaryLabel | default: 'Donate now' }}</a> <a class="button" href="{{ section.secondaryHref | default: '/about' }}">{{ section.secondaryLabel | default: 'Our story' }}</a> </div> </div> </div> </div></section>{% else %}{% comment %} Form mode: the grid pulls up over the band, copy on the photo, the widget riding the seam onto the gradient below (never a stacked strip). {% endcomment %}<section class="lq-homehero lq-homehero--vista lq-homehero--vista-form{% if heroCount > 1 %} lq-homehero--carousel{% endif %}"> <div class="lq-vista"> <img class="lq-vista-sizer" src="{{ section.images.first.imageUrl }}" alt="{{ section.images.first.alt }}" style="object-position: {{ heroFocus }}" fetchpriority="high" decoding="async"> {% if heroCount > 1 %}{% island 'hero_carousel' %}{% else %}<div class="lq-vista-scrim" aria-hidden="true"></div>{% endif %} </div> <div class="container lq-homehero-inner lq-vista-overlap"> <div> <p class="lq-kicker">{{ section.eyebrow | default: site.brand.name }}</p> <h1>{{ section.title | default: site.brand.tagline | default: site.brand.name }}</h1> {% if section.lead %}<p class="lq-homehero-lead">{{ section.lead }}</p>{% endif %} <div class="lq-homehero-actions"> {% if section.action or section.primaryLabel %}<a class="button button-primary" href="{{ section.primaryHref | default: section.action.href | default: '#donate' }}">{{ section.primaryLabel | default: section.action.label | default: 'Donate now' }}</a>{% endif %} <a class="button" href="{{ section.secondaryHref | default: '/about' }}">{{ section.secondaryLabel | default: 'Our story' }}</a> </div> </div> <aside id="donate"> {%- comment -%} The site focus decides which widget leads (section.primary); this island becomes it: the donation widget or the volunteer sign-up. {%- endcomment -%} {% island 'primary_action_widget' %} </aside> </div></section>{% endif %}{% else %}<section class="lq-homehero{% if heroPhoto %} lq-homehero--photo{% endif %}{% if heroCount > 1 %} lq-homehero--carousel{% endif %}"{% if heroPhoto %} style="background-image: url('{{ heroPhoto }}'); background-position: {{ heroFocus }}"{% endif %}> {% comment %} Several photos → the BEHAVIOUR carousel (data-p60-carousel, engine-wired, the template ships no JavaScript): complete static markup, one data-p60-slide per photo, auto-advance opt-in. Hiding non-active slides is .p60-js-scoped in theme.css so a no-JS visitor sees the photos stacked, never a blank hero. The vista framings above show the other route: the ready-made hero_carousel island. A template chooses either. {% endcomment %} {% if heroCount > 1 %} <div class="lq-hero-slides" data-p60-carousel="auto" aria-label="Hero photographs"> {% for image in section.images %} <img class="lq-hero-slide{% if forloop.first %} is-active{% endif %}" data-p60-slide src="{{ image.imageUrl }}" alt="{{ image.alt }}" style="object-position: {{ heroFocus }}" {% if forloop.first %}fetchpriority="high"{% else %}loading="lazy"{% endif %} decoding="async"> {% endfor %} </div> {% endif %} <div class="container lq-homehero-inner"> <div> <p class="lq-kicker">{{ section.eyebrow | default: site.brand.name }}</p> <h1>{{ section.title | default: site.brand.tagline | default: site.brand.name }}</h1> {% if section.lead %}<p class="lq-homehero-lead">{{ section.lead }}</p>{% endif %} <div class="lq-homehero-actions"> {% if section.actionStyle == 'button' or section.givingStyle == 'button' %} {% if section.action or section.primaryLabel %}<a class="button button-primary" href="{{ section.primaryHref | default: section.action.href | default: '/donate' }}">{{ section.primaryLabel | default: section.action.label | default: 'Donate now' }}</a>{% endif %} {% else %} {% if section.action or section.primaryLabel %}<a class="button button-primary" href="{{ section.primaryHref | default: section.action.href | default: '#donate' }}">{{ section.primaryLabel | default: section.action.label | default: 'Donate now' }}</a>{% endif %} {% endif %} <a class="button" href="{{ section.secondaryHref | default: '/about' }}">{{ section.secondaryLabel | default: 'Our story' }}</a> </div> </div> {% unless section.actionStyle == 'button' or section.givingStyle == 'button' %} <aside id="donate"> {% island 'primary_action_widget' %} </aside> {% endunless %} </div></section>{% endif %}sections/impactMap.liquid
Section titled “sections/impactMap.liquid”<section class="section"> <div class="container"> {% island 'map' %} </div></section>sections/people.liquid
Section titled “sections/people.liquid”{% comment %} People / team, the reference implementation: simple cards (name, role, optional photo and one-line statement), a grid that collapses to one column on phones. No placeholder faces: a person without a photo just renders text (derive or omit, never fabricate).{% endcomment %}<section class="lq-section"> <div class="container"> {% if section.eyebrow or section.heading or section.text %} <div class="lq-section-head"> {% if section.eyebrow %}<p class="lq-kicker">{{ section.eyebrow }}</p>{% endif %} {% if section.heading %}<h2>{{ section.heading }}</h2>{% endif %} {% if section.text %}<p class="lq-muted">{{ section.text }}</p>{% endif %} </div> {% endif %} <div class="lq-people"> {% for person in section.items %} <article class="lq-person"> {% if person.imageUrl %}<img class="lq-person-photo" src="{{ person.imageUrl }}" alt="" loading="lazy">{% endif %} <h3>{{ person.name }}</h3> <p class="lq-person-role">{{ person.role }}</p> {% if person.statement %}<p class="lq-person-statement">“{{ person.statement }}”</p>{% endif %} </article> {% endfor %} </div> </div></section>sections/values.liquid
Section titled “sections/values.liquid”<section class="lq-values"> <div class="container"> <h2>{{ section.heading | default: "What guides us" }}</h2> {% comment %} data-p60-reveal-group: the engine stamps --p60-reveal-index on each child and adds is-revealed when the rail scrolls into view, the stagger and motion are OURS, in theme.css, and .p60-js-scoped so a no-JS visitor sees the rail complete and still. {% endcomment %} <ol class="lq-value-rail" data-p60-reveal-group> {% for item in section.items %} <li> <span class="lq-value-dot"></span> <h3>{{ item.title }}</h3> <p>{{ item.description }}</p> </li> {% endfor %} </ol> </div></section>
