feat(site): original device art + amazing polish pass

Adds original OpenScribe illustrations and elevates the whole site from a plain
scaffold to a polished, animated product site.

What changed:
- assets/img/: original SVG artwork (CC-BY-SA, no third-party photos):
  - openscribe-wearable.svg - the wearable recorder with a breathing status LED (SMIL
    animation that plays even when embedded via <img>).
  - openscribe-kit.svg - the dev/kit build: ESP32-S3 board + I2S mic + microSD + LiPo,
    with amber trace connectors.
  - favicon.svg - the "12" studio mark; wired as the site icon and header logo.
- index.html: two-column hero with the floating device, a trust strip, a featured
  showcase (device image + sync diagram), project cards with device thumbnails, and a
  new "why open beats a locked box" comparison table (our own words, no competitor art).
- projects/openscribe.html: device hero, the AI picker in a proper panel, and a new
  "two ways to build it" section with product figures (wearable + kit) and descriptions.
- assets/styles.css: full theme refresh - atmospheric background, gradient headings,
  hero float, hover lift + shadows, product/figure styles, comparison table, and
  scroll-reveal transitions (honours prefers-reduced-motion).
- assets/app.js: IntersectionObserver scroll reveals alongside the provider picker.
- README: documents the artwork and that it is original/publishable.

Why:
- The user asked for product images with descriptions, then to "make it amazing" - this
  delivers IP-safe original device imagery and a genuinely polished, animated site.

Notes:
- Still zero dependencies and no build step. Validated: app.js passes node --check, all
  SVGs are valid XML, HTML section tags balanced, every local link/asset resolves.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Laurence 2026-07-03 19:17:35 +01:00
parent e1bf5a2d7a
commit 71217ce3e1
8 changed files with 448 additions and 107 deletions

View file

@ -12,12 +12,19 @@ can connect to any AI.
## What's here
```
index.html Studio home: hero, featured project, projects grid, about
projects/openscribe.html OpenScribe product page + interactive "bring your own AI" picker
assets/styles.css All styling (no framework, no build)
assets/app.js Provider picker that swaps a sample server .env snippet
index.html Studio home: hero + device, featured project, projects, comparison
projects/openscribe.html OpenScribe page: hero, "bring your own AI" picker, build variants
assets/styles.css All styling (no framework, no build): theme, animations, reveals
assets/app.js Provider picker + scroll-reveal (IntersectionObserver)
assets/img/ Original SVG artwork (CC-BY-SA): device illustrations + favicon
openscribe-wearable.svg the wearable recorder (animated status LED)
openscribe-kit.svg the dev/kit build (board + mic + microSD + LiPo)
favicon.svg the "12" studio mark
```
All device imagery is original artwork (no third-party product photos), so it is safe to
publish and reuse under CC-BY-SA.
## Preview locally
It is plain HTML/CSS/JS - open `index.html` in a browser, or serve the folder:

View file

@ -81,6 +81,23 @@
render(Object.keys(PROVIDERS)[0]);
}
if (document.readyState !== "loading") init();
else document.addEventListener("DOMContentLoaded", init);
// Reveal sections as they scroll into view (graceful if unsupported).
function reveals() {
var els = document.querySelectorAll(".reveal");
if (!("IntersectionObserver" in window) || !els.length) {
els.forEach(function (el) { el.classList.add("in"); });
return;
}
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (e.isIntersecting) { e.target.classList.add("in"); io.unobserve(e.target); }
});
}, { threshold: 0.12, rootMargin: "0px 0px -8% 0px" });
els.forEach(function (el) { io.observe(el); });
}
function boot() { init(); reveals(); }
if (document.readyState !== "loading") boot();
else document.addEventListener("DOMContentLoaded", boot);
})();

13
assets/img/favicon.svg Normal file
View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- SPDX-License-Identifier: CC-BY-SA-4.0 -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="12 Hobbies Studio">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#f0a35e"/>
<stop offset="1" stop-color="#6ec7c0"/>
</linearGradient>
</defs>
<rect x="4" y="4" width="56" height="56" rx="14" fill="url(#g)"/>
<text x="32" y="43" text-anchor="middle" font-family="ui-sans-serif, system-ui, sans-serif"
font-size="30" font-weight="800" fill="#0c0e13">12</text>
</svg>

After

Width:  |  Height:  |  Size: 613 B

