hlassiege/darylpublic Fork
⑂ 9c77724
Commits
⬇ Clone ▾
git clone https://git.rickub.com/hlassiege/daryl.git
git clone ssh://git@rickub.com/hlassiege/daryl.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

feat: add the website

hlassiege committed 2026-11-12T14:05:50+01:00 Browse files
9c77724 parent: e80d1d3
modified .gitignore +3 -0
@@ -35,3 +35,6 @@ Thumbs.db
3535 /docs/
3636 /backend/scripts/compare_stt.py
3737 /editing-os-starter/
38+
39+# site construit (node site/build.mjs)
40+/site/dist/
@@ -35,3 +35,6 @@ Thumbs.db
35 /docs/35 /docs/
36 /backend/scripts/compare_stt.py36 /backend/scripts/compare_stt.py
37 /editing-os-starter/37 /editing-os-starter/
38+
39+# site construit (node site/build.mjs)
40+/site/dist/
added site/build.mjs +15 -0
new file mode 100644
@@ -0,0 +1,15 @@
1+// Builds the website into site/dist: the static files of site/public, plus the screenshots and the logo,
2+// taken from the repository so they have a single source.
3+// No dependencies: `node site/build.mjs`, then open site/dist/index.html or serve the folder.
4+import { cpSync, rmSync } from 'node:fs'
5+import { fileURLToPath } from 'node:url'
6+
7+const root = (path) => fileURLToPath(new URL(`../${path}`, import.meta.url))
8+const dist = root('site/dist')
9+
10+rmSync(dist, { recursive: true, force: true })
11+cpSync(root('site/public'), dist, { recursive: true })
12+cpSync(root('media/screenshots'), `${dist}/screenshots`, { recursive: true })
13+cpSync(root('web/public/favicon.svg'), `${dist}/favicon.svg`)
14+
15+console.log(`Site built in ${dist}`)
new file mode 100644
@@ -0,0 +1,15 @@
1+// Builds the website into site/dist: the static files of site/public, plus the screenshots and the logo,
2+// taken from the repository so they have a single source.
3+// No dependencies: `node site/build.mjs`, then open site/dist/index.html or serve the folder.
4+import { cpSync, rmSync } from 'node:fs'
5+import { fileURLToPath } from 'node:url'
6+
7+const root = (path) => fileURLToPath(new URL(`../${path}`, import.meta.url))
8+const dist = root('site/dist')
9+
10+rmSync(dist, { recursive: true, force: true })
11+cpSync(root('site/public'), dist, { recursive: true })
12+cpSync(root('media/screenshots'), `${dist}/screenshots`, { recursive: true })
13+cpSync(root('web/public/favicon.svg'), `${dist}/favicon.svg`)
14+
15+console.log(`Site built in ${dist}`)
added site/public/fonts/OFL.txt +93 -0
new file mode 100644
@@ -0,0 +1,93 @@
1+Copyright 2019 IBM Corp. All rights reserved. IBMPlexSans-Italic[wdth,wght].ttf: Copyright 2019 IBM Corp. All rights reserved.
2+
3+This Font Software is licensed under the SIL Open Font License, Version 1.1.
4+This license is copied below, and is also available with a FAQ at:
5+http://scripts.sil.org/OFL
6+
7+
8+-----------------------------------------------------------
9+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10+-----------------------------------------------------------
11+
12+PREAMBLE
13+The goals of the Open Font License (OFL) are to stimulate worldwide
14+development of collaborative font projects, to support the font creation
15+efforts of academic and linguistic communities, and to provide a free and
16+open framework in which fonts may be shared and improved in partnership
17+with others.
18+
19+The OFL allows the licensed fonts to be used, studied, modified and
20+redistributed freely as long as they are not sold by themselves. The
21+fonts, including any derivative works, can be bundled, embedded,
22+redistributed and/or sold with any software provided that any reserved
23+names are not used by derivative works. The fonts and derivatives,
24+however, cannot be released under any other type of license. The
25+requirement for fonts to remain under this license does not apply
26+to any document created using the fonts or their derivatives.
27+
28+DEFINITIONS
29+"Font Software" refers to the set of files released by the Copyright
30+Holder(s) under this license and clearly marked as such. This may
31+include source files, build scripts and documentation.
32+
33+"Reserved Font Name" refers to any names specified as such after the
34+copyright statement(s).
35+
36+"Original Version" refers to the collection of Font Software components as
37+distributed by the Copyright Holder(s).
38+
39+"Modified Version" refers to any derivative made by adding to, deleting,
40+or substituting -- in part or in whole -- any of the components of the
41+Original Version, by changing formats or by porting the Font Software to a
42+new environment.
43+
44+"Author" refers to any designer, engineer, programmer, technical
45+writer or other person who contributed to the Font Software.
46+
47+PERMISSION & CONDITIONS
48+Permission is hereby granted, free of charge, to any person obtaining
49+a copy of the Font Software, to use, study, copy, merge, embed, modify,
50+redistribute, and sell modified and unmodified copies of the Font
51+Software, subject to the following conditions:
52+
53+1) Neither the Font Software nor any of its individual components,
54+in Original or Modified Versions, may be sold by itself.
55+
56+2) Original or Modified Versions of the Font Software may be bundled,
57+redistributed and/or sold with any software, provided that each copy
58+contains the above copyright notice and this license. These can be
59+included either as stand-alone text files, human-readable headers or
60+in the appropriate machine-readable metadata fields within text or
61+binary files as long as those fields can be easily viewed by the user.
62+
63+3) No Modified Version of the Font Software may use the Reserved Font
64+Name(s) unless explicit written permission is granted by the corresponding
65+Copyright Holder. This restriction only applies to the primary font name as
66+presented to the users.
67+
68+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69+Software shall not be used to promote, endorse or advertise any
70+Modified Version, except to acknowledge the contribution(s) of the
71+Copyright Holder(s) and the Author(s) or with their explicit written
72+permission.
73+
74+5) The Font Software, modified or unmodified, in part or in whole,
75+must be distributed entirely under this license, and must not be
76+distributed under any other license. The requirement for fonts to
77+remain under this license does not apply to any document created
78+using the Font Software.
79+
80+TERMINATION
81+This license becomes null and void if any of the above conditions are
82+not met.
83+
84+DISCLAIMER
85+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93+OTHER DEALINGS IN THE FONT SOFTWARE.
new file mode 100644
@@ -0,0 +1,93 @@
1+Copyright 2019 IBM Corp. All rights reserved. IBMPlexSans-Italic[wdth,wght].ttf: Copyright 2019 IBM Corp. All rights reserved.
2+
3+This Font Software is licensed under the SIL Open Font License, Version 1.1.
4+This license is copied below, and is also available with a FAQ at:
5+http://scripts.sil.org/OFL
6+
7+
8+-----------------------------------------------------------
9+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10+-----------------------------------------------------------
11+
12+PREAMBLE
13+The goals of the Open Font License (OFL) are to stimulate worldwide
14+development of collaborative font projects, to support the font creation
15+efforts of academic and linguistic communities, and to provide a free and
16+open framework in which fonts may be shared and improved in partnership
17+with others.
18+
19+The OFL allows the licensed fonts to be used, studied, modified and
20+redistributed freely as long as they are not sold by themselves. The
21+fonts, including any derivative works, can be bundled, embedded,
22+redistributed and/or sold with any software provided that any reserved
23+names are not used by derivative works. The fonts and derivatives,
24+however, cannot be released under any other type of license. The
25+requirement for fonts to remain under this license does not apply
26+to any document created using the fonts or their derivatives.
27+
28+DEFINITIONS
29+"Font Software" refers to the set of files released by the Copyright
30+Holder(s) under this license and clearly marked as such. This may
31+include source files, build scripts and documentation.
32+
33+"Reserved Font Name" refers to any names specified as such after the
34+copyright statement(s).
35+
36+"Original Version" refers to the collection of Font Software components as
37+distributed by the Copyright Holder(s).
38+
39+"Modified Version" refers to any derivative made by adding to, deleting,
40+or substituting -- in part or in whole -- any of the components of the
41+Original Version, by changing formats or by porting the Font Software to a
42+new environment.
43+
44+"Author" refers to any designer, engineer, programmer, technical
45+writer or other person who contributed to the Font Software.
46+
47+PERMISSION & CONDITIONS
48+Permission is hereby granted, free of charge, to any person obtaining
49+a copy of the Font Software, to use, study, copy, merge, embed, modify,
50+redistribute, and sell modified and unmodified copies of the Font
51+Software, subject to the following conditions:
52+
53+1) Neither the Font Software nor any of its individual components,
54+in Original or Modified Versions, may be sold by itself.
55+
56+2) Original or Modified Versions of the Font Software may be bundled,
57+redistributed and/or sold with any software, provided that each copy
58+contains the above copyright notice and this license. These can be
59+included either as stand-alone text files, human-readable headers or
60+in the appropriate machine-readable metadata fields within text or
61+binary files as long as those fields can be easily viewed by the user.
62+
63+3) No Modified Version of the Font Software may use the Reserved Font
64+Name(s) unless explicit written permission is granted by the corresponding
65+Copyright Holder. This restriction only applies to the primary font name as
66+presented to the users.
67+
68+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69+Software shall not be used to promote, endorse or advertise any
70+Modified Version, except to acknowledge the contribution(s) of the
71+Copyright Holder(s) and the Author(s) or with their explicit written
72+permission.
73+
74+5) The Font Software, modified or unmodified, in part or in whole,
75+must be distributed entirely under this license, and must not be
76+distributed under any other license. The requirement for fonts to
77+remain under this license does not apply to any document created
78+using the Font Software.
79+
80+TERMINATION
81+This license becomes null and void if any of the above conditions are
82+not met.
83+
84+DISCLAIMER
85+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93+OTHER DEALINGS IN THE FONT SOFTWARE.
added site/public/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
new file mode 100644
Binary files /dev/null and b/site/public/fonts/ibm-plex-mono-latin-400-normal.woff2 differ
new file mode 100644
Binary files /dev/null and b/site/public/fonts/ibm-plex-mono-latin-400-normal.woff2 differBinary files /dev/null and b/site/public/fonts/ibm-plex-mono-latin-400-normal.woff2 differ
added site/public/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
new file mode 100644
Binary files /dev/null and b/site/public/fonts/ibm-plex-mono-latin-500-normal.woff2 differ
new file mode 100644
Binary files /dev/null and b/site/public/fonts/ibm-plex-mono-latin-500-normal.woff2 differBinary files /dev/null and b/site/public/fonts/ibm-plex-mono-latin-500-normal.woff2 differ
added site/public/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
new file mode 100644
Binary files /dev/null and b/site/public/fonts/ibm-plex-sans-latin-400-normal.woff2 differ
new file mode 100644
Binary files /dev/null and b/site/public/fonts/ibm-plex-sans-latin-400-normal.woff2 differBinary files /dev/null and b/site/public/fonts/ibm-plex-sans-latin-400-normal.woff2 differ
added site/public/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
new file mode 100644
Binary files /dev/null and b/site/public/fonts/ibm-plex-sans-latin-500-normal.woff2 differ
new file mode 100644
Binary files /dev/null and b/site/public/fonts/ibm-plex-sans-latin-500-normal.woff2 differBinary files /dev/null and b/site/public/fonts/ibm-plex-sans-latin-500-normal.woff2 differ
added site/public/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
new file mode 100644
Binary files /dev/null and b/site/public/fonts/ibm-plex-sans-latin-600-normal.woff2 differ
new file mode 100644
Binary files /dev/null and b/site/public/fonts/ibm-plex-sans-latin-600-normal.woff2 differBinary files /dev/null and b/site/public/fonts/ibm-plex-sans-latin-600-normal.woff2 differ
added site/public/index.html +233 -0
new file mode 100644
@@ -0,0 +1,233 @@
1+<!doctype html>
2+<html lang="en">
3+<head>
4+ <meta charset="utf-8">
5+ <meta name="viewport" content="width=device-width, initial-scale=1">
6+ <title>Daryl: a local studio for YouTube videos, from the idea to the upload</title>
7+ <meta name="description" content="Angle, titles, thumbnails, script, rough cut, motion design, editing, shorts and upload in one app that runs on your machine, with Claude Code as a co-author. Free and open source.">
8+ <link rel="canonical" href="https://daryl.eventuallymaking.io/">
9+ <meta name="theme-color" content="#FAFAF8">
10+
11+ <meta property="og:type" content="website">
12+ <meta property="og:url" content="https://daryl.eventuallymaking.io/">
13+ <meta property="og:title" content="Daryl: a local studio for YouTube videos">
14+ <meta property="og:description" content="From the idea to the upload, in one app that runs on your machine, with Claude Code as a co-author. Free and open source.">
15+ <meta property="og:image" content="https://daryl.eventuallymaking.io/screenshots/editing.png">
16+ <meta name="twitter:card" content="summary_large_image">
17+
18+ <link rel="icon" type="image/svg+xml" href="favicon.svg">
19+ <link rel="preload" href="fonts/ibm-plex-sans-latin-400-normal.woff2" as="font" type="font/woff2" crossorigin>
20+ <link rel="preload" href="fonts/ibm-plex-sans-latin-600-normal.woff2" as="font" type="font/woff2" crossorigin>
21+ <link rel="stylesheet" href="styles.css">
22+ <script>document.documentElement.classList.add('js')</script>
23+ <script src="main.js" defer></script>
24+</head>
25+<body>
26+ <a class="skip" href="#main">Skip to content</a>
27+
28+ <header class="top">
29+ <a class="brand" href="./" aria-label="Daryl, home">
30+ <img src="favicon.svg" width="28" height="28" alt="">
31+ <span>daryl</span>
32+ </a>
33+ <nav class="top-nav" aria-label="Sections">
34+ <a href="#tour">Tour</a>
35+ <a href="#how">How it works</a>
36+ <a href="#try">Try it</a>
37+ </nav>
38+ <a class="btn btn-ink btn-sm" href="https://rickub.com/hlassiege/daryl"><svg class="btn-icon" viewBox="0 0 64 64" aria-hidden="true"><path d="M32 8 A24 24 0 1 1 12 44"/><path d="M32 18 A14 14 0 1 0 46 32"/><circle cx="32" cy="32" r="6.5"/></svg>Source code</a>
39+ </header>
40+
41+ <main id="main">
42+ <section class="hero">
43+ <div class="hero-copy">
44+ <h1>Your YouTube video, from the idea to the upload.</h1>
45+ <p class="lead">
46+ Daryl is a studio that runs on your machine. It takes a video through every step, with Claude Code
47+ as a co-author and editing assistant. Daryl proposes, you decide.
48+ </p>
49+ <div class="actions">
50+ <a class="btn btn-red" href="#try">Try it</a>
51+ <a class="btn btn-line" href="https://rickub.com/hlassiege/daryl"><svg class="btn-icon" viewBox="0 0 64 64" aria-hidden="true"><path d="M32 8 A24 24 0 1 1 12 44"/><path d="M32 18 A14 14 0 1 0 46 32"/><circle cx="32" cy="32" r="6.5"/></svg>Source code</a>
52+ </div>
53+ <p class="perk">
54+ <svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>
55+ Free and open source, nothing to buy
56+ </p>
57+ </div>
58+ </section>
59+
60+ <!-- The reel: every step of a project laid out as an editing timeline. Static, it shows the finished
61+ project; with JS, the playhead sweeps it once and each clip fills in as the playhead reaches it.
62+ Positions are on a 30-unit timeline: --a is where a clip starts, --b how long it lasts. -->
63+ <figure class="reel" data-reel aria-labelledby="reel-caption">
64+ <div class="reel-scroll">
65+ <div class="reel-inner">
66+ <ol class="ruler" aria-hidden="true">
67+ <li>00:00</li><li>00:05</li><li>00:10</li><li>00:15</li><li>00:20</li><li>00:25</li><li>00:30</li>
68+ </ol>
69+ <div class="lanes">
70+ <div class="lane" style="--c: var(--motion)">
71+ <span class="lane-name">Writing</span>
72+ <ul>
73+ <li style="--a:0; --b:2"><span>Angle</span></li>
74+ <li style="--a:2; --b:1.5"><span>Title</span></li>
75+ <li style="--a:3.5; --b:2.5"><span>Thumbnail</span></li>
76+ <li style="--a:6; --b:2"><span>Script</span></li>
77+ <li style="--a:8; --b:2"><span>Roast</span></li>
78+ </ul>
79+ </div>
80+ <div class="lane" style="--c: var(--zoom)">
81+ <span class="lane-name">Shoot</span>
82+ <ul>
83+ <li style="--a:10; --b:3"><span>Teleprompter</span></li>
84+ </ul>
85+ </div>
86+ <div class="lane" style="--c: var(--broll)">
87+ <span class="lane-name">Editing</span>
88+ <ul>
89+ <li style="--a:13; --b:2.5"><span>Rough cut</span></li>
90+ <li style="--a:15.5; --b:2.75"><span>Storyboard</span></li>
91+ <li style="--a:18.25; --b:3.25"><span>Editing</span></li>
92+ <li style="--a:21.5; --b:2"><span>Shorts</span></li>
93+ <li style="--a:23.5; --b:2"><span>Render</span></li>
94+ </ul>
95+ </div>
96+ <div class="lane" style="--c: var(--music)">
97+ <span class="lane-name">Publishing</span>
98+ <ul>
99+ <li style="--a:25.5; --b:2.25"><span>Details</span></li>
100+ <li style="--a:27.75; --b:2.25"><span>Upload</span></li>
101+ </ul>
102+ </div>
103+ <div class="playhead" aria-hidden="true"></div>
104+ </div>
105+ </div>
106+ </div>
107+ <figcaption id="reel-caption">Every step of a video, one project folder on your disk.</figcaption>
108+ </figure>
109+
110+ <section class="tour" id="tour" aria-labelledby="tour-title">
111+ <h2 class="sr-only" id="tour-title">A tour of Daryl</h2>
112+ <div class="tour-tabs" role="tablist" aria-label="Screens" aria-orientation="vertical">
113+ <button role="tab" id="tab-projects" aria-controls="shot-projects" aria-selected="true">
114+ <span class="tab-title">Every video, every step</span>
115+ <span class="tab-desc">Each project keeps its brief, script, footage, edit and renders together, and shows where you left off.</span>
116+ </button>
117+ <button role="tab" id="tab-angle" aria-controls="shot-angle" aria-selected="false" tabindex="-1">
118+ <span class="tab-title">Find the angle</span>
119+ <span class="tab-desc">Daryl researches the topic from your brief and notes, then suggests angles to keep or rework.</span>
120+ </button>
121+ <button role="tab" id="tab-titles" aria-controls="shot-titles" aria-selected="false" tabindex="-1">
122+ <span class="tab-title">Thirty titles, keep the best</span>
123+ <span class="tab-desc">Daryl writes 30 titles from the angle. Keep two or three for the A/B test.</span>
124+ </button>
125+ <button role="tab" id="tab-thumbnail" aria-controls="shot-thumbnail" aria-selected="false" tabindex="-1">
126+ <span class="tab-title">A thumbnail per title</span>
127+ <span class="tab-desc">Generate variants from your own photos and references, or import the ones you made elsewhere.</span>
128+ </button>
129+ <button role="tab" id="tab-script" aria-controls="shot-script" aria-selected="false" tabindex="-1">
130+ <span class="tab-title">Write the script</span>
131+ <span class="tab-desc">Write it yourself or start from Daryl's draft, with versions, an outline and an assistant on the side.</span>
132+ </button>
133+ <button role="tab" id="tab-derush" aria-controls="shot-derush" aria-selected="false" tabindex="-1">
134+ <span class="tab-title">Rough cut in one pass</span>
135+ <span class="tab-desc">Your footage is transcribed, then silences and bad takes are cut. Check every cut before you apply it.</span>
136+ </button>
137+ <button role="tab" id="tab-storyboard" aria-controls="shot-storyboard" aria-selected="false" tabindex="-1">
138+ <span class="tab-title">Plan the motion design</span>
139+ <span class="tab-desc">Daryl reads what you say and places a motion beat on the right words. Review each one.</span>
140+ </button>
141+ <button role="tab" id="tab-editing" aria-controls="shot-editing" aria-selected="false" tabindex="-1">
142+ <span class="tab-title">Edit with Daryl</span>
143+ <span class="tab-desc">Zooms, B-roll, motion design, sound and music on a real timeline. Ask Daryl or do it by hand, then export to DaVinci Resolve.</span>
144+ </button>
145+ <button role="tab" id="tab-details" aria-controls="shot-details" aria-selected="false" tabindex="-1">
146+ <span class="tab-title">Publish to YouTube</span>
147+ <span class="tab-desc">Title, description, chapters and A/B variants, checked, then uploaded.</span>
148+ </button>
149+ </div>
150+ <div class="tour-shots">
151+ <div class="frame" id="shot-projects" role="tabpanel" aria-labelledby="tab-projects"><img src="screenshots/projects.png" width="2880" height="1800" alt="The projects page, with the last opened project and the progress of each video" fetchpriority="high"></div>
152+ <div class="frame" id="shot-angle" role="tabpanel" aria-labelledby="tab-angle"><img src="screenshots/angle.png" width="2880" height="1800" alt="The Angle step: the brief, the suggested angles and Daryl's chat" loading="lazy"></div>
153+ <div class="frame" id="shot-titles" role="tabpanel" aria-labelledby="tab-titles"><img src="screenshots/titles.png" width="2880" height="1800" alt="The Title step: the titles kept for the A/B test and Daryl's suggestions with their score" loading="lazy"></div>
154+ <div class="frame" id="shot-thumbnail" role="tabpanel" aria-labelledby="tab-thumbnail"><img src="screenshots/thumbnail.png" width="2880" height="1800" alt="The Thumbnail step: three variants and the brief of a touch-up" loading="lazy"></div>
155+ <div class="frame" id="shot-script" role="tabpanel" aria-labelledby="tab-script"><img src="screenshots/script.png" width="2880" height="1800" alt="The Script step: the script, its sections and its estimated length" loading="lazy"></div>
156+ <div class="frame" id="shot-derush" role="tabpanel" aria-labelledby="tab-derush"><img src="screenshots/derush.png" width="2880" height="1800" alt="The Rough cut step: preview, transcript and the timeline with its cuts" loading="lazy"></div>
157+ <div class="frame" id="shot-storyboard" role="tabpanel" aria-labelledby="tab-storyboard"><img src="screenshots/storyboard.png" width="2880" height="1800" alt="The Storyboard step: motion beats anchored on the transcript, with their place on screen" loading="lazy"></div>
158+ <div class="frame" id="shot-editing" role="tabpanel" aria-labelledby="tab-editing"><img src="screenshots/editing.png" width="2880" height="1800" alt="The Editing step: preview, timeline with chapters, motion, zooms, B-roll and music, and Daryl's chat" loading="lazy"></div>
159+ <div class="frame" id="shot-details" role="tabpanel" aria-labelledby="tab-details"><img src="screenshots/details.png" width="2880" height="1800" alt="The Details step: A/B variants, description and chapters" loading="lazy"></div>
160+ </div>
161+ </section>
162+
163+ <section class="how" id="how" aria-labelledby="how-title">
164+ <h2 id="how-title">A studio on your machine, an assistant that asks first</h2>
165+ <dl class="facts">
166+ <div>
167+ <dt>Your files stay with you</dt>
168+ <dd>Projects, footage and renders live in a folder on your disk. Whisper, ffmpeg and the renderer run locally.</dd>
169+ </div>
170+ <div>
171+ <dt>Runs on your Claude Code plan</dt>
172+ <dd>Daryl drives Claude Code with your subscription. No API key to set up, no second bill.</dd>
173+ </div>
174+ <div>
175+ <dt>Nothing changes without you</dt>
176+ <dd>Cuts, titles, beats and edits are suggestions. They land in your project when you accept them.</dd>
177+ </div>
178+ </dl>
179+
180+ <div class="skills">
181+ <div class="skills-copy">
182+ <h3>Editing jobs for Claude Code</h3>
183+ <p>Some jobs work better from a Claude Code session opened in the repository. Daryl ships them as skills.</p>
184+ </div>
185+ <ul class="skills-list">
186+ <li><code>/music-bed</code><span>Scores the edit: one track per sequence, cut to cover it and ducked under the voice.</span></li>
187+ <li><code>/replace-aroll-take</code><span>Swaps a stretch of A-roll for a re-recorded take and moves everything on top of it.</span></li>
188+ <li><code>/snatch-cut</code><span>Proposes a burst of close-ups with snap zooms and punchy sounds, with a preview.</span></li>
189+ </ul>
190+ </div>
191+ </section>
192+
193+ <section class="honest" aria-labelledby="honest-title">
194+ <h2 id="honest-title">A personal tool, shared as is</h2>
195+ <div class="honest-body">
196+ <p>
197+ I make every video of <a href="https://www.youtube.com/@eventuallycoding">my channel</a> with Daryl.
198+ I share it because people asked to see it, not to sell anything: there is no course, no paid plan,
199+ no waiting list.
200+ </p>
201+ <p>
202+ It follows how I work and evolves in that direction. It is built for one person on Windows, and
203+ maintained on a best-effort basis. Fork it, take what you need, or use it for ideas.
204+ </p>
205+ </div>
206+ </section>
207+
208+ <section class="try" id="try" aria-labelledby="try-title">
209+ <h2 id="try-title">Try it</h2>
210+ <p class="section-lead">You need Windows, Python 3.12, Node.js with pnpm, ffmpeg and Claude Code logged in.</p>
211+ <div class="code">
212+ <pre><code id="install-cmd">git clone https://rickub.com/hlassiege/daryl.git
213+cd daryl
214+py -3.12 -m venv .venv
215+.venv\Scripts\pip install -r backend\requirements.txt
216+cd web; pnpm install; pnpm build; cd ..\backend
217+..\.venv\Scripts\python -m studio</code></pre>
218+ <button class="btn btn-line btn-sm copy" type="button" data-copy="install-cmd">Copy</button>
219+ </div>
220+ <p class="note">Then open <code>http://localhost:4400</code>. <a href="https://rickub.com/hlassiege/daryl">Read the README</a> for the optional services: cloud transcription, thumbnails, YouTube upload, music.</p>
221+ </section>
222+ </main>
223+
224+ <footer class="foot">
225+ <p>Open source under the AGPL-3.0. Made by <a href="https://eventuallymaking.io">Hugo Lassiège</a>.</p>
226+ <nav aria-label="Elsewhere">
227+ <a href="https://rickub.com/hlassiege/daryl">Source code</a>
228+ <a href="https://github.com/hlassiege/daryl">GitHub mirror</a>
229+ <a href="https://panoplie.eventuallymaking.io">panoplie</a>
230+ </nav>
231+ </footer>
232+</body>
233+</html>
new file mode 100644
@@ -0,0 +1,233 @@
1+<!doctype html>
2+<html lang="en">
3+<head>
4+ <meta charset="utf-8">
5+ <meta name="viewport" content="width=device-width, initial-scale=1">
6+ <title>Daryl: a local studio for YouTube videos, from the idea to the upload</title>
7+ <meta name="description" content="Angle, titles, thumbnails, script, rough cut, motion design, editing, shorts and upload in one app that runs on your machine, with Claude Code as a co-author. Free and open source.">
8+ <link rel="canonical" href="https://daryl.eventuallymaking.io/">
9+ <meta name="theme-color" content="#FAFAF8">
10+
11+ <meta property="og:type" content="website">
12+ <meta property="og:url" content="https://daryl.eventuallymaking.io/">
13+ <meta property="og:title" content="Daryl: a local studio for YouTube videos">
14+ <meta property="og:description" content="From the idea to the upload, in one app that runs on your machine, with Claude Code as a co-author. Free and open source.">
15+ <meta property="og:image" content="https://daryl.eventuallymaking.io/screenshots/editing.png">
16+ <meta name="twitter:card" content="summary_large_image">
17+
18+ <link rel="icon" type="image/svg+xml" href="favicon.svg">
19+ <link rel="preload" href="fonts/ibm-plex-sans-latin-400-normal.woff2" as="font" type="font/woff2" crossorigin>
20+ <link rel="preload" href="fonts/ibm-plex-sans-latin-600-normal.woff2" as="font" type="font/woff2" crossorigin>
21+ <link rel="stylesheet" href="styles.css">
22+ <script>document.documentElement.classList.add('js')</script>
23+ <script src="main.js" defer></script>
24+</head>
25+<body>
26+ <a class="skip" href="#main">Skip to content</a>
27+
28+ <header class="top">
29+ <a class="brand" href="./" aria-label="Daryl, home">
30+ <img src="favicon.svg" width="28" height="28" alt="">
31+ <span>daryl</span>
32+ </a>
33+ <nav class="top-nav" aria-label="Sections">
34+ <a href="#tour">Tour</a>
35+ <a href="#how">How it works</a>
36+ <a href="#try">Try it</a>
37+ </nav>
38+ <a class="btn btn-ink btn-sm" href="https://rickub.com/hlassiege/daryl"><svg class="btn-icon" viewBox="0 0 64 64" aria-hidden="true"><path d="M32 8 A24 24 0 1 1 12 44"/><path d="M32 18 A14 14 0 1 0 46 32"/><circle cx="32" cy="32" r="6.5"/></svg>Source code</a>
39+ </header>
40+
41+ <main id="main">
42+ <section class="hero">
43+ <div class="hero-copy">
44+ <h1>Your YouTube video, from the idea to the upload.</h1>
45+ <p class="lead">
46+ Daryl is a studio that runs on your machine. It takes a video through every step, with Claude Code
47+ as a co-author and editing assistant. Daryl proposes, you decide.
48+ </p>
49+ <div class="actions">
50+ <a class="btn btn-red" href="#try">Try it</a>
51+ <a class="btn btn-line" href="https://rickub.com/hlassiege/daryl"><svg class="btn-icon" viewBox="0 0 64 64" aria-hidden="true"><path d="M32 8 A24 24 0 1 1 12 44"/><path d="M32 18 A14 14 0 1 0 46 32"/><circle cx="32" cy="32" r="6.5"/></svg>Source code</a>
52+ </div>
53+ <p class="perk">
54+ <svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>
55+ Free and open source, nothing to buy
56+ </p>
57+ </div>
58+ </section>
59+
60+ <!-- The reel: every step of a project laid out as an editing timeline. Static, it shows the finished
61+ project; with JS, the playhead sweeps it once and each clip fills in as the playhead reaches it.
62+ Positions are on a 30-unit timeline: --a is where a clip starts, --b how long it lasts. -->
63+ <figure class="reel" data-reel aria-labelledby="reel-caption">
64+ <div class="reel-scroll">
65+ <div class="reel-inner">
66+ <ol class="ruler" aria-hidden="true">
67+ <li>00:00</li><li>00:05</li><li>00:10</li><li>00:15</li><li>00:20</li><li>00:25</li><li>00:30</li>
68+ </ol>
69+ <div class="lanes">
70+ <div class="lane" style="--c: var(--motion)">
71+ <span class="lane-name">Writing</span>
72+ <ul>
73+ <li style="--a:0; --b:2"><span>Angle</span></li>
74+ <li style="--a:2; --b:1.5"><span>Title</span></li>
75+ <li style="--a:3.5; --b:2.5"><span>Thumbnail</span></li>
76+ <li style="--a:6; --b:2"><span>Script</span></li>
77+ <li style="--a:8; --b:2"><span>Roast</span></li>
78+ </ul>
79+ </div>
80+ <div class="lane" style="--c: var(--zoom)">
81+ <span class="lane-name">Shoot</span>
82+ <ul>
83+ <li style="--a:10; --b:3"><span>Teleprompter</span></li>
84+ </ul>
85+ </div>
86+ <div class="lane" style="--c: var(--broll)">
87+ <span class="lane-name">Editing</span>
88+ <ul>
89+ <li style="--a:13; --b:2.5"><span>Rough cut</span></li>
90+ <li style="--a:15.5; --b:2.75"><span>Storyboard</span></li>
91+ <li style="--a:18.25; --b:3.25"><span>Editing</span></li>
92+ <li style="--a:21.5; --b:2"><span>Shorts</span></li>
93+ <li style="--a:23.5; --b:2"><span>Render</span></li>
94+ </ul>
95+ </div>
96+ <div class="lane" style="--c: var(--music)">
97+ <span class="lane-name">Publishing</span>
98+ <ul>
99+ <li style="--a:25.5; --b:2.25"><span>Details</span></li>
100+ <li style="--a:27.75; --b:2.25"><span>Upload</span></li>
101+ </ul>
102+ </div>
103+ <div class="playhead" aria-hidden="true"></div>
104+ </div>
105+ </div>
106+ </div>
107+ <figcaption id="reel-caption">Every step of a video, one project folder on your disk.</figcaption>
108+ </figure>
109+
110+ <section class="tour" id="tour" aria-labelledby="tour-title">
111+ <h2 class="sr-only" id="tour-title">A tour of Daryl</h2>
112+ <div class="tour-tabs" role="tablist" aria-label="Screens" aria-orientation="vertical">
113+ <button role="tab" id="tab-projects" aria-controls="shot-projects" aria-selected="true">
114+ <span class="tab-title">Every video, every step</span>
115+ <span class="tab-desc">Each project keeps its brief, script, footage, edit and renders together, and shows where you left off.</span>
116+ </button>
117+ <button role="tab" id="tab-angle" aria-controls="shot-angle" aria-selected="false" tabindex="-1">
118+ <span class="tab-title">Find the angle</span>
119+ <span class="tab-desc">Daryl researches the topic from your brief and notes, then suggests angles to keep or rework.</span>
120+ </button>
121+ <button role="tab" id="tab-titles" aria-controls="shot-titles" aria-selected="false" tabindex="-1">
122+ <span class="tab-title">Thirty titles, keep the best</span>
123+ <span class="tab-desc">Daryl writes 30 titles from the angle. Keep two or three for the A/B test.</span>
124+ </button>
125+ <button role="tab" id="tab-thumbnail" aria-controls="shot-thumbnail" aria-selected="false" tabindex="-1">
126+ <span class="tab-title">A thumbnail per title</span>
127+ <span class="tab-desc">Generate variants from your own photos and references, or import the ones you made elsewhere.</span>
128+ </button>
129+ <button role="tab" id="tab-script" aria-controls="shot-script" aria-selected="false" tabindex="-1">
130+ <span class="tab-title">Write the script</span>
131+ <span class="tab-desc">Write it yourself or start from Daryl's draft, with versions, an outline and an assistant on the side.</span>
132+ </button>
133+ <button role="tab" id="tab-derush" aria-controls="shot-derush" aria-selected="false" tabindex="-1">
134+ <span class="tab-title">Rough cut in one pass</span>
135+ <span class="tab-desc">Your footage is transcribed, then silences and bad takes are cut. Check every cut before you apply it.</span>
136+ </button>
137+ <button role="tab" id="tab-storyboard" aria-controls="shot-storyboard" aria-selected="false" tabindex="-1">
138+ <span class="tab-title">Plan the motion design</span>
139+ <span class="tab-desc">Daryl reads what you say and places a motion beat on the right words. Review each one.</span>
140+ </button>
141+ <button role="tab" id="tab-editing" aria-controls="shot-editing" aria-selected="false" tabindex="-1">
142+ <span class="tab-title">Edit with Daryl</span>
143+ <span class="tab-desc">Zooms, B-roll, motion design, sound and music on a real timeline. Ask Daryl or do it by hand, then export to DaVinci Resolve.</span>
144+ </button>
145+ <button role="tab" id="tab-details" aria-controls="shot-details" aria-selected="false" tabindex="-1">
146+ <span class="tab-title">Publish to YouTube</span>
147+ <span class="tab-desc">Title, description, chapters and A/B variants, checked, then uploaded.</span>
148+ </button>
149+ </div>
150+ <div class="tour-shots">
151+ <div class="frame" id="shot-projects" role="tabpanel" aria-labelledby="tab-projects"><img src="screenshots/projects.png" width="2880" height="1800" alt="The projects page, with the last opened project and the progress of each video" fetchpriority="high"></div>
152+ <div class="frame" id="shot-angle" role="tabpanel" aria-labelledby="tab-angle"><img src="screenshots/angle.png" width="2880" height="1800" alt="The Angle step: the brief, the suggested angles and Daryl's chat" loading="lazy"></div>
153+ <div class="frame" id="shot-titles" role="tabpanel" aria-labelledby="tab-titles"><img src="screenshots/titles.png" width="2880" height="1800" alt="The Title step: the titles kept for the A/B test and Daryl's suggestions with their score" loading="lazy"></div>
154+ <div class="frame" id="shot-thumbnail" role="tabpanel" aria-labelledby="tab-thumbnail"><img src="screenshots/thumbnail.png" width="2880" height="1800" alt="The Thumbnail step: three variants and the brief of a touch-up" loading="lazy"></div>
155+ <div class="frame" id="shot-script" role="tabpanel" aria-labelledby="tab-script"><img src="screenshots/script.png" width="2880" height="1800" alt="The Script step: the script, its sections and its estimated length" loading="lazy"></div>
156+ <div class="frame" id="shot-derush" role="tabpanel" aria-labelledby="tab-derush"><img src="screenshots/derush.png" width="2880" height="1800" alt="The Rough cut step: preview, transcript and the timeline with its cuts" loading="lazy"></div>
157+ <div class="frame" id="shot-storyboard" role="tabpanel" aria-labelledby="tab-storyboard"><img src="screenshots/storyboard.png" width="2880" height="1800" alt="The Storyboard step: motion beats anchored on the transcript, with their place on screen" loading="lazy"></div>
158+ <div class="frame" id="shot-editing" role="tabpanel" aria-labelledby="tab-editing"><img src="screenshots/editing.png" width="2880" height="1800" alt="The Editing step: preview, timeline with chapters, motion, zooms, B-roll and music, and Daryl's chat" loading="lazy"></div>
159+ <div class="frame" id="shot-details" role="tabpanel" aria-labelledby="tab-details"><img src="screenshots/details.png" width="2880" height="1800" alt="The Details step: A/B variants, description and chapters" loading="lazy"></div>
160+ </div>
161+ </section>
162+
163+ <section class="how" id="how" aria-labelledby="how-title">
164+ <h2 id="how-title">A studio on your machine, an assistant that asks first</h2>
165+ <dl class="facts">
166+ <div>
167+ <dt>Your files stay with you</dt>
168+ <dd>Projects, footage and renders live in a folder on your disk. Whisper, ffmpeg and the renderer run locally.</dd>
169+ </div>
170+ <div>
171+ <dt>Runs on your Claude Code plan</dt>
172+ <dd>Daryl drives Claude Code with your subscription. No API key to set up, no second bill.</dd>
173+ </div>
174+ <div>
175+ <dt>Nothing changes without you</dt>
176+ <dd>Cuts, titles, beats and edits are suggestions. They land in your project when you accept them.</dd>
177+ </div>
178+ </dl>
179+
180+ <div class="skills">
181+ <div class="skills-copy">
182+ <h3>Editing jobs for Claude Code</h3>
183+ <p>Some jobs work better from a Claude Code session opened in the repository. Daryl ships them as skills.</p>
184+ </div>
185+ <ul class="skills-list">
186+ <li><code>/music-bed</code><span>Scores the edit: one track per sequence, cut to cover it and ducked under the voice.</span></li>
187+ <li><code>/replace-aroll-take</code><span>Swaps a stretch of A-roll for a re-recorded take and moves everything on top of it.</span></li>
188+ <li><code>/snatch-cut</code><span>Proposes a burst of close-ups with snap zooms and punchy sounds, with a preview.</span></li>
189+ </ul>
190+ </div>
191+ </section>
192+
193+ <section class="honest" aria-labelledby="honest-title">
194+ <h2 id="honest-title">A personal tool, shared as is</h2>
195+ <div class="honest-body">
196+ <p>
197+ I make every video of <a href="https://www.youtube.com/@eventuallycoding">my channel</a> with Daryl.
198+ I share it because people asked to see it, not to sell anything: there is no course, no paid plan,
199+ no waiting list.
200+ </p>
201+ <p>
202+ It follows how I work and evolves in that direction. It is built for one person on Windows, and
203+ maintained on a best-effort basis. Fork it, take what you need, or use it for ideas.
204+ </p>
205+ </div>
206+ </section>
207+
208+ <section class="try" id="try" aria-labelledby="try-title">
209+ <h2 id="try-title">Try it</h2>
210+ <p class="section-lead">You need Windows, Python 3.12, Node.js with pnpm, ffmpeg and Claude Code logged in.</p>
211+ <div class="code">
212+ <pre><code id="install-cmd">git clone https://rickub.com/hlassiege/daryl.git
213+cd daryl
214+py -3.12 -m venv .venv
215+.venv\Scripts\pip install -r backend\requirements.txt
216+cd web; pnpm install; pnpm build; cd ..\backend
217+..\.venv\Scripts\python -m studio</code></pre>
218+ <button class="btn btn-line btn-sm copy" type="button" data-copy="install-cmd">Copy</button>
219+ </div>
220+ <p class="note">Then open <code>http://localhost:4400</code>. <a href="https://rickub.com/hlassiege/daryl">Read the README</a> for the optional services: cloud transcription, thumbnails, YouTube upload, music.</p>
221+ </section>
222+ </main>
223+
224+ <footer class="foot">
225+ <p>Open source under the AGPL-3.0. Made by <a href="https://eventuallymaking.io">Hugo Lassiège</a>.</p>
226+ <nav aria-label="Elsewhere">
227+ <a href="https://rickub.com/hlassiege/daryl">Source code</a>
228+ <a href="https://github.com/hlassiege/daryl">GitHub mirror</a>
229+ <a href="https://panoplie.eventuallymaking.io">panoplie</a>
230+ </nav>
231+ </footer>
232+</body>
233+</html>
added site/public/main.js +78 -0
new file mode 100644
@@ -0,0 +1,78 @@
1+// Progressive enhancement only: without this file every screenshot is listed and the reel shows the finished project.
2+
3+// Tour: screenshots as tabs (WAI-ARIA tabs pattern; up/down on the vertical list, left/right on mobile)
4+const tabs = [...document.querySelectorAll('.tour-tabs [role="tab"]')]
5+
6+function select(tab, focus) {
7+ for (const t of tabs) {
8+ const on = t === tab
9+ t.setAttribute('aria-selected', String(on))
10+ t.tabIndex = on ? 0 : -1
11+ document.getElementById(t.getAttribute('aria-controls')).hidden = !on
12+ }
13+ if (focus) {
14+ tab.focus()
15+ tab.scrollIntoView({ block: 'nearest', inline: 'nearest' })
16+ }
17+}
18+
19+for (const tab of tabs) {
20+ tab.addEventListener('click', () => select(tab, false))
21+ tab.addEventListener('keydown', (e) => {
22+ const i = tabs.indexOf(tab)
23+ const next = { ArrowDown: i + 1, ArrowRight: i + 1, ArrowUp: i - 1, ArrowLeft: i - 1, Home: 0, End: tabs.length - 1 }[e.key]
24+ if (next === undefined) return
25+ e.preventDefault()
26+ select(tabs[(next + tabs.length) % tabs.length], true)
27+ })
28+}
29+if (tabs.length) select(tabs[0], false)
30+
31+// Copy buttons
32+for (const button of document.querySelectorAll('[data-copy]')) {
33+ button.addEventListener('click', async () => {
34+ const text = document.getElementById(button.dataset.copy).textContent
35+ try {
36+ await navigator.clipboard.writeText(text)
37+ button.textContent = 'Copied'
38+ } catch {
39+ button.textContent = 'Copy failed'
40+ }
41+ setTimeout(() => { button.textContent = 'Copy' }, 1600)
42+ })
43+}
44+
45+// Reel: the playhead sweeps the project once, the first time it comes into view
46+const reel = document.querySelector('[data-reel]')
47+if (reel) {
48+ const scroller = reel.querySelector('.reel-scroll')
49+ const playhead = reel.querySelector('.playhead')
50+
51+ // On narrow screens the reel scrolls: keep the playhead in view while it plays, like an editor does
52+ function follow() {
53+ if (scroller.scrollWidth <= scroller.clientWidth) return
54+ const end = performance.now() + 5000
55+ const step = () => {
56+ const x = playhead.getBoundingClientRect().left - scroller.getBoundingClientRect().left + scroller.scrollLeft
57+ scroller.scrollLeft = x - scroller.clientWidth / 2
58+ if (performance.now() < end) requestAnimationFrame(step)
59+ }
60+ requestAnimationFrame(step)
61+ }
62+
63+ const run = () => setTimeout(() => {
64+ reel.classList.add('run')
65+ if (!matchMedia('(prefers-reduced-motion: reduce)').matches) follow()
66+ }, 350)
67+ if (!('IntersectionObserver' in window)) {
68+ reel.classList.add('run')
69+ } else {
70+ const observer = new IntersectionObserver((entries) => {
71+ if (entries.some((e) => e.isIntersecting)) {
72+ observer.disconnect()
73+ document.fonts.ready.then(run)
74+ }
75+ }, { threshold: 0.35 })
76+ observer.observe(reel)
77+ }
78+}
new file mode 100644
@@ -0,0 +1,78 @@
1+// Progressive enhancement only: without this file every screenshot is listed and the reel shows the finished project.
2+
3+// Tour: screenshots as tabs (WAI-ARIA tabs pattern; up/down on the vertical list, left/right on mobile)
4+const tabs = [...document.querySelectorAll('.tour-tabs [role="tab"]')]
5+
6+function select(tab, focus) {
7+ for (const t of tabs) {
8+ const on = t === tab
9+ t.setAttribute('aria-selected', String(on))
10+ t.tabIndex = on ? 0 : -1
11+ document.getElementById(t.getAttribute('aria-controls')).hidden = !on
12+ }
13+ if (focus) {
14+ tab.focus()
15+ tab.scrollIntoView({ block: 'nearest', inline: 'nearest' })
16+ }
17+}
18+
19+for (const tab of tabs) {
20+ tab.addEventListener('click', () => select(tab, false))
21+ tab.addEventListener('keydown', (e) => {
22+ const i = tabs.indexOf(tab)
23+ const next = { ArrowDown: i + 1, ArrowRight: i + 1, ArrowUp: i - 1, ArrowLeft: i - 1, Home: 0, End: tabs.length - 1 }[e.key]
24+ if (next === undefined) return
25+ e.preventDefault()
26+ select(tabs[(next + tabs.length) % tabs.length], true)
27+ })
28+}
29+if (tabs.length) select(tabs[0], false)
30+
31+// Copy buttons
32+for (const button of document.querySelectorAll('[data-copy]')) {
33+ button.addEventListener('click', async () => {
34+ const text = document.getElementById(button.dataset.copy).textContent
35+ try {
36+ await navigator.clipboard.writeText(text)
37+ button.textContent = 'Copied'
38+ } catch {
39+ button.textContent = 'Copy failed'
40+ }
41+ setTimeout(() => { button.textContent = 'Copy' }, 1600)
42+ })
43+}
44+
45+// Reel: the playhead sweeps the project once, the first time it comes into view
46+const reel = document.querySelector('[data-reel]')
47+if (reel) {
48+ const scroller = reel.querySelector('.reel-scroll')
49+ const playhead = reel.querySelector('.playhead')
50+
51+ // On narrow screens the reel scrolls: keep the playhead in view while it plays, like an editor does
52+ function follow() {
53+ if (scroller.scrollWidth <= scroller.clientWidth) return
54+ const end = performance.now() + 5000
55+ const step = () => {
56+ const x = playhead.getBoundingClientRect().left - scroller.getBoundingClientRect().left + scroller.scrollLeft
57+ scroller.scrollLeft = x - scroller.clientWidth / 2
58+ if (performance.now() < end) requestAnimationFrame(step)
59+ }
60+ requestAnimationFrame(step)
61+ }
62+
63+ const run = () => setTimeout(() => {
64+ reel.classList.add('run')
65+ if (!matchMedia('(prefers-reduced-motion: reduce)').matches) follow()
66+ }, 350)
67+ if (!('IntersectionObserver' in window)) {
68+ reel.classList.add('run')
69+ } else {
70+ const observer = new IntersectionObserver((entries) => {
71+ if (entries.some((e) => e.isIntersecting)) {
72+ observer.disconnect()
73+ document.fonts.ready.then(run)
74+ }
75+ }, { threshold: 0.35 })
76+ observer.observe(reel)
77+ }
78+}
added site/public/styles.css +415 -0
new file mode 100644
@@ -0,0 +1,415 @@
1+/* Daryl site. Colors and fonts come from the app (web/src/styles.css, montage/Timeline.vue),
2+ so the screenshots sit at home. */
3+
4+@font-face { font-family: 'IBM Plex Sans'; font-weight: 400; font-display: swap; src: url(fonts/ibm-plex-sans-latin-400-normal.woff2) format('woff2'); }
5+@font-face { font-family: 'IBM Plex Sans'; font-weight: 500; font-display: swap; src: url(fonts/ibm-plex-sans-latin-500-normal.woff2) format('woff2'); }
6+@font-face { font-family: 'IBM Plex Sans'; font-weight: 600; font-display: swap; src: url(fonts/ibm-plex-sans-latin-600-normal.woff2) format('woff2'); }
7+@font-face { font-family: 'IBM Plex Mono'; font-weight: 400; font-display: swap; src: url(fonts/ibm-plex-mono-latin-400-normal.woff2) format('woff2'); }
8+@font-face { font-family: 'IBM Plex Mono'; font-weight: 500; font-display: swap; src: url(fonts/ibm-plex-mono-latin-500-normal.woff2) format('woff2'); }
9+
10+:root {
11+ --bg: #FAFAF8;
12+ --panel: #FFFFFF;
13+ --ink: #18191B;
14+ --ink-2: #2B2C2E;
15+ --muted: #5D5E5A;
16+ --faint: #8B8C88;
17+ --line: #E6E6E2;
18+ --line-soft: #EFEFEC;
19+ --hatch-a: #E8E8E4;
20+ --hatch-b: #F2F2EF;
21+ --red: #E5484D;
22+ --red-ink: #C93A3F;
23+
24+ /* the editing timeline lanes */
25+ --motion: #7C5CE0;
26+ --zoom: #E8B02F;
27+ --broll: #3B82C4;
28+ --music: #2F9E83;
29+
30+ --sans: 'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif;
31+ --mono: 'IBM Plex Mono', ui-monospace, Consolas, monospace;
32+ --page: 1160px;
33+ --gutter: clamp(20px, 4vw, 40px);
34+}
35+
36+/* ------------------------------------------------------------------ base */
37+*, *::before, *::after { box-sizing: border-box; }
38+
39+html { scroll-behavior: smooth; scroll-padding-top: 24px; }
40+
41+body {
42+ margin: 0;
43+ font-family: var(--sans);
44+ font-size: 1.0625rem;
45+ line-height: 1.55;
46+ color: var(--ink);
47+ background: var(--bg);
48+ -webkit-font-smoothing: antialiased;
49+ text-rendering: optimizeLegibility;
50+}
51+
52+::selection { background: color-mix(in oklch, var(--red) 24%, transparent); }
53+
54+h1, h2, h3, p, dl, dd, ul, ol, figure { margin: 0; }
55+ul, ol { padding: 0; list-style: none; }
56+
57+a { color: inherit; text-underline-offset: 3px; text-decoration-thickness: 1px; }
58+a:hover { color: var(--red-ink); }
59+
60+code {
61+ font-family: var(--mono);
62+ font-size: 0.875em;
63+ padding: 0.05em 0.35em;
64+ border-radius: 4px;
65+ background: var(--line-soft);
66+}
67+
68+:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; border-radius: 4px; }
69+
70+[hidden] { display: none !important; }
71+
72+.sr-only {
73+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
74+ overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
75+}
76+
77+.skip {
78+ position: absolute; left: 12px; top: -48px; z-index: 10;
79+ padding: 8px 14px; border-radius: 6px; background: var(--ink); color: #fff;
80+}
81+.skip:focus { top: 12px; }
82+
83+main > section,
84+main > figure,
85+.top,
86+.foot {
87+ width: min(100% - 2 * var(--gutter), var(--page));
88+ margin-inline: auto;
89+}
90+
91+main > section + section { margin-top: clamp(96px, 12vw, 152px); }
92+
93+h2 {
94+ font-weight: 600;
95+ font-size: clamp(1.625rem, 3vw, 2.25rem);
96+ line-height: 1.15;
97+ letter-spacing: -0.025em;
98+ max-width: 24ch;
99+ text-wrap: balance;
100+}
101+
102+h3 { font-size: 1.125rem; font-weight: 600; line-height: 1.35; }
103+
104+.section-lead { margin-top: 14px; max-width: 62ch; color: var(--muted); }
105+
106+.note { margin-top: 16px; color: var(--muted); font-size: 0.9375rem; max-width: 70ch; }
107+
108+/* ------------------------------------------------------------------ buttons */
109+.btn {
110+ display: inline-flex; align-items: center; justify-content: center;
111+ height: 44px; padding: 0 20px;
112+ border-radius: 6px;
113+ font: 500 0.9375rem/1 var(--sans);
114+ text-decoration: none;
115+ white-space: nowrap;
116+ cursor: pointer;
117+ border: 0;
118+}
119+.btn-sm { height: 34px; padding: 0 14px; font-size: 0.875rem; }
120+
121+/* The rickub portal mark, drawn in the button's text color */
122+.btn-icon { flex: none; width: 1.1em; height: 1.1em; margin-right: 0.5em; }
123+.btn-icon path { fill: none; stroke: currentColor; stroke-width: 7; stroke-linecap: round; }
124+.btn-icon circle { fill: currentColor; }
125+
126+.btn-ink { background: var(--ink); color: #fff; }
127+.btn-ink:hover { color: #fff; background: var(--ink-2); }
128+.btn-red { background: var(--red); color: #fff; }
129+.btn-red:hover { color: #fff; background: var(--red-ink); }
130+.btn-line { background: var(--panel); color: var(--ink); box-shadow: inset 0 0 0 1px var(--line); }
131+.btn-line:hover { color: var(--ink); box-shadow: inset 0 0 0 1px var(--faint); }
132+
133+/* ------------------------------------------------------------------ header */
134+.top {
135+ display: flex; align-items: center; gap: 24px;
136+ padding-block: 20px;
137+}
138+
139+.brand {
140+ display: inline-flex; align-items: center; gap: 10px;
141+ font-weight: 600; font-size: 1.1875rem; letter-spacing: -0.01em; text-decoration: none;
142+}
143+.brand:hover { color: inherit; }
144+.brand img { display: block; border-radius: 7px; }
145+
146+.top-nav { display: flex; gap: 4px; margin-left: auto; }
147+.top-nav a {
148+ padding: 6px 12px; border-radius: 6px;
149+ color: var(--muted); text-decoration: none; font-size: 0.9375rem;
150+}
151+.top-nav a:hover { color: var(--ink); background: var(--line-soft); }
152+
153+@media (max-width: 720px) {
154+ .top-nav { display: none; }
155+ .top .btn { margin-left: auto; }
156+}
157+
158+/* ------------------------------------------------------------------ hero */
159+.hero { padding-top: clamp(32px, 5vw, 64px); }
160+
161+.hero h1 {
162+ font-weight: 600;
163+ font-size: clamp(2.375rem, 5.2vw, 4.25rem);
164+ line-height: 1.03;
165+ letter-spacing: -0.04em;
166+ max-width: 17ch;
167+ text-wrap: balance;
168+}
169+
170+.lead {
171+ margin-top: 24px;
172+ max-width: 58ch;
173+ font-size: clamp(1.125rem, 1.6vw, 1.25rem);
174+ color: var(--muted);
175+}
176+
177+.actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 32px; }
178+
179+.perk {
180+ display: flex; align-items: center; gap: 8px;
181+ margin-top: 18px;
182+ color: var(--muted); font-size: 0.9375rem;
183+}
184+.perk svg { flex: none; fill: none; stroke: var(--music); stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; }
185+
186+/* ------------------------------------------------------------------ the reel: a project as an editing timeline */
187+.reel { margin-top: clamp(40px, 5vw, 64px); }
188+
189+.reel-scroll { overflow-x: auto; scrollbar-width: thin; }
190+
191+.reel-inner {
192+ --name: 96px;
193+ --units: 30;
194+ --dur: 4.5s;
195+ min-width: 960px;
196+ padding: 14px 18px 18px;
197+ border-radius: 10px;
198+ background: var(--panel);
199+ box-shadow: 0 0 0 1px var(--line), 0 30px 60px -36px rgba(24, 25, 27, 0.35);
200+}
201+
202+.ruler {
203+ display: flex; justify-content: space-between;
204+ margin-left: var(--name);
205+ padding-bottom: 8px;
206+ border-bottom: 1px solid var(--line);
207+ font-family: var(--mono); font-size: 0.75rem; color: var(--faint);
208+}
209+.ruler li { width: 0; display: flex; justify-content: center; }
210+.ruler li:first-child { justify-content: flex-start; }
211+.ruler li:last-child { justify-content: flex-end; }
212+
213+.lanes { position: relative; padding-top: 10px; }
214+
215+.lane {
216+ display: grid;
217+ grid-template-columns: var(--name) 1fr;
218+ align-items: center;
219+ height: 40px;
220+}
221+.lane + .lane { margin-top: 6px; }
222+
223+.lane-name { font-size: 0.8125rem; color: var(--muted); }
224+
225+.lane ul { position: relative; height: 100%; }
226+
227+.lane li {
228+ position: absolute; top: 0; bottom: 0;
229+ left: calc(var(--a) / var(--units) * 100%);
230+ width: calc(var(--b) / var(--units) * 100% - 4px);
231+ display: flex; align-items: center;
232+ padding: 0 8px;
233+ border-radius: 5px;
234+ background: var(--c);
235+ color: #fff;
236+ font-size: 0.84375rem; font-weight: 500;
237+ white-space: nowrap; overflow: hidden;
238+}
239+.lane[style*="--zoom"] li { color: var(--ink); }
240+
241+/* The playhead: a red line with the logo's head, over every lane */
242+.playhead {
243+ position: absolute; top: -6px; bottom: -6px;
244+ left: calc(var(--name) + 100% - var(--name));
245+ width: 2px; margin-left: -1px;
246+ background: var(--red);
247+ pointer-events: none;
248+}
249+.playhead::before {
250+ content: "";
251+ position: absolute; top: -10px; left: 50%;
252+ width: 12px; height: 10px; margin-left: -6px;
253+ border-radius: 3px;
254+ background: var(--red);
255+}
256+
257+.reel figcaption { margin-top: 14px; color: var(--faint); font-size: 0.9375rem; }
258+
259+/* The one orchestrated moment. Before .run, the project is empty (hatched like an empty lane in the app);
260+ then the playhead sweeps once and every clip fills in as the playhead reaches it. */
261+.js .reel .lane li::before {
262+ content: "";
263+ position: absolute; inset: 0;
264+ background: repeating-linear-gradient(-45deg, var(--hatch-a) 0 6px, var(--hatch-b) 6px 12px);
265+ opacity: 0;
266+ transition: opacity 160ms ease-out;
267+}
268+.js .reel .lane li { transition: color 160ms ease-out; }
269+.js .reel .lane li span { position: relative; }
270+.js .reel:not(.run) .lane li { color: var(--faint); }
271+.js .reel:not(.run) .lane li::before { opacity: 1; }
272+.js .reel.run .lane li,
273+.js .reel.run .lane li::before {
274+ transition-delay: calc(var(--a) / var(--units) * var(--dur) + 60ms);
275+}
276+.js .reel:not(.run) .playhead { left: var(--name); }
277+.js .reel.run .playhead { animation: sweep var(--dur) linear both; }
278+
279+@keyframes sweep {
280+ from { left: var(--name); }
281+ to { left: 100%; }
282+}
283+
284+/* ------------------------------------------------------------------ tour: tabs on the left, screenshot on the right */
285+.reel + .tour { margin-top: clamp(96px, 12vw, 152px); }
286+
287+.tour {
288+ display: grid;
289+ grid-template-columns: minmax(0, 290px) minmax(0, 1fr);
290+ gap: 40px;
291+ align-items: start;
292+}
293+
294+.tour-tabs { display: flex; flex-direction: column; gap: 2px; }
295+
296+.tour-tabs button {
297+ display: block;
298+ width: 100%;
299+ padding: 10px 14px 10px 16px;
300+ border: 0; border-radius: 6px;
301+ background: transparent;
302+ color: var(--ink);
303+ font: inherit;
304+ text-align: left;
305+ cursor: pointer;
306+ position: relative;
307+}
308+.tab-title { display: block; font-weight: 500; line-height: 1.35; color: var(--muted); }
309+.tab-desc { display: none; margin-top: 4px; font-size: 0.9375rem; line-height: 1.45; color: var(--muted); }
310+
311+.tour-tabs button:hover .tab-title { color: var(--ink); }
312+.tour-tabs button[aria-selected="true"] { background: var(--panel); box-shadow: 0 0 0 1px var(--line); }
313+.tour-tabs button[aria-selected="true"]::before {
314+ content: ""; position: absolute; left: 0; top: 10px; bottom: 10px; width: 3px; border-radius: 0 2px 2px 0; background: var(--red);
315+}
316+.tour-tabs button[aria-selected="true"] .tab-title { color: var(--ink); font-weight: 600; }
317+.tour-tabs button[aria-selected="true"] .tab-desc { display: block; }
318+/* without JS every screenshot is listed, so every description shows */
319+html:not(.js) .tab-desc { display: block; }
320+
321+.tour-shots { display: grid; gap: 24px; }
322+
323+.frame {
324+ aspect-ratio: 2880 / 1800;
325+ border-radius: 10px;
326+ overflow: hidden;
327+ background: #fff;
328+ box-shadow: 0 0 0 1px var(--line), 0 30px 60px -36px rgba(24, 25, 27, 0.35);
329+}
330+.frame img { display: block; width: 100%; height: 100%; object-fit: contain; object-position: top; }
331+
332+/* Small screens: titles become a scrolling row above the screenshot */
333+@media (max-width: 900px) {
334+ .tour { grid-template-columns: 1fr; gap: 16px; }
335+ .tour-tabs { flex-direction: row; overflow-x: auto; scrollbar-width: none; margin-inline: calc(-1 * var(--gutter)); padding: 1px var(--gutter); }
336+ .tour-tabs::-webkit-scrollbar { display: none; }
337+ .tour-tabs button { flex: none; width: auto; padding: 8px 14px; white-space: nowrap; }
338+ .tour-tabs button[aria-selected="true"]::before { display: none; }
339+ .tour-tabs button[aria-selected="true"] .tab-desc { display: none; }
340+ .frame { border-radius: 8px; }
341+}
342+
343+/* ------------------------------------------------------------------ how */
344+.facts {
345+ display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 40px;
346+ margin-top: 48px;
347+}
348+.facts > div { padding-top: 18px; border-top: 2px solid var(--ink); }
349+.facts dt { font-weight: 600; font-size: 1.125rem; }
350+.facts dd { margin-top: 8px; color: var(--muted); }
351+
352+@media (max-width: 820px) { .facts { grid-template-columns: 1fr; gap: 28px; } }
353+
354+.skills {
355+ display: grid; grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr); gap: 40px;
356+ margin-top: 72px;
357+ padding: clamp(24px, 4vw, 40px);
358+ border-radius: 10px;
359+ background: var(--ink);
360+ color: #fff;
361+}
362+.skills-copy p { margin-top: 10px; color: #B5B6B2; max-width: 40ch; }
363+
364+.skills-list { display: grid; gap: 18px; }
365+.skills-list li { display: grid; gap: 4px; }
366+.skills-list code {
367+ justify-self: start;
368+ padding: 0; background: none;
369+ color: #fff; font-size: 0.9375rem; font-weight: 500;
370+}
371+.skills-list span { color: #B5B6B2; font-size: 0.9375rem; }
372+
373+@media (max-width: 820px) { .skills { grid-template-columns: 1fr; gap: 24px; } }
374+
375+/* ------------------------------------------------------------------ a personal tool */
376+.honest {
377+ display: grid; grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr); gap: 40px;
378+}
379+.honest-body { display: grid; gap: 16px; max-width: 60ch; color: var(--muted); }
380+.honest-body p:first-child { color: var(--ink); font-size: 1.1875rem; }
381+
382+@media (max-width: 820px) { .honest { grid-template-columns: 1fr; gap: 20px; } }
383+
384+/* ------------------------------------------------------------------ try it */
385+.code {
386+ position: relative;
387+ margin-top: 28px;
388+ max-width: 760px;
389+ border-radius: 10px;
390+ background: var(--ink);
391+ color: #fff;
392+}
393+.code pre { margin: 0; padding: 22px 24px; overflow-x: auto; }
394+.code code {
395+ background: none; padding: 0;
396+ font-size: 0.9375rem; line-height: 1.75;
397+}
398+.code .copy { position: absolute; top: 14px; right: 14px; }
399+@media (max-width: 720px) { .code pre { padding-top: 60px; } }
400+
401+/* ------------------------------------------------------------------ footer */
402+.foot {
403+ display: flex; flex-wrap: wrap; justify-content: space-between; gap: 12px 32px;
404+ margin-top: clamp(96px, 12vw, 152px);
405+ padding-block: 28px 40px;
406+ border-top: 1px solid var(--line);
407+ color: var(--muted); font-size: 0.9375rem;
408+}
409+.foot nav { display: flex; flex-wrap: wrap; gap: 8px 24px; }
410+
411+/* ------------------------------------------------------------------ motion */
412+@media (prefers-reduced-motion: reduce) {
413+ html { scroll-behavior: auto; }
414+ *, *::before, *::after { transition: none !important; animation: none !important; }
415+}
new file mode 100644
@@ -0,0 +1,415 @@
1+/* Daryl site. Colors and fonts come from the app (web/src/styles.css, montage/Timeline.vue),
2+ so the screenshots sit at home. */
3+
4+@font-face { font-family: 'IBM Plex Sans'; font-weight: 400; font-display: swap; src: url(fonts/ibm-plex-sans-latin-400-normal.woff2) format('woff2'); }
5+@font-face { font-family: 'IBM Plex Sans'; font-weight: 500; font-display: swap; src: url(fonts/ibm-plex-sans-latin-500-normal.woff2) format('woff2'); }
6+@font-face { font-family: 'IBM Plex Sans'; font-weight: 600; font-display: swap; src: url(fonts/ibm-plex-sans-latin-600-normal.woff2) format('woff2'); }
7+@font-face { font-family: 'IBM Plex Mono'; font-weight: 400; font-display: swap; src: url(fonts/ibm-plex-mono-latin-400-normal.woff2) format('woff2'); }
8+@font-face { font-family: 'IBM Plex Mono'; font-weight: 500; font-display: swap; src: url(fonts/ibm-plex-mono-latin-500-normal.woff2) format('woff2'); }
9+
10+:root {
11+ --bg: #FAFAF8;
12+ --panel: #FFFFFF;
13+ --ink: #18191B;
14+ --ink-2: #2B2C2E;
15+ --muted: #5D5E5A;
16+ --faint: #8B8C88;
17+ --line: #E6E6E2;
18+ --line-soft: #EFEFEC;
19+ --hatch-a: #E8E8E4;
20+ --hatch-b: #F2F2EF;
21+ --red: #E5484D;
22+ --red-ink: #C93A3F;
23+
24+ /* the editing timeline lanes */
25+ --motion: #7C5CE0;
26+ --zoom: #E8B02F;
27+ --broll: #3B82C4;
28+ --music: #2F9E83;
29+
30+ --sans: 'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif;
31+ --mono: 'IBM Plex Mono', ui-monospace, Consolas, monospace;
32+ --page: 1160px;
33+ --gutter: clamp(20px, 4vw, 40px);
34+}
35+
36+/* ------------------------------------------------------------------ base */
37+*, *::before, *::after { box-sizing: border-box; }
38+
39+html { scroll-behavior: smooth; scroll-padding-top: 24px; }
40+
41+body {
42+ margin: 0;
43+ font-family: var(--sans);
44+ font-size: 1.0625rem;
45+ line-height: 1.55;
46+ color: var(--ink);
47+ background: var(--bg);
48+ -webkit-font-smoothing: antialiased;
49+ text-rendering: optimizeLegibility;
50+}
51+
52+::selection { background: color-mix(in oklch, var(--red) 24%, transparent); }
53+
54+h1, h2, h3, p, dl, dd, ul, ol, figure { margin: 0; }
55+ul, ol { padding: 0; list-style: none; }
56+
57+a { color: inherit; text-underline-offset: 3px; text-decoration-thickness: 1px; }
58+a:hover { color: var(--red-ink); }
59+
60+code {
61+ font-family: var(--mono);
62+ font-size: 0.875em;
63+ padding: 0.05em 0.35em;
64+ border-radius: 4px;
65+ background: var(--line-soft);
66+}
67+
68+:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; border-radius: 4px; }
69+
70+[hidden] { display: none !important; }
71+
72+.sr-only {
73+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
74+ overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
75+}
76+
77+.skip {
78+ position: absolute; left: 12px; top: -48px; z-index: 10;
79+ padding: 8px 14px; border-radius: 6px; background: var(--ink); color: #fff;
80+}
81+.skip:focus { top: 12px; }
82+
83+main > section,
84+main > figure,
85+.top,
86+.foot {
87+ width: min(100% - 2 * var(--gutter), var(--page));
88+ margin-inline: auto;
89+}
90+
91+main > section + section { margin-top: clamp(96px, 12vw, 152px); }
92+
93+h2 {
94+ font-weight: 600;
95+ font-size: clamp(1.625rem, 3vw, 2.25rem);
96+ line-height: 1.15;
97+ letter-spacing: -0.025em;
98+ max-width: 24ch;
99+ text-wrap: balance;
100+}
101+
102+h3 { font-size: 1.125rem; font-weight: 600; line-height: 1.35; }
103+
104+.section-lead { margin-top: 14px; max-width: 62ch; color: var(--muted); }
105+
106+.note { margin-top: 16px; color: var(--muted); font-size: 0.9375rem; max-width: 70ch; }
107+
108+/* ------------------------------------------------------------------ buttons */
109+.btn {
110+ display: inline-flex; align-items: center; justify-content: center;
111+ height: 44px; padding: 0 20px;
112+ border-radius: 6px;
113+ font: 500 0.9375rem/1 var(--sans);
114+ text-decoration: none;
115+ white-space: nowrap;
116+ cursor: pointer;
117+ border: 0;
118+}
119+.btn-sm { height: 34px; padding: 0 14px; font-size: 0.875rem; }
120+
121+/* The rickub portal mark, drawn in the button's text color */
122+.btn-icon { flex: none; width: 1.1em; height: 1.1em; margin-right: 0.5em; }
123+.btn-icon path { fill: none; stroke: currentColor; stroke-width: 7; stroke-linecap: round; }
124+.btn-icon circle { fill: currentColor; }
125+
126+.btn-ink { background: var(--ink); color: #fff; }
127+.btn-ink:hover { color: #fff; background: var(--ink-2); }
128+.btn-red { background: var(--red); color: #fff; }
129+.btn-red:hover { color: #fff; background: var(--red-ink); }
130+.btn-line { background: var(--panel); color: var(--ink); box-shadow: inset 0 0 0 1px var(--line); }
131+.btn-line:hover { color: var(--ink); box-shadow: inset 0 0 0 1px var(--faint); }
132+
133+/* ------------------------------------------------------------------ header */
134+.top {
135+ display: flex; align-items: center; gap: 24px;
136+ padding-block: 20px;
137+}
138+
139+.brand {
140+ display: inline-flex; align-items: center; gap: 10px;
141+ font-weight: 600; font-size: 1.1875rem; letter-spacing: -0.01em; text-decoration: none;
142+}
143+.brand:hover { color: inherit; }
144+.brand img { display: block; border-radius: 7px; }
145+
146+.top-nav { display: flex; gap: 4px; margin-left: auto; }
147+.top-nav a {
148+ padding: 6px 12px; border-radius: 6px;
149+ color: var(--muted); text-decoration: none; font-size: 0.9375rem;
150+}
151+.top-nav a:hover { color: var(--ink); background: var(--line-soft); }
152+
153+@media (max-width: 720px) {
154+ .top-nav { display: none; }
155+ .top .btn { margin-left: auto; }
156+}
157+
158+/* ------------------------------------------------------------------ hero */
159+.hero { padding-top: clamp(32px, 5vw, 64px); }
160+
161+.hero h1 {
162+ font-weight: 600;
163+ font-size: clamp(2.375rem, 5.2vw, 4.25rem);
164+ line-height: 1.03;
165+ letter-spacing: -0.04em;
166+ max-width: 17ch;
167+ text-wrap: balance;
168+}
169+
170+.lead {
171+ margin-top: 24px;
172+ max-width: 58ch;
173+ font-size: clamp(1.125rem, 1.6vw, 1.25rem);
174+ color: var(--muted);
175+}
176+
177+.actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 32px; }
178+
179+.perk {
180+ display: flex; align-items: center; gap: 8px;
181+ margin-top: 18px;
182+ color: var(--muted); font-size: 0.9375rem;
183+}
184+.perk svg { flex: none; fill: none; stroke: var(--music); stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; }
185+
186+/* ------------------------------------------------------------------ the reel: a project as an editing timeline */
187+.reel { margin-top: clamp(40px, 5vw, 64px); }
188+
189+.reel-scroll { overflow-x: auto; scrollbar-width: thin; }
190+
191+.reel-inner {
192+ --name: 96px;
193+ --units: 30;
194+ --dur: 4.5s;
195+ min-width: 960px;
196+ padding: 14px 18px 18px;
197+ border-radius: 10px;
198+ background: var(--panel);
199+ box-shadow: 0 0 0 1px var(--line), 0 30px 60px -36px rgba(24, 25, 27, 0.35);
200+}
201+
202+.ruler {
203+ display: flex; justify-content: space-between;
204+ margin-left: var(--name);
205+ padding-bottom: 8px;
206+ border-bottom: 1px solid var(--line);
207+ font-family: var(--mono); font-size: 0.75rem; color: var(--faint);
208+}
209+.ruler li { width: 0; display: flex; justify-content: center; }
210+.ruler li:first-child { justify-content: flex-start; }
211+.ruler li:last-child { justify-content: flex-end; }
212+
213+.lanes { position: relative; padding-top: 10px; }
214+
215+.lane {
216+ display: grid;
217+ grid-template-columns: var(--name) 1fr;
218+ align-items: center;
219+ height: 40px;
220+}
221+.lane + .lane { margin-top: 6px; }
222+
223+.lane-name { font-size: 0.8125rem; color: var(--muted); }
224+
225+.lane ul { position: relative; height: 100%; }
226+
227+.lane li {
228+ position: absolute; top: 0; bottom: 0;
229+ left: calc(var(--a) / var(--units) * 100%);
230+ width: calc(var(--b) / var(--units) * 100% - 4px);
231+ display: flex; align-items: center;
232+ padding: 0 8px;
233+ border-radius: 5px;
234+ background: var(--c);
235+ color: #fff;
236+ font-size: 0.84375rem; font-weight: 500;
237+ white-space: nowrap; overflow: hidden;
238+}
239+.lane[style*="--zoom"] li { color: var(--ink); }
240+
241+/* The playhead: a red line with the logo's head, over every lane */
242+.playhead {
243+ position: absolute; top: -6px; bottom: -6px;
244+ left: calc(var(--name) + 100% - var(--name));
245+ width: 2px; margin-left: -1px;
246+ background: var(--red);
247+ pointer-events: none;
248+}
249+.playhead::before {
250+ content: "";
251+ position: absolute; top: -10px; left: 50%;
252+ width: 12px; height: 10px; margin-left: -6px;
253+ border-radius: 3px;
254+ background: var(--red);
255+}
256+
257+.reel figcaption { margin-top: 14px; color: var(--faint); font-size: 0.9375rem; }
258+
259+/* The one orchestrated moment. Before .run, the project is empty (hatched like an empty lane in the app);
260+ then the playhead sweeps once and every clip fills in as the playhead reaches it. */
261+.js .reel .lane li::before {
262+ content: "";
263+ position: absolute; inset: 0;
264+ background: repeating-linear-gradient(-45deg, var(--hatch-a) 0 6px, var(--hatch-b) 6px 12px);
265+ opacity: 0;
266+ transition: opacity 160ms ease-out;
267+}
268+.js .reel .lane li { transition: color 160ms ease-out; }
269+.js .reel .lane li span { position: relative; }
270+.js .reel:not(.run) .lane li { color: var(--faint); }
271+.js .reel:not(.run) .lane li::before { opacity: 1; }
272+.js .reel.run .lane li,
273+.js .reel.run .lane li::before {
274+ transition-delay: calc(var(--a) / var(--units) * var(--dur) + 60ms);
275+}
276+.js .reel:not(.run) .playhead { left: var(--name); }
277+.js .reel.run .playhead { animation: sweep var(--dur) linear both; }
278+
279+@keyframes sweep {
280+ from { left: var(--name); }
281+ to { left: 100%; }
282+}
283+
284+/* ------------------------------------------------------------------ tour: tabs on the left, screenshot on the right */
285+.reel + .tour { margin-top: clamp(96px, 12vw, 152px); }
286+
287+.tour {
288+ display: grid;
289+ grid-template-columns: minmax(0, 290px) minmax(0, 1fr);
290+ gap: 40px;
291+ align-items: start;
292+}
293+
294+.tour-tabs { display: flex; flex-direction: column; gap: 2px; }
295+
296+.tour-tabs button {
297+ display: block;
298+ width: 100%;
299+ padding: 10px 14px 10px 16px;
300+ border: 0; border-radius: 6px;
301+ background: transparent;
302+ color: var(--ink);
303+ font: inherit;
304+ text-align: left;
305+ cursor: pointer;
306+ position: relative;
307+}
308+.tab-title { display: block; font-weight: 500; line-height: 1.35; color: var(--muted); }
309+.tab-desc { display: none; margin-top: 4px; font-size: 0.9375rem; line-height: 1.45; color: var(--muted); }
310+
311+.tour-tabs button:hover .tab-title { color: var(--ink); }
312+.tour-tabs button[aria-selected="true"] { background: var(--panel); box-shadow: 0 0 0 1px var(--line); }
313+.tour-tabs button[aria-selected="true"]::before {
314+ content: ""; position: absolute; left: 0; top: 10px; bottom: 10px; width: 3px; border-radius: 0 2px 2px 0; background: var(--red);
315+}
316+.tour-tabs button[aria-selected="true"] .tab-title { color: var(--ink); font-weight: 600; }
317+.tour-tabs button[aria-selected="true"] .tab-desc { display: block; }
318+/* without JS every screenshot is listed, so every description shows */
319+html:not(.js) .tab-desc { display: block; }
320+
321+.tour-shots { display: grid; gap: 24px; }
322+
323+.frame {
324+ aspect-ratio: 2880 / 1800;
325+ border-radius: 10px;
326+ overflow: hidden;
327+ background: #fff;
328+ box-shadow: 0 0 0 1px var(--line), 0 30px 60px -36px rgba(24, 25, 27, 0.35);
329+}
330+.frame img { display: block; width: 100%; height: 100%; object-fit: contain; object-position: top; }
331+
332+/* Small screens: titles become a scrolling row above the screenshot */
333+@media (max-width: 900px) {
334+ .tour { grid-template-columns: 1fr; gap: 16px; }
335+ .tour-tabs { flex-direction: row; overflow-x: auto; scrollbar-width: none; margin-inline: calc(-1 * var(--gutter)); padding: 1px var(--gutter); }
336+ .tour-tabs::-webkit-scrollbar { display: none; }
337+ .tour-tabs button { flex: none; width: auto; padding: 8px 14px; white-space: nowrap; }
338+ .tour-tabs button[aria-selected="true"]::before { display: none; }
339+ .tour-tabs button[aria-selected="true"] .tab-desc { display: none; }
340+ .frame { border-radius: 8px; }
341+}
342+
343+/* ------------------------------------------------------------------ how */
344+.facts {
345+ display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 40px;
346+ margin-top: 48px;
347+}
348+.facts > div { padding-top: 18px; border-top: 2px solid var(--ink); }
349+.facts dt { font-weight: 600; font-size: 1.125rem; }
350+.facts dd { margin-top: 8px; color: var(--muted); }
351+
352+@media (max-width: 820px) { .facts { grid-template-columns: 1fr; gap: 28px; } }
353+
354+.skills {
355+ display: grid; grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr); gap: 40px;
356+ margin-top: 72px;
357+ padding: clamp(24px, 4vw, 40px);
358+ border-radius: 10px;
359+ background: var(--ink);
360+ color: #fff;
361+}
362+.skills-copy p { margin-top: 10px; color: #B5B6B2; max-width: 40ch; }
363+
364+.skills-list { display: grid; gap: 18px; }
365+.skills-list li { display: grid; gap: 4px; }
366+.skills-list code {
367+ justify-self: start;
368+ padding: 0; background: none;
369+ color: #fff; font-size: 0.9375rem; font-weight: 500;
370+}
371+.skills-list span { color: #B5B6B2; font-size: 0.9375rem; }
372+
373+@media (max-width: 820px) { .skills { grid-template-columns: 1fr; gap: 24px; } }
374+
375+/* ------------------------------------------------------------------ a personal tool */
376+.honest {
377+ display: grid; grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr); gap: 40px;
378+}
379+.honest-body { display: grid; gap: 16px; max-width: 60ch; color: var(--muted); }
380+.honest-body p:first-child { color: var(--ink); font-size: 1.1875rem; }
381+
382+@media (max-width: 820px) { .honest { grid-template-columns: 1fr; gap: 20px; } }
383+
384+/* ------------------------------------------------------------------ try it */
385+.code {
386+ position: relative;
387+ margin-top: 28px;
388+ max-width: 760px;
389+ border-radius: 10px;
390+ background: var(--ink);
391+ color: #fff;
392+}
393+.code pre { margin: 0; padding: 22px 24px; overflow-x: auto; }
394+.code code {
395+ background: none; padding: 0;
396+ font-size: 0.9375rem; line-height: 1.75;
397+}
398+.code .copy { position: absolute; top: 14px; right: 14px; }
399+@media (max-width: 720px) { .code pre { padding-top: 60px; } }
400+
401+/* ------------------------------------------------------------------ footer */
402+.foot {
403+ display: flex; flex-wrap: wrap; justify-content: space-between; gap: 12px 32px;
404+ margin-top: clamp(96px, 12vw, 152px);
405+ padding-block: 28px 40px;
406+ border-top: 1px solid var(--line);
407+ color: var(--muted); font-size: 0.9375rem;
408+}
409+.foot nav { display: flex; flex-wrap: wrap; gap: 8px 24px; }
410+
411+/* ------------------------------------------------------------------ motion */
412+@media (prefers-reduced-motion: reduce) {
413+ html { scroll-behavior: auto; }
414+ *, *::before, *::after { transition: none !important; animation: none !important; }
415+}