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 ## What's here
``` ```
index.html Studio home: hero, featured project, projects grid, about index.html Studio home: hero + device, featured project, projects, comparison
projects/openscribe.html OpenScribe product page + interactive "bring your own AI" picker projects/openscribe.html OpenScribe page: hero, "bring your own AI" picker, build variants
assets/styles.css All styling (no framework, no build) assets/styles.css All styling (no framework, no build): theme, animations, reveals
assets/app.js Provider picker that swaps a sample server .env snippet 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 ## Preview locally
It is plain HTML/CSS/JS - open `index.html` in a browser, or serve the folder: 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]); render(Object.keys(PROVIDERS)[0]);
} }
if (document.readyState !== "loading") init(); // Reveal sections as they scroll into view (graceful if unsupported).
else document.addEventListener("DOMContentLoaded", init); 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. */ 12 Hobbies Studio - site styles. No framework, no build step. */
:root { :root {
--ink: #10131a; --ink: #0d0f15;
--ink-2: #171b24; --ink-2: #171b24;
--panel: #1e2330; --panel: #1a1f2b;
--line: #2c3444; --panel-2: #1e2431;
--text: #e7ecf3; --line: #2a3140;
--muted: #9aa6b8; --text: #e9eef5;
--muted: #97a3b6;
--accent: #f0a35e; /* warm amber */ --accent: #f0a35e; /* warm amber */
--accent-2: #6ec7c0; /* teal */ --accent-2: #6ec7c0; /* teal */
--radius: 14px; --radius: 16px;
--wrap: 1080px; --wrap: 1120px;
--sans: ui-sans-serif, system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; --sans: ui-sans-serif, system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--mono: ui-monospace, "SF Mono", "Cascadia Code", "Fira Code", Consolas, monospace; --mono: ui-monospace, "SF Mono", "Cascadia Code", "Fira Code", Consolas, monospace;
} }
* { box-sizing: border-box; } * { box-sizing: border-box; }
html { scroll-behavior: smooth; } html { scroll-behavior: smooth; }
body { body {
margin: 0; margin: 0;
font-family: var(--sans); font-family: var(--sans);
color: var(--text); 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: background:
radial-gradient(1200px 600px at 80% -10%, rgba(240,163,94,0.10), transparent 60%), radial-gradient(1100px 620px at 82% -8%, rgba(240,163,94,0.14), transparent 60%),
radial-gradient(1000px 500px at -10% 10%, rgba(110,199,192,0.08), transparent 55%), radial-gradient(1000px 560px at -8% 4%, rgba(110,199,192,0.10), transparent 58%),
var(--ink); radial-gradient(800px 800px at 50% 120%, rgba(110,199,192,0.06), transparent 60%);
line-height: 1.6;
} }
a { color: var(--accent); text-decoration: none; } a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; } 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 */ /* Header */
.site-header { .site-header {
position: sticky; top: 0; z-index: 10; position: sticky; top: 0; z-index: 20;
backdrop-filter: blur(8px); backdrop-filter: blur(10px);
background: rgba(16,19,26,0.72); background: rgba(13,15,21,0.7);
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
} }
.site-header .wrap { display: flex; align-items: center; justify-content: space-between; height: 62px; } .site-header .wrap { display: flex; align-items: center; justify-content: space-between; height: 64px; }
.brand { display: flex; align-items: center; gap: 10px; font-weight: 700; letter-spacing: 0.2px; color: var(--text); } .brand { display: flex; align-items: center; gap: 11px; font-weight: 700; letter-spacing: 0.2px; color: var(--text); font-size: 1.02rem; }
.brand .mark { .brand:hover { text-decoration: none; }
display: grid; place-items: center; width: 30px; height: 30px; border-radius: 8px; .brand img { width: 30px; height: 30px; display: block; }
background: linear-gradient(135deg, var(--accent), var(--accent-2)); color: #0c0e13; font-weight: 800; .nav a { color: var(--muted); margin-left: 24px; font-weight: 500; font-size: 0.96rem; }
}
.nav a { color: var(--muted); margin-left: 22px; font-weight: 500; }
.nav a:hover { color: var(--text); text-decoration: none; } .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 */
.hero { padding: 84px 0 40px; } .hero { padding: 72px 0 34px; }
.eyebrow { color: var(--accent-2); font-family: var(--mono); font-size: 0.82rem; letter-spacing: 2px; text-transform: uppercase; } .hero-grid { display: grid; grid-template-columns: 1.15fr 0.85fr; gap: 34px; align-items: center; }
.hero h1 { font-size: clamp(2.1rem, 5vw, 3.4rem); line-height: 1.08; margin: 12px 0 14px; } .eyebrow { color: var(--accent-2); font-family: var(--mono); font-size: 0.8rem; letter-spacing: 2.5px; text-transform: uppercase; }
.hero p.lead { font-size: 1.18rem; color: var(--muted); max-width: 640px; } .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); } .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 */ /* Buttons */
.btn { .btn {
display: inline-block; padding: 11px 18px; border-radius: 10px; font-weight: 600; 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); 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.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 */ /* Sections */
section { padding: 46px 0; } section { padding: 52px 0; }
h2 { font-size: 1.7rem; margin: 0 0 6px; } h2 { font-size: 1.85rem; margin: 0 0 6px; letter-spacing: -0.01em; }
.section-sub { color: var(--muted); margin: 0 0 26px; } .section-sub { color: var(--muted); margin: 0 0 26px; max-width: 680px; }
/* Cards / grid */ /* Cards / grid */
.grid { display: grid; gap: 18px; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); } .grid { display: grid; gap: 18px; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); }
.card { .card {
background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); 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 h3 { margin: 0 0 6px; font-size: 1.12rem; }
.card p { color: var(--muted); margin: 0; } .card p { color: var(--muted); margin: 0; }
.card .tag { display: inline-block; font-family: var(--mono); font-size: 0.72rem; color: var(--accent-2); .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; } border: 1px solid var(--line); border-radius: 999px; padding: 2px 10px; margin-bottom: 10px; }
.card.soon { opacity: 0.72; } .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 */
.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)); 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 .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 { 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 { .diagram {
font-family: var(--mono); font-size: 0.8rem; color: var(--muted); font-family: var(--mono); font-size: 0.78rem; color: var(--muted);
background: #0c0f16; border: 1px solid var(--line); border-radius: 12px; padding: 16px; white-space: pre; overflow-x: auto; 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) */ /* Product figures */
.picker { display: flex; flex-wrap: wrap; gap: 8px; margin: 4px 0 16px; } .product-grid { display: grid; gap: 18px; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.picker button { figure.product {
font-family: var(--sans); font-size: .92rem; cursor: pointer; margin: 0; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);
background: var(--panel); color: var(--text); border: 1px solid var(--line); overflow: hidden; transition: border-color .16s, transform .16s, box-shadow .16s;
border-radius: 999px; padding: 8px 15px;
} }
.picker button[aria-pressed="true"] { border-color: var(--accent); background: rgba(240,163,94,0.14); color: var(--text); } figure.product:hover { border-color: var(--accent); transform: translateY(-3px); box-shadow: 0 16px 40px rgba(0,0,0,0.35); }
pre.code { figure.product .shot {
font-family: var(--mono); font-size: 0.86rem; color: #d7e2f0; display: grid; place-items: center; padding: 26px;
background: #0c0f16; border: 1px solid var(--line); border-radius: 12px; padding: 16px; overflow-x: auto; 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); } figure.product .shot img { width: 100%; max-width: 210px; height: auto; }
pre.code .k { color: var(--accent-2); } 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 */ /* 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 { border-left: 2px solid var(--accent); padding-left: 14px; }
.feature h4 { margin: 0 0 3px; } .feature h4 { margin: 0 0 3px; }
.feature p { margin: 0; color: var(--muted); font-size: 0.96rem; } .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 */ /* Footer */
.site-footer { border-top: 1px solid var(--line); padding: 30px 0; color: var(--muted); } .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; } .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); } .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" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>12 Hobbies Studio - open, self-hosted maker projects</title> <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." /> <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" /> <link rel="stylesheet" href="assets/styles.css" />
</head> </head>
<body> <body>
<header class="site-header"> <header class="site-header">
<div class="wrap"> <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"> <nav class="nav">
<a href="#projects">Projects</a> <a href="#projects">Projects</a>
<a href="#about">About</a> <a href="#compare">Why open</a>
<a href="projects/openscribe.html">OpenScribe</a> <a href="projects/openscribe.html">OpenScribe</a>
</nav> </nav>
</div> </div>
@ -22,21 +23,41 @@
<main> <main>
<section class="hero"> <section class="hero">
<div class="wrap"> <div class="wrap hero-grid">
<div>
<div class="eyebrow">A maker studio</div> <div class="eyebrow">A maker studio</div>
<h1>Twelve hobbies, one habit:<br /><span class="accent">build it open, run it yourself.</span></h1> <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 <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, hardware and software. No lock-in, no subscriptions you cannot leave, no data you do not own.</p>
no data you do not own.</p>
<div class="btn-row"> <div class="btn-row">
<a class="btn primary" href="projects/openscribe.html">Meet OpenScribe</a> <a class="btn primary" href="projects/openscribe.html">Meet OpenScribe &rarr;</a>
<a class="btn" href="#projects">Browse projects</a> <a class="btn" href="#projects">Browse projects</a>
</div> </div>
</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>
<section> <div class="trust">
<div class="wrap"> <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 class="featured">
<div> <div>
<div class="kicker">Flagship project</div> <div class="kicker">Flagship project</div>
@ -55,25 +76,26 @@
<a class="btn" href="https://git.discworld.casa/laurence/openscribe">Source</a> <a class="btn" href="https://git.discworld.casa/laurence/openscribe">Source</a>
</div> </div>
</div> </div>
<div class="diagram">mic <div class="showcase">
-&gt; ESP32-S3 (record to SD) <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 | BLE / WiFi sync
v v self-hosted server
self-hosted server
transcribe -&gt; summarise transcribe -&gt; summarise
| (any AI provider) | (any AI provider)
v v phone app + open API</div>
phone app + open API</div> </div>
</div> </div>
</div> </div>
</section> </section>
<section id="projects"> <section id="projects">
<div class="wrap"> <div class="wrap reveal">
<h2>Projects</h2> <h2>Projects</h2>
<p class="section-sub">One shipped, more in the workshop. Everything here is open source.</p> <p class="section-sub">One shipped, more in the workshop. Everything here is open source.</p>
<div class="grid"> <div class="grid">
<a class="card" href="projects/openscribe.html"> <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> <span class="tag">hardware + AI</span>
<h3>OpenScribe</h3> <h3>OpenScribe</h3>
<p>Self-hosted AI voice recorder. Bring your own AI, open or commercial.</p> <p>Self-hosted AI voice recorder. Bring your own AI, open or commercial.</p>
@ -92,10 +114,33 @@
</div> </div>
</section> </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"> <section id="about">
<div class="wrap"> <div class="wrap reveal">
<h2>About the studio</h2> <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 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, 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 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><a href="https://git.discworld.casa/laurence/openscribe">OpenScribe source</a></div>
</div> </div>
</footer> </footer>
<script src="assets/app.js"></script>
</body> </body>
</html> </html>

View file

@ -6,12 +6,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OpenScribe - open, self-hosted AI voice recorder | 12 Hobbies Studio</title> <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." /> <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" /> <link rel="stylesheet" href="../assets/styles.css" />
</head> </head>
<body> <body>
<header class="site-header"> <header class="site-header">
<div class="wrap"> <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"> <nav class="nav">
<a href="../index.html#projects">Projects</a> <a href="../index.html#projects">Projects</a>
<a href="#any-ai">Any AI</a> <a href="#any-ai">Any AI</a>
@ -22,34 +23,73 @@
<main> <main>
<section class="hero"> <section class="hero">
<div class="wrap"> <div class="wrap hero-grid">
<div>
<div class="eyebrow">Project - hardware + AI</div> <div class="eyebrow">Project - hardware + AI</div>
<h1>OpenScribe</h1> <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. <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 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> transcripts and summaries with <span class="accent">any AI you choose</span>.</p>
<div class="btn-row"> <div class="btn-row">
<a class="btn primary" href="https://git.discworld.casa/laurence/openscribe">Get the source</a> <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> <a class="btn" href="#build">How to build</a>
</div> </div>
</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>
<section id="any-ai"> <section id="any-ai">
<div class="wrap"> <div class="wrap reveal">
<h2>Bring your own AI</h2> <h2>Bring your own AI</h2>
<p class="section-sub">This is the difference. A commercial recorder locks you to its cloud. <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, 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> an open-standard endpoint, or a commercial API. Pick one and drop it in your server config:</p>
<div class="panel">
<div id="provider-picker" class="picker" aria-label="Choose an AI provider"></div> <div id="provider-picker" class="picker" aria-label="Choose an AI provider"></div>
<pre class="code"><code id="env-out"></code></pre> <pre class="code"><code id="env-out"></code></pre>
<p class="section-sub">Transcription is just as flexible: local <code>faster-whisper</code>, or </div>
any OpenAI-compatible audio endpoint (OpenAI, Groq, or your own whisper server).</p> <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> </div>
</section> </section>
<section> <section>
<div class="wrap"> <div class="wrap reveal">
<h2>What it does</h2> <h2>What it does</h2>
<p class="section-sub">The expected recorder features, without the lock-in.</p> <p class="section-sub">The expected recorder features, without the lock-in.</p>
<div class="features"> <div class="features">
@ -64,7 +104,7 @@
</section> </section>
<section id="build"> <section id="build">
<div class="wrap"> <div class="wrap reveal">
<h2>Build one</h2> <h2>Build one</h2>
<p class="section-sub">Off-the-shelf parts, a printed case, and a server you run.</p> <p class="section-sub">Off-the-shelf parts, a printed case, and a server you run.</p>
<div class="grid"> <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 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>
<div class="btn-row"> <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>
</div> </div>
</section> </section>
<section> <section>
<div class="wrap"> <div class="wrap reveal">
<h2>Open all the way down</h2> <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 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 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. independent project inspired by the Plaud class of device; it is not affiliated with Plaud.