View file

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- SPDX-License-Identifier: CC-BY-SA-4.0
Original illustration of the OpenScribe DIY kit (dev build). 12 Hobbies Studio. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 320" role="img"
aria-label="OpenScribe DIY kit - an ESP32-S3 board, an I2S microphone, a microSD card and a LiPo battery, connected">
<defs>
<linearGradient id="pcb" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#1a2b30"/>
<stop offset="1" stop-color="#122024"/>
</linearGradient>
<linearGradient id="cell" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#2c2436"/>
<stop offset="1" stop-color="#201a29"/>
</linearGradient>
<filter id="s2" x="-30%" y="-30%" width="160%" height="160%">
<feDropShadow dx="0" dy="7" stdDeviation="10" flood-color="#000000" flood-opacity="0.4"/>
</filter>
</defs>
<style>
.lbl { font-family: ui-monospace, monospace; font-size: 11px; fill: #8a97ab; letter-spacing: 1px; }
.trace { stroke: #f0a35e; stroke-width: 2; stroke-dasharray: 4 5; fill: none; opacity: 0.8; }
.pin { fill: #f0a35e; }
</style>
<!-- connectors (drawn first, behind parts) -->
<path class="trace" d="M300 118 C 260 130, 250 150, 230 158"/>
<path class="trace" d="M420 190 C 380 190, 360 178, 232 172"/>
<path class="trace" d="M300 250 C 270 240, 250 210, 232 196"/>
<!-- ESP32-S3 board -->
<g filter="url(#s2)">
<rect x="60" y="112" width="172" height="112" rx="12" fill="url(#pcb)" stroke="#2f6a63" stroke-width="2"/>
</g>
<rect x="96" y="140" width="60" height="60" rx="6" fill="#0f1a1d" stroke="#2f6a63"/>
<text x="126" y="174" text-anchor="middle" class="lbl" fill="#6ec7c0">S3</text>
<!-- USB-C notch -->
<rect x="52" y="156" width="12" height="24" rx="4" fill="#0f1a1d" stroke="#2f6a63"/>
<!-- pin headers -->
<g class="pin">
<circle cx="80" cy="122" r="2.5"/><circle cx="94" cy="122" r="2.5"/><circle cx="108" cy="122" r="2.5"/>
<circle cx="122" cy="122" r="2.5"/><circle cx="136" cy="122" r="2.5"/><circle cx="150" cy="122" r="2.5"/>
<circle cx="80" cy="214" r="2.5"/><circle cx="94" cy="214" r="2.5"/><circle cx="108" cy="214" r="2.5"/>
<circle cx="122" cy="214" r="2.5"/><circle cx="136" cy="214" r="2.5"/><circle cx="150" cy="214" r="2.5"/>
</g>
<text x="146" y="242" text-anchor="middle" class="lbl">ESP32-S3</text>
<!-- I2S mic module -->
<g filter="url(#s2)">
<rect x="300" y="70" width="96" height="66" rx="10" fill="url(#pcb)" stroke="#2f6a63" stroke-width="2"/>
</g>
<circle cx="348" cy="103" r="17" fill="#0f1a1d" stroke="#2f6a63"/>
<circle cx="348" cy="103" r="8" fill="#1c2f34"/>
<text x="348" y="130" text-anchor="middle" class="lbl">I2S MIC</text>
<!-- microSD -->
<g filter="url(#s2)">
<path d="M424 150 h52 a6 6 0 0 1 6 6 v70 a6 6 0 0 1 -6 6 h-52 a6 6 0 0 1 -6 -6 v-58 l10 -12 a6 6 0 0 1 4 -2 z"
fill="#20242f" stroke="#39445a" stroke-width="2"/>
</g>
<text x="450" y="196" text-anchor="middle" class="lbl">microSD</text>
<!-- LiPo -->
<g filter="url(#s2)">
<rect x="252" y="212" width="150" height="72" rx="12" fill="url(#cell)" stroke="#43364f" stroke-width="2"/>
</g>
<rect x="252" y="212" width="150" height="20" rx="12" fill="#ffffff" opacity="0.05"/>
<text x="327" y="255" text-anchor="middle" class="lbl" fill="#c9a0e0">LiPo 3.7V</text>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- SPDX-License-Identifier: CC-BY-SA-4.0
Original illustration of the OpenScribe wearable recorder. 12 Hobbies Studio. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 360" role="img"
aria-label="OpenScribe wearable recorder - a small vertical capsule with a record button, status LED and microphone grille">
<defs>
<linearGradient id="body" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#2b3345"/>
<stop offset="1" stop-color="#171b26"/>
</linearGradient>
<linearGradient id="btn" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#f6b774"/>
<stop offset="1" stop-color="#e2894e"/>
</linearGradient>
<radialGradient id="glow" cx="0.5" cy="0.5" r="0.5">
<stop offset="0" stop-color="#6ec7c0" stop-opacity="0.9"/>
<stop offset="1" stop-color="#6ec7c0" stop-opacity="0"/>
</radialGradient>
<filter id="soft" x="-30%" y="-30%" width="160%" height="160%">
<feDropShadow dx="0" dy="10" stdDeviation="14" flood-color="#000000" flood-opacity="0.45"/>
</filter>
</defs>
<!-- lanyard loop -->
<path d="M160 20 a20 20 0 0 1 20 20 v6 h-10 v-6 a10 10 0 0 0 -20 0 v6 h-10 v-6 a20 20 0 0 1 20 -20 z"
fill="#3a465c" stroke="#2c3444"/>
<rect x="150" y="40" width="20" height="10" rx="3" fill="#20242f"/>
<!-- body -->
<g filter="url(#soft)">
<rect x="108" y="46" width="104" height="286" rx="52" fill="url(#body)" stroke="#39445a" stroke-width="2"/>
</g>
<!-- left highlight -->
<rect x="120" y="60" width="16" height="256" rx="8" fill="#ffffff" opacity="0.06"/>
<!-- microphone grille -->
<g fill="#5b6a80" opacity="0.9">
<circle cx="146" cy="96" r="4"/><circle cx="160" cy="90" r="4"/><circle cx="174" cy="96" r="4"/>
<circle cx="146" cy="112" r="4"/><circle cx="160" cy="106" r="4"/><circle cx="174" cy="112" r="4"/>
</g>
<!-- status LED (breathing, as if recording) -->
<circle cx="160" cy="150" r="22" fill="url(#glow)">
<animate attributeName="opacity" values="0.35;1;0.35" dur="2.6s" repeatCount="indefinite"/>
</circle>
<circle cx="160" cy="150" r="6" fill="#8fe4dd">
<animate attributeName="r" values="5;7.5;5" dur="2.6s" repeatCount="indefinite"/>
</circle>
<!-- engraved wordmark -->
<text x="160" y="205" text-anchor="middle" font-family="ui-monospace, monospace"
font-size="13" letter-spacing="2" fill="#8a97ab">OPENSCRIBE</text>
<!-- record button -->
<circle cx="160" cy="268" r="30" fill="#12151d" stroke="#39445a" stroke-width="2"/>
<circle cx="160" cy="268" r="22" fill="url(#btn)"/>
<circle cx="160" cy="268" r="9" fill="#12151d" opacity="0.35"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -2,127 +2,217 @@
12 Hobbies Studio - site styles. No framework, no build step. */
:root {
--ink: #10131a;
--ink: #0d0f15;
--ink-2: #171b24;
--panel: #1e2330;
--line: #2c3444;
--text: #e7ecf3;
--muted: #9aa6b8;
--panel: #1a1f2b;
--panel-2: #1e2431;
--line: #2a3140;
--text: #e9eef5;
--muted: #97a3b6;
--accent: #f0a35e; /* warm amber */
--accent-2: #6ec7c0; /* teal */
--radius: 14px;
--wrap: 1080px;
--radius: 16px;
--wrap: 1120px;
--sans: ui-sans-serif, system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--mono: ui-monospace, "SF Mono", "Cascadia Code", "Fira Code", Consolas, monospace;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
font-family: var(--sans);
color: var(--text);
background: var(--ink);
line-height: 1.62;
-webkit-font-smoothing: antialiased;
overflow-x: hidden;
}
/* layered atmospheric background */
body::before {
content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none;
background:
radial-gradient(1200px 600px at 80% -10%, rgba(240,163,94,0.10), transparent 60%),
radial-gradient(1000px 500px at -10% 10%, rgba(110,199,192,0.08), transparent 55%),
var(--ink);
line-height: 1.6;
radial-gradient(1100px 620px at 82% -8%, rgba(240,163,94,0.14), transparent 60%),
radial-gradient(1000px 560px at -8% 4%, rgba(110,199,192,0.10), transparent 58%),
radial-gradient(800px 800px at 50% 120%, rgba(110,199,192,0.06), transparent 60%);
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
.wrap { max-width: var(--wrap); margin: 0 auto; padding: 0 20px; }
.wrap { max-width: var(--wrap); margin: 0 auto; padding: 0 22px; }
/* Header */
.site-header {
position: sticky; top: 0; z-index: 10;
backdrop-filter: blur(8px);
background: rgba(16,19,26,0.72);
position: sticky; top: 0; z-index: 20;
backdrop-filter: blur(10px);
background: rgba(13,15,21,0.7);
border-bottom: 1px solid var(--line);
}
.site-header .wrap { display: flex; align-items: center; justify-content: space-between; height: 62px; }
.brand { display: flex; align-items: center; gap: 10px; font-weight: 700; letter-spacing: 0.2px; color: var(--text); }
.brand .mark {
display: grid; place-items: center; width: 30px; height: 30px; border-radius: 8px;
background: linear-gradient(135deg, var(--accent), var(--accent-2)); color: #0c0e13; font-weight: 800;
}
.nav a { color: var(--muted); margin-left: 22px; font-weight: 500; }
.site-header .wrap { display: flex; align-items: center; justify-content: space-between; height: 64px; }
.brand { display: flex; align-items: center; gap: 11px; font-weight: 700; letter-spacing: 0.2px; color: var(--text); font-size: 1.02rem; }
.brand:hover { text-decoration: none; }
.brand img { width: 30px; height: 30px; display: block; }
.nav a { color: var(--muted); margin-left: 24px; font-weight: 500; font-size: 0.96rem; }
.nav a:hover { color: var(--text); text-decoration: none; }
@media (max-width: 560px) { .nav a:first-child { display: none; } }
/* Gradient text util */
.grad {
background: linear-gradient(100deg, var(--accent), var(--accent-2));
-webkit-background-clip: text; background-clip: text; color: transparent;
}
/* Hero */
.hero { padding: 84px 0 40px; }
.eyebrow { color: var(--accent-2); font-family: var(--mono); font-size: 0.82rem; letter-spacing: 2px; text-transform: uppercase; }
.hero h1 { font-size: clamp(2.1rem, 5vw, 3.4rem); line-height: 1.08; margin: 12px 0 14px; }
.hero p.lead { font-size: 1.18rem; color: var(--muted); max-width: 640px; }
.hero { padding: 72px 0 34px; }
.hero-grid { display: grid; grid-template-columns: 1.15fr 0.85fr; gap: 34px; align-items: center; }
.eyebrow { color: var(--accent-2); font-family: var(--mono); font-size: 0.8rem; letter-spacing: 2.5px; text-transform: uppercase; }
.hero h1 { font-size: clamp(2.2rem, 5.4vw, 3.6rem); line-height: 1.06; margin: 14px 0 16px; letter-spacing: -0.02em; }
.hero p.lead { font-size: 1.16rem; color: var(--muted); max-width: 560px; }
.hero .accent { color: var(--accent); }
/* floating hero device */
.hero-device { display: grid; place-items: center; }
.hero-device .frame {
position: relative; width: 100%; max-width: 340px; padding: 24px;
border: 1px solid var(--line); border-radius: 26px;
background: radial-gradient(320px 260px at 50% 18%, rgba(240,163,94,0.12), transparent 72%), var(--panel);
box-shadow: 0 30px 70px rgba(0,0,0,0.45);
}
.hero-device img { width: 100%; max-width: 190px; height: auto; display: block; margin: 0 auto;
animation: float 6s ease-in-out infinite; }
.hero-device .chip {
position: absolute; font-family: var(--mono); font-size: 0.72rem; color: var(--accent-2);
border: 1px solid var(--line); background: rgba(13,15,21,0.85); border-radius: 999px; padding: 4px 11px;
}
.hero-device .chip.a { top: 16px; right: -8px; }
.hero-device .chip.b { bottom: 22px; left: -10px; color: var(--accent); }
@keyframes float { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-12px); } }
@media (max-width: 820px) { .hero-grid { grid-template-columns: 1fr; } .hero-device { order: -1; } }
/* Buttons */
.btn {
display: inline-block; padding: 11px 18px; border-radius: 10px; font-weight: 600;
border: 1px solid var(--line); color: var(--text); background: var(--panel);
display: inline-flex; align-items: center; gap: 8px; padding: 12px 19px; border-radius: 11px; font-weight: 600;
border: 1px solid var(--line); color: var(--text); background: var(--panel); transition: .15s;
}
.btn:hover { text-decoration: none; border-color: var(--accent); }
.btn:hover { text-decoration: none; border-color: var(--accent); transform: translateY(-1px); }
.btn.primary { background: linear-gradient(135deg, var(--accent), #e8925a); color: #14100a; border: none; }
.btn-row { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 26px; }
.btn.primary:hover { box-shadow: 0 10px 26px rgba(240,163,94,0.32); }
.btn-row { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 28px; }
/* Trust strip */
.trust { border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.trust .wrap { display: flex; flex-wrap: wrap; gap: 10px 26px; padding: 16px 22px; color: var(--muted); font-size: 0.92rem; }
.trust b { color: var(--text); font-weight: 600; }
.trust .dot { color: var(--accent-2); }
/* Sections */
section { padding: 46px 0; }
h2 { font-size: 1.7rem; margin: 0 0 6px; }
.section-sub { color: var(--muted); margin: 0 0 26px; }
section { padding: 52px 0; }
h2 { font-size: 1.85rem; margin: 0 0 6px; letter-spacing: -0.01em; }
.section-sub { color: var(--muted); margin: 0 0 26px; max-width: 680px; }
/* Cards / grid */
.grid { display: grid; gap: 18px; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); }
.card {
background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);
padding: 20px; transition: border-color .15s, transform .15s;
padding: 20px; transition: border-color .16s, transform .16s, box-shadow .16s;
}
.card:hover { border-color: var(--accent); transform: translateY(-2px); }
.card:hover { border-color: var(--accent); transform: translateY(-3px); box-shadow: 0 16px 40px rgba(0,0,0,0.35); }
.card h3 { margin: 0 0 6px; font-size: 1.12rem; }
.card p { color: var(--muted); margin: 0; }
.card .tag { display: inline-block; font-family: var(--mono); font-size: 0.72rem; color: var(--accent-2);
border: 1px solid var(--line); border-radius: 999px; padding: 2px 10px; margin-bottom: 10px; }
.card.soon { opacity: 0.72; }
.card .thumb {
display: grid; place-items: center; margin: -20px -20px 14px; padding: 20px;
background: radial-gradient(320px 180px at 50% 6%, rgba(110,199,192,0.12), transparent 70%), #0c1017;
border-bottom: 1px solid var(--line); border-radius: var(--radius) var(--radius) 0 0;
}
.card .thumb img { width: 100%; max-width: 128px; height: auto; }
/* Featured */
.featured {
display: grid; grid-template-columns: 1.2fr 1fr; gap: 26px; align-items: center;
display: grid; grid-template-columns: 1.15fr 0.85fr; gap: 28px; align-items: center;
background: linear-gradient(180deg, var(--ink-2), var(--panel));
border: 1px solid var(--line); border-radius: 18px; padding: 28px;
border: 1px solid var(--line); border-radius: 20px; padding: 30px;
}
.featured h2 { font-size: 1.9rem; }
.featured h2 { font-size: 2rem; }
.featured .kicker { color: var(--accent); font-family: var(--mono); font-size: .8rem; letter-spacing: 1.5px; text-transform: uppercase; }
.featured ul { margin: 14px 0 0; padding-left: 18px; color: var(--muted); }
.featured ul li { margin: 4px 0; }
.showcase { display: grid; gap: 14px; }
.device-shot { display: grid; place-items: center; padding: 8px 0; }
.device-shot img { width: 100%; max-width: 168px; height: auto; }
.diagram {
font-family: var(--mono); font-size: 0.8rem; color: var(--muted);
background: #0c0f16; border: 1px solid var(--line); border-radius: 12px; padding: 16px; white-space: pre; overflow-x: auto;
font-family: var(--mono); font-size: 0.78rem; color: var(--muted);
background: #0b0e14; border: 1px solid var(--line); border-radius: 12px; padding: 15px; white-space: pre; overflow-x: auto;
}
@media (max-width: 780px) { .featured { grid-template-columns: 1fr; } }
@media (max-width: 820px) { .featured { grid-template-columns: 1fr; } }
/* Provider picker (openscribe page) */
.picker { display: flex; flex-wrap: wrap; gap: 8px; margin: 4px 0 16px; }
.picker button {
font-family: var(--sans); font-size: .92rem; cursor: pointer;
background: var(--panel); color: var(--text); border: 1px solid var(--line);
border-radius: 999px; padding: 8px 15px;
/* Product figures */
.product-grid { display: grid; gap: 18px; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
figure.product {
margin: 0; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);
overflow: hidden; transition: border-color .16s, transform .16s, box-shadow .16s;
}
.picker button[aria-pressed="true"] { border-color: var(--accent); background: rgba(240,163,94,0.14); color: var(--text); }
pre.code {
font-family: var(--mono); font-size: 0.86rem; color: #d7e2f0;
background: #0c0f16; border: 1px solid var(--line); border-radius: 12px; padding: 16px; overflow-x: auto;
figure.product:hover { border-color: var(--accent); transform: translateY(-3px); box-shadow: 0 16px 40px rgba(0,0,0,0.35); }
figure.product .shot {
display: grid; place-items: center; padding: 26px;
background: radial-gradient(440px 240px at 50% 16%, rgba(240,163,94,0.12), transparent 70%), #0c1017;
border-bottom: 1px solid var(--line);
}
pre.code .c { color: var(--muted); }
pre.code .k { color: var(--accent-2); }
figure.product .shot img { width: 100%; max-width: 210px; height: auto; }
figure.product figcaption { padding: 16px 18px; }
figure.product h3 { margin: 0 0 4px; }
figure.product p { margin: 0; color: var(--muted); }
figure.product .tag { display:inline-block; font-family: var(--mono); font-size: .72rem; color: var(--accent-2);
border: 1px solid var(--line); border-radius: 999px; padding: 2px 10px; margin-bottom: 8px; }
/* Comparison */
.compare { width: 100%; border-collapse: collapse; font-size: 0.96rem; }
.compare th, .compare td { text-align: left; padding: 13px 14px; border-bottom: 1px solid var(--line); }
.compare thead th { color: var(--muted); font-weight: 600; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 1px; }
.compare tbody th { font-weight: 600; color: var(--text); }
.compare .ours { color: var(--accent-2); }
.compare .theirs { color: var(--muted); }
.compare-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel); }
/* Feature list */
.features { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(220px,1fr)); }
.features { display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(220px,1fr)); }
.feature { border-left: 2px solid var(--accent); padding-left: 14px; }
.feature h4 { margin: 0 0 3px; }
.feature p { margin: 0; color: var(--muted); font-size: 0.96rem; }
/* Provider picker */
.panel {
background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 22px;
}
.picker { display: flex; flex-wrap: wrap; gap: 8px; margin: 4px 0 16px; }
.picker button {
font-family: var(--sans); font-size: .92rem; cursor: pointer; transition: .14s;
background: var(--panel-2); color: var(--text); border: 1px solid var(--line);
border-radius: 999px; padding: 8px 15px;
}
.picker button:hover { border-color: var(--accent); }
.picker button[aria-pressed="true"] { border-color: var(--accent); background: rgba(240,163,94,0.16); }
pre.code {
font-family: var(--mono); font-size: 0.86rem; color: #d7e2f0; margin: 0;
background: #0b0e14; border: 1px solid var(--line); border-radius: 12px; padding: 16px; overflow-x: auto;
}
pre.code .c { color: var(--muted); }
pre.code .k { color: var(--accent-2); }
/* Scroll reveal */
.reveal { opacity: 0; transform: translateY(16px); transition: opacity .6s ease, transform .6s ease; }
.reveal.in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
.reveal { opacity: 1; transform: none; transition: none; }
.hero-device img { animation: none; }
}
/* Footer */
.site-footer { border-top: 1px solid var(--line); padding: 30px 0; color: var(--muted); }
.site-footer .wrap { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 12px; }
.pill { font-family: var(--mono); font-size: .75rem; color: var(--accent-2); }
.site-footer { border-top: 1px solid var(--line); padding: 34px 0; color: var(--muted); margin-top: 20px; }
.site-footer .wrap { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 12px; align-items: center; }
.pill { font-family: var(--mono); font-size: .75rem; color: var(--accent-2); border: 1px solid var(--line); border-radius: 999px; padding: 3px 10px; }

