New design.

This commit is contained in:
2026-10-14 19:29:31 +02:00
parent 047adf7b92
commit 6e60f1c117
51 changed files with 2598 additions and 2606 deletions
+2
View File
@@ -1 +1,3 @@
config.json
.secrets.env
shifu.db
+764
View File
@@ -0,0 +1,764 @@
# AGENTS.md — Building and editing websites with Shifu
This file tells an AI agent how to create and maintain a website built with
[Shifu](https://github.com/emvi/shifu), a Git/file-based CMS that renders Go
`html/template` files from JSON content. Read it fully before generating or
changing anything. When the rules here conflict with habits from other
frameworks (React, Hugo, Next.js, Tailwind build pipelines, …), the rules here win.
The goal of every change: **a site that renders correctly AND stays fully editable
in Shifu's admin UI** (on-page editor at `/admin`). Code that renders but cannot be
edited in the admin is considered broken.
---
## 1. Mental model
| Concept | Where it lives | What it is |
|---|---|---|
| **Template** | `tpl/**/<name>.html` | Go `html/template` for one element type (section, component, layout part). |
| **Admin config** | `admin/tpl/**/<name>.json` | Makes the template editable: label, editable fields, child content areas. |
| **Page** | `content/pages/**/<file>.json` | JSON with `path` (routes per language) and a tree of elements. |
| **Reference** ("ref") | `content/refs/<name>.json` | A standalone, reusable element (header, footer, head, …) shared by many pages. |
| **Sass** | `assets/scss/` | Compiled by Shifu (`sass` CLI) into `static/`. |
| **JS/TS** | `assets/js/` | Bundled + minified by Shifu (embedded esbuild) into `static/`. |
| **Static files** | `static/` | Served as-is under `/static/`. Admin uploads go to `static/media/`. |
| **Config** | `config.json` (+ `.secrets.env`) | Server, Sass/JS build, admin UI, etc. |
Rendering: a request matches a page `path` → Shifu walks `content.content` (the
root list of elements) in order → each element renders `tpl/.../<tpl>.html` with
`.CMS`, `.Args`, `.Page`, `.Content` → elements render their own children via
`.CMS.Render`.
### The "one element type = matching files" rule
Every editable element type `<name>` consists of files that share the same base name:
```
tpl/sections/hero.html # required — markup
admin/tpl/sections/hero.json # required — makes it editable/addable in the admin
assets/scss/_hero.scss # optional — styles, scoped to .hero
assets/js/hero.js # optional — behaviour, bound via data-component="hero"
```
Names: lowercase `snake_case`, **globally unique across all subdirectories** (Shifu
identifies templates by file base name only, folders are just for humans).
---
## 2. Project structure
Use this layout for new sites (it matches `shifu init` and the admin UI's own paths):
```
.
├── config.json
├── .secrets.env # secrets, never commit (add to .gitignore)
├── .gitignore
├── admin/
│ └── tpl/
│ ├── layout/ # head.json, end.json, header.json, footer.json
│ ├── sections/ # hero.json, text.json, cards.json, …
│ └── components/ # card.json, button.json, nav_item.json, …
├── assets/
│ ├── scss/
│ │ ├── main.scss # Sass entrypoint (only file referenced in config)
│ │ ├── _variables.scss
│ │ ├── _base.scss
│ │ └── _hero.scss … # one partial per element type, FLAT (no subfolders)
│ └── js/
│ ├── main.js # JS entrypoint
│ └── hero.js … # one module per interactive element type, FLAT
├── content/
│ ├── pages/ # pages (admin creates pages here)
│ │ ├── home.json
│ │ └── not-found.json
│ └── refs/ # references (admin creates refs here)
│ ├── head.json
│ └── end.json
├── static/ # served under /static/
│ ├── media/ # admin media library (uploads)
│ ├── main.css # GENERATED by Shifu — never write by hand
│ └── main.js # GENERATED by Shifu — never write by hand
└── tpl/
├── layout/ # head.html, end.html, header.html, footer.html
├── sections/
├── components/
└── partials/ # non-editable helpers used via {{ template }}
```
If you are editing an **existing** site, keep its structure, naming and file
locations (check `config.json` for the real `sass`/`js` dirs, entrypoints and output
paths) instead of migrating it to this layout.
`shifu.db` (SQLite, created at runtime when the admin UI is enabled) stores admin
users, sessions and the list of references. Don't commit it.
---
## 3. config.json
Minimal configuration for local development:
```json
{
"dev": true,
"log_level": "info",
"server": {
"host": "localhost",
"port": 8080,
"hostname": "http://localhost:8080"
},
"content": {
"provider": "fs",
"not_found": {
"en": "/not-found",
"de": "/de/nicht-gefunden"
}
},
"sass": {
"dir": "assets/scss",
"entrypoint": "main.scss",
"out": "static/main.css",
"out_source_map": "static/main.css.map",
"watch": true
},
"js": {
"dir": "assets/js",
"entrypoint": "main.js",
"out": "static/main.js",
"source_map": true,
"watch": true
},
"ui": {
"path": "/admin",
"admin_password": "${ADMIN_PASSWORD}"
}
}
```
`.secrets.env` (one `KEY=value` per line, referenced as `${KEY}` in `config.json`):
```env
ADMIN_PASSWORD=admin
```
Notes:
* `dev: true` reloads templates and content on every request (local only). For
production set `dev: false`, set `server.host` to `""`, `server.hostname` to the
public origin (e.g. `https://example.com`, used for canonical links/sitemap), and
`secure_cookies: true`.
* Keys starting with `_` are treated as comments by convention (they are ignored).
* Optional sections (`api`, `remote`, `git`, `analytics`, `cors`) are documented in
the Shifu README — only add them when asked.
* Run with `shifu run .` (requires the `sass` CLI: `npm i -g sass`). JS needs no
Node.js, esbuild is built in.
---
## 4. Templates (`tpl/`)
Go `html/template` syntax plus Shifu functions and all
[Sprig](https://masterminds.github.io/sprig/) functions.
### 4.1 Variables available in every template
| Variable | Meaning |
|---|---|
| `.CMS` | CMS instance. `.CMS.Render`, `.CMS.LastUpdate` (cache-busting string). |
| `.Args` | `map[string]string` of route variables, e.g. `{{ index .Args "slug" }}`. |
| `.Page` | The page: `.Page.Language`, `.Page.Path` (map lang → path), `.Page.CanonicalLink`, `.Page.File`. |
| `.Content` | The element being rendered: `.Content.Position`, `.Content.Data`, `.Content.Copy`, `.Content.Content`. |
### 4.2 Shifu template functions
| Function | Use |
|---|---|
| `copy .Page .Content "key"` | Translated text for the page language; falls back to `en`. Returns `nil` if missing. |
| `get .Content "key"` | Language-independent value from `data`. Returns `nil` if missing. |
| `html <string>` | Output trusted HTML (needed for `html` type fields). |
| `htmlAttr <string>` | Output a trusted attribute. |
| `fmt "%s/%s" a b` | `fmt.Sprintf`. |
| `dict "k" v "k2" v2` | Build a map, e.g. to pass to partials. |
| `default a b c` | **Shifu's version**: returns the first non-nil argument (NOT Sprig's `default "fallback" .x`). |
| `int "12"`, `uint64`, `formatInt`, `formatFloat`, `formatDate`, `gtFloat`, `ltFloat`, `shuffle`, `year` | Helpers. |
| `markdown "static/x.md" .`, `markdownBlock "static/x.md" "block" .` | Render a Markdown file (path relative to project root). |
| `hostname`, `config` | `server.hostname`, full config. |
| `adminHead .Page`, `adminBody .Page`, `loggedIn .Page` | Admin UI hooks (see §4.4). |
### 4.3 Element template contract (REQUIRED for admin editing)
Every editable element template must:
1. Have **exactly one top-level HTML element** carrying
`data-shifu-element="{{ .Content.Position }}"`.
2. For each child content area `<area>` declared in its admin config, render the
children **and** place a slot directly after them:
```gotemplate
{{ .CMS.Render .Args .Page (fmt "%s.<area>" .Content.Position) (index .Content.Content "<area>") }}
<slot name="{{ .Content.Position }}/<area>"></slot>
```
The `<area>` string must be identical in all three places (position suffix,
`index` key, slot name) and in the admin config's `content` keys.
3. Read every editable value through `copy` (translatable text) or `get` (data), and
handle missing values — new elements added in the admin start **empty**.
Canonical section template — `tpl/sections/text.html`:
```gotemplate
{{- $headline := copy .Page .Content "headline" -}}
{{- $text := copy .Page .Content "text" -}}
{{- $background := get .Content "background" -}}
<section class="text{{ with $background }} text--{{ . }}{{ end }}" data-shifu-element="{{ .Content.Position }}">
<div class="text__inner">
{{ with $headline }}<h2 class="text__headline">{{ . }}</h2>{{ end }}
{{ with $text }}<div class="text__body">{{ html . }}</div>{{ end }}
</div>
</section>
```
Container with child elements — `tpl/sections/cards.html`:
```gotemplate
{{- $headline := copy .Page .Content "headline" -}}
<section class="cards" data-shifu-element="{{ .Content.Position }}">
{{ with $headline }}<h2 class="cards__headline">{{ . }}</h2>{{ end }}
<div class="cards__grid">
{{ .CMS.Render .Args .Page (fmt "%s.items" .Content.Position) (index .Content.Content "items") }}
<slot name="{{ .Content.Position }}/items"></slot>
</div>
</section>
```
Child element — `tpl/components/card.html`:
```gotemplate
{{- $title := copy .Page .Content "title" -}}
{{- $text := copy .Page .Content "text" -}}
{{- $href := copy .Page .Content "href" -}}
{{- $image := get .Content "image" -}}
<article class="card" data-shifu-element="{{ .Content.Position }}">
{{ with $image }}<img class="card__image" src="{{ . }}" alt="{{ copy $.Page $.Content "image_alt" }}" loading="lazy" />{{ end }}
{{ with $title }}<h3 class="card__title">{{ . }}</h3>{{ end }}
{{ with $text }}<div class="card__text">{{ html . }}</div>{{ end }}
{{ with $href }}<a class="card__link" href="{{ . }}">{{ default (copy $.Page $.Content "link_label") "Read more" }}</a>{{ end }}
</article>
```
Multiple content areas (e.g. two columns) — `tpl/sections/columns.html`:
```gotemplate
<section class="columns" data-shifu-element="{{ .Content.Position }}">
<div class="columns__col">
{{ .CMS.Render .Args .Page (fmt "%s.left" .Content.Position) (index .Content.Content "left") }}
<slot name="{{ .Content.Position }}/left"></slot>
</div>
<div class="columns__col">
{{ .CMS.Render .Args .Page (fmt "%s.right" .Content.Position) (index .Content.Content "right") }}
<slot name="{{ .Content.Position }}/right"></slot>
</div>
</section>
```
Pitfalls:
* Inside `with`/`range`, `.` changes — use `$.Page` / `$.Content`.
* `{{ html $x }}` with a missing value prints the literal text `<nil>`. Always wrap
it: `{{ with $x }}{{ html . }}{{ end }}`.
* Never hard-code visible text in element templates — put it in `copy` so it is
editable and translatable. (Fallback labels via `default` are fine.)
* Don't use Go's `{{ block }}`/`{{ define }}` inheritance for page layouts. Layout
in Shifu is composition: pages are lists of elements (`head`, `header`, sections,
`footer`, `end`).
* Template file base names must be unique across all of `tpl/`; `tpl/a/card.html`
and `tpl/b/card.html` would clash.
### 4.4 Layout templates (head / end)
Every page starts with a `head` element and ends with an `end` element (normally
used as references, see §6). They split the HTML document. The admin UI needs
`adminHead` inside `<head>` and `adminBody` before `</body>`. They output nothing for
regular visitors.
`tpl/layout/head.html`:
```gotemplate
{{- $title := copy .Page .Content "title" -}}
{{- $description := copy .Page .Content "meta_description" -}}
<!DOCTYPE html>
<html lang="{{ .Page.Language }}">
<head>
<base href="/" />
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ $title }}</title>
<meta name="description" content="{{ $description }}" />
<link rel="canonical" href="{{ .Page.CanonicalLink }}" />
{{- range $language, $path := .Page.Path }}
<link rel="alternate" hreflang="{{ $language }}" href="{{ hostname }}{{ $path }}" />
{{- end }}
<link rel="stylesheet" href="/static/main.css?v={{ .CMS.LastUpdate }}" />
<script defer src="/static/main.js?v={{ .CMS.LastUpdate }}"></script>
{{ adminHead .Page }}
</head>
<body>
```
`tpl/layout/end.html`:
```gotemplate
{{ adminBody .Page }}
</body>
</html>
```
The `/static/...` file names must match `sass.out` and `js.out` in `config.json`.
The head is a *layout* element and is not wrapped in `data-shifu-element`
(`<head>` content cannot be edited on-page; its fields are editable from the
admin's page tree). For per-page titles, override the head reference's copy on
each page (§6).
### 4.5 Partials (non-editable helpers)
Put reusable markup that is *not* an admin element in `tpl/partials/` and call it
by file name. Do **not** create an admin config for partials.
```gotemplate
{{/* tpl/partials/button.html */}}
<a class="button button--{{ default .Variant "primary" }}" href="{{ .Href }}">{{ .Label }}</a>
```
```gotemplate
{{ with copy .Page .Content "cta_label" }}
{{ template "button.html" (dict "Label" . "Href" (copy $.Page $.Content "cta_href") "Variant" (get $.Content "cta_variant")) }}
{{ end }}
```
---
## 5. Admin configuration (`admin/tpl/`)
One JSON file per element type, **same base name as the template**. Without it the
element renders but cannot be edited, and it cannot be added from the admin.
```json
{
"label": "Cards",
"layout": false,
"content": {
"items": {
"label": "Cards",
"tpl_filter": ["card"]
}
},
"copy": {
"headline": { "label": "Headline" }
},
"data": {
"background": {
"label": "Background",
"type": "select",
"options": {
"": "Default",
"light": "Light",
"dark": "Dark"
}
}
}
}
```
| Key | Meaning |
|---|---|
| `label` | Display name in the admin's element picker. |
| `layout` | `true` for structural elements (head, end, header, footer). Disables some actions; new elements are inserted *before* a trailing layout element. |
| `content` | Child content areas: key = area name used in the template, `label`, optional `tpl_filter` (list of template names allowed in that area; omit to allow all). |
| `copy` | Translatable fields (stored per language under `copy.<lang>`). |
| `data` | Language-independent fields (stored under `data`). |
Field `type` (for `copy` and `data`):
| Type | Editor | Template usage |
|---|---|---|
| *(omitted)* / `text` | single-line input | `{{ $x }}` |
| `html` | rich-text editor (Trix) | `{{ with $x }}{{ html . }}{{ end }}` |
| `img` | media library picker, stores a path like `/static/media/photo.jpg` | `<img src="{{ $x }}">` |
| `file` | media library picker, stores a path | `<a href="{{ $x }}">` |
| `boolean` | checkbox, stores `"on"` when checked, key removed when unchecked | `{{ if $x }}` |
| `select` | dropdown from `options` (`value: label`) | compare as string: `{{ if eq $x "dark" }}` |
| `color` | color picker, stores `#rrggbb` | `style="--accent: {{ $x }}"` |
Rules derived from how the admin saves (important):
* **When an editor saves an element, its `copy` and `data` are replaced by exactly
the configured fields.** Any key the template reads but the admin config does not
declare is deleted on the first save. ⇒ Declare *every* `copy`/`data` key the
template uses. Don't store arrays/objects in `data` — they cannot be edited and
are lost. Model repeatable things (cards, slides, nav items, FAQ entries, team
members) as **child elements** in a content area instead.
* All saved values are **strings**. Write data values as strings in JSON too
(`"columns": "3"`, not `3`), otherwise `select` comparisons fail. Use
`{{ int (get .Content "columns") }}` when you need a number.
* Copy is edited for the languages present in the page's `path`.
* Use `copy` for anything a translator would change (text, alt text, link URLs that
differ by language, button labels); use `data` for design choices (variant, color,
image, alignment, size).
* Choose `select` over free text for design options and keep option keys stable —
they are referenced by CSS modifier classes.
* Prefixing a `label` with `_` (e.g. `"_HTML Head"`) sorts it first in the picker;
the demo uses this for layout elements.
Layout configs:
```json
{ "label": "_HTML Head", "layout": true, "copy": { "title": { "label": "Title" }, "meta_description": { "label": "Meta Description" } } }
```
```json
{ "label": "_HTML End", "layout": true }
```
Admin config changes are picked up automatically for files directly inside
`admin/tpl/`; restart Shifu after adding files in its subdirectories.
---
## 6. Content (`content/`)
### 6.1 Pages — `content/pages/<name>.json`
A file with `path` and **no top-level `tpl`** is a page.
```json
{
"display_name": "Home",
"path": {
"en": "/",
"de": "/de"
},
"sitemap": { "priority": "1.0" },
"content": {
"content": [
{
"ref": "head",
"copy": {
"en": { "title": "Acme — Home", "meta_description": "Acme builds things." },
"de": { "title": "Acme — Start", "meta_description": "Acme baut Dinge." }
}
},
{ "ref": "header" },
{
"tpl": "hero",
"copy": {
"en": { "headline": "We build things", "text": "<div>Fast and reliable.</div>" },
"de": { "headline": "Wir bauen Dinge", "text": "<div>Schnell und zuverlässig.</div>" }
},
"data": { "image": "/static/media/hero.jpg", "align": "center" }
},
{
"tpl": "cards",
"copy": { "en": { "headline": "Services" }, "de": { "headline": "Leistungen" } },
"content": {
"items": [
{
"tpl": "card",
"copy": {
"en": { "title": "Design", "text": "<div>…</div>" },
"de": { "title": "Design", "text": "<div>…</div>" }
},
"content": {}
}
]
}
},
{ "ref": "footer" },
{ "ref": "end" }
]
}
}
```
Rules:
* The page root must use a **single** content area named `content`
(`"content": { "content": [ … ] }`). Elements are rendered in array order.
* Each element has either `tpl` (template name, lowercase, no extension) **or**
`ref` (reference name), plus optional `copy`, `data`, `content`, `display_name`.
* Every content area declared in an element's admin config should exist in the JSON
(use `[]` when empty), e.g. `"content": { "items": [] }`.
* `path` keys are language codes; the value is the exact route (no trailing slash,
must start with `/`). Every language in `path` should have `copy` for every
element; missing languages fall back to `en`.
* Dynamic routes: always give the variable a regular expression, e.g.
`"/blog/{slug:[a-z0-9-]+}"` or `"/p/{id:[0-9]+}"` (a bare `{slug}` does not match
in the current version). Each `{…}` matches exactly one path segment; read it with
`{{ index .Args "slug" }}`. Longer paths are matched first.
* 404: create a page whose path matches `content.not_found` in `config.json`
(default `/404`).
* Optional page keys: `disable_cache`, `header` (HTTP headers map), `handler` (Go
custom handler, only if the backend defines it), `analytics` (Pirsch tags / A/B
experiments — see the Shifu README).
* Rich-text (`html`) values are HTML strings. The Trix editor produces `<div>`,
`<br>`, `<strong>`, `<em>`, `<a>`, `<ul>/<ol>/<li>`, `<h1>`, `<blockquote>`,
`<pre>`. Stick to these tags so content stays editable.
* Image paths should point to `static/media/…` (the admin media library), e.g.
`/static/media/hero.jpg`, and the file must exist there.
* Media files go in `static/media/` only; don't put site assets (logos used by
templates, icons, fonts) there — use `static/img/`, `static/fonts/` instead.
### 6.2 References — `content/refs/<name>.json`
A content file with a **top-level `tpl`** is a reference (a standalone element).
Its name is the lowercase file name without `.json`.
```json
{
"tpl": "head",
"copy": {
"en": { "title": "Acme", "meta_description": "Acme builds things." },
"de": { "title": "Acme", "meta_description": "Acme baut Dinge." }
}
}
```
```json
{
"tpl": "header",
"data": { "logo": "/static/media/logo.svg" },
"copy": { "en": { "home_label": "Acme" }, "de": { "home_label": "Acme" } },
"content": {
"nav": [
{ "tpl": "nav_item", "copy": { "en": { "label": "About", "href": "/about" }, "de": { "label": "Über uns", "href": "/de/ueber-uns" } }, "content": {} }
]
}
}
```
* Use refs for anything shared across pages: `head`, `header`, `footer`, `end`,
newsletter box, CTA banners.
* Pages use them with `{ "ref": "<name>" }`. Non-empty `copy`/`data` on the page
element override the ref's values for that page only (typical: page title).
* Ref names must be unique across **all** files in `content/` — never give a page
file the same name as a ref (e.g. no `content/pages/header.json`).
* The admin's "add reference" picker lists refs from `shifu.db`, not from disk. Refs
created by hand still render and are editable where used, but to make them
insertable from the admin register them (with Shifu stopped, after it has run
once so `shifu.db` exists; needs the `sqlite3` CLI):
```sh
sqlite3 shifu.db "INSERT OR IGNORE INTO reference (name) VALUES ('head'), ('header'), ('footer'), ('end');"
```
---
## 7. Styles (`assets/scss/`)
Shifu compiles the Sass entrypoint itself (`sass --style=compressed`) on start and
on every save. **Never** pre-compile, never write or edit `static/main.css` by hand,
never add PostCSS/Tailwind/Vite/webpack or npm build scripts, never inline large
`<style>` blocks in templates.
`assets/scss/main.scss`:
```scss
@use "variables";
@use "base";
@use "layout";
// one partial per element type, same name as the template
@use "head";
@use "header";
@use "footer";
@use "hero";
@use "text";
@use "cards";
@use "card";
```
`assets/scss/_variables.scss`:
```scss
$color-text: #1d1d1f;
$color-accent: #0a64d6;
$max-width: 1120px;
$radius: 12px;
$breakpoint-md: 768px;
@mixin md {
@media (min-width: $breakpoint-md) { @content; }
}
```
`assets/scss/_card.scss`:
```scss
@use "variables" as *;
.card {
border-radius: $radius;
overflow: hidden;
&__image { display: block; width: 100%; height: auto; }
&__title { margin: 16px 0 8px; }
&__link { color: $color-accent; }
}
```
Conventions:
* Use the module system (`@use`/`@forward`), not deprecated `@import`.
* Scope styles to the element's root class (BEM: `.card`, `.card__title`,
`.card--dark`). Modifier names come from `select` option keys.
* Style rich-text output by element (`.text__body p, .text__body ul …`) since editors
can't add classes.
* Expose admin `color` fields as CSS custom properties
(`style="--accent: {{ $color }}"` + `color: var(--accent, #{$color-accent})`).
* Keep partials flat inside the Sass dir: Shifu only watches that directory itself
(not subfolders) for changes. Every subfolder is also a Sass load path, so names
must be unique.
* Fonts/images referenced from CSS go in `static/` and use absolute URLs
(`url("/static/fonts/inter.woff2")`).
* Requires the Dart Sass CLI (`sass`) on the machine running Shifu.
---
## 8. JavaScript (`assets/js/`)
Shifu bundles and minifies the JS entrypoint with esbuild on start and on save (ES
modules, TypeScript `.ts` also works). **Never** pre-bundle, never edit
`static/main.js`, never add a separate bundler. `import` between files in
`assets/js/` is fine. Avoid npm dependencies unless the user asks; if one is needed,
prefer copying a small ES-module library into `assets/js/` (e.g. `vendor_glide.js`).
Keep JS files flat in that directory: like Sass, only the directory itself is
watched for changes, not its subfolders.
Pattern: progressive enhancement, components bound by `data-component`, idempotent
initialisation that also handles elements the admin inserts or re-renders on the fly.
`assets/js/main.js`:
```js
import { initNav } from "./nav.js";
import { initSlider } from "./slider.js";
const components = {
nav: initNav,
slider: initSlider,
};
function init(root = document) {
root.querySelectorAll("[data-component]").forEach((el) => {
if (el.dataset.componentReady) {
return;
}
const fn = components[el.dataset.component];
if (fn) {
fn(el);
el.dataset.componentReady = "true";
}
});
}
document.addEventListener("DOMContentLoaded", () => {
init();
// the admin UI swaps element HTML in place after edits
new MutationObserver(() => init()).observe(document.body, { childList: true, subtree: true });
});
```
`assets/js/nav.js`:
```js
export function initNav(el) {
const toggle = el.querySelector(".header__toggle");
if (!toggle) {
return;
}
toggle.addEventListener("click", () => {
const open = el.classList.toggle("header--open");
toggle.setAttribute("aria-expanded", String(open));
});
}
```
Template side: `<header class="header" data-component="nav" data-shifu-element="{{ .Content.Position }}">`.
Rules:
* The site must be usable without JS; JS enhances.
* No inline `<script>` in element templates; no `onclick=` attributes.
* Pass configuration via `data-*` attributes rendered from `copy`/`data`.
* Don't rely on the DOM being static — the admin replaces element markup when
editors save. Never store state outside the element or bind listeners in a way
that breaks if an element is replaced.
* Load the bundle once, in `head.html`, with `defer`.
---
## 9. Workflows
### 9.1 Create a whole new site
1. Write `config.json`, `.secrets.env`, `.gitignore`
(`.secrets.env`, `shifu.db`, `static/main.css*`, `static/main.js*`).
2. Plan the element inventory from the design: layout parts (head, header, footer,
end), sections (hero, text, image, cards, columns, cta, faq, contact, …),
components (card, nav_item, faq_item, button…). Prefer a small set of flexible
sections with `select` options over many near-duplicates.
3. For each element type create: template, admin config, Sass partial (+ `@use` in
`main.scss`), and JS module (+ register in `main.js`) when interactive.
4. Create refs: `content/refs/head.json`, `header.json`, `footer.json`, `end.json`.
5. Create pages in `content/pages/`, each starting with `head` and ending with `end`,
with `copy` for every language in its `path`. Include a 404 page.
6. Put referenced images in `static/media/`.
7. Run `shifu run .`, open every page, check the log for errors.
### 9.2 Edit an existing site
1. Read `config.json`, list `tpl/`, `admin/tpl/`, `content/` and the Sass/JS
entrypoints first to learn the existing element inventory and conventions.
2. Reuse existing element types before creating new ones.
3. Edit content JSON surgically; keep keys you don't touch (`display_name`,
`analytics`, `header`, …). The admin writes compact single-line JSON — that's fine,
keep it valid; reformatting is allowed but not required.
4. Renaming a `copy`/`data` key or content area → update the template, the admin
config **and every page/ref JSON** that uses it (search all of `content/`).
5. Removing an element type → first remove all its uses from `content/`.
6. Adding a field → add it to template *and* admin config; existing content may lack
it, so templates must handle the missing value.
### 9.3 Add a new page
Create `content/pages/<slug>.json` (unique file name, not equal to any ref name),
copy the head/header/footer/end refs from an existing page, set `path` for all site
languages, override the head `title`/`meta_description`, and add nav entries in the
header ref if the page should be linked.
---
## 10. Verification checklist
Before finishing, confirm:
- [ ] Every element type has `tpl/**/<name>.html` and `admin/tpl/**/<name>.json` with the same unique base name (partials excepted).
- [ ] Every element template has one root element with `data-shifu-element="{{ .Content.Position }}"`.
- [ ] Every content area appears identically in the template (`fmt "%s.<area>"`, `index … "<area>"`, `<slot name=".../<area>">`), the admin config `content`, and the JSON.
- [ ] Every `copy`/`data` key read in a template is declared in the admin config with the right `type`.
- [ ] No arrays/objects in `data`; all `copy`/`data` values are strings.
- [ ] `html` values are wrapped in `{{ with }}`; no `<nil>` can appear.
- [ ] `head.html` contains `{{ adminHead .Page }}`, `end.html` contains `{{ adminBody .Page }}`, and the CSS/JS URLs match `sass.out`/`js.out`.
- [ ] Pages: root area named `content`, start with `head`, end with `end`, `copy` for all languages in `path`, paths unique and starting with `/`.
- [ ] Refs live in `content/refs/`, names don't collide with any other file name in `content/`.
- [ ] No hand-written files in `static/` for CSS/JS output; no extra build tooling.
- [ ] Every Sass partial is `@use`d by the entrypoint; every JS module is imported by the entrypoint.
- [ ] JSON files are valid (e.g. `python3 -m json.tool file.json`).
- [ ] `shifu run .` starts without "Error compiling sass", "Error compiling js", "Error rendering template" or "Error parsing website content file" in the log; no page shows `Reference "…" not found!` or `<nil>`.
- [ ] Logged in at `/admin`, each element can be selected, edited, and new children can be added to every content area.
+1
View File
@@ -0,0 +1 @@
{ "label": "_HTML End", "layout": true }
+15
View File
@@ -0,0 +1,15 @@
{
"label": "_Footer",
"layout": true,
"copy": {
"headline": { "label": "Überschrift" },
"text": { "label": "Text" },
"copyright": { "label": "Copyright" }
},
"data": {
"email": { "label": "E-Mail" },
"phone": { "label": "Telefon" },
"instagram": { "label": "Instagram URL" },
"vimeo": { "label": "Vimeo URL" }
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"label": "_HTML Head",
"layout": true,
"copy": {
"title": { "label": "Title" },
"meta_description": { "label": "Meta Description" }
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"label": "Instagram",
"copy": {
"headline": { "label": "Überschrift" }
},
"data": {
"widget": { "label": "LightWidget URL" },
"profile": { "label": "Profil URL" }
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"label": "Intro (Hero-Video)",
"copy": {
"kicker": { "label": "Dachzeile" },
"headline": { "label": "Überschrift" },
"cta_label": { "label": "Button-Text" },
"cta_href": { "label": "Button-Link" },
"ticker": { "label": "Laufband-Text" }
},
"data": {
"video": { "label": "Video", "type": "file" },
"poster": { "label": "Vorschaubild", "type": "img" }
}
}
+1
View File
@@ -0,0 +1 @@
{ "label": "Impressum & Datenschutz" }
+12
View File
@@ -0,0 +1,12 @@
{
"label": "_Navigation",
"layout": true,
"copy": {
"service_label": { "label": "Link: Gewerke" },
"work_label": { "label": "Link: Referenzen" },
"contact_label": { "label": "Link: Kontakt" }
},
"data": {
"full": { "label": "Volle Navigation anzeigen", "type": "boolean" }
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"label": "Gewerke",
"content": {
"items": { "label": "Gewerke", "tpl_filter": ["service_item"] }
},
"copy": {
"headline": { "label": "Überschrift" },
"text": { "label": "Text", "type": "html" },
"image_caption": { "label": "Bildunterschrift" }
},
"data": {
"image": { "label": "Bild", "type": "img" }
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"label": "Gewerk (Klappe)",
"copy": {
"title": { "label": "Titel" },
"text": { "label": "Text" }
},
"data": {
"color": {
"label": "Farbe",
"type": "select",
"options": {
"red": "Rot",
"orange": "Orange",
"yellow": "Gelb",
"pink": "Pink",
"purple": "Lila"
}
}
}
}
+1
View File
@@ -0,0 +1 @@
{ "label": "AGB" }
+10
View File
@@ -0,0 +1,10 @@
{
"label": "Referenzen",
"content": {
"content": { "label": "Referenzen", "tpl_filter": ["work_entry"] }
},
"copy": {
"headline": { "label": "Überschrift" },
"text": { "label": "Text", "type": "html" }
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"label": "Referenz (Video)",
"copy": {
"h2": { "label": "Titel" },
"text": { "label": "Text" }
},
"data": {
"video": { "label": "Embed-URL (YouTube/Vimeo)" }
}
}
+38 -1
View File
@@ -1 +1,38 @@
console.log("Hi!");
import { initNav } from "./nav.js";
import { initReveal } from "./reveal.js";
import { initTimecode } from "./timecode.js";
document.documentElement.classList.add("js");
const components = {
nav: initNav,
reveal: initReveal,
timecode: initTimecode,
};
function init(root = document) {
root.querySelectorAll("[data-component]").forEach((el) => {
if (el.dataset.componentReady) {
return;
}
const fn = components[el.dataset.component];
if (fn) {
fn(el);
el.dataset.componentReady = "true";
}
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", start);
} else {
start();
}
function start() {
init();
// the admin UI swaps element HTML in place after edits
new MutationObserver(() => init()).observe(document.body, { childList: true, subtree: true });
}
+21
View File
@@ -0,0 +1,21 @@
export function initNav(el) {
const toggle = el.querySelector(".nav__toggle");
if (!toggle) {
return;
}
const setOpen = (open) => {
el.classList.toggle("nav--open", open);
toggle.setAttribute("aria-expanded", String(open));
};
toggle.addEventListener("click", () => setOpen(!el.classList.contains("nav--open")));
el.querySelectorAll(".nav__link").forEach((link) => link.addEventListener("click", () => setOpen(false)));
el.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
setOpen(false);
toggle.focus();
}
});
}
+25
View File
@@ -0,0 +1,25 @@
let observer = null;
function getObserver() {
if (!observer) {
observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("is-visible");
observer.unobserve(entry.target);
}
});
}, { rootMargin: "0px 0px -8% 0px", threshold: 0.05 });
}
return observer;
}
export function initReveal(el) {
if (!("IntersectionObserver" in window)) {
el.classList.add("is-visible");
return;
}
getObserver().observe(el);
}
+25
View File
@@ -0,0 +1,25 @@
const FPS = 25;
function pad(n) {
return String(n).padStart(2, "0");
}
export function initTimecode(el) {
const start = performance.now();
const tick = (now) => {
if (!el.isConnected) {
return;
}
const frames = Math.floor(((now - start) / 1000) * FPS);
const f = frames % FPS;
const s = Math.floor(frames / FPS) % 60;
const m = Math.floor(frames / (FPS * 60)) % 60;
const h = Math.floor(frames / (FPS * 3600));
el.textContent = `${pad(h)}:${pad(m)}:${pad(s)}:${pad(f)}`;
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
}
+204
View File
@@ -0,0 +1,204 @@
@use "variables" as *;
:root {
--beige: #{$beige};
--yellow: #{$yellow};
--orange: #{$orange};
--red: #{$red};
--pink: #{$pink};
--purple: #{$purple};
--ink: #{$ink};
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
scroll-padding-top: 88px;
}
body {
min-height: 100vh;
color: $ink;
background: $beige;
font-family: "Manrope", -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
font-size: 18px;
line-height: 1.6;
font-weight: 500;
overflow-x: hidden;
// film grain
&::after {
content: "";
position: fixed;
inset: -50%;
z-index: 9999;
pointer-events: none;
opacity: 0.07;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
animation: grain 0.8s steps(4) infinite;
}
}
@keyframes grain {
0% { transform: translate(0, 0); }
25% { transform: translate(-3%, 2%); }
50% { transform: translate(2%, -3%); }
75% { transform: translate(-2%, -1%); }
100% { transform: translate(1%, 3%); }
}
img,
video,
iframe {
display: block;
max-width: 100%;
}
a {
color: inherit;
}
strong, b { font-weight: 700; }
em, i { font-style: italic; }
::selection {
background: $yellow;
color: $ink;
}
:focus-visible {
outline: 3px solid $orange;
outline-offset: 3px;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
// shared section title: "01 Gewerke"
.section-title {
display: flex;
align-items: flex-start;
gap: 16px;
font-size: clamp(40px, 7vw, 88px);
line-height: 1;
font-weight: 800;
letter-spacing: -0.03em;
&__nr {
flex: none;
margin-top: 0.35em;
padding: 4px 10px;
border: 2px solid currentColor;
border-radius: 999px;
font-size: 14px;
line-height: 1;
font-weight: 700;
letter-spacing: 0.1em;
font-variant-numeric: tabular-nums;
}
&--light {
color: $beige;
}
}
// buttons
.button {
display: inline-flex;
align-items: center;
gap: 12px;
padding: 16px 28px 16px 18px;
border: 3px solid $ink;
border-radius: 999px;
font-size: 18px;
font-weight: 700;
text-decoration: none;
cursor: pointer;
box-shadow: 4px 4px 0 $ink;
transition: transform 0.25s $ease, box-shadow 0.25s $ease;
&:hover {
transform: translate(-2px, -2px) rotate(-1.5deg);
box-shadow: 7px 7px 0 $ink;
}
&:active {
transform: translate(3px, 3px);
box-shadow: 0 0 0 $ink;
}
&--yellow {
background: $yellow;
color: $ink;
}
&--outline {
background: transparent;
padding: 12px 22px;
font-size: 16px;
}
&__play {
position: relative;
width: 32px;
height: 32px;
flex: none;
border-radius: 50%;
background: $ink;
transition: transform 0.4s $ease;
&::after {
content: "";
position: absolute;
top: 50%;
left: 55%;
transform: translate(-50%, -50%);
border-style: solid;
border-width: 6px 0 6px 10px;
border-color: transparent transparent transparent $yellow;
}
}
&:hover &__play {
transform: rotate(360deg) scale(1.1);
}
}
// scroll reveal (only hidden once JS is running)
.js [data-component~="reveal"] {
opacity: 0;
transform: translateY(40px) rotate(1deg);
transition: opacity 0.8s $ease-out, transform 0.9s $ease-out;
&.is-visible {
opacity: 1;
transform: none;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
.js [data-component~="reveal"] {
opacity: 1;
transform: none;
}
}
+137
View File
@@ -0,0 +1,137 @@
@use "variables" as *;
.footer {
position: relative;
overflow: hidden;
padding: 120px 0 32px;
background: $red;
color: $cream;
border-top: 3px solid $ink;
@include md {
padding-top: 160px;
}
// big rotating "film reel" decoration
&::before {
content: "";
position: absolute;
right: -160px;
top: -160px;
width: 480px;
height: 480px;
border-radius: 50%;
background:
radial-gradient(circle at 50% 50%, $red 0 34px, transparent 35px),
radial-gradient(circle at 50% 22%, $red 0 46px, transparent 47px),
radial-gradient(circle at 50% 78%, $red 0 46px, transparent 47px),
radial-gradient(circle at 22% 50%, $red 0 46px, transparent 47px),
radial-gradient(circle at 78% 50%, $red 0 46px, transparent 47px),
$ink;
opacity: 0.25;
animation: reel 18s linear infinite;
pointer-events: none;
}
&__inner {
@include inner;
position: relative;
}
&__headline {
font-size: clamp(56px, 12vw, 180px);
line-height: 0.9;
font-weight: 800;
letter-spacing: -0.05em;
color: $cream;
}
&__text {
max-width: 32ch;
margin-top: 24px;
font-size: clamp(20px, 2.4vw, 28px);
font-weight: 600;
}
&__mail {
margin-top: 40px;
font-size: clamp(16px, 2vw, 22px);
}
&__bottom {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 16px;
margin-top: 96px;
padding-top: 24px;
border-top: 2px solid rgba($cream, 0.35);
font-size: 15px;
}
&__legal {
display: flex;
gap: 24px;
a {
text-decoration: none;
&:hover {
text-decoration: underline wavy;
text-underline-offset: 4px;
}
}
}
}
// end-credits style contact list
.credits {
display: grid;
gap: 4px;
max-width: 640px;
margin: 96px 0 0;
&__row {
display: flex;
align-items: baseline;
gap: 12px;
font-size: clamp(18px, 2vw, 22px);
&::before {
content: "";
order: 1;
flex: 1;
border-bottom: 2px dotted rgba($cream, 0.5);
transform: translateY(-5px);
}
dt {
order: 0;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
opacity: 0.8;
}
dd {
order: 2;
margin: 0;
font-weight: 700;
a {
text-decoration: none;
background: linear-gradient($yellow, $yellow) 0 100% / 0 3px no-repeat;
transition: background-size 0.3s $ease-out;
&:hover {
background-size: 100% 3px;
}
}
}
}
}
@keyframes reel {
to { transform: rotate(360deg); }
}
-966
View File
@@ -1,966 +0,0 @@
.container {
max-width: 1600px;
width: 100%;
margin-right: auto;
margin-left: auto;
position: relative;
box-sizing: border-box;
}
.row {
box-sizing: border-box;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-flex: 0;
-ms-flex: 0 1 auto;
flex: 0 1 auto;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
}
.row.reverse {
-webkit-box-orient: horizontal;
-webkit-box-direction: reverse;
-ms-flex-direction: row-reverse;
flex-direction: row-reverse;
}
.col.reverse {
-webkit-box-orient: vertical;
-webkit-box-direction: reverse;
-ms-flex-direction: column-reverse;
flex-direction: column-reverse;
}
.col-xs,
.col-xs-1,
.col-xs-2,
.col-xs-3,
.col-xs-4,
.col-xs-5,
.col-xs-6,
.col-xs-7,
.col-xs-8,
.col-xs-9,
.col-xs-10,
.col-xs-11,
.col-xs-12,
.col-xs-offset-0,
.col-xs-offset-1,
.col-xs-offset-2,
.col-xs-offset-3,
.col-xs-offset-4,
.col-xs-offset-5,
.col-xs-offset-6,
.col-xs-offset-7,
.col-xs-offset-8,
.col-xs-offset-9,
.col-xs-offset-10,
.col-xs-offset-11,
.col-xs-offset-12 {
box-sizing: border-box;
-webkit-box-flex: 0;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
}
.col-xs {
-webkit-box-flex: 1;
-ms-flex-positive: 1;
flex-grow: 1;
-ms-flex-preferred-size: 0;
flex-basis: 0;
max-width: 100%;
}
.col-xs-1 {
-ms-flex-preferred-size: 8.33333333%;
flex-basis: 8.33333333%;
max-width: 8.33333333%;
}
.col-xs-2 {
-ms-flex-preferred-size: 16.66666667%;
flex-basis: 16.66666667%;
max-width: 16.66666667%;
}
.col-xs-3 {
-ms-flex-preferred-size: 25%;
flex-basis: 25%;
max-width: 25%;
}
.col-xs-4 {
-ms-flex-preferred-size: 33.33333333%;
flex-basis: 33.33333333%;
max-width: 33.33333333%;
}
.col-xs-5 {
-ms-flex-preferred-size: 41.66666667%;
flex-basis: 41.66666667%;
max-width: 41.66666667%;
}
.col-xs-6 {
-ms-flex-preferred-size: 50%;
flex-basis: 50%;
max-width: 50%;
}
.col-xs-7 {
-ms-flex-preferred-size: 58.33333333%;
flex-basis: 58.33333333%;
max-width: 58.33333333%;
}
.col-xs-8 {
-ms-flex-preferred-size: 66.66666667%;
flex-basis: 66.66666667%;
max-width: 66.66666667%;
}
.col-xs-9 {
-ms-flex-preferred-size: 75%;
flex-basis: 75%;
max-width: 75%;
}
.col-xs-10 {
-ms-flex-preferred-size: 83.33333333%;
flex-basis: 83.33333333%;
max-width: 83.33333333%;
}
.col-xs-11 {
-ms-flex-preferred-size: 91.66666667%;
flex-basis: 91.66666667%;
max-width: 91.66666667%;
}
.col-xs-12 {
-ms-flex-preferred-size: 100%;
flex-basis: 100%;
max-width: 100%;
}
.col-xs-offset-0 {
margin-left: 0;
}
.col-xs-offset-1 {
margin-left: 8.33333333%;
}
.col-xs-offset-2 {
margin-left: 16.66666667%;
}
.col-xs-offset-3 {
margin-left: 25%;
}
.col-xs-offset-4 {
margin-left: 33.33333333%;
}
.col-xs-offset-5 {
margin-left: 41.66666667%;
}
.col-xs-offset-6 {
margin-left: 50%;
}
.col-xs-offset-7 {
margin-left: 58.33333333%;
}
.col-xs-offset-8 {
margin-left: 66.66666667%;
}
.col-xs-offset-9 {
margin-left: 75%;
}
.col-xs-offset-10 {
margin-left: 83.33333333%;
}
.col-xs-offset-11 {
margin-left: 91.66666667%;
}
.start-xs {
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
text-align: start;
}
.center-xs {
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
text-align: center;
}
.end-xs {
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
text-align: end;
}
.top-xs {
-webkit-box-align: start;
-ms-flex-align: start;
align-items: flex-start;
}
.middle-xs {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.bottom-xs {
-webkit-box-align: end;
-ms-flex-align: end;
align-items: flex-end;
}
.around-xs {
-ms-flex-pack: distribute;
justify-content: space-around;
}
.between-xs {
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
}
.first-xs {
-webkit-box-ordinal-group: 0;
-ms-flex-order: -1;
order: -1;
}
.last-xs {
-webkit-box-ordinal-group: 2;
-ms-flex-order: 1;
order: 1;
}
@media only screen and (max-width: 511px) {
.hide-xs {
display: none !important;
}
}
@media only screen and (min-width: 512px) {
.col-sm,
.col-sm-1,
.col-sm-2,
.col-sm-3,
.col-sm-4,
.col-sm-5,
.col-sm-6,
.col-sm-7,
.col-sm-8,
.col-sm-9,
.col-sm-10,
.col-sm-11,
.col-sm-12,
.col-sm-offset-0,
.col-sm-offset-1,
.col-sm-offset-2,
.col-sm-offset-3,
.col-sm-offset-4,
.col-sm-offset-5,
.col-sm-offset-6,
.col-sm-offset-7,
.col-sm-offset-8,
.col-sm-offset-9,
.col-sm-offset-10,
.col-sm-offset-11,
.col-sm-offset-12 {
box-sizing: border-box;
-webkit-box-flex: 0;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
}
.col-sm {
-webkit-box-flex: 1;
-ms-flex-positive: 1;
flex-grow: 1;
-ms-flex-preferred-size: 0;
flex-basis: 0;
max-width: 100%;
}
.col-sm-1 {
-ms-flex-preferred-size: 8.33333333%;
flex-basis: 8.33333333%;
max-width: 8.33333333%;
}
.col-sm-2 {
-ms-flex-preferred-size: 16.66666667%;
flex-basis: 16.66666667%;
max-width: 16.66666667%;
}
.col-sm-3 {
-ms-flex-preferred-size: 25%;
flex-basis: 25%;
max-width: 25%;
}
.col-sm-4 {
-ms-flex-preferred-size: 33.33333333%;
flex-basis: 33.33333333%;
max-width: 33.33333333%;
}
.col-sm-5 {
-ms-flex-preferred-size: 41.66666667%;
flex-basis: 41.66666667%;
max-width: 41.66666667%;
}
.col-sm-6 {
-ms-flex-preferred-size: 50%;
flex-basis: 50%;
max-width: 50%;
}
.col-sm-7 {
-ms-flex-preferred-size: 58.33333333%;
flex-basis: 58.33333333%;
max-width: 58.33333333%;
}
.col-sm-8 {
-ms-flex-preferred-size: 66.66666667%;
flex-basis: 66.66666667%;
max-width: 66.66666667%;
}
.col-sm-9 {
-ms-flex-preferred-size: 75%;
flex-basis: 75%;
max-width: 75%;
}
.col-sm-10 {
-ms-flex-preferred-size: 83.33333333%;
flex-basis: 83.33333333%;
max-width: 83.33333333%;
}
.col-sm-11 {
-ms-flex-preferred-size: 91.66666667%;
flex-basis: 91.66666667%;
max-width: 91.66666667%;
}
.col-sm-12 {
-ms-flex-preferred-size: 100%;
flex-basis: 100%;
max-width: 100%;
}
.col-sm-offset-0 {
margin-left: 0;
}
.col-sm-offset-1 {
margin-left: 8.33333333%;
}
.col-sm-offset-2 {
margin-left: 16.66666667%;
}
.col-sm-offset-3 {
margin-left: 25%;
}
.col-sm-offset-4 {
margin-left: 33.33333333%;
}
.col-sm-offset-5 {
margin-left: 41.66666667%;
}
.col-sm-offset-6 {
margin-left: 50%;
}
.col-sm-offset-7 {
margin-left: 58.33333333%;
}
.col-sm-offset-8 {
margin-left: 66.66666667%;
}
.col-sm-offset-9 {
margin-left: 75%;
}
.col-sm-offset-10 {
margin-left: 83.33333333%;
}
.col-sm-offset-11 {
margin-left: 91.66666667%;
}
.start-sm {
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
text-align: start;
}
.center-sm {
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
text-align: center;
}
.end-sm {
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
text-align: end;
}
.top-sm {
-webkit-box-align: start;
-ms-flex-align: start;
align-items: flex-start;
}
.middle-sm {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.bottom-sm {
-webkit-box-align: end;
-ms-flex-align: end;
align-items: flex-end;
}
.around-sm {
-ms-flex-pack: distribute;
justify-content: space-around;
}
.between-sm {
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
}
.first-sm {
-webkit-box-ordinal-group: 0;
-ms-flex-order: -1;
order: -1;
}
.last-sm {
-webkit-box-ordinal-group: 2;
-ms-flex-order: 1;
order: 1;
}
.show-xs {
display: none !important;
}
}
@media only screen and (max-width: 767px) {
.hide-sm {
display: none !important;
}
}
@media only screen and (min-width: 768px) {
.col-md,
.col-md-1,
.col-md-2,
.col-md-3,
.col-md-4,
.col-md-5,
.col-md-6,
.col-md-7,
.col-md-8,
.col-md-9,
.col-md-10,
.col-md-11,
.col-md-12,
.col-md-offset-0,
.col-md-offset-1,
.col-md-offset-2,
.col-md-offset-3,
.col-md-offset-4,
.col-md-offset-5,
.col-md-offset-6,
.col-md-offset-7,
.col-md-offset-8,
.col-md-offset-9,
.col-md-offset-10,
.col-md-offset-11,
.col-md-offset-12 {
box-sizing: border-box;
-webkit-box-flex: 0;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
}
.col-md {
-webkit-box-flex: 1;
-ms-flex-positive: 1;
flex-grow: 1;
-ms-flex-preferred-size: 0;
flex-basis: 0;
max-width: 100%;
}
.col-md-1 {
-ms-flex-preferred-size: 8.33333333%;
flex-basis: 8.33333333%;
max-width: 8.33333333%;
}
.col-md-2 {
-ms-flex-preferred-size: 16.66666667%;
flex-basis: 16.66666667%;
max-width: 16.66666667%;
}
.col-md-3 {
-ms-flex-preferred-size: 25%;
flex-basis: 25%;
max-width: 25%;
}
.col-md-4 {
-ms-flex-preferred-size: 33.33333333%;
flex-basis: 33.33333333%;
max-width: 33.33333333%;
}
.col-md-5 {
-ms-flex-preferred-size: 41.66666667%;
flex-basis: 41.66666667%;
max-width: 41.66666667%;
}
.col-md-6 {
-ms-flex-preferred-size: 50%;
flex-basis: 50%;
max-width: 50%;
}
.col-md-7 {
-ms-flex-preferred-size: 58.33333333%;
flex-basis: 58.33333333%;
max-width: 58.33333333%;
}
.col-md-8 {
-ms-flex-preferred-size: 66.66666667%;
flex-basis: 66.66666667%;
max-width: 66.66666667%;
}
.col-md-9 {
-ms-flex-preferred-size: 75%;
flex-basis: 75%;
max-width: 75%;
}
.col-md-10 {
-ms-flex-preferred-size: 83.33333333%;
flex-basis: 83.33333333%;
max-width: 83.33333333%;
}
.col-md-11 {
-ms-flex-preferred-size: 91.66666667%;
flex-basis: 91.66666667%;
max-width: 91.66666667%;
}
.col-md-12 {
-ms-flex-preferred-size: 100%;
flex-basis: 100%;
max-width: 100%;
}
.col-md-offset-0 {
margin-left: 0;
}
.col-md-offset-1 {
margin-left: 8.33333333%;
}
.col-md-offset-2 {
margin-left: 16.66666667%;
}
.col-md-offset-3 {
margin-left: 25%;
}
.col-md-offset-4 {
margin-left: 33.33333333%;
}
.col-md-offset-5 {
margin-left: 41.66666667%;
}
.col-md-offset-6 {
margin-left: 50%;
}
.col-md-offset-7 {
margin-left: 58.33333333%;
}
.col-md-offset-8 {
margin-left: 66.66666667%;
}
.col-md-offset-9 {
margin-left: 75%;
}
.col-md-offset-10 {
margin-left: 83.33333333%;
}
.col-md-offset-11 {
margin-left: 91.66666667%;
}
.start-md {
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
text-align: start;
}
.center-md {
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
text-align: center;
}
.end-md {
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
text-align: end;
}
.top-md {
-webkit-box-align: start;
-ms-flex-align: start;
align-items: flex-start;
}
.middle-md {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.bottom-md {
-webkit-box-align: end;
-ms-flex-align: end;
align-items: flex-end;
}
.around-md {
-ms-flex-pack: distribute;
justify-content: space-around;
}
.between-md {
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
}
.first-md {
-webkit-box-ordinal-group: 0;
-ms-flex-order: -1;
order: -1;
}
.last-md {
-webkit-box-ordinal-group: 2;
-ms-flex-order: 1;
order: 1;
}
.show-sm {
display: none !important;
}
}
@media only screen and (max-width: 1023px) {
.hide-md {
display: none !important;
}
}
@media only screen and (min-width: 1024px) {
.col-lg,
.col-lg-1,
.col-lg-2,
.col-lg-3,
.col-lg-4,
.col-lg-5,
.col-lg-6,
.col-lg-7,
.col-lg-8,
.col-lg-9,
.col-lg-10,
.col-lg-11,
.col-lg-12,
.col-lg-offset-0,
.col-lg-offset-1,
.col-lg-offset-2,
.col-lg-offset-3,
.col-lg-offset-4,
.col-lg-offset-5,
.col-lg-offset-6,
.col-lg-offset-7,
.col-lg-offset-8,
.col-lg-offset-9,
.col-lg-offset-10,
.col-lg-offset-11,
.col-lg-offset-12 {
box-sizing: border-box;
-webkit-box-flex: 0;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
}
.col-lg {
-webkit-box-flex: 1;
-ms-flex-positive: 1;
flex-grow: 1;
-ms-flex-preferred-size: 0;
flex-basis: 0;
max-width: 100%;
}
.col-lg-1 {
-ms-flex-preferred-size: 8.33333333%;
flex-basis: 8.33333333%;
max-width: 8.33333333%;
}
.col-lg-2 {
-ms-flex-preferred-size: 16.66666667%;
flex-basis: 16.66666667%;
max-width: 16.66666667%;
}
.col-lg-3 {
-ms-flex-preferred-size: 25%;
flex-basis: 25%;
max-width: 25%;
}
.col-lg-4 {
-ms-flex-preferred-size: 33.33333333%;
flex-basis: 33.33333333%;
max-width: 33.33333333%;
}
.col-lg-5 {
-ms-flex-preferred-size: 41.66666667%;
flex-basis: 41.66666667%;
max-width: 41.66666667%;
}
.col-lg-6 {
-ms-flex-preferred-size: 50%;
flex-basis: 50%;
max-width: 50%;
}
.col-lg-7 {
-ms-flex-preferred-size: 58.33333333%;
flex-basis: 58.33333333%;
max-width: 58.33333333%;
}
.col-lg-8 {
-ms-flex-preferred-size: 66.66666667%;
flex-basis: 66.66666667%;
max-width: 66.66666667%;
}
.col-lg-9 {
-ms-flex-preferred-size: 75%;
flex-basis: 75%;
max-width: 75%;
}
.col-lg-10 {
-ms-flex-preferred-size: 83.33333333%;
flex-basis: 83.33333333%;
max-width: 83.33333333%;
}
.col-lg-11 {
-ms-flex-preferred-size: 91.66666667%;
flex-basis: 91.66666667%;
max-width: 91.66666667%;
}
.col-lg-12 {
-ms-flex-preferred-size: 100%;
flex-basis: 100%;
max-width: 100%;
}
.col-lg-offset-0 {
margin-left: 0;
}
.col-lg-offset-1 {
margin-left: 8.33333333%;
}
.col-lg-offset-2 {
margin-left: 16.66666667%;
}
.col-lg-offset-3 {
margin-left: 25%;
}
.col-lg-offset-4 {
margin-left: 33.33333333%;
}
.col-lg-offset-5 {
margin-left: 41.66666667%;
}
.col-lg-offset-6 {
margin-left: 50%;
}
.col-lg-offset-7 {
margin-left: 58.33333333%;
}
.col-lg-offset-8 {
margin-left: 66.66666667%;
}
.col-lg-offset-9 {
margin-left: 75%;
}
.col-lg-offset-10 {
margin-left: 83.33333333%;
}
.col-lg-offset-11 {
margin-left: 91.66666667%;
}
.start-lg {
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
text-align: start;
}
.center-lg {
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
text-align: center;
}
.end-lg {
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
text-align: end;
}
.top-lg {
-webkit-box-align: start;
-ms-flex-align: start;
align-items: flex-start;
}
.middle-lg {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.bottom-lg {
-webkit-box-align: end;
-ms-flex-align: end;
align-items: flex-end;
}
.around-lg {
-ms-flex-pack: distribute;
justify-content: space-around;
}
.between-lg {
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
}
.first-lg {
-webkit-box-ordinal-group: 0;
-ms-flex-order: -1;
order: -1;
}
.last-lg {
-webkit-box-ordinal-group: 2;
-ms-flex-order: 1;
order: 1;
}
.show-md {
display: none !important;
}
}
+38
View File
@@ -0,0 +1,38 @@
@use "variables" as *;
.insta {
padding: 120px 0;
@include md {
padding: 160px 0;
}
&__inner {
@include inner;
}
&__head {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
justify-content: space-between;
gap: 24px;
margin-bottom: 56px;
}
&__frame {
padding: 16px;
border: 3px solid $ink;
border-radius: $radius-lg;
background: $cream;
box-shadow: 10px 10px 0 $pink;
transform: rotate(-0.75deg);
}
&__widget {
width: 100%;
border: 0;
overflow: hidden;
border-radius: $radius - 6px;
}
}
+267
View File
@@ -0,0 +1,267 @@
@use "variables" as *;
.intro {
position: relative;
padding: 48px 0 0;
@include md {
padding-top: 72px;
}
&__inner {
@include inner;
position: relative;
}
&__kicker {
display: inline-flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
padding: 6px 14px;
border-radius: 999px;
background: $ink;
color: $beige;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
rotate: -2deg;
}
&__kicker-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: $red;
animation: blink 1s steps(1) infinite;
}
&__headline {
max-width: 12ch;
font-size: clamp(52px, 11vw, 168px);
line-height: 0.92;
// background-clip: text only paints inside the box; keep descenders (g, p, …) visible
padding-bottom: 0.3em;
margin-bottom: -0.3em;
font-weight: 800;
letter-spacing: -0.045em;
background: linear-gradient(100deg, $ink 0 48%, $red 68%, $orange 88%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
}
&__cta {
margin-top: 32px;
@include md {
position: absolute;
top: 0;
right: 48px;
margin: 0;
rotate: 4deg;
transition: rotate 0.4s $ease, opacity 0.8s $ease-out, transform 0.9s $ease-out;
&:hover {
rotate: -2deg;
}
}
}
}
@keyframes blink {
50% { opacity: 0; }
}
.viewfinder {
position: relative;
margin-top: 48px;
overflow: hidden;
border: 3px solid $ink;
border-radius: $radius-lg;
background: $ink;
box-shadow: 10px 10px 0 $orange;
aspect-ratio: 16 / 9;
min-height: 320px;
@include md {
margin-top: 64px;
box-shadow: 16px 16px 0 $orange;
}
&__video {
width: 100%;
height: 100%;
object-fit: cover;
}
&__overlay {
position: absolute;
inset: 0;
pointer-events: none;
color: #fff;
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.08em;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
@include md {
font-size: 16px;
}
}
&__corner {
position: absolute;
width: 36px;
height: 36px;
border: 0 solid rgba(#fff, 0.9);
&--tl { top: 20px; left: 20px; border-top-width: 3px; border-left-width: 3px; }
&--tr { top: 20px; right: 20px; border-top-width: 3px; border-right-width: 3px; }
&--bl { bottom: 20px; left: 20px; border-bottom-width: 3px; border-left-width: 3px; }
&--br { bottom: 20px; right: 20px; border-bottom-width: 3px; border-right-width: 3px; }
}
&__rec {
position: absolute;
top: 30px;
left: 70px;
display: flex;
align-items: center;
gap: 8px;
&::before {
content: "";
width: 12px;
height: 12px;
border-radius: 50%;
background: $red;
animation: blink 1s steps(1) infinite;
}
}
&__timecode {
position: absolute;
top: 30px;
right: 70px;
font-variant-numeric: tabular-nums;
}
&__meta {
position: absolute;
bottom: 30px;
left: 70px;
opacity: 0.85;
}
&__battery {
position: absolute;
right: 74px;
bottom: 32px;
width: 30px;
height: 14px;
border: 2px solid #fff;
border-radius: 3px;
&::before {
content: "";
position: absolute;
inset: 2px 30% 2px 2px;
background: #fff;
}
&::after {
content: "";
position: absolute;
top: 3px;
right: -5px;
width: 3px;
height: 4px;
background: #fff;
}
}
&__focus {
position: absolute;
top: 50%;
left: 50%;
width: 64px;
height: 64px;
transform: translate(-50%, -50%);
border: 2px solid rgba($yellow, 0.9);
animation: focus-hunt 5s $ease infinite;
&::before,
&::after {
content: "";
position: absolute;
background: rgba($yellow, 0.9);
}
&::before { top: 50%; left: 50%; width: 12px; height: 2px; transform: translate(-50%, -50%); }
&::after { top: 50%; left: 50%; width: 2px; height: 12px; transform: translate(-50%, -50%); }
}
@media (max-width: 639px) {
&__meta,
&__battery {
display: none;
}
&__rec { left: 32px; top: 28px; }
&__timecode { right: 32px; top: 28px; }
&__corner { width: 22px; height: 22px; }
}
}
@keyframes focus-hunt {
0%, 100% { transform: translate(-50%, -50%) scale(1); }
10% { transform: translate(-50%, -50%) scale(1.25); }
18% { transform: translate(-50%, -50%) scale(0.9); }
24% { transform: translate(-50%, -50%) scale(1); }
60% { transform: translate(-30%, -60%) scale(1); }
70% { transform: translate(-30%, -60%) scale(1.2); }
76% { transform: translate(-30%, -60%) scale(1); }
}
.ticker {
position: relative;
z-index: 2;
margin: 72px -5vw 0;
padding: 18px 0;
overflow: hidden;
border-block: 3px solid $ink;
background: $yellow;
transform: rotate(-2deg);
@include md {
margin-top: 96px;
}
&__track {
display: flex;
width: max-content;
animation: ticker 40s linear infinite;
span {
padding-right: 0.5em;
white-space: nowrap;
font-size: clamp(24px, 4vw, 44px);
line-height: 1;
font-weight: 800;
letter-spacing: -0.02em;
text-transform: uppercase;
}
}
&:hover &__track {
animation-play-state: paused;
}
}
@keyframes ticker {
to { transform: translateX(-50%); }
}
+167
View File
@@ -0,0 +1,167 @@
@use "variables" as *;
.nav {
position: sticky;
top: 0;
z-index: 100;
padding: 12px 0;
transition: padding 0.3s $ease-out;
&__inner {
@include inner;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
}
&__brand {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 10px 18px;
border: 3px solid $ink;
border-radius: 999px;
background: $cream;
box-shadow: 4px 4px 0 $ink;
text-decoration: none;
font-size: 22px;
font-weight: 800;
letter-spacing: -0.02em;
transition: transform 0.3s $ease;
&:hover {
transform: rotate(-3deg) scale(1.04);
}
}
&__rec {
width: 12px;
height: 12px;
border-radius: 50%;
background: $red;
box-shadow: 0 0 0 0 rgba($red, 0.6);
animation: rec-pulse 1.6s infinite;
}
&__word {
display: inline-flex;
align-items: center;
}
&__cut {
display: inline-block;
margin: 0 1px;
color: $orange;
font-size: 0.75em;
transform: rotate(-90deg);
transition: transform 0.5s $ease;
}
&__brand:hover &__cut {
transform: rotate(-90deg) translateY(-3px) scaleX(0.6);
}
&__toggle {
display: inline-flex;
flex-direction: column;
justify-content: center;
gap: 6px;
width: 52px;
height: 52px;
padding: 0 14px;
border: 3px solid $ink;
border-radius: 50%;
background: $yellow;
box-shadow: 4px 4px 0 $ink;
cursor: pointer;
@include md {
display: none;
}
}
&__toggle-bar {
display: block;
height: 3px;
border-radius: 3px;
background: $ink;
transition: transform 0.3s $ease;
}
&--open &__toggle-bar:first-child { transform: translateY(4.5px) rotate(45deg); }
&--open &__toggle-bar:nth-child(2) { transform: translateY(-4.5px) rotate(-45deg); }
&__links {
display: none;
gap: 8px;
padding: 6px;
border: 3px solid $ink;
border-radius: 999px;
background: $cream;
box-shadow: 4px 4px 0 $ink;
@include md {
display: flex;
}
}
&__link {
position: relative;
padding: 8px 18px;
border-radius: 999px;
font-size: 16px;
font-weight: 700;
text-decoration: none;
transition: background 0.2s, transform 0.3s $ease;
&:hover {
background: $ink;
color: $beige;
transform: rotate(-2deg);
}
&--cta {
background: $red;
color: $cream;
}
}
// mobile overlay menu
&--open &__links {
position: fixed;
inset: 84px 16px auto;
display: flex;
flex-direction: column;
gap: 4px;
padding: 16px;
border-radius: $radius-lg;
animation: menu-pop 0.4s $ease;
.nav__link {
padding: 16px 20px;
font-size: 28px;
font-weight: 800;
&::before {
content: attr(data-nr);
margin-right: 12px;
font-size: 14px;
vertical-align: middle;
opacity: 0.5;
}
}
}
}
@keyframes rec-pulse {
0% { box-shadow: 0 0 0 0 rgba($red, 0.6); opacity: 1; }
50% { opacity: 0.35; }
70% { box-shadow: 0 0 0 10px rgba($red, 0); }
100% { box-shadow: 0 0 0 0 rgba($red, 0); opacity: 1; }
}
@keyframes menu-pop {
from { opacity: 0; transform: scale(0.9) rotate(-2deg); }
to { opacity: 1; transform: none; }
}
+1 -3
View File
@@ -14,7 +14,6 @@ time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
outline: none;
font: inherit;
text-decoration: none;
vertical-align: baseline;
@@ -54,5 +53,4 @@ table {
}
textarea:focus, input:focus, button:focus {
outline: none;
}
}
+99
View File
@@ -0,0 +1,99 @@
@use "variables" as *;
.service {
padding: 120px 0;
@include md {
padding: 160px 0;
}
&__inner {
@include inner;
}
&__head {
display: grid;
gap: 24px;
margin-bottom: 64px;
@include md {
grid-template-columns: 1fr 1fr;
align-items: end;
}
}
&__text {
max-width: 46ch;
font-size: 20px;
}
&__grid {
display: grid;
gap: 56px;
@include md {
grid-template-columns: 5fr 7fr;
align-items: start;
gap: 64px;
}
}
&__items {
display: grid;
gap: 32px;
}
}
.polaroid {
position: relative;
margin: 0;
padding: 16px 16px 72px;
border: 3px solid $ink;
background: $cream;
box-shadow: 10px 10px 0 $ink;
rotate: 2.5deg;
transition: rotate 0.5s $ease, scale 0.5s $ease, opacity 0.8s $ease-out, transform 0.9s $ease-out;
@include md {
position: sticky;
top: 120px;
}
&:hover {
rotate: -1deg;
scale: 1.02;
}
// tape
&::before {
content: "";
position: absolute;
top: -18px;
left: 50%;
width: 140px;
height: 36px;
transform: translateX(-50%) rotate(-4deg);
background: rgba($yellow, 0.85);
border: 2px dashed rgba($ink, 0.25);
}
&__image {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
object-position: center 33%;
border: 3px solid $ink;
}
&__caption {
position: absolute;
right: 16px;
bottom: 18px;
left: 16px;
font-size: 18px;
font-weight: 700;
font-style: italic;
text-align: center;
transform: rotate(-1deg);
}
}
+125
View File
@@ -0,0 +1,125 @@
@use "variables" as *;
.slate {
--slate-color: #{$red};
position: relative;
counter-increment: slate;
padding-top: 30px;
@each $name, $color in $colors {
&--#{$name} {
--slate-color: #{$color};
}
}
&__clapper {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 34px;
border: 3px solid $ink;
border-radius: 10px 10px 0 0;
background: repeating-linear-gradient(
-55deg,
var(--slate-color) 0 20px,
$ink 20px 40px
);
transform-origin: 6px 100%;
transform: rotate(-9deg);
transition: transform 0.35s $ease;
// hinge
&::before {
content: "";
position: absolute;
left: 2px;
bottom: -9px;
width: 12px;
height: 12px;
border: 3px solid $ink;
border-radius: 50%;
background: $cream;
}
}
&:hover &__clapper {
animation: clap 0.45s $ease;
transform: rotate(0);
}
&__body {
position: relative;
padding: 28px 28px 28px;
border: 3px solid $ink;
border-radius: 0 0 $radius $radius;
background: $ink;
color: $beige;
box-shadow: 8px 8px 0 var(--slate-color);
transition: transform 0.35s $ease, box-shadow 0.35s $ease;
// top stripe bar of the slate body
&::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 14px;
background: repeating-linear-gradient(
-55deg,
var(--slate-color) 0 20px,
$ink 20px 40px
);
border-bottom: 3px solid $ink;
}
// "SCENE 01"
&::after {
content: "Szene 0" counter(slate);
position: absolute;
top: 30px;
right: 24px;
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
opacity: 0.6;
}
}
&:hover &__body {
transform: translate(-3px, -3px);
box-shadow: 12px 12px 0 var(--slate-color);
}
&__title {
margin-top: 12px;
font-size: clamp(32px, 4vw, 48px);
line-height: 1.1;
font-weight: 800;
letter-spacing: -0.03em;
color: var(--slate-color);
}
&__text {
margin-top: 8px;
font-size: 18px;
color: rgba($beige, 0.85);
}
// playful alternating tilt
&:nth-child(odd) { rotate: -1deg; }
&:nth-child(even) { rotate: 1deg; }
}
.service__items {
counter-reset: slate;
}
@keyframes clap {
0% { transform: rotate(-9deg); }
40% { transform: rotate(-22deg); }
70% { transform: rotate(1deg); }
100% { transform: rotate(0); }
}
+37
View File
@@ -0,0 +1,37 @@
@use "variables" as *;
.text {
padding: 72px 0 120px;
&__inner {
@include inner;
max-width: 880px;
}
h3 {
margin-bottom: 40px;
font-size: clamp(40px, 6vw, 72px);
line-height: 1;
font-weight: 800;
letter-spacing: -0.03em;
}
h4 {
display: inline-block;
margin: 40px 0 12px;
padding: 4px 12px;
background: $yellow;
border: 2px solid $ink;
border-radius: 8px;
font-size: 18px;
font-weight: 800;
transform: rotate(-1deg);
}
p {
margin-bottom: 16px;
font-size: 17px;
line-height: 1.7;
hyphens: auto;
}
}
+55
View File
@@ -0,0 +1,55 @@
$beige: #f7eee0;
$cream: #fff8ec;
$yellow: #febb28;
$orange: #fe6732;
$red: #f5303c;
$pink: #ce2867;
$purple: #571b47;
$ink: #0e0c0b;
$grey: #3d3d3d;
$max-width: 1440px;
$radius: 20px;
$radius-lg: 32px;
$ease: cubic-bezier(0.34, 1.56, 0.64, 1); // springy
$ease-out: cubic-bezier(0.22, 1, 0.36, 1);
$shadow: 6px 6px 0 $ink;
$colors: (
"red": $red,
"orange": $orange,
"yellow": $yellow,
"pink": $pink,
"purple": $purple
);
@mixin sm {
@media (min-width: 640px) { @content; }
}
@mixin md {
@media (min-width: 900px) { @content; }
}
@mixin lg {
@media (min-width: 1200px) { @content; }
}
@mixin inner {
width: 100%;
max-width: $max-width;
margin: 0 auto;
padding: 0 20px;
@include md {
padding: 0 48px;
}
}
@mixin stripes($color, $size: 22px) {
background: repeating-linear-gradient(
-45deg,
$color 0 $size,
$ink $size ($size * 2)
);
}
+61
View File
@@ -0,0 +1,61 @@
@use "variables" as *;
.work {
position: relative;
padding: 0;
background: $ink;
color: $beige;
counter-reset: take;
&__inner {
@include inner;
padding-top: 120px;
padding-bottom: 120px;
@include md {
padding-top: 160px;
padding-bottom: 160px;
}
}
&__head {
display: grid;
gap: 24px;
margin-bottom: 80px;
@include md {
grid-template-columns: 1fr 1fr;
align-items: end;
}
}
&__text {
max-width: 46ch;
font-size: 20px;
color: rgba($beige, 0.8);
}
&__list {
display: grid;
gap: 96px;
@include md {
gap: 140px;
}
}
}
// sprocket holes
.filmstrip {
height: 44px;
background-color: $ink;
background-image: radial-gradient(ellipse 9px 8px at center, $beige 96%, transparent 100%);
background-size: 40px 20px;
background-position: 0 12px;
background-repeat: repeat-x;
animation: filmstrip 2s linear infinite;
}
@keyframes filmstrip {
to { background-position: 40px 12px; }
}
+95
View File
@@ -0,0 +1,95 @@
@use "variables" as *;
.take {
counter-increment: take;
display: grid;
gap: 32px;
align-items: center;
@include md {
grid-template-columns: 4fr 8fr;
gap: 64px;
&:nth-child(even) {
grid-template-columns: 8fr 4fr;
.take__info { order: 2; }
.take__screen { order: 1; transform: rotate(1.5deg); }
}
}
&__info {
position: relative;
}
&__label {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 14px;
border: 2px solid $beige;
border-radius: 8px;
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
font-size: 13px;
letter-spacing: 0.12em;
text-transform: uppercase;
&::after {
content: counter(take, decimal-leading-zero);
padding-left: 8px;
border-left: 2px solid $beige;
color: $yellow;
}
}
&__title {
margin-top: 20px;
font-size: clamp(32px, 4.5vw, 56px);
line-height: 1.05;
font-weight: 800;
letter-spacing: -0.03em;
}
&__text {
margin-top: 16px;
color: rgba($beige, 0.75);
}
&__screen {
position: relative;
aspect-ratio: 16 / 9;
border: 3px solid $beige;
border-radius: $radius;
overflow: hidden;
background: #000;
transform: rotate(-1.5deg);
transition: transform 0.5s $ease, box-shadow 0.5s $ease;
box-shadow: 10px 10px 0 var(--take-color, #{$red});
}
&:hover &__screen {
transform: rotate(0) scale(1.02);
box-shadow: 16px 16px 0 var(--take-color, #{$red});
}
&:nth-child(4n+1) { --take-color: #{$pink}; }
&:nth-child(4n+2) { --take-color: #{$red}; }
&:nth-child(4n+3) { --take-color: #{$orange}; }
&:nth-child(4n+4) { --take-color: #{$yellow}; }
&:hover &__title {
color: var(--take-color);
}
&__title {
transition: color 0.3s;
}
&__video {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border: 0;
}
}
+11 -832
View File
@@ -1,833 +1,12 @@
@import "_reset.scss";
@import "_grid.scss";
@use "reset";
@use "base";
@use "nav";
@use "intro";
@use "service";
@use "service_item";
@use "work";
@use "work_entry";
@use "insta";
@use "footer";
@use "text";
* {
color: var(--black);
box-sizing: border-box;
}
html {
color: var(--black);
font-family: "Manrope", -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
--white: #fff;
--beige: #f7eee0;
--yellow: #febb28;
--orange: #fe6732;
--red: #f5303c;
--pink: #ce2867;
--purple: #571b47;
--grey: #3d3d3d;
--black: #000;
}
body {
background-color: var(--beige);
padding: 0 16px;
overflow-x: hidden;
}
section {
width: 100%;
}
h1, h2, h3, h4, p {
margin-block-start: 0;
margin-block-end: 0;
line-height: 1.5;
}
h1, h2, h3 {
font-weight: 500;
}
h1 {
font-size: 96px;
}
h2 {
font-size: 48px;
}
h3 {
font-size: 32px;
}
h4 {
font-size: 20px;
font-weight: 600;
}
p {
font-size: 18px;
font-weight: 500;
a {
color: var(--red);
&:hover {
text-decoration: underline;
}
}
}
button, a.button {
font-size: 18px;
font-weight: 600;
padding: 16px 24px;
cursor: pointer;
}
[data-aos="fade-in"] {
opacity: 0;
transition-property: opacity;
&.aos-animate {
opacity: 1;
}
}
.header {
position: sticky;
top: 0;
left: 0;
height: 64px;
width: 100%;
background-color: var(--beige);
z-index: 100;
-webkit-backface-visibility: hidden; /* Correct fixed positioning on mobile */
.container {
height: 100%;
display: flex;
align-items: center;
.wordmark {
font-size: 24px;
font-weight: 700;
cursor: pointer;
}
ul {
display: flex;
flex: 1;
justify-content: flex-end;
text-align: right;
li {
position: relative;
display: inline-flex;
margin-left: 48px;
font-size: 18px;
line-height: 1.5;
font-weight: 500;
color: inherit;
cursor: pointer;
&:nth-of-type(1):hover::before {
background-color: var(--pink);
}
&:nth-of-type(2):hover::before {
background-color: var(--red);
}
&:nth-of-type(3):hover::before {
background-color: var(--orange);
}
&:nth-of-type(4):hover::before {
background-color: var(--yellow);
}
&::before {
content: "";
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 2px;
z-index: -1;
}
}
}
}
}
.mobile-menu {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.8);
z-index: 1000;
animation: fade-in 0.2s ease-in-out;
-webkit-backface-visibility: hidden; /* Correct fixed positioning on mobile */
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
li {
color: white;
font-size: 24px;
line-height: 64px;
font-weight: 600;
text-align: center;
cursor: pointer;
}
}
.section-start {
padding: 128px 0;
.headline {
display: flex;
margin-bottom: 96px;
h1 {
flex: 1;
a {
color: var(--red);
}
}
img {
cursor: pointer;
}
}
.hero {
width: 100%;
max-width: 100%;
overflow: hidden;
z-index: 1;
video {
height: 100%;
min-height: 512px;
width: 100%;
object-fit: cover;
transform: scale(1.004);
}
}
}
.section-service {
padding: 128px 0;
h2 {
margin: 0 64px 24px 0;
padding-bottom: 12px;
border-bottom: 8px solid;
&:nth-of-type(1) {
border-color: var(--red);
}
&:nth-of-type(2) {
border-color: var(--orange);
}
&:nth-of-type(3) {
border-color: var(--yellow);
}
}
p {
margin: 0 64px 64px 0;
&:last-of-type {
margin-bottom: 16px;
}
}
.img {
min-height: 512px;
background-image: url("1.jpg");
background-position: center 33%;
background-size: cover;
overflow: hidden;
}
}
.section-work {
padding: 128px 0;
.video {
margin-bottom: 128px;
&:nth-of-type(4n+1) h2 {
border-color: var(--pink);
}
&:nth-of-type(4n+2) h2 {
border-color: var(--red);
}
&:nth-of-type(4n+3) h2 {
border-color: var(--orange);
}
&:nth-of-type(4n+4) h2 {
border-color: var(--yellow);
}
&:last-of-type {
margin-bottom: 0;
}
}
h2 {
margin: 0 0 24px 64px;
padding-bottom: 12px;
border-bottom: 8px solid;
}
p {
margin-left: 64px;
}
}
.section-rental {
padding: 128px 0;
.square {
position: sticky;
top: 128px;
padding: 32px 0;
h2 {
margin-bottom: 24px;
padding-bottom: 12px;
border-bottom: 8px solid var(--red);
}
button, a.button {
display: inline-flex;
color: var(--black);
margin: 48px 12px 0 0;
&:hover {
box-shadow: inset 0 0 0 2px var(--black);
}
&.primary {
background-color: var(--yellow);
}
&.secondary {
background-color: var(--orange);
}
}
}
.parts {
position: relative;
max-width: 720px;
min-height: 256px;
margin: 0 auto;
img:not(#part0) {
position: absolute;
filter: contrast(1.3) saturate(0.6);
}
#part0 {
width: 100%;
}
#part1 {
width: 29%;
left: 34%;
}
#part2 {
width: 8.5%;
left: 24%;
margin-top: 2%;
}
#part3 {
width: 19%;
left: 0;
margin-top: 2%;
}
#part4 {
width: 17%;
left: 2%;
margin-top: 15%;
}
#part5 {
width: 5.5%;
left: 70%;
margin-top: 9%;
}
#part6 {
width: 22%;
left: 64%;
margin-top: 1%;
}
#part7 {
width: 12.5%;
left: 87.5%;
margin-top: 1%;
}
#part8 {
width: 37%;
left: 30%;
margin-top: 28%;
}
#part9 {
width: 22.5%;
left: 0;
margin-top: 28%;
}
#part10 {
width: 49%;
left: 20%;
margin-top: 40%;
}
#part11 {
width: 14%;
left: 2%;
margin-top: 36%;
}
#part12 {
width: 27%;
left: 72%;
margin-top: 31%;
}
#part13 {
width: 24%;
left: 48%;
margin-top: 48%;
}
#part14 {
width: 19%;
left: 24%;
margin-top: 48%;
}
#part15 {
width: 4.5%;
left: 15%;
margin-top: 47%;
}
#part16 {
width: 24%;
left: 75%;
margin-top: 48%;
}
#part17 {
width: 7.5%;
left: 2%;
margin-top: 44%;
}
#part18-1 {
width: 14%;
left: 46%;
margin-top: 72%;
}
#part18-2 {
width: 14%;
left: 46%;
margin-top: 86%;
}
#part19-1 {
width: 17%;
left: 22%;
margin-top: 70%;
}
#part19-2 {
width: 17%;
left: 22%;
margin-top: 84%;
}
#part20 {
width: 13.5%;
left: 66%;
margin-top: 73%;
}
#part21 {
width: 11.5%;
left: 6%;
margin-top: 73%;
}
#part22 {
width: 13.5%;
left: 84%;
margin-top: 73%;
}
#part23 {
width: 28%;
left: 22%;
margin-top: 102%;
}
#part24 {
width: 37.5%;
left: 60%;
margin-top: 100%;
}
#part25 {
width: 20%;
left: 14%;
margin-top: 105%;
}
#part26 {
width: 40%;
left: 43%;
margin-top: 106%;
}
#part27 {
width: 22.5%;
left: 20%;
margin-top: 115%;
}
#part28 {
width: 22%;
left: 47%;
margin-top: 130%;
}
#part29-1 {
width: 3%;
left: 4%;
margin-top: 106%;
}
#part29-2 {
width: 3%;
left: 8%;
margin-top: 106%;
}
#part30 {
width: 19%;
left: 70%;
margin-top: 116%;
}
#part31-1 {
width: 2.5%;
left: 13%;
margin-top: 125.5%;
}
#part31-2 {
width: 2.5%;
left: 16%;
margin-top: 125.5%;
}
#part32-1 {
width: 2.75%;
left: 20%;
margin-top: 126%;
}
#part32-2 {
width: 2.75%;
left: 24%;
margin-top: 126%;
}
}
}
.section-instagram {
padding: 128px 0;
}
.section-text {
padding: 128px 0;
h3 {
margin-bottom: 32px;
}
h4 {
margin: 16px 0 12px;
}
p {
margin-bottom: 12px;
}
}
footer {
padding: 128px 0;
ul {
display: flex;
justify-content: space-between;
li {
font-size: 18px;
line-height: 1.5;
font-weight: 500;
&:first-of-type {
flex: 1;
}
&:not(:first-of-type) {
margin-left: 48px;
}
&:nth-of-type(4n+1):hover a::before {
background-color: var(--pink);
}
&:nth-of-type(4n+2):hover a::before {
background-color: var(--red);
}
&:nth-of-type(4n+3):hover a::before {
background-color: var(--orange);
}
&:nth-of-type(4n+4):hover a::before {
background-color: var(--yellow);
}
a {
position: relative;
&::before {
content: "";
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 2px;
z-index: -1;
}
}
}
}
}
@media only screen and (max-width: 1023px) {
h1 {
font-size: 64px;
}
h2 {
font-size: 36px;
}
h3 {
font-size: 24px;
}
h4 {
font-size: 20px;
}
.section-start {
padding: 64px 0;
.headline {
margin-bottom: 64px;
}
}
.section-service {
padding: 64px 0;
h2 {
margin: 0 32px 24px 0;
}
p {
margin: 0 32px 32px 0;
}
.img {
margin-top: 64px;
}
}
.section-work {
padding: 64px 0;
h2 {
margin: 0 0 24px 0;
}
p {
margin: 0 0 32px 0;
}
.video {
margin-bottom: 64px;
}
}
.section-rental {
padding: 64px 0;
overflow-x: hidden;
.square {
padding: 0;
h2 {
margin: 0 0 24px 0;
}
}
.parts {
margin: 64px 0 0;
overflow: hidden;
}
}
.section-instagram {
padding: 64px 0;
}
.section-text {
padding: 64px 0;
}
footer {
padding: 64px 0;
ul {
display: block;
li {
margin: 12px 0 !important;
}
}
}
}
@media only screen and (max-width: 767px) {
.section-service {
h2 {
margin: 0 0 16px 0;
padding-bottom: 8px;
}
p {
margin: 0 0 24px 0;
}
}
.section-work {
h2 {
margin: 0 0 16px 0;
padding-bottom: 8px;
}
p {
margin: 0 0 24px 0;
}
}
.section-rental {
.square h2 {
margin: 0 0 16px 0;
padding-bottom: 8px;
}
}
}
@media only screen and (max-width: 511px) {
h1 {
font-size: 40px;
}
h2 {
font-size: 32px;
}
.section-start {
padding: 32px 0 0;
.headline {
margin-bottom: 32px;
}
}
.section-service {
padding: 64px 0 0;
h2 {
border-bottom: 4px solid;
}
.img {
margin-top: 32px;
}
}
.section-work {
padding: 64px 0 0;
.video {
margin-bottom: 32px;
}
h2 {
border-bottom: 4px solid;
}
}
.section-rental {
padding: 64px 0 0;
.square h2 {
border-bottom: 4px solid var(--red);
}
}
.section-instagram {
padding: 32px 0;
}
.section-text {
padding: 32px 0;
}
footer {
padding: 32px 0;
}
}
+108 -15
View File
@@ -7,18 +7,87 @@
},
"content": {
"content": [
{"ref": "head"},
{
"ref": "head"
},
{
"ref": "nav",
"data": {
"full": true
"full": "on"
}
},
{
"tpl": "intro"
"tpl": "intro",
"copy": {
"de": {
"kicker": "Videoproduktion aus Ostwestfalen",
"headline": "Film, auf den Frame genau.",
"cta_label": "Projekt anfragen",
"cta_href": "mailto:hi@schnittfest.gmbh",
"ticker": "Film ✦ Animation ✦ Sound ✦ Schnitt ✦ Color Grading ✦ Imagefilm ✦ Commercial ✦"
}
},
"data": {
"video": "/static/hero.mp4",
"poster": "/static/1.jpg"
},
"content": {}
},
{
"tpl": "service"
"tpl": "service",
"copy": {
"de": {
"headline": "Gewerke",
"text": "<div>Von der ersten Idee bis zum finalen Frame: Wir planen, drehen, animieren und vertonen – alles aus einer Hand.</div>",
"image_caption": "Am Set, irgendwo zwischen Take 7 und 8."
}
},
"data": {
"image": "/static/1.jpg"
},
"content": {
"items": [
{
"tpl": "service_item",
"copy": {
"de": {
"title": "Film",
"text": "sleep.shoot.eat.repeat."
}
},
"data": {
"color": "red"
},
"content": {}
},
{
"tpl": "service_item",
"copy": {
"de": {
"title": "Animation",
"text": "Herausstechen und Welten erschaffen."
}
},
"data": {
"color": "orange"
},
"content": {}
},
{
"tpl": "service_item",
"copy": {
"de": {
"title": "Sound",
"text": "Keine Musik wäre wie Butter ohne Brot."
}
},
"data": {
"color": "yellow"
},
"content": {}
}
]
}
},
{
"tpl": "work",
@@ -34,7 +103,8 @@
"h2": "Imagefilm",
"text": "Schüco Stories auf den schwedischen Inseln in der Nähe von Stockholm. Bei diesem Bauprojekt wurde das komplette vorhaben von der Entstehung bis zum fertigen Objekt filmisch begleitet."
}
}
},
"content": {}
},
{
"tpl": "work_entry",
@@ -46,7 +116,8 @@
"h2": "Artwork Film",
"text": "Seit 25 Jahren bringt DansArt Tanz und Kultur nach Bielefeld. In vier Tanzstudios und einem eigenen Theater finden regelmäßig Tanzkurse, Ausbildungen für angehende Profitänzer *innen, Festivals, Workshops, Performances und Konzerte statt."
}
}
},
"content": {}
},
{
"tpl": "work_entry",
@@ -58,7 +129,8 @@
"h2": "Imagefilm",
"text": "Neubau, Erweiterung und Renovation – das sind die Geschäftsfelder von H.O. Schlüter. Die H.O. Schlüter GmbH arbeitet als Partner zusammen mit Schüco International KG. Der Metallbau Lübz stellt sich in diesem Unternehmensfilm vor."
}
}
},
"content": {}
},
{
"tpl": "work_entry",
@@ -70,7 +142,8 @@
"h2": "Commercial",
"text": "Umbauten von Verbrennern zu Elektrofahrzeugen. ESDI EV Technologies ist eine Firma, die sich auf dem Schwerpunkt Elektroumbauten fokussiert hat. Mit Tesla-Technik, Nissan Leaf oder vielen weiteren baut ESDI EV VW Käfer, Porsche, Mini Cooper und mehr zu Elektrofahrzeugen."
}
}
},
"content": {}
},
{
"tpl": "work_entry",
@@ -82,7 +155,8 @@
"h2": "Imagefilm",
"text": "GOLDBECK und Schüco, das Fenstersystem AWE80 in der Entwicklung und Produktion in Bielefeld."
}
}
},
"content": {}
},
{
"tpl": "work_entry",
@@ -94,7 +168,8 @@
"h2": "Porträtfilm",
"text": "Digitalisierung im Metallbau - Die berühmt-berüchtigte Industrie 4.0."
}
}
},
"content": {}
},
{
"tpl": "work_entry",
@@ -106,7 +181,8 @@
"h2": "Kampagne",
"text": "Azubi Recruiting Film für die Berufe Metallbauer, Metalltechnik, Technischer Systemplaner, Büromanagement."
}
}
},
"content": {}
},
{
"tpl": "work_entry",
@@ -118,16 +194,33 @@
"h2": "Eventfilm",
"text": "50 führende Familienunternehmen aus ganz Deutschland zum Karrieretag Familienunternehmen in Bielefeld bei Schüco - Miele, Hilti, Haribo und viele mehr."
}
}
},
"content": {}
}
]
},
"copy": {
"de": {
"headline": "Referenzen",
"text": "<div>Popcorn bereit? Eine Auswahl unserer Lieblingsprojekte.</div>"
}
}
},
{
"tpl": "insta"
"tpl": "insta",
"copy": {
"de": {
"headline": "Hinter den Kulissen"
}
},
{"ref": "footer"},
{"ref": "end"}
"content": {}
},
{
"ref": "footer"
},
{
"ref": "end"
}
]
}
}
+4
View File
@@ -34,5 +34,9 @@
"analytics": {
"provider": "pirsch",
"client_secret": ""
},
"ui": {
"path": "/admin",
"admin_password": "${ADMIN_PASSWORD}"
}
}
-51
View File
@@ -1,51 +0,0 @@
var controller = new ScrollMagic.Controller();
var backgroundColor = new TweenMax.to(".bg-beige-to-black", 1, {
backgroundColor: "#000",
ease: Power1.easeOut
});
var backgroundColorScene = new ScrollMagic.Scene({
triggerElement: "#work",
duration: 256
})
.setTween(backgroundColor)
.addTo(controller);
var textColor = new TweenMax.to(".black-to-beige", 1, {
color: "#f7eee0",
ease: Power1.easeOut
});
var textColorScene = new ScrollMagic.Scene({
triggerElement: "#work",
duration: 256
})
.setTween(textColor)
.addTo(controller);
var reverseBackgroundColor = new TweenMax.to(".bg-beige-to-black", 1, {
backgroundColor: "#f7eee0",
ease: Power1.easeOut
});
var reverseBackgroundColorScene = new ScrollMagic.Scene({
triggerElement: "#rental",
duration: 256
})
.setTween(reverseBackgroundColor)
.addTo(controller);
var reverseTextColor = new TweenMax.to(".black-to-beige", 1, {
color: "#000",
ease: Power1.easeOut
});
var reverseTextColorScene = new ScrollMagic.Scene({
triggerElement: "#rental",
duration: 256
})
.setTween(reverseTextColor)
.addTo(controller);
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
(()=>{console.log("Hi!");})();
(()=>{function a(t){let e=t.querySelector(".nav__toggle");if(!e)return;let n=o=>{t.classList.toggle("nav--open",o),e.setAttribute("aria-expanded",String(o))};e.addEventListener("click",()=>n(!t.classList.contains("nav--open"))),t.querySelectorAll(".nav__link").forEach(o=>o.addEventListener("click",()=>n(!1))),t.addEventListener("keydown",o=>{o.key==="Escape"&&(n(!1),e.focus())})}var i=null;function g(){return i||(i=new IntersectionObserver(t=>{t.forEach(e=>{e.isIntersecting&&(e.target.classList.add("is-visible"),i.unobserve(e.target))})},{rootMargin:"0px 0px -8% 0px",threshold:.05})),i}function c(t){if(!("IntersectionObserver"in window)){t.classList.add("is-visible");return}g().observe(t)}function s(t){return String(t).padStart(2,"0")}function d(t){let e=performance.now(),n=o=>{if(!t.isConnected)return;let r=Math.floor((o-e)/1e3*25),l=r%25,m=Math.floor(r/25)%60,v=Math.floor(r/1500)%60,p=Math.floor(r/(25*3600));t.textContent=`${s(p)}:${s(v)}:${s(m)}:${s(l)}`,requestAnimationFrame(n)};requestAnimationFrame(n)}document.documentElement.classList.add("js");var b={nav:a,reveal:c,timecode:d};function f(t=document){t.querySelectorAll("[data-component]").forEach(e=>{if(e.dataset.componentReady)return;let n=b[e.dataset.component];n&&(n(e),e.dataset.componentReady="true")})}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",u):u();function u(){f(),new MutationObserver(()=>f()).observe(document.body,{childList:!0,subtree:!0})}})();
-532
View File
@@ -1,532 +0,0 @@
var offset = window.screen.width > 512 ? 20 : 10;
var controller = new ScrollMagic.Controller();
var part2 = new TweenMax.from("#part2", 1, {
x: "-100vw",
ease: Power2.easeOut
});
var part2Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
})
.setTween(part2)
.addTo(controller);
var part3 = new TweenMax.from("#part3", 1, {
x: "-100vw",
ease: Power2.easeOut
});
var part3Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: this.offset
})
.setTween(part3)
.addTo(controller);
var part4 = new TweenMax.from("#part4", 1, {
x: "-100vw",
y: "100%",
rotation: 2,
ease: Power2.easeOut
});
var part4Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 2*this.offset
})
.setTween(part4)
.addTo(controller);
var part5 = new TweenMax.from("#part5", 1, {
x: "100vw",
ease: Power2.easeOut
});
var part5Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250
})
.setTween(part5)
.addTo(controller);
var part6 = new TweenMax.from("#part6", 1, {
x: "100vw",
ease: Power2.easeOut
});
var part6Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 3*this.offset
})
.setTween(part6)
.addTo(controller);
var part7 = new TweenMax.from("#part7", 1, {
x: "100vw",
ease: Power2.easeOut
});
var part7Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 4*this.offset
})
.setTween(part7)
.addTo(controller);
var part8 = new TweenMax.from("#part8", 1, {
x: "-100vw",
y: "200%",
rotation: 4,
ease: Power2.easeOut
});
var part8Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 5*this.offset
})
.setTween(part8)
.addTo(controller);
var part9 = new TweenMax.from("#part9", 1, {
x: "-100vw",
y: "300%",
rotation: 4,
ease: Power2.easeOut
});
var part9Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 5*this.offset
})
.setTween(part9)
.addTo(controller);
var part10 = new TweenMax.from("#part10", 1, {
x: "100vw",
y: "600%",
rotation: -12,
ease: Power2.easeOut
});
var part10Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 6*this.offset
})
.setTween(part10)
.addTo(controller);
var part11 = new TweenMax.from("#part11", 1, {
x: "-100vw",
y: "800%",
rotation: 12,
ease: Power2.easeOut
});
var part11Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 7*this.offset
})
.setTween(part11)
.addTo(controller);
var part12 = new TweenMax.from("#part12", 1, {
x: "100vw",
y: "500%",
rotation: -16,
ease: Power2.easeOut
});
var part12Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 8*this.offset
})
.setTween(part12)
.addTo(controller);
var part13 = new TweenMax.from("#part13", 1, {
x: "100vw",
y: "400%",
rotation: -24,
ease: Power2.easeOut
});
var part13Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 9*this.offset
})
.setTween(part13)
.addTo(controller);
var part14 = new TweenMax.from("#part14", 1, {
x: "-100vw",
y: "400%",
rotation: 24,
ease: Power2.easeOut
});
var part14Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 10*this.offset
})
.setTween(part14)
.addTo(controller);
var part15 = new TweenMax.from("#part15", 1, {
x: "-100vw",
y: "400%",
rotation: 24,
ease: Power2.easeOut
});
var part15Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 11*this.offset
})
.setTween(part15)
.addTo(controller);
var part16 = new TweenMax.from("#part16", 1, {
x: "100vw",
y: "400%",
rotation: 24,
ease: Power2.easeOut
});
var part16Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 12*this.offset
})
.setTween(part16)
.addTo(controller);
var part17 = new TweenMax.from("#part17", 1, {
x: "-100vw",
y: "400%",
rotation: 24,
ease: Power2.easeOut
});
var part17Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 13*this.offset
})
.setTween(part17)
.addTo(controller);
var part181 = new TweenMax.from("#part18-1", 1, {
x: "100vw",
y: "800%",
rotation: -32,
ease: Power2.easeOut
});
var part181Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 14*this.offset
})
.setTween(part181)
.addTo(controller);
var part182 = new TweenMax.from("#part18-2", 1, {
x: "100vw",
y: "800%",
rotation: -32,
ease: Power2.easeOut
});
var part182Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 15*this.offset
})
.setTween(part182)
.addTo(controller);
var part191 = new TweenMax.from("#part19-1", 1, {
x: "-100vw",
y: "800%",
rotation: 32,
ease: Power2.easeOut
});
var part191Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 16*this.offset
})
.setTween(part191)
.addTo(controller);
var part192 = new TweenMax.from("#part19-2", 1, {
x: "-100vw",
y: "800%",
rotation: 32,
ease: Power2.easeOut
});
var part192Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 18*this.offset
})
.setTween(part192)
.addTo(controller);
var part20 = new TweenMax.from("#part20", 1, {
x: "100vw",
y: "800%",
rotation: -32,
ease: Power2.easeOut
});
var part20Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 19*this.offset
})
.setTween(part20)
.addTo(controller);
var part21 = new TweenMax.from("#part21", 1, {
x: "-100vw",
y: "800%",
rotation: 32,
ease: Power2.easeOut
});
var part21Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 20*this.offset
})
.setTween(part21)
.addTo(controller);
var part22 = new TweenMax.from("#part22", 1, {
x: "100vw",
y: "800%",
rotation: -32,
ease: Power2.easeOut
});
var part22Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 21*this.offset
})
.setTween(part22)
.addTo(controller);
var part23 = new TweenMax.from("#part23", 1, {
x: "-100vw",
y: "2400%",
rotation: 32,
ease: Power2.easeOut
});
var part23Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 22*this.offset
})
.setTween(part23)
.addTo(controller);
var part24 = new TweenMax.from("#part24", 1, {
x: "100vw",
y: "1000%",
rotation: -32,
ease: Power2.easeOut
});
var part24Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 23*this.offset
})
.setTween(part24)
.addTo(controller);
var part25 = new TweenMax.from("#part25", 1, {
x: "-100vw",
y: "1000%",
rotation: 32,
ease: Power2.easeOut
});
var part25Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 24*this.offset
})
.setTween(part25)
.addTo(controller);
var part26 = new TweenMax.from("#part26", 1, {
x: "100vw",
y: "1000%",
rotation: -32,
ease: Power2.easeOut
});
var part26Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 25*this.offset
})
.setTween(part26)
.addTo(controller);
var part27 = new TweenMax.from("#part27", 1, {
x: "-100vw",
y: "1600%",
rotation: 32,
ease: Power2.easeOut
});
var part27Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 26*this.offset
})
.setTween(part27)
.addTo(controller);
var part28 = new TweenMax.from("#part28", 1, {
x: "100vw",
y: "1600%",
rotation: -32,
ease: Power2.easeOut
});
var part28Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 33*this.offset
})
.setTween(part28)
.addTo(controller);
var part291 = new TweenMax.from("#part29-1", 1, {
x: "-100vw",
y: "1600%",
rotation: 32,
ease: Power2.easeOut
});
var part291Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 32*this.offset
})
.setTween(part291)
.addTo(controller);
var part292 = new TweenMax.from("#part29-2", 1, {
x: "-100vw",
y: "1600%",
rotation: 32,
ease: Power2.easeOut
});
var part292Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 31*this.offset
})
.setTween(part292)
.addTo(controller);
var part30 = new TweenMax.from("#part30", 1, {
x: "100vw",
y: "1600%",
rotation: -32,
ease: Power2.easeOut
});
var part30Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 34*this.offset
})
.setTween(part30)
.addTo(controller);
var part311 = new TweenMax.from("#part31-1", 1, {
x: "-100vw",
y: "3200%",
rotation: 32,
ease: Power2.easeOut
});
var part311Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 30*this.offset
})
.setTween(part311)
.addTo(controller);
var part312 = new TweenMax.from("#part31-2", 1, {
x: "-100vw",
y: "3200%",
rotation: 32,
ease: Power2.easeOut
});
var part312Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 29*this.offset
})
.setTween(part312)
.addTo(controller);
var part321 = new TweenMax.from("#part32-1", 1, {
x: "-100vw",
y: "3200%",
rotation: 32,
ease: Power2.easeOut
});
var part321Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 28*this.offset
})
.setTween(part321)
.addTo(controller);
var part322 = new TweenMax.from("#part32-2", 1, {
x: "-100vw",
y: "3200%",
rotation: 32,
ease: Power2.easeOut
});
var part322Scene = new ScrollMagic.Scene({
triggerElement: "#parts",
duration: 250,
offset: 27*this.offset
})
.setTween(part322)
.addTo(controller);
+1
View File
@@ -1,2 +1,3 @@
{{ adminBody .Page }}
</body>
</html>
+32 -109
View File
@@ -1,111 +1,34 @@
<footer id="footer">
<div class="container">
<ul>
<li>© 2024 Schnittfest GmbH</li>
<li>
<a href="tel:+49017693124984">+49 176 93124984</a>
</li>
<li>
<a href="mailto:hi@schnitffest.gmbh">hi@schnittfest.gmbh</a>
</li>
<li>
<a href="https://instagram.com/schnittfest.gmbh">Instagram</a>
</li>
<li>
<a href="https://vimeo.com/schnittfest">Vimeo</a>
</li>
<li>
<a href="/agb" target="_blank">AGB</a>
</li>
<li>
<a href="/impressum" target="_blank">Impressum</a>
</li>
</ul>
{{- $headline := default (copy .Page .Content "headline") "Und… Action?" -}}
{{- $text := default (copy .Page .Content "text") "Erzähl uns von deiner Idee – wir machen einen Film daraus." -}}
{{- $email := default (get .Content "email") "hi@schnittfest.gmbh" -}}
{{- $phone := default (get .Content "phone") "+49 176 93124984" -}}
{{- $instagram := default (get .Content "instagram") "https://instagram.com/schnittfest.gmbh" -}}
{{- $vimeo := default (get .Content "vimeo") "https://vimeo.com/schnittfest" -}}
{{- $copyright := default (copy .Page .Content "copyright") "Schnittfest GmbH" -}}
<footer class="footer" id="footer" data-shifu-element="{{ .Content.Position }}">
<div class="footer__inner">
<div class="footer__cta" data-component="reveal">
<h2 class="footer__headline">{{ $headline }}</h2>
<p class="footer__text">{{ $text }}</p>
<a class="button button--yellow footer__mail" href="mailto:{{ $email }}">
<span class="button__play" aria-hidden="true"></span>{{ $email }}
</a>
</div>
<dl class="credits" data-component="reveal">
<div class="credits__row"><dt>Telefon</dt><dd><a href="tel:{{ $phone | replace " " "" }}">{{ $phone }}</a></dd></div>
<div class="credits__row"><dt>E-Mail</dt><dd><a href="mailto:{{ $email }}">{{ $email }}</a></dd></div>
<div class="credits__row"><dt>Instagram</dt><dd><a href="{{ $instagram }}" target="_blank" rel="noopener">schnittfest.gmbh</a></dd></div>
<div class="credits__row"><dt>Vimeo</dt><dd><a href="{{ $vimeo }}" target="_blank" rel="noopener">schnittfest</a></dd></div>
</dl>
<div class="footer__bottom">
<span>© {{ year }} {{ $copyright }}</span>
<span class="footer__legal">
<a href="/agb">AGB</a>
<a href="/impressum">Impressum</a>
</span>
</div>
</div>
</footer>
<script type="text/javascript" src="https://player.vimeo.com/api/player.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.0.4/gsap.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/ScrollMagic/2.0.7/ScrollMagic.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/ScrollMagic/2.0.7/plugins/animation.gsap.min.js"></script>
<script type="text/javascript" src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
<script type="text/javascript" src="https://cdn.lightwidget.com/widgets/lightwidget.js"></script>
<script type="text/javascript" src="static/js/color.js"></script>
<script type="text/javascript" src="static/js/parts.js"></script>
<script>
AOS.init({
duration: 1000,
easing: "cubic-bezier(0.4, 0.0, 0.2, 1)",
once: true,
offset: 32
});
// navbar mobile menu
let mobileMenu = document.getElementById("mobileMenu");
let mobileMenuOpen = document.getElementById("mobileMenuOpen");
let mobileMenuClose = document.getElementById("mobileMenuClose");
function toggleMobileMenu() {
let hidden = mobileMenu.style.display;
if(hidden) {
mobileMenu.style.display = "";
} else {
mobileMenu.style.display = "none"
}
}
mobileMenuOpen.addEventListener("click", e => {
e.preventDefault();
e.stopPropagation();
toggleMobileMenu();
});
// scroll to from navigation
let scrollToService = document.getElementsByClassName("scrollToService");
let scrollToWork = document.getElementsByClassName("scrollToWork");
let scrollToRental = document.getElementsByClassName("scrollToRental");
let scrollToFooter = document.getElementsByClassName("scrollToFooter");
function addScrollTo(nodes, anchor) {
for (let node of nodes) {
node.addEventListener("click", () => {
toggleMobileMenu();
let section = document.getElementById(anchor);
if (window.location.pathname === "/") {
window.scrollTo({top: section.offsetTop, behavior: "smooth"});
} else {
window.location = `/#${anchor}`;
}
});
}
}
document.addEventListener("click", e => {
if(!mobileMenu.style.display) {
mobileMenu.style.display = "none"
}
});
addScrollTo(scrollToService, "service");
addScrollTo(scrollToWork, "work");
addScrollTo(scrollToRental, "rental");
addScrollTo(scrollToFooter, "footer");
// click on logo navigation
let homeLink = document.getElementById("homeLink");
homeLink.addEventListener("click", e => {
e.preventDefault();
e.stopPropagation();
if(window.location.pathname === "/") {
window.scrollTo({top: 0, behavior: "smooth"});
}
else {
window.location = "/";
}
});
</script>
+25 -25
View File
@@ -1,37 +1,37 @@
{{- $title := default (copy .Page .Content "title") "Schnittfest – Film, auf den Frame genau." -}}
{{- $description := default (copy .Page .Content "meta_description") "Film, Animation und Sound aus Ostwestfalen. Schnittfest produziert Imagefilme, Commercials und Kampagnen – auf den Frame genau." -}}
<!DOCTYPE html>
<html lang="de">
<html lang="{{ .Page.Language }}">
<head>
<base href="/" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ $title }}</title>
<meta name="description" content="{{ $description }}" />
<meta name="copyright" content="Schnittfest GmbH" />
<meta name="author" content="Emvi Software GmbH" />
<meta name="title" content="Schnittfest" />
<meta name="description" content="Film, auf den Frame genau." />
<meta name="msapplication-TileColor" content="#000000" />
<meta name="theme-color" content="#000000" />
<meta name="twitter:card" content="product" />
<meta name="twitter:title" content="Schnittfest" />
<meta name="twitter:description" content="Film, auf den Frame genau." />
<meta name="twitter:image" content="/static/1.jpg" />
<meta property="og:url" content="https://schnittfest.gmbh/" />
<meta property="og:title" content="Schnittfest" />
<meta property="og:description" content="Film, auf den Frame genau." />
<meta property="og:image" content="/static/1.jpg" />
<link rel="stylesheet" type="text/css" href="/static/web/main.css" />
<link rel="stylesheet" type="text/css" href="/static/web/manrope.css" />
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet" />
<meta name="theme-color" content="#0e0c0b" />
<meta name="msapplication-TileColor" content="#0e0c0b" />
<meta name="apple-mobile-web-app-title" content="Schnittfest" />
<meta name="application-name" content="Schnittfest" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="{{ $title }}" />
<meta name="twitter:description" content="{{ $description }}" />
<meta name="twitter:image" content="{{ hostname }}/static/1.jpg" />
<meta property="og:url" content="{{ .Page.CanonicalLink }}" />
<meta property="og:title" content="{{ $title }}" />
<meta property="og:description" content="{{ $description }}" />
<meta property="og:image" content="{{ hostname }}/static/1.jpg" />
<link rel="canonical" href="{{ .Page.CanonicalLink }}" />
<link rel="apple-touch-icon" sizes="180x180" href="/static/favicon/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon/favicon-16x16.png" />
<link rel="mask-icon" href="/static/favicon/safari-pinned-tab.svg" color="#000000" />
<link rel="mask-icon" href="/static/favicon/safari-pinned-tab.svg" color="#0e0c0b" />
<link rel="shortcut icon" href="/static/favicon/favicon.ico" />
<meta name="apple-mobile-web-app-title" content="Schnittfest" />
<meta name="application-name" content="Schnittfest" />
<meta name="msapplication-TileColor" content="#000000" />
<meta name="theme-color" content="#000000" />
<title>Schnittfest</title>
<link rel="preload" href="/static/fonts/Manrope-ExtraBold.woff2" as="font" type="font/woff2" crossorigin />
<link rel="stylesheet" href="/static/web/manrope.css" />
<link rel="stylesheet" href="/static/web/main.css?v={{ .CMS.LastUpdate }}" />
<script defer src="/static/web/main.min.js?v={{ .CMS.LastUpdate }}"></script>
{{ adminHead .Page }}
</head>
<body class="bg-beige-to-black">
<body>
+13 -3
View File
@@ -1,6 +1,16 @@
<section id="instagram" class="section-instagram">
<div class="container">
{{- $headline := default (copy .Page .Content "headline") "Hinter den Kulissen" -}}
{{- $widget := default (get .Content "widget") "https://cdn.lightwidget.com/widgets/ea2fc8dd2d55541dbafdc312dbe28aee.html" -}}
{{- $profile := default (get .Content "profile") "https://instagram.com/schnittfest.gmbh" -}}
<section class="insta" id="instagram" data-shifu-element="{{ .Content.Position }}">
<div class="insta__inner">
<div class="insta__head" data-component="reveal">
<h2 class="section-title"><span class="section-title__nr">03</span>{{ $headline }}</h2>
<a class="button button--outline" href="{{ $profile }}" target="_blank" rel="noopener">@schnittfest.gmbh</a>
</div>
<div class="insta__frame" data-component="reveal">
<script src="https://cdn.lightwidget.com/widgets/lightwidget.js"></script>
<iframe src="https://cdn.lightwidget.com/widgets/ea2fc8dd2d55541dbafdc312dbe28aee.html" scrolling="no" allowtransparency="true" class="lightwidget-widget" style="width:100%;border:0;overflow:hidden;"></iframe>
<iframe src="{{ $widget }}" scrolling="no" allowtransparency="true" class="lightwidget-widget insta__widget" title="Instagram"></iframe>
</div>
</div>
</section>
+34 -9
View File
@@ -1,13 +1,38 @@
<section id="start" class="section-start">
<div class="container" data-aos="fade-in">
<div class="headline">
<h1 data-aos="fade-in">
Film, auf den Frame genau.
<a href="mailto:hi@schnittfest.gmbh">&#10230;</a>
</h1>
{{- $headline := default (copy .Page .Content "headline") "Film, auf den Frame genau." -}}
{{- $kicker := default (copy .Page .Content "kicker") "Videoproduktion aus Ostwestfalen" -}}
{{- $ctaLabel := default (copy .Page .Content "cta_label") "Projekt anfragen" -}}
{{- $ctaHref := default (copy .Page .Content "cta_href") "mailto:hi@schnittfest.gmbh" -}}
{{- $ticker := default (copy .Page .Content "ticker") "Film ✦ Animation ✦ Sound ✦ Schnitt ✦ Color Grading ✦ Drohne ✦ Imagefilm ✦ Commercial ✦" -}}
{{- $video := default (get .Content "video") "/static/hero.mp4" -}}
{{- $poster := default (get .Content "poster") "/static/1.jpg" -}}
<section class="intro" id="start" data-shifu-element="{{ .Content.Position }}">
<div class="intro__inner">
<p class="intro__kicker" data-component="reveal"><span class="intro__kicker-dot"></span>{{ $kicker }}</p>
<h1 class="intro__headline" data-component="reveal">{{ $headline }}</h1>
<div class="intro__cta" data-component="reveal">
<a class="button button--yellow" href="{{ $ctaHref }}"><span class="button__play" aria-hidden="true"></span>{{ $ctaLabel }}</a>
</div>
<div id="hero" class="hero" data-aos="fade-in" data-aos-delay="200">
<video src="/static/hero.mp4" autoplay loop muted playsinline />
<div class="viewfinder" data-component="reveal">
<video class="viewfinder__video" src="{{ $video }}" poster="{{ $poster }}" autoplay loop muted playsinline></video>
<div class="viewfinder__overlay" aria-hidden="true">
<span class="viewfinder__corner viewfinder__corner--tl"></span>
<span class="viewfinder__corner viewfinder__corner--tr"></span>
<span class="viewfinder__corner viewfinder__corner--bl"></span>
<span class="viewfinder__corner viewfinder__corner--br"></span>
<span class="viewfinder__rec">REC</span>
<span class="viewfinder__timecode" data-component="timecode">00:00:00:00</span>
<span class="viewfinder__meta">4K · 25 FPS · ISO 800</span>
<span class="viewfinder__battery"></span>
<span class="viewfinder__focus"></span>
</div>
</div>
</div>
<div class="ticker" aria-hidden="true">
<div class="ticker__track">
<span>{{ $ticker }}</span><span>{{ $ticker }}</span><span>{{ $ticker }}</span><span>{{ $ticker }}</span>
</div>
</div>
</section>
+2 -2
View File
@@ -1,5 +1,5 @@
<section id="text" class="section-text">
<div class="container">
<section class="text" id="text" data-shifu-element="{{ .Content.Position }}">
<div class="text__inner">
<h3>Impressum</h3>
<p>
Schnittfest GmbH<br>
+24 -27
View File
@@ -1,28 +1,25 @@
{{if get .Content "full"}}
<nav class="header bg-beige-to-black">
<div class="container" data-aos="fade-in" data-aos-delay="50">
<div id="homeLink" class="wordmark black-to-beige">Schnittfest</div>
<ul class="links hide-sm black-to-beige">
<li class="scrollToService">Gewerke</li>
<li class="scrollToWork">Referenzen</li>
<li class="scrollToFooter">Kontakt</li>
</ul>
<ul class="show-sm black-to-beige">
<li id="mobileMenuOpen">Menü</li>
</ul>
{{- $full := get .Content "full" -}}
{{- $service := default (copy .Page .Content "service_label") "Gewerke" -}}
{{- $work := default (copy .Page .Content "work_label") "Referenzen" -}}
{{- $contact := default (copy .Page .Content "contact_label") "Kontakt" -}}
<header class="nav{{ if $full }} nav--full{{ end }}" data-component="nav" data-shifu-element="{{ .Content.Position }}">
<div class="nav__inner">
<a class="nav__brand" href="/" aria-label="Schnittfest – Startseite">
<span class="nav__rec" aria-hidden="true"></span>
<span class="nav__word">Schnitt<span class="nav__cut" aria-hidden="true">✂</span>fest</span>
</a>
{{ if $full }}
<button class="nav__toggle" type="button" aria-expanded="false" aria-controls="nav-links">
<span class="nav__toggle-bar"></span>
<span class="nav__toggle-bar"></span>
<span class="visually-hidden">Menü</span>
</button>
<nav class="nav__links" id="nav-links">
<a class="nav__link" href="/#service" data-nr="01">{{ $service }}</a>
<a class="nav__link" href="/#work" data-nr="02">{{ $work }}</a>
<a class="nav__link nav__link--cta" href="/#footer" data-nr="03">{{ $contact }}</a>
</nav>
{{ end }}
</div>
</nav>
<nav id="mobileMenu" class="mobile-menu show-sm" style="display:none">
<ul>
<li class="scrollToService">Gewerke</li>
<li class="scrollToWork">Referenzen</li>
<li class="scrollToFooter">Kontakt</li>
</ul>
</nav>
{{else}}
<nav class="header bg-beige">
<div class="container">
<div id="homeLink" class="wordmark">Schnittfest</div>
</div>
</nav>
{{end}}
</header>
+19 -11
View File
@@ -1,15 +1,23 @@
<section id="service" class="section-service bg-beige">
<div class="container">
<div class="services row">
<div class="col-lg-4 col-md-4 col-sm-12 col-xs-12">
<h2 class="black-to-beige" data-aos="fade-in" data-aos-delay="100">Film</h2>
<p class="black-to-beige" data-aos="fade-in" data-aos-delay="100">sleep.shoot.eat.repeat. </p>
<h2 class="black-to-beige" data-aos="fade-in" data-aos-delay="200">Animation</h2>
<p class="black-to-beige" data-aos="fade-in" data-aos-delay="200">Herausstechen und Welten erschaffen.</p>
<h2 class="black-to-beige" data-aos="fade-in" data-aos-delay="300">Sound</h2>
<p class="black-to-beige" data-aos="fade-in" data-aos-delay="300">Keine Musik wäre wie Butter ohne Brot.</p>
{{- $headline := default (copy .Page .Content "headline") "Gewerke" -}}
{{- $text := copy .Page .Content "text" -}}
{{- $image := default (get .Content "image") "/static/1.jpg" -}}
{{- $caption := copy .Page .Content "image_caption" -}}
<section class="service" id="service" data-shifu-element="{{ .Content.Position }}">
<div class="service__inner">
<div class="service__head" data-component="reveal">
<h2 class="section-title"><span class="section-title__nr">01</span>{{ $headline }}</h2>
{{ with $text }}<div class="service__text">{{ html . }}</div>{{ end }}
</div>
<div class="col-lg-8 col-md-8 col-sm-12 col-xs-12 img" data-aos="fade-in"></div>
<div class="service__grid">
<div class="service__items">
{{ .CMS.Render .Args .Page (fmt "%s.items" .Content.Position) (index .Content.Content "items") }}
<slot name="{{ .Content.Position }}/items"></slot>
</div>
<figure class="polaroid" data-component="reveal">
<img class="polaroid__image" src="{{ $image }}" alt="{{ default $caption "Schnittfest am Set" }}" loading="lazy" />
{{ with $caption }}<figcaption class="polaroid__caption">{{ . }}</figcaption>{{ end }}
</figure>
</div>
</div>
</section>
+11
View File
@@ -0,0 +1,11 @@
{{- $title := copy .Page .Content "title" -}}
{{- $text := copy .Page .Content "text" -}}
{{- $color := default (get .Content "color") "red" -}}
<article class="slate slate--{{ $color }}" data-component="reveal" data-shifu-element="{{ .Content.Position }}">
<div class="slate__clapper" aria-hidden="true"></div>
<div class="slate__body">
{{ with $title }}<h3 class="slate__title">{{ . }}</h3>{{ end }}
{{ with $text }}<p class="slate__text">{{ . }}</p>{{ end }}
</div>
</article>
+2 -2
View File
@@ -1,5 +1,5 @@
<section id="text" class="section-text">
<div class="container">
<section class="text" id="text" data-shifu-element="{{ .Content.Position }}">
<div class="text__inner">
<h3>Allgemeine Geschäftsbedingungen</h3>
<p>
Alle Preise verstehen sich zzgl. 3% Versicherungsgebühr und 19% MwSt.
+15 -3
View File
@@ -1,5 +1,17 @@
<section id="work" class="section-work">
<div class="container">
{{.CMS.Render .Args .Page (fmt "%s.content" .Content.Position) (index .Content.Content "content")}}
{{- $headline := default (copy .Page .Content "headline") "Referenzen" -}}
{{- $text := copy .Page .Content "text" -}}
<section class="work" id="work" data-shifu-element="{{ .Content.Position }}">
<div class="filmstrip" aria-hidden="true"></div>
<div class="work__inner">
<div class="work__head" data-component="reveal">
<h2 class="section-title section-title--light"><span class="section-title__nr">02</span>{{ $headline }}</h2>
{{ with $text }}<div class="work__text">{{ html . }}</div>{{ end }}
</div>
<div class="work__list">
{{ .CMS.Render .Args .Page (fmt "%s.content" .Content.Position) (index .Content.Content "content") }}
<slot name="{{ .Content.Position }}/content"></slot>
</div>
</div>
<div class="filmstrip filmstrip--bottom" aria-hidden="true"></div>
</section>
+14 -9
View File
@@ -1,11 +1,16 @@
<div class="video row reverse">
<div class="col-lg-4 col-md-12 col-sm-12 col-xs-12">
<h2 class="black-to-beige" data-aos="fade-in" data-aos-delay="100">{{copy .Page .Content "h2"}}</h2>
<p class="black-to-beige" data-aos="fade-in" data-aos-delay="200">{{copy .Page .Content "text"}}</p>
{{- $title := copy .Page .Content "h2" -}}
{{- $text := copy .Page .Content "text" -}}
{{- $video := get .Content "video" -}}
<article class="take" data-component="reveal" data-shifu-element="{{ .Content.Position }}">
<div class="take__info">
<span class="take__label" aria-hidden="true">Take</span>
{{ with $title }}<h3 class="take__title">{{ . }}</h3>{{ end }}
{{ with $text }}<p class="take__text">{{ . }}</p>{{ end }}
</div>
<div class="col-lg-8 col-md-12 col-sm-12 col-xs-12">
<div style="padding:56.25% 0 0 0;position:relative;" data-aos="fade-in">
<iframe src="{{get .Content "video"}}" style="position:absolute;top:0;left:0;width:100%;height:100%;" frameborder="0" allowfullscreen></iframe>
<div class="take__screen">
{{ with $video }}
<iframe class="take__video" src="{{ . }}" title="{{ default $title "Video" }}" loading="lazy" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe>
{{ end }}
</div>
</div>
</div>
</article>