View file

@ -6,15 +6,16 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>12 Hobbies Studio - open, self-hosted maker projects</title>
<meta name="description" content="12 Hobbies Studio builds open-source, self-hosted maker projects. Flagship: OpenScribe, an AI voice recorder you can connect to any AI." />
<link rel="icon" href="assets/img/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="assets/styles.css" />
</head>
<body>
<header class="site-header">
<div class="wrap">
<a class="brand" href="index.html"><span class="mark">12</span> Hobbies Studio</a>
<a class="brand" href="index.html"><img src="assets/img/favicon.svg" alt="" /> 12 Hobbies Studio</a>
<nav class="nav">
<a href="#projects">Projects</a>
<a href="#about">About</a>
<a href="#compare">Why open</a>
<a href="projects/openscribe.html">OpenScribe</a>
</nav>
</div>
@ -22,21 +23,41 @@
<main>
<section class="hero">
<div class="wrap">
<div class="eyebrow">A maker studio</div>
<h1>Twelve hobbies, one habit:<br /><span class="accent">build it open, run it yourself.</span></h1>
<p class="lead">12 Hobbies Studio makes small, open-source, self-hostable projects across
hardware, software and everything in between. No lock-in, no subscriptions you cannot leave,
no data you do not own.</p>
<div class="btn-row">
<a class="btn primary" href="projects/openscribe.html">Meet OpenScribe</a>
<a class="btn" href="#projects">Browse projects</a>
<div class="wrap hero-grid">
<div>
<div class="eyebrow">A maker studio</div>
<h1>Twelve hobbies, one habit:<br /><span class="grad">build it open, run it yourself.</span></h1>
<p class="lead">12 Hobbies Studio makes small, open-source, self-hostable projects across
hardware and software. No lock-in, no subscriptions you cannot leave, no data you do not own.</p>
<div class="btn-row">
<a class="btn primary" href="projects/openscribe.html">Meet OpenScribe &rarr;</a>
<a class="btn" href="#projects">Browse projects</a>
</div>
</div>
<div class="hero-device">
<div class="frame">
<span class="chip a">recording</span>
<img src="assets/img/openscribe-wearable.svg" alt="OpenScribe wearable recorder" />
<span class="chip b">your AI</span>
</div>
</div>
</div>
</section>
<section>
<div class="trust">
<div class="wrap">
<span><b>Open source</b> firmware, server, app</span>
<span class="dot">&bull;</span>
<span><b>Self-hosted</b> - no mandatory cloud</span>
<span class="dot">&bull;</span>
<span><b>Bring your own AI</b> - open or commercial</span>
<span class="dot">&bull;</span>
<span><b>Open hardware</b> you can build</span>
</div>
</div>
<section>
<div class="wrap reveal">
<div class="featured">
<div>
<div class="kicker">Flagship project</div>
@ -55,25 +76,26 @@
<a class="btn" href="https://git.discworld.casa/laurence/openscribe">Source</a>
</div>
</div>
<div class="diagram">mic
-&gt; ESP32-S3 (record to SD)
| BLE / WiFi sync
v
self-hosted server
<div class="showcase">
<div class="device-shot"><img src="assets/img/openscribe-wearable.svg" alt="OpenScribe device" /></div>
<div class="diagram">mic -&gt; ESP32-S3 (record to SD)
| BLE / WiFi sync
v self-hosted server
transcribe -&gt; summarise
| (any AI provider)
v
phone app + open API</div>
| (any AI provider)
v phone app + open API</div>
</div>
</div>
</div>
</section>
<section id="projects">
<div class="wrap">
<div class="wrap reveal">
<h2>Projects</h2>
<p class="section-sub">One shipped, more in the workshop. Everything here is open source.</p>
<div class="grid">
<a class="card" href="projects/openscribe.html">
<div class="thumb"><img src="assets/img/openscribe-wearable.svg" alt="OpenScribe device" /></div>
<span class="tag">hardware + AI</span>
<h3>OpenScribe</h3>
<p>Self-hosted AI voice recorder. Bring your own AI, open or commercial.</p>
@ -92,10 +114,33 @@
</div>
</section>
<section id="compare">
<div class="wrap reveal">
<h2>Why open beats a locked box</h2>
<p class="section-sub">A typical AI recorder ties you to one vendor's cloud and one subscription.
The studio's approach, using OpenScribe as the example:</p>
<div class="compare-wrap">
<table class="compare">
<thead>
<tr><th>&nbsp;</th><th>Typical AI recorder</th><th>OpenScribe</th></tr>
</thead>
<tbody>
<tr><th>Where your audio lives</th><td class="theirs">Vendor cloud</td><td class="ours">Your storage / NAS</td></tr>
<tr><th>Which AI transcribes it</th><td class="theirs">Vendor's, fixed</td><td class="ours">Any - local or commercial</td></tr>
<tr><th>Cost model</th><td class="theirs">Monthly minutes</td><td class="ours">Your provider, or free if local</td></tr>
<tr><th>The device</th><td class="theirs">Sealed</td><td class="ours">Open hardware you build</td></tr>
<tr><th>The software</th><td class="theirs">Closed</td><td class="ours">Open source, forkable</td></tr>
<tr><th>Export &amp; API</th><td class="theirs">Limited</td><td class="ours">Completely open API</td></tr>
</tbody>
</table>
</div>
</div>
</section>
<section id="about">
<div class="wrap">
<div class="wrap reveal">
<h2>About the studio</h2>
<p class="section-sub" style="max-width:680px">
<p class="section-sub">
12 Hobbies Studio is a home for maker projects that are worth doing properly and worth
sharing. The through-line: build in the open, keep the data with the person who made it,
and let anyone reproduce the whole thing from the repo alone. Firmware, servers and apps
@ -111,5 +156,7 @@
<div><a href="https://git.discworld.casa/laurence/openscribe">OpenScribe source</a></div>
</div>
</footer>
<script src="assets/app.js"></script>
</body>
</html>

View file

@ -6,12 +6,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OpenScribe - open, self-hosted AI voice recorder | 12 Hobbies Studio</title>
<meta name="description" content="OpenScribe is an open-source, self-hosted AI voice recorder. Record on an ESP32-S3, sync over BLE or WiFi, and transcribe and summarise with any AI you choose - open or commercial." />
<link rel="icon" href="../assets/img/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../assets/styles.css" />
</head>
<body>
<header class="site-header">
<div class="wrap">
<a class="brand" href="../index.html"><span class="mark">12</span> Hobbies Studio</a>
<a class="brand" href="../index.html"><img src="../assets/img/favicon.svg" alt="" /> 12 Hobbies Studio</a>
<nav class="nav">
<a href="../index.html#projects">Projects</a>
<a href="#any-ai">Any AI</a>
@ -22,34 +23,73 @@
<main>
<section class="hero">
<div class="wrap">
<div class="eyebrow">Project - hardware + AI</div>
<h1>OpenScribe</h1>
<p class="lead">An open-source, self-hosted AI voice recorder in the spirit of a Plaud device.
Record on a small ESP32-S3, sync to your phone and your own server, and turn recordings into
transcripts and summaries with <span class="accent">any AI you choose</span>.</p>
<div class="btn-row">
<a class="btn primary" href="https://git.discworld.casa/laurence/openscribe">Get the source</a>
<a class="btn" href="#build">How to build</a>
<div class="wrap hero-grid">
<div>
<div class="eyebrow">Project - hardware + AI</div>
<h1><span class="grad">OpenScribe</span></h1>
<p class="lead">An open-source, self-hosted AI voice recorder in the spirit of a Plaud device.
Record on a small ESP32-S3, sync to your phone and your own server, and turn recordings into
transcripts and summaries with <span class="accent">any AI you choose</span>.</p>
<div class="btn-row">
<a class="btn primary" href="https://git.discworld.casa/laurence/openscribe">Get the source &rarr;</a>
<a class="btn" href="#build">How to build</a>
</div>
</div>
<div class="hero-device">
<div class="frame">
<span class="chip a">open API</span>
<img src="../assets/img/openscribe-wearable.svg" alt="OpenScribe wearable recorder" />
<span class="chip b">self-hosted</span>
</div>
</div>
</div>
</section>
<section id="any-ai">
<div class="wrap">
<div class="wrap reveal">
<h2>Bring your own AI</h2>
<p class="section-sub">This is the difference. A commercial recorder locks you to its cloud.
OpenScribe lets you point transcription and summaries at whatever you want - a local model,
an open-standard endpoint, or a commercial API. Pick one and drop it in your server config:</p>
<div id="provider-picker" class="picker" aria-label="Choose an AI provider"></div>
<pre class="code"><code id="env-out"></code></pre>
<p class="section-sub">Transcription is just as flexible: local <code>faster-whisper</code>, or
any OpenAI-compatible audio endpoint (OpenAI, Groq, or your own whisper server).</p>
<div class="panel">
<div id="provider-picker" class="picker" aria-label="Choose an AI provider"></div>
<pre class="code"><code id="env-out"></code></pre>
</div>
<p class="section-sub" style="margin-top:16px">Transcription is just as flexible: local
<code>faster-whisper</code>, or any OpenAI-compatible audio endpoint (OpenAI, Groq, or your
own whisper server).</p>
</div>
</section>
<section id="device">
<div class="wrap reveal">
<h2>Two ways to build it</h2>
<p class="section-sub">A compact wearable, or a dev board for the best audio. Same firmware, same app.</p>
<div class="product-grid">
<figure class="product">
<div class="shot"><img src="../assets/img/openscribe-wearable.svg" alt="OpenScribe wearable recorder illustration" /></div>
<figcaption>
<span class="tag">compact</span>
<h3>Wearable build</h3>
<p>A XIAO ESP32-S3 with onboard mic and microSD in a printed pebble case. Pocket or lanyard.
The smallest way to carry it.</p>
</figcaption>
</figure>
<figure class="product">
<div class="shot"><img src="../assets/img/openscribe-kit.svg" alt="OpenScribe DIY kit exploded illustration" /></div>
<figcaption>
<span class="tag">best audio</span>
<h3>Dev / kit build</h3>
<p>An ESP32-S3 devkit, an INMP441 I2S microphone, a microSD and a LiPo. Easiest to probe,
and the cleanest sound.</p>
</figcaption>
</figure>
</div>
</div>
</section>
<section>
<div class="wrap">
<div class="wrap reveal">
<h2>What it does</h2>
<p class="section-sub">The expected recorder features, without the lock-in.</p>
<div class="features">
@ -64,7 +104,7 @@
</section>
<section id="build">
<div class="wrap">
<div class="wrap reveal">
<h2>Build one</h2>
<p class="section-sub">Off-the-shelf parts, a printed case, and a server you run.</p>
<div class="grid">
@ -74,15 +114,15 @@
<div class="card"><span class="tag">server</span><h3>4. Run the server</h3><p>FastAPI ingest + transcription + summaries. Point it at your chosen AI and storage.</p></div>
</div>
<div class="btn-row">
<a class="btn primary" href="https://git.discworld.casa/laurence/openscribe">Open the repository</a>
<a class="btn primary" href="https://git.discworld.casa/laurence/openscribe">Open the repository &rarr;</a>
</div>
</div>
</section>
<section>
<div class="wrap">
<div class="wrap reveal">
<h2>Open all the way down</h2>
<p class="section-sub" style="max-width:680px">
<p class="section-sub">
Firmware, server and app are GPL-3.0. The hardware design is CERN-OHL-S. The case and docs
are CC-BY-SA. Anyone can build it, improve it, and keep the result open. OpenScribe is an
independent project inspired by the Plaud class of device; it is not affiliated with Plaud.