← Commit history

video editing skillpack: thumbnails, titles, descriptions, arrows, logo sting assets

anastasia ·8bab15c671 ·2d ago ·parent cf9d77f
40 files changed +1991−31
README.md+37−2SKILL.md+65assets/logo-sting/README.md+74assets/logo-sting/adom-logo-sfx.wavassets/logo-sting/adom-orbital-sting-cutout-1080p.webmassets/logo-sting/adom-orbital-to-fullscreen-fadeout-1080p.webmassets/logo-sting/adom-orbital-to-fullscreen-hold-1080p.webmassets/logo-sting/previews/adom-orbital-sting-cutout.gifassets/logo-sting/previews/adom-orbital-to-fullscreen-fadeout.gifassets/logo-sting/previews/adom-orbital-to-fullscreen-hold.gifinstall.sh+23package.json+95page.json+13−29screenshots/example-thumbnail.jpgscreenshots/showcase.pngskills/adom-arrows/SKILL.md+63skills/adom-arrows/assets/arrow-curve-left.pngskills/adom-arrows/assets/arrow-hook-down.pngskills/adom-arrows/assets/arrow-straight-right.pngskills/adom-arrows/assets/arrow-swoosh-right.pngskills/adom-youtube-descriptions/SKILL.md+187skills/adom-youtube-thumbnails/SKILL.md+231skills/adom-youtube-thumbnails/assets/adom-icon.svg+12skills/adom-youtube-thumbnails/assets/adom-orbital-white-1024.pngskills/adom-youtube-thumbnails/assets/adom-orbital-white-512.pngskills/adom-youtube-thumbnails/assets/arrow-curve-left.pngskills/adom-youtube-thumbnails/assets/arrow-hook-down.pngskills/adom-youtube-thumbnails/assets/arrow-straight-right.pngskills/adom-youtube-thumbnails/assets/arrow-swoosh-right.pngskills/adom-youtube-thumbnails/assets/fonts/FamiljenGrotesk-OFL.txt+93skills/adom-youtube-thumbnails/assets/fonts/FamiljenGrotesk-Variable.ttfskills/adom-youtube-thumbnails/assets/fonts/Satoshi-Bold.ttfskills/adom-youtube-thumbnails/assets/fonts/Satoshi-Variable.ttfskills/adom-youtube-thumbnails/assets/gradient.json+18skills/adom-youtube-thumbnails/examples/altium-A.json+92skills/adom-youtube-thumbnails/examples/altium-B.json+10skills/adom-youtube-thumbnails/examples/altium-C.json+73skills/adom-youtube-thumbnails/make_thumbnail.py+711skills/adom-youtube-titles/SKILL.md+189uninstall.sh+5
README.md+37−2
@@ -1,3 +1,38 @@-# Video Editing Skillpack+# Video editing skillpack -On-brand YouTube thumbnails, titles and descriptions, the house arrow, and the animated Adom logo sting for Premiere.+Adom's kit for packaging a video for YouTube and X: the house rules for **thumbnails**, **titles** and **descriptions**, plus the **animated Adom logo sting** ready to drop into Premiere.++```bash+adom-wiki pkg install adom/video-editing-skillpack+```++## Skill map++| Skill | What it's for |+|---|---|+| `adom-youtube-thumbnails` | Thumbnail style measured from the channel, X-safe and YouTube-safe zones, the white orbital logo rule, house arrows and prompt bubbles, and `make_thumbnail.py` to build and check a 1280x720 thumbnail |+| `adom-youtube-titles` | The title formula `AI <verb> <what> in <tool>`, length and casing rules, series naming, 5 options best first |+| `adom-youtube-descriptions` | The description template in house order, chapter rules and tags |+| `adom-arrows` | The house arrow for any Adom graphic: exact sizes, don'ts, ready-made transparent PNGs |++![Example thumbnail made with the skill](screenshots/example-thumbnail.jpg)++## The animated logo sting++Every clip has a transparent background, so it drops over footage on a higher track. Editing masters (ProRes 4444 with alpha, 4K and 1080p, sound embedded) are in the media Drive under `Video Editing Assets/Adom Logo Sting`. Web copies are in `assets/logo-sting/`.++| Preview | Clip | Use it for |+|---|---|---|+| ![cutout](assets/logo-sting/previews/adom-orbital-sting-cutout.gif) | `adom-orbital-sting-cutout` | The orbital alone, cut out on transparency |+| ![fadeout](assets/logo-sting/previews/adom-orbital-to-fullscreen-fadeout.gif) | `adom-orbital-to-fullscreen-fadeout` | The orbital blooms into the full-screen ADOM wordmark, then fades to the next shot: a scene transition |+| ![hold](assets/logo-sting/previews/adom-orbital-to-fullscreen-hold.gif) | `adom-orbital-to-fullscreen-hold` | The same, holding on the full-screen wordmark: an end card |++The sound effect is `assets/logo-sting/adom-logo-sfx.wav`. For a full-screen version, line the clip's **1.65 s** mark up with the cut: that is when the screen is fully covered. The previews show transparency as a checkerboard.++## House arrows++One arrow style for every Adom graphic: a smooth curved teal stroke with round caps, a soft glow and a solid, slightly rounded head. See the [adom-arrows skill](skills/adom-arrows/SKILL.md).++| Swoosh | Hook | Curve | Straight |+|---|---|---|---|+| ![](skills/adom-arrows/assets/arrow-swoosh-right.png) | ![](skills/adom-arrows/assets/arrow-hook-down.png) | ![](skills/adom-arrows/assets/arrow-curve-left.png) | ![](skills/adom-arrows/assets/arrow-straight-right.png) |
SKILL.mdadded+65
@@ -0,0 +1,65 @@+---+name: video-editing-skillpack+description: >-+  Adom's video editing kit for YouTube and social. Routes to the house skills for thumbnails, titles and descriptions, and ships the animated Adom logo sting (the orbital cut out on transparency, and the version that blooms into the full-screen ADOM wordmark) with its sound effect, ready to drop into Premiere. Also holds the house arrow style (adom-arrows). Use when editing or packaging an Adom video for YouTube or X. Trigger words: video editing, adom video, youtube upload, thumbnail, video title, video description, logo sting, logo animation, orbital animation, adom logo transition, end card, intro logo, logo sound effect, adom arrow, video editing skillpack.+---++# Video editing skillpack++Everything an editor needs to make an Adom video look and read like the rest of the channel+(https://www.youtube.com/@adominc), in one install:++```bash+adom-wiki pkg install adom/video-editing-skillpack+```++## Skill map++| Skill | What it's for |+|---|---|+| `adom-youtube-thumbnails` | The thumbnail style measured from the channel, the X-safe and YouTube-safe zones, the white orbital logo rule, house arrows and prompt bubbles, and `make_thumbnail.py` to build and check a 1280x720 thumbnail |+| `adom-youtube-titles` | The title formula (`AI <verb> <what> in <tool>`), length and casing rules, series naming, and a "5 options, best first" procedure |+| `adom-youtube-descriptions` | The description template in house order (hook, project files, what's shown, the prompt, related parts, CTA, chapters, adom.inc), chapter rules and tags |+| `adom-arrows` | The house arrow for any Adom graphic: one curved teal stroke with a soft glow and a solid rounded head, exact sizes, don'ts, ready-made PNGs |++Route by the job: making or checking a thumbnail goes to `adom-youtube-thumbnails`, naming a+video to `adom-youtube-titles`, the text under the video to `adom-youtube-descriptions`, and any arrow in any Adom graphic to `adom-arrows`.++## The animated Adom logo sting++The orbital animation used in Adom videos, prepared as drop-in clips. All of them have alpha.++| File | Use it for |+|---|---|+| `adom-orbital-sting-cutout` | The orbital alone on a transparent background. Drop it over footage as an accent or a cut. About 2.0 s of animation, 3.9 s with the sound tail |+| `adom-orbital-to-fullscreen-fadeout` | The orbital blooms over the footage, the navy background fills the screen, the ADOM wordmark forms, then everything fades away to reveal the next shot. This is the transition between two scenes |+| `adom-orbital-to-fullscreen-hold` | The same opening, then it holds on the full-screen ADOM wordmark. Use it for an end card |+| `adom-logo-sfx.wav` | The sting's sound effect, starting on the sting's first frame |++Where the files are:+- **Editing masters** (ProRes 4444 with alpha, 4K and 1080p, sound embedded): the media Drive,+  folder `Video Editing Assets/Adom Logo Sting`.+- **Web copies** (1080p WebM with alpha), previews and the WAV: this package's `assets/logo-sting/`,+  installed to `~/.claude/skills/video-editing-skillpack/assets/logo-sting/`.++Placing a full-screen version in Premiere: put it on a track above the footage so that the clip's+**1.65 s** mark lands on the cut. That is the moment the screen is fully covered, so the cut under+it is invisible. Timing details are in `assets/logo-sting/README.md`.++| Time from the sting's first frame | What happens |+|---|---|+| 0.00 s | The orbital starts blooming over the footage; the sound effect starts |+| 1.05 to 1.65 s | The navy background fades in to full |+| about 2.5 to 3.3 s | The ADOM wordmark forms |+| 3.35 to 3.85 s | `-fadeout` fades back to the footage underneath; `-hold` stays on the wordmark |++## House arrows++Arrows in thumbnails and other Adom graphics share one style: a single smooth curved teal stroke+with round caps, a soft glow, and a solid, slightly rounded arrowhead. The spec and ready-made+PNGs are in the `adom-arrows` skill; `adom-youtube-thumbnails` draws them with the same spec.++## Brand++All of this follows the adom/brand guide (`adom-wiki brand show adom`): its colors, Familjen+Grotesk for headlines, Satoshi for body text, and its voice (plain, precise, no hype).
assets/logo-sting/README.mdadded+74
@@ -0,0 +1,74 @@+# Adom logo sting++The animated Adom logo from the Altium Bridge video: the 3D orbital blooms, resolves into the+Adom mark, and (in the full-screen versions) lands on the ADOM wordmark over the navy background.+Every file has an alpha channel, so it sits on top of your footage with no keying.++Masters (ProRes 4444 with alpha, 4K and 1080p, SFX embedded) are in the media Drive:+`My Drive/Video Editing Assets/Adom Logo Sting/` (account [email protected]).+This folder holds the web copies (1080p WebM with alpha), the sound effect, and previews.++## Files++| File | What it is | Length | Use it for |+|---|---|---|---|+| `adom-orbital-sting-cutout-{2160p,1080p}.mov` | Orbital animation only, cut out on transparency. The orbital is visible from 0 to 2.03 s; the rest is transparent so the SFX tail can play out. | 3.9 s | A logo accent over footage, a bumper over b-roll, or as the top layer if you build the full-screen transition yourself. |+| `adom-orbital-to-fullscreen-fadeout-{2160p,1080p}.mov` | Orbital blooms over transparency, the navy background fades in under it, it resolves into the ADOM wordmark, then the whole frame fades back out to transparent. | 3.9 s | A transition between two shots (the Altium video uses it to go from the intro into the screen recording). |+| `adom-orbital-to-fullscreen-hold-{2160p,1080p}.mov` | Same opening, but it ends fully opaque on the finished wordmark and holds it. | 5.3 s | An end card, or an opener that cuts to a title. Trim the hold to taste. |+| `adom-orbital-sting-cutout-1080p.webm` | Web copy of the cutout (VP9, alpha). | 3.9 s | Web pages, the wiki. Plays with alpha in Chrome, Edge, Firefox. |+| `adom-orbital-to-fullscreen-fadeout-1080p.webm` | Web copy of the fade-out transition (VP9, alpha). | 3.9 s | Web. |+| `adom-orbital-to-fullscreen-hold-1080p.webm` | Web copy of the hold version (VP9, alpha). | 5.3 s | Web. |+| `adom-logo-sfx.wav` | The sound effect on its own (48 kHz stereo, 3.9 s). Already embedded in every .mov and .webm. | 3.9 s | When you placed the video without its audio, or want to mix it separately. |+| `previews/*.gif` | 480 px previews over a checkerboard (the checkerboard is the transparent area). | | Looking before downloading. |++All video is 30 fps. The 4K files are 3840 x 2160, the 1080p files 1920 x 1080. Safari does not+show WebM alpha; for Safari use the ProRes 4444 masters or a HEVC-with-alpha export.++## Timing (relative to the start of the file, S = 0)++| Time | What happens |+|---|---|+| 0.000 s | SFX starts. Frame is transparent; the orbital appears as a speck and blooms. |+| 1.047 s | Navy background starts fading in under the orbital (opacity 0%). |+| 1.647 s | Background at 100%: **the screen is fully covered**. |+| 2.033 s | The cut-out sting ends; the background layer's own orbital carries on seamlessly into the Adom mark and then the ADOM wordmark. |+| 3.347 s | `-fadeout`: the whole frame starts fading to transparent. `-hold`: stays at 100%. |+| 3.847 s | `-fadeout`: fully transparent; whatever is underneath shows. |+| 3.3 to 5.3 s | `-hold`: frozen on the finished wordmark (the intro's frame at 3.27 s). |++How they were built (so you can rebuild or adjust): the sting+(`adom_logo_cutout_v2_smooth.mov`, 1280 x 1080) is scaled 150% and centred at x 0.484, y 0.5 of a+3840 x 2160 frame (top-left at 899, 270). Under it, the first 3.85 s of the Fusion 360 v8 master+(2560 x 1440, navy background) is scaled 150% to fill 4K, with opacity 0% until 1.047 s, 100% at+1.647 s, 100% until 3.347 s, 0% at 3.847 s. The 1080p files use the same layout at half size.+Scripts: `tools/render.sh` (ffmpeg filter_complex), `tools/check.sh` (check frames).++## Placing it in Premiere++1. Import the .mov that matches your sequence: `-2160p` in a 4K sequence, `-1080p` in a 1080p+   sequence. Both are already positioned; leave Scale at 100% and Position at the default. (A 4K+   file in a 1080p sequence: Scale 50%, or right-click, Set to Frame Size.)+2. Put it on a video track **above** the footage, and its audio on a free audio track.+3. **Transition (`-fadeout`)**: you are cutting from shot A to shot B. Place the sting so its+   **1.65 s mark sits on the cut** between A and B (start the clip 1.65 s before the cut). Before+   1.65 s the orbital plays over the end of A while the navy fades in; at 1.65 s the screen is+   fully covered, so the cut is hidden; from 3.35 s the logo fades away to reveal B.+4. **End card or opener (`-hold`)**: start it 1.65 s before the end of your last shot (or at the+   start of the video over black or footage). Trim the end of the hold to the length you need.+5. **Accent only (`-cutout`)**: drop it anywhere over footage. Trim the transparent tail after+   2.03 s if you do not want the SFX tail.+6. Mix the SFX against your music: in the Altium video it plays at full level from the start of the+   sting.++The longer source variant (`adom_logo_cutout_full_smooth.mov`, 5.6 s, used as the outro in the+Fusion video) and `logo_end_frame.png` are still in the original source folder+(`~/Downloads/what-it-cost-graphic/` on the editing Mac) and were not re-exported here.++## Checks done++Frames from every file were composited over a checkerboard and a bright test pattern at 0.05,+0.5, 1.2, 1.7, 2.5, 3.3, 3.6 and 3.85 s (5.25 s for `-hold`) and inspected: transparent at the+start, background partly in at 1.2 s, fully opaque at 1.7 s, fading at 3.6 s and gone at 3.85 s+(`-fadeout`), still opaque at 5.25 s (`-hold`). The sting was overlaid at 50% on the intro at+0.6, 1.0, 1.2, 1.5, 1.8 and 2.0 s with no ghosting, so the hand-off does not jump.+Contact sheets: `checks/`.
assets/logo-sting/adom-logo-sfx.wavadded
⋯ 1 unchanged line ⋯
assets/logo-sting/adom-orbital-sting-cutout-1080p.webmadded
⋯ 1 unchanged line ⋯
assets/logo-sting/adom-orbital-to-fullscreen-fadeout-1080p.webmadded
⋯ 1 unchanged line ⋯
assets/logo-sting/adom-orbital-to-fullscreen-hold-1080p.webmadded
⋯ 1 unchanged line ⋯
assets/logo-sting/previews/adom-orbital-sting-cutout.gifadded
⋯ 1 unchanged line ⋯
assets/logo-sting/previews/adom-orbital-to-fullscreen-fadeout.gifadded
⋯ 1 unchanged line ⋯
assets/logo-sting/previews/adom-orbital-to-fullscreen-hold.gifadded
⋯ 1 unchanged line ⋯
install.shadded+23
@@ -0,0 +1,23 @@+#!/usr/bin/env bash+# Install the video editing skillpack and every skill bundled in it.+set -e+cd "$(dirname "$0")"+DEST="$HOME/.claude/skills"+install_skill() { mkdir -p "$DEST/$1"; cp "$2" "$DEST/$1/SKILL.md"; }++# main skill, plus the logo sting web assets beside it+install_skill video-editing-skillpack SKILL.md+mkdir -p "$DEST/video-editing-skillpack/assets"+cp -r assets/logo-sting "$DEST/video-editing-skillpack/assets/"++# bundled skills+install_skill adom-youtube-titles       skills/adom-youtube-titles/SKILL.md+install_skill adom-youtube-descriptions skills/adom-youtube-descriptions/SKILL.md+install_skill adom-youtube-thumbnails   skills/adom-youtube-thumbnails/SKILL.md+install_skill adom-arrows               skills/adom-arrows/SKILL.md+cp -r skills/adom-arrows/assets "$DEST/adom-arrows/"+# the thumbnail skill also ships its script, fonts, assets and examples+cp -r skills/adom-youtube-thumbnails/. "$DEST/adom-youtube-thumbnails/"++echo "OK: Installed video-editing-skillpack plus 4 bundled skills to $DEST/."+echo "Hint: ask Claude 'make a thumbnail for this video', 'give me 5 title options', or 'write the YouTube description'."
package.jsonadded+95
@@ -0,0 +1,95 @@+{+  "slug": "video-editing-skillpack",+  "version": "1.0.0",+  "type": "skill",+  "description": "Adom's video editing kit: house skills for YouTube thumbnails (X-safe, brand-styled, with a builder script), titles and descriptions, the house arrow style, plus the animated Adom logo sting (cut out on transparency, or blooming into the full-screen ADOM wordmark) with its sound effect, ready for Premiere.",+  "dependencies": {},+  "files": [+    "README.md",+    "SKILL.md",+    "assets/logo-sting/README.md",+    "assets/logo-sting/adom-logo-sfx.wav",+    "assets/logo-sting/adom-orbital-sting-cutout-1080p.webm",+    "assets/logo-sting/adom-orbital-to-fullscreen-fadeout-1080p.webm",+    "assets/logo-sting/adom-orbital-to-fullscreen-hold-1080p.webm",+    "assets/logo-sting/previews/adom-orbital-sting-cutout.gif",+    "assets/logo-sting/previews/adom-orbital-to-fullscreen-fadeout.gif",+    "assets/logo-sting/previews/adom-orbital-to-fullscreen-hold.gif",+    "install.sh",+    "package.json",+    "page.json",+    "screenshots/example-thumbnail.jpg",+    "skills/adom-arrows/SKILL.md",+    "skills/adom-arrows/assets/arrow-curve-left.png",+    "skills/adom-arrows/assets/arrow-hook-down.png",+    "skills/adom-arrows/assets/arrow-straight-right.png",+    "skills/adom-arrows/assets/arrow-swoosh-right.png",+    "skills/adom-youtube-descriptions/SKILL.md",+    "skills/adom-youtube-thumbnails/SKILL.md",+    "skills/adom-youtube-thumbnails/assets/adom-icon.svg",+    "skills/adom-youtube-thumbnails/assets/adom-orbital-white-1024.png",+    "skills/adom-youtube-thumbnails/assets/adom-orbital-white-512.png",+    "skills/adom-youtube-thumbnails/assets/arrow-curve-left.png",+    "skills/adom-youtube-thumbnails/assets/arrow-hook-down.png",+    "skills/adom-youtube-thumbnails/assets/arrow-straight-right.png",+    "skills/adom-youtube-thumbnails/assets/arrow-swoosh-right.png",+    "skills/adom-youtube-thumbnails/assets/fonts/FamiljenGrotesk-OFL.txt",+    "skills/adom-youtube-thumbnails/assets/fonts/FamiljenGrotesk-Variable.ttf",+    "skills/adom-youtube-thumbnails/assets/fonts/Satoshi-Bold.ttf",+    "skills/adom-youtube-thumbnails/assets/fonts/Satoshi-Variable.ttf",+    "skills/adom-youtube-thumbnails/assets/gradient.json",+    "skills/adom-youtube-thumbnails/examples/altium-A.json",+    "skills/adom-youtube-thumbnails/examples/altium-B.json",+    "skills/adom-youtube-thumbnails/examples/altium-C.json",+    "skills/adom-youtube-thumbnails/make_thumbnail.py",+    "skills/adom-youtube-titles/SKILL.md",+    "uninstall.sh"+  ],+  "scripts": {+    "install": "./install.sh",+    "uninstall": "./uninstall.sh"+  },+  "tags": [+    "video",+    "youtube",+    "thumbnail",+    "premiere",+    "video-editing",+    "brand",+    "logo",+    "skillpack"+  ],+  "discovery_triggers": [+    "make a youtube thumbnail",+    "write a youtube description",+    "give me title options for the video",+    "add the adom logo animation",+    "adom logo sting",+    "logo transition for my video",+    "end card with the adom logo",+    "thumbnail safe for x",+    "upload our video to youtube",+    "video editing skillpack",+    "draw an adom arrow",+    "brand arrow style"+  ],+  "discovery_pitch": "Package an Adom video for YouTube and X: on-brand thumbnails, titles and descriptions, plus the animated Adom logo sting for Premiere.",+  "sample_prompts": [+    {+      "label": "Make a thumbnail",+      "prompt": "make a youtube thumbnail for this video in the adom style"+    },+    {+      "label": "Title options",+      "prompt": "give me 5 youtube title options for this video"+    },+    {+      "label": "Write the description",+      "prompt": "write the youtube description for this video"+    },+    {+      "label": "Add the logo sting",+      "prompt": "add the adom logo transition between these two clips"+    }+  ]+}\ No newline at end of file
page.json+13−29
@@ -1,33 +1,17 @@ {-  "schema_version": 1,   "slug": "video-editing-skillpack",-  "type": "skill",   "title": "Video Editing Skillpack",-  "brief": "On-brand YouTube thumbnails, titles and descriptions, the house arrow, and the animated Adom logo sting for Premiere.",-  "version": "0.1.0",-  "hero": {-    "type": "billboard",-    "headline": "Package every video the Adom way",-    "subhead": "Thumbnails, titles, descriptions and the Adom logo sting for Premiere",-    "screenshot": "screenshots/showcase.png"-  },-  "author": {-    "name": "anastasia",-    "email": "[email protected]"-  },-  "tags": [],-  "license": "proprietary",-  "visibility": {-    "public": true-  },-  "sample_prompts": [],-  "discovery_triggers": [],-  "discovery_pitch": null,-  "metadata": {},-  "created_at": "2026-10-21T21:22:18.144Z",-  "updated_at": "2026-10-21T21:22:18.144Z",-  "source_path": "SKILL.md",-  "sub_skills": [],-  "parent_app": null,-  "org": "adom"+  "brief": "Adom's video editing kit: house skills for YouTube thumbnails (X-safe, brand-styled, with a builder script), titles and descriptions, plus the animated Adom logo sting (cut out on transparency, or blooming into the full-screen ADOM wordmark) with its sound effect, ready for Premiere.",+  "type": "skill",+  "tags": [+    "video",+    "youtube",+    "thumbnail",+    "premiere",+    "brand",+    "skillpack",+    "logo",+    "editing"+  ],+  "visibility": "public" }\ No newline at end of file
screenshots/example-thumbnail.jpgadded
⋯ 1 unchanged line ⋯
screenshots/showcase.pngadded
⋯ 1 unchanged line ⋯
skills/adom-arrows/SKILL.mdadded+63
@@ -0,0 +1,63 @@+---+name: adom-arrows+description: >-+  The house arrow for every Adom graphic: thumbnails, slides, diagrams, social cards, video overlays. One smooth curved teal (#00e6dc) stroke with round caps, a soft same-colour glow and a solid, slightly rounded triangular head, with exact sizes relative to the frame, curvature rules, don'ts, ready-made transparent PNGs, and the reference renderer. Use whenever drawing, choosing or checking an arrow in Adom-branded work. Trigger words: arrow, adom arrow, draw an arrow, arrowhead, pointer arrow, callout arrow, curved arrow, brand arrow, thumbnail arrow.+---++Parent skill: **video-editing-skillpack**++# Adom arrows++Measured from the two arrows already published on the+Adom YouTube channel (the swoosh under "AI Engineered PCBs." in `vKUMzy261zs` and the hook in+"I asked AI for a radio." in `wFHON9yi8Yc`). Example PNGs are in this skill's `assets/` folder.++## Rule++An Adom arrow is one smooth curved stroke in the accent colour with a soft glow of the same colour+and a solid triangular head. Use it to point from a claim to the thing that proves it. One arrow per+image.++## Spec++Sizes are given for a 1280 x 720 frame; scale them with the frame width (multiply by width / 1280).++| Property | Value | Relative |+|---|---|---|+| Colour | `#00e6dc` (brand accent; measured `#06e3d9` and `#05e0d5` after JPEG) | |+| Stroke width | 11 px | 0.86 % of frame width |+| Stroke | one quadratic curve, uniform width, round caps, no dashes, no taper | |+| Head shape | solid filled triangle, corners softened by 0.15 x stroke | |+| Head base width | 44 px | 4.0 x stroke |+| Head length | 40 px | 3.6 x stroke |+| Head alignment | axis on the curve's end tangent; the stroke ends inside the head | |+| Glow | same colour, gaussian sigma 8 px, about 60 % opacity at the stroke edge, gone by about 24 px | sigma 0.73 x stroke |+| Curvature | control point offset 0.25 to 0.35 of the chord length, or a hook (leave sideways, land vertical: control point at end x, start y) | |+| Length | 250 to 360 px of chord | 20 to 28 % of frame width |+| Count | 1 per image | |++Measured values: swoosh stroke 9 to 11 px, head 34 px across its diagonal span; hook stroke+10 to 12 px, head 44 px wide x 40 px long. Glow at 9 px from the edge lifts the background from+about 25 to 56 levels in green, i.e. 13 to 15 % of full colour there.++## Don't++- No straight clip-art arrows with thin heads, no outlined or hollow heads, no double heads.+- No other colours (no white, no purple, no gradient) and no dashed or tapered strokes.+- No arrow into the X title-pill zone (bottom-left 60 % x 22 % of a link card) when the image is+  a YouTube thumbnail.++## Examples (transparent PNG, glow included)++| File | Shape |+|---|---|+| `assets/arrow-swoosh-right.png` | left to right, sagging (KiCad thumbnail) |+| `assets/arrow-hook-down.png` | sideways then straight down (radio thumbnail) |+| `assets/arrow-curve-left.png` | right to left, sagging |+| `assets/arrow-straight-right.png` | straight, for tight spaces |++## Reference implementation++`arrow_layer(size, start, end, bend=0.25, scale=1.0, control=None)` in+`~/.claude/skills/adom-youtube-thumbnails/make_thumbnail.py`. It renders at 4x and downsamples.+`scale` multiplies every size above.
skills/adom-arrows/assets/arrow-curve-left.pngadded
⋯ 1 unchanged line ⋯
skills/adom-arrows/assets/arrow-hook-down.pngadded
⋯ 1 unchanged line ⋯
skills/adom-arrows/assets/arrow-straight-right.pngadded
⋯ 1 unchanged line ⋯
skills/adom-arrows/assets/arrow-swoosh-right.pngadded
⋯ 1 unchanged line ⋯
skills/adom-youtube-descriptions/SKILL.mdadded+187
@@ -0,0 +1,187 @@+---+name: adom-youtube-descriptions+description: >-+  House format for titles, descriptions, chapters, tags and thumbnails on Adom's YouTube channel+  (@adominc, channel id UCiWS0GITCbr3I-BDRbF4zkg). Use when writing or checking a YouTube+  title, a video description, chapters or timestamps, tags, or a thumbnail brief for an Adom+  video, or before an upload to YouTube. Derived from the videos already published on the+  channel and from the adom/brand voice. Trigger words: youtube description, video description,+  youtube title, upload to youtube, adom youtube, chapters, timestamps, youtube tags, thumbnail,+  video title, publish video.+---++Parent skill: **video-editing-skillpack**++# Adom YouTube titles and descriptions++Channel: https://www.youtube.com/@adominc (Adom Industries, `UCiWS0GITCbr3I-BDRbF4zkg`).+Posting identity and cadence live in `social-media-skillpack`. This skill covers the words and+the thumbnail.++Source videos this format was built from (October 2026, 4 long-form, 0 Shorts):++| Date | Title | Chars | Chapters |+|---|---|---|---|+| 2026-09-29 | Introducing Hydrogen - Adom Industries | 38 | none |+| 2026-10-03 | AI PCB Design for Engineers | 27 | yes |+| 2026-10-09 | AI Designed This Radio Circuit Board | 36 | yes |+| 2026-10-11 | 1st Ever: AI Designs a Complete PCB in Fusion 360 | 49 | none |++To refresh the list: `curl -sL "https://www.youtube.com/feeds/videos.xml?channel_id=UCiWS0GITCbr3I-BDRbF4zkg"`.++## Voice++From `adom-wiki brand show adom --json`: plain, precise, no hype. No buzzwords, no exclamation+marks, no emoji. The channel already follows this in most lines. Where it slipped, use the fix:++| Seen on the channel | Use instead |+|---|---|+| "on a mission to revolutionize electronics prototyping" | "builds AI-first tools for electronics prototyping" |+| "an MCP server on steroids" | "an MCP server that drives every part of Fusion" |+| "a revolutionary set of tools" | "a new set of tools" |++Concrete numbers beat adjectives: "2 h 42 min from prompt to finished board", "58 labeled test+pads", "six iterations".++## Title formula++Titles have their own skill: **`adom-youtube-titles`** (formula, length checks, series naming,+keyword placement, thumbnail pairing, examples, and the "5 options, best first" procedure). Use it+for every title. In short: `AI <verb> <what> in <tool>`, Title Case, 30 to 55 characters, a colon+only after a short factual opener (`1st Ever:`, `Part 3:`), no `|`, no brand suffix, no hype.+Example: `1st Ever: AI Designs a Complete PCB in Fusion 360`.++## Description template++Fill the `<...>` parts. Keep the fixed lines character for character. One blank line between+blocks. Plain text only: YouTube does not render Markdown.++```+<HOOK: 1 to 2 sentences. Who, by role and first name ("Our CEO John", "Our engineer Noah"),+what the AI built, the key spec, and the tool. This is what shows above "...more".>++Project files: <wiki.adom.inc URL of the project page>++<WHAT'S SHOWN: 2 to 4 sentences. The steps in order, with real numbers.>++Try it yourself in Hydrogen with Claude Code, Codex, Gemini/Antigravity, or Kimi Code. Here's the prompt:++"<exact prompt used in the video>"+Naming adom-aiflow in the prompt is what brings in the copper pours, the analysis, and the rework.++<RELATED, one line each, oldest first:>+Part 1, the same board in KiCad: https://www.youtube.com/watch?v=vKUMzy261zs+Part 2, the same board in Fusion 360: https://www.youtube.com/watch?v=iMIVb8FOXnI++<OPTIONAL, only when a build or test video is coming:>+The board is being built now. Subscribe to see it tested in the follow-up video.++What should we ask it to build next? Let us know in the comments.++0:00 Intro+<m:ss> <Chapter>+<m:ss> Step 1: <Chapter>+<m:ss> Step 2: <Chapter>++https://adom.inc+```++Optional blocks, use only when they apply:+- adom-aiflow line: only when the prompt names adom-aiflow.+- Tool page: `<Tool> Bridge: <wiki.adom.inc/adom/... URL>` directly after the Project files line,+  when the video is about a bridge.+- Company line, for launch or first-in-a-series videos, placed before the CTA:+  `Adom Industries is a startup in Fort Worth, Texas, funded in part by the U.S. National Science Foundation. We build AI-first tools for everyone who makes hardware.`++Not used on this channel, keep it that way: emoji, hashtags in the body, social link lists,+"Smash that like button" style CTAs. The only sign-off is the bare `https://adom.inc` line.++## Rules++- **Length**: hook under 200 characters is ideal (it is the search snippet). Whole description+  usually 900 to 1,600 characters, hard limit 5,000.+- **Names**: first name plus role in the hook. Product names: Adom Hydrogen (then Hydrogen),+  Adom Wiki, Adom KiCad Bridge, Adom Fusion Bridge, Altium Bridge, Altium Designer, KiCad 10,+  Fusion 360. Units with a space: `12 V`, `1 A`, `2 h 42 min`.+- **Links**: full `https://` URLs on their own line or after a colon, so YouTube makes them clickable.+- **Chapters** (YouTube's rules plus ours):+  - First line is `0:00 Intro`. At least 3 chapters, each at least 10 seconds long.+  - Format `m:ss Title` with a single space, no dash, no brackets, ascending order.+  - Demo steps read `Step N: <what happens>`, sentence case. Non-step chapters are short nouns+    (`The board`, `AI token cost`, `How hard was it?`).+  - Do not put a chapter on the end card. Chapters go at the end, just above `https://adom.inc`.+  - Every video over 2 minutes gets chapters (the Fusion video missed them).+- **Tags field**: always fill it (only 1 of 4 videos has tags). 10 to 20 tags: the tools, the+  chip, the circuit, "AI PCB design", "Adom", "Adom Hydrogen".+- **Hashtags**: none in the description by default. If the team wants them, at most 3, on their+  own last line above `https://adom.inc`, for example `#AI #PCBDesign #Altium`.+- **Dashes**: no em or en dashes anywhere. Use a colon, a comma, or a new sentence.++## Thumbnail style++**The detailed authority is the `adom-youtube-thumbnails` skill**+(`~/.claude/skills/adom-youtube-thumbnails/SKILL.md`, with `make_thumbnail.py`). Use it to make+or check any thumbnail; where this summary and that skill differ, that skill wins.++In short (1280 x 720): navy `#0d1117` background with a faint 40 px grid and a teal and purple+glow along the bottom; a huge Familjen Grotesk Bold headline on the left whose last line is the+teal-to-purple gradient ending in a period; a 3D board or app screenshot plus a presenter cutout+on the right; at most one or two extras (house arrow, prompt bubble, starburst). The logo is the+white Adom orbital only, 58 px, top-left (top-right when the headline is on the right). Keep the+headline, faces and product out of the X title-pill zone (x 0 to 768, y 562 to 720) and the+YouTube duration zone (x 1088 to 1280, y 634 to 720). The headline adds to the title, it does+not repeat it.++## Checklist before publishing++- [ ] Title follows the formula, 30 to 55 characters, Title Case, no `|`, no brand suffix.+- [ ] Hook names the person and role, the thing built, the spec, the tool.+- [ ] Project files link opens (`curl -s -o /dev/null -w '%{http_code}' <url>` returns 200).+- [ ] Prompt is the exact one used in the video, in straight double quotes.+- [ ] Related videos linked with full watch URLs.+- [ ] Chapters: `0:00 Intro` first, 3 or more, each at least 10 s, times match the final cut.+- [ ] Last line is `https://adom.inc`.+- [ ] No emoji, no exclamation marks, no buzzwords, no em or en dashes.+- [ ] Tags field filled. Thumbnail follows the style above. Category: Science & Technology.+- [ ] After publishing: link the video from the project's wiki page, and add the new watch URL+      to the Related lines of earlier videos in the series.++## Worked example (real: "AI PCB Design for Engineers", vKUMzy261zs)++Title: `AI PCB Design for Engineers` (27 characters)++```+Our CEO John used Claude in Adom Hydrogen to design a 12 V to 5 V, 1 A buck converter (TI TPS54202): datasheet math, SPICE in three engines, layout and copper pours in KiCad 10, current and thermal analysis, a rework, and a DRC-clean board.++Project files: https://wiki.adom.inc/john/buck-12v5v-molecule++Try it yourself in Hydrogen with Claude Code, Codex, Gemini/Antigravity, or Kimi Code. Here's the prompt:++"Use adom-aiflow to design a 12 V to 5 V, 1 A buck converter on the TI TPS54202 in KiCad 10."+Naming adom-aiflow in the prompt is what brings in the copper pours, the analysis, and the rework.++What should we ask it to build next? Let us know in the comments.++0:00 Intro+0:15 The board+0:30 Step 1: Schematic and chip outlines (adom-symbol, Adom Library)+1:00 Step 2: SPICE in ngspice, LTspice, and PSpice for TI+1:28 Step 3: KiCad, driven through the Adom KiCad Bridge+1:41 Step 4: Routing and copper pours+1:55 Step 5: Current and heat (Adom Fields)+2:10 Step 6: The rework+2:27 Step 7: Silkscreen+2:38 Step 8: Part numbers on the 3D parts+2:54 Step 9: Parts on the Adom wiki+3:08 AI token cost+3:27 Inside Hydrogen++https://adom.inc+```++Tags used: AI PCB design, AI electronics, Adom, Adom Hydrogen, Claude, Claude Code, KiCad, KiCad 10,+LTspice, PSpice, SPICE, TPS54202, buck converter, hardware design, ngspice.++Here the hook carries the "what's shown" content, which is fine for a short video. Today it would+also get a Related line pointing to the Fusion 360 and Altium versions of the same board, and+"Adom Wiki" capitalized.
skills/adom-youtube-thumbnails/SKILL.mdadded+231
@@ -0,0 +1,231 @@+---+name: adom-youtube-thumbnails+description: >-+  The authority on thumbnails for Adom's YouTube channel (@adominc) and on how they look when+  the link is shared on X. Style spec measured from the published thumbnails (navy grid, teal+  and purple glow, huge Familjen Grotesk headline with a teal-to-purple last line ending in a+  period, presenter cutout, 3D board or app screenshot), X-safe and YouTube-safe zones in px,+  the white orbital logo rule, house arrows and prompt bubbles, copy rules, a checklist, and+  make_thumbnail.py which composes a 1280x720 thumbnail and renders the safe-zone check.+  Trigger words: youtube thumbnail, thumbnail, video thumbnail, x card, twitter card, thumbnail+  headline, safe zone, make a thumbnail, thumbnail check.+---++Parent skill: **video-editing-skillpack**++# Adom YouTube thumbnails++Titles live in `adom-youtube-titles`, descriptions in `adom-youtube-descriptions`. This skill+owns the picture. Everything below was measured from the 4 published thumbnails+(`iMIVb8FOXnI` Fusion 360, `k-_wNFG1R3o` Hydrogen, `vKUMzy261zs` KiCad / Claude Code,+`wFHON9yi8Yc` radio), 1280 x 720, and from `adom-wiki brand show adom --json`.++```+make_thumbnail.py spec.json -o out.jpg --check     # compose + safe-zone report + out-xcheck.png+make_thumbnail.py -o out.jpg --bg frame.png --cutout person.png \+    --headline "AI PCBs in|Altium." --orbital left --check   # quick mode+make_thumbnail.py --check-only someone-elses.jpg   # overlay the zones on any thumbnail+make_thumbnail.py --arrow-assets DIR               # re-render the standard arrow PNGs+```++Needs Pillow and numpy (`pip install --target <dir> numpy` and set `PYTHONPATH` if the system+Python lacks it). Fonts and logo load from `assets/`, so run it from anywhere.++## 1. Style spec (measured)++| Element | Spec at 1280 x 720 |+|---|---|+| Canvas | 1280 x 720, JPG (quality 92, auto-lowered) or PNG, under 2 MB |+| Background | `#0d1117` base lifting to `#0c0e1a` at the top; 40 px square grid, 1 px white lines at about 4 % opacity (measured +4 to +10 levels over the base) |+| Glow | teal `#00b8b1` radial from beyond the bottom-left corner (centre -60, 800; radius 700; about 55 % at the core, measured `#0c525e` at 10, 700); purple `#8c6bf7` radial from beyond the bottom-right (1340, 820; radius 640; about 50 %, measured `#342868` at 1270, 710) |+| Headline font | Familjen Grotesk Bold (700). Confirmed by the single-story "a" in "radio", "town". Not Satoshi, not Inter |+| Headline size | cap height 73 to 100 px (KiCad 73, Fusion 78, radio 85, Hydrogen 100), i.e. 112 to 150 px font size (Familjen cap = 0.65 em). Default 120 px. A long 3-line claim drops to 84 to 90 px |+| Headline position | left-aligned at x = 46 to 59 (Hydrogen 83); default x = 52 |+| Line pitch | 0.96 x font size (Fusion 115 px at 120 px). 2 or 3 lines |+| Line colours | all lines white `#ffffff` except the LAST line, which carries the gradient and ends with a period |+| Gradient | horizontal across the last line's ink: `#00b8b1` 0 %, `#2fd0c3` 40 %, `#5fb0e6` 70 %, `#8c6bf7` 100 % (`assets/gradient.json`) |+| Kicker (optional) | Familjen Grotesk Bold, about 40 to 46 px caps, tracked +14 %, accent `#00e6dc`, 34 px above the headline ("ELECTRONICS PROTOTYPING") |+| Sub line (optional) | Familjen Grotesk Bold 40 to 46 px, white with teal keywords ("Meet **Hydrogen**") |+| Visual | right 45 to 50 % of the frame: tilted app screenshot (perspective, 1 px `#30363d` border, rounded, teal edge glow), a 3D board render popping out in front, presenter cutout between them |+| Cutouts | soft black drop shadow, blur 18 px, 55 %, offset 0, 10 |+| Extras | at most 1 or 2: house arrow, prompt bubble, teal starburst (`#00e6dc`, 22 points, navy text, rotated about 12 deg), teal glow outline |++Small text (bubbles, labels) is Satoshi Bold; body copy is Satoshi Medium. Familjen Grotesk is+for the headline, kicker and sub line only.++## 2. Safe zones (hard rule)++When an Adom YouTube link is posted on X, X lays a title pill over the bottom-left of the card+(on the radio thumbnail it covered "for a radio."). YouTube puts the duration badge bottom-right.++```+ 0                                  768                   1088      1280+ 0 +--------------------------------------------------------------++   | [orb]                                                  [orb]  |  orbital slots+   |    24 px edge margin for text and faces                       |  30,28 / 1192,28, 58 px+   |                                                               |+   |   HEADLINE / faces / product: anywhere above y = 562          |+   |   on the left, anywhere outside the orange box on the right   |+562|- - - - - - - - - - - - - - - - - +                            |+   |  X TITLE PILL ZONE                |                            |+   |  x 0..768, y 562..720             |              +-----------+ 634+   |  (left 60 %, bottom 22 %)         |              | YT 12:34  |+720+-----------------------------------+--------------+-----------++                                                     x 1088..1280, y 634..720+```++| Zone | px at 1280 x 720 | Fraction |+|---|---|---|+| X title pill | x 0 to 768, y 562 to 720 | left 60 %, bottom 22 % |+| YouTube duration badge | x 1088 to 1280, y 634 to 720 | right 15 %, bottom 12 % |+| Edge margin | 24 px on every side for text and faces | |++Nothing important in either zone: no headline, no face, no key product (board, logo, bubble).+Background, glow, bodies below the chin, desks and laptops may run through them.+`--check` reports every checked element (headline, bubble, badge, text, orbital and any layer+marked `"important": true`) that crosses a zone, and writes `<out>-xcheck.png`: the thumbnail+with the zones drawn (red X zone, orange YouTube zone, teal orbital slots, yellow boxes for+checked elements) plus a 320 x 180 preview and a mock X card underneath.++Practical consequence: the headline block ends above y = 562. A 3-line headline at 86 px that+starts at y = 196 ends at about 430. The radio thumbnail's headline (y 420 to 638) breaks this+rule; do not copy that placement.++## 3. Logo rule++- The Adom **orbital mark only** (no wordmark, no "+ partner" pill), as a **white** cutout:+  `assets/adom-orbital-white-512.png` or `-1024.png` (white fill, transparent background,+  rasterised from `assets/adom-icon.svg`, the brand page's `assets/adom-icon.svg`).+- **58 x 58 px**, **30 px** from the side, **28 px** from the top (measured on 3 of the 4+  thumbnails). Soft shadow 8 px, 35 %.+- **Top-left** when the headline sits on the left (the default, like every existing thumbnail).+  **Top-right** when the headline or the main subject sits on the right, or when the top-left+  corner is busy. Pick the less busy corner. Never both, never a bottom corner.+- Partner logos (Altium, KiCad, Claude) may appear as content, never in the orbital's corner.+  Recolour a black partner wordmark to white for the dark background, keep its mark colours.++## 4. Arrows (house style)++Every arrow matches the ones in `vKUMzy261zs` (swoosh under "PCBs.") and `wFHON9yi8Yc` (hook+down to the car screen). The full arrow spec and ready-made PNGs are in the `adom-arrows` skill.++| Property | Value (1280 x 720) |+|---|---|+| Colour | accent `#00e6dc` (measured `#06e3d9` / `#05e0d5` after JPEG) |+| Stroke | one smooth quadratic curve, uniform **11 px** (0.86 % of the width), round caps |+| Head | solid filled triangle, base **44 px** (4.0 x stroke), length **40 px** (3.6 x stroke), corners softened by 0.15 x stroke, aligned to the curve's end tangent, the stroke ends inside the head |+| Glow | same colour, gaussian sigma **8 px**, about **60 %** opacity at the stroke edge, fading out by about 24 px |+| Curve | control point offset 0.25 to 0.35 of the chord (`bend`); a hook uses an explicit control point at (end x, start y) so it leaves sideways and lands vertically |+| Length | 250 to 360 px; points from the claim to the thing (headline to board, prompt to result) |+| Count | one per thumbnail |++`{"type": "arrow", "from": [x, y], "to": [x, y], "bend": 0.28}` (positive bulges to the right+of the direction of travel, so a left-to-right arrow sags), or `"control": [x, y]`, and+`"scale"` (1.0 = 11 px). PNGs: `assets/arrow-swoosh-right.png`, `arrow-hook-down.png`,+`arrow-curve-left.png`, `arrow-straight-right.png`.++## 5. Prompt bubbles++Any prompt shown in a thumbnail uses the "Make me an AM/FM radio." bubble style (`wFHON9yi8Yc`).++| Property | Value (1280 x 720) |+|---|---|+| Shape | rounded rectangle, radius **20 px**, **no tail** |+| Fill | `#0b0e13` at **94 %** (measured constant over a varied background) |+| Border | **2 px**, `#e6edf3` at **35 %** |+| Text | Satoshi **Bold 37 px** (cap height about 26 px), white, curly quotes `“…”`, the period inside the closing quote |+| Padding | **46 px** left and right; height = 37 px + 2 x 31 px (about 99 px) |+| Shadow | black, blur 16 px, 55 %, offset 0, 6 |+| Tilt | **3 deg counter-clockwise** (right end higher), as measured |+| Width | follows the text; keep the prompt to one line, scale 0.8 to 1.0 for long prompts |++`{"type": "bubble", "text": "Design a 12 V to 5 V buck converter in Altium.", "center": [x, y], "scale": 0.82}`.+The script adds the curly quotes and the final period if missing.++## 6. Composition recipes (the three archetypes)++**A. Presenter + board** (Fusion 360 "1st EVER", KiCad "AI Engineered PCBs.")+Brand background. Headline left (x 52). Right half: app screenshot tilted slightly+(quad about [740, 92] [1292, 58] [1292, 430] [740, 418]), presenter cutout(s) chest-up over it+with a 60 px bottom fade, the 3D board in front, bottom edge above y 615 and left edge right of+x 768. One extra: arrow from the headline to the board, or the starburst.++**B. POV / vlog** (radio)+Full-bleed photo from the video (`"type": "photo"`, blur 0 for a real POV shot, 12 to 16 for a+lab backdrop behind cutouts), navy darken 35 to 55 % and a scrim on the headline side. People+large on the side opposite the headline, faces above y 562. Prompt bubble near the top, clear of+faces and the orbital. The headline moves UP compared with the radio original.++**C. App screenshot** (Hydrogen "New app in town.")+Brand background. Headline left, optional kicker and sub line. A big screenshot in perspective+running off the right edge with the teal edge glow, the bottom-right corner above y 620 or+treated as background. Optional partner logo or the board over the screenshot. No presenter.++Sources: grab 4K frames read-only with+`adom-bridge --target <mac> premiere_preview_frame '{"time":<s>,"width":3840}'` (the PNG is+`.image.base64`; stream the JSON into `/home/...`, not `/tmp`). Avoid frames with lower-third+name titles. Cut people out with `rembg` (`isnet-general-use`, run on a half-size crop: the+4K crop is killed on a 4 GB box); a 3D board rendered on a flat background cuts out by+thresholding against the background colour plus a flood fill.++## 7. Copy rules++- One idea. Either 2 to 4 words per line over 2 or 3 lines ("AI PCBs in / Fusion 360.",+  "New app / in town."), **or a 3-line factual claim** ("Less than 2 hours / from prompt to /+  finished board."). Break lines at phrase boundaries; the last line is the payoff.+- Ends with a period on the gradient line. No exclamation marks, no question bait, no emoji,+  no all-caps headline (the kicker is the only caps).+- Adds to the title, does not repeat it. Title "AI Designs a PCB in Altium" + headline "Less than+  2 hours from prompt to finished board." is the model: the title says what, the thumbnail says+  how fast. Claims must be checkable (the video shows 1 h 48 min).+- Numbers as digits ("2 hours", "12 V"). Brand voice: plain, precise, no hype.+- A prompt bubble quotes the real prompt, shortened only by cutting, never reworded.++## 8. Spec file++```json+{+  "background": {"type": "brand"} | {"type": "photo", "path": "f.jpg", "blur": 16, "darken": 0.5, "scrim": "left|right"},+  "orbital": "left" | "right",+  "layers": [+    {"type": "screen", "path": "shot.jpg", "quad": [[x,y],[x,y],[x,y],[x,y]], "dim": 0.1, "glow": true, "crop": [l,t,r,b]},+    {"type": "image", "path": "cutout.png", "box": [x, y, width], "fade_bottom": 60, "shadow": true,+     "important": true, "important_box": [l, t, r, b], "name": "board"},+    {"type": "headline", "lines": ["...", "..."], "x": 52, "y": 220, "size": 120, "max_width": 640,+     "align": "left|right", "leading": 0.96, "kicker": "Part 3", "sub": [["Meet ", "white"], ["Hydrogen", "teal"]]},+    {"type": "arrow", "from": [x,y], "to": [x,y], "bend": 0.28},+    {"type": "bubble", "text": "...", "center": [x,y], "scale": 1.0, "rotate": 3},+    {"type": "badge", "lines": ["1st", "EVER"], "center": [x,y], "r": 130},+    {"type": "text", "text": "...", "at": [x,y], "size": 40, "font": "display|body|bodybold", "color": "#ffffff"}+  ]+}+```++Layers draw in order; the orbital is drawn last. `y` of a headline is the top of the first+line's capitals. `max_width` shrinks the size until every line fits. Relative paths resolve+next to the spec. Worked examples: `examples/altium-A.json`, `-B.json`, `-C.json` (their+source images are in `/home/adom/work/thumbs/altium/specs/`).++## 9. Checklist before publishing++- [ ] 1280 x 720, under 2 MB, JPG or PNG.+- [ ] `--check` prints `safe zones: clear`; look at the `-xcheck.png` yourself: nothing+      important in the red (X) or orange (YouTube) box, nothing within 24 px of an edge.+- [ ] Headline in Familjen Grotesk Bold, white lines plus one gradient last line ending in a period.+- [ ] Readable in the 320 x 180 preview (cap height at least about 55 px at full size).+- [ ] White orbital only, 58 px at 30 / 28, top-left (or top-right when the headline is right).+- [ ] At most 2 extras; any arrow and bubble rendered by the script (house style).+- [ ] Faces clean: no lower-third titles, no background people, no hard cut edges at the chest+      unless something covers them.+- [ ] Copy adds to the title, checkable claim, no exclamation marks, no emoji.+- [ ] Partner logo legible on dark (white wordmark), never in the orbital's corner.++## Files++- `make_thumbnail.py`: composer, arrow and bubble renderers, safe-zone checker.+- `assets/adom-orbital-white-512.png`, `assets/adom-orbital-white-1024.png`, `assets/adom-icon.svg`.+- `assets/gradient.json`: headline gradient stops and the measurements behind them.+- `assets/arrow-*.png`: standard arrows (transparent, glow included).+- `assets/fonts/`: Familjen Grotesk (SIL OFL, `FamiljenGrotesk-OFL.txt`) and Satoshi (Fontshare+  free licence, shipped by the adom/brand page), converted to TTF for Pillow.+- `examples/`: the specs of the three Altium example thumbnails.
skills/adom-youtube-thumbnails/assets/adom-icon.svgadded+12
@@ -0,0 +1,12 @@+<?xml version="1.0" encoding="UTF-8"?>+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1061.33 1061.38">+  <defs>+    <style>+      .cls-1 {+        stroke-width: 0px;+      }+    </style>+  </defs>+  <path class="cls-1" d="M957.65,103.68c-116.2-116.2-282.77-138.02-372.05-48.73-36.23,36.23-54.16,85.18-54.92,138.34h0s0,.07,0,.07c0,.17,0,.35,0,.52v329.6c0,3.99,3.24,7.23,7.23,7.23h326.06c54.75.14,105.25-17.79,142.42-54.97,89.28-89.28,67.46-255.85-48.74-372.05Z"/>+  <path class="cls-1" d="M103.68,957.7c116.2,116.2,282.77,138.02,372.05,48.73,36.23-36.23,54.16-85.18,54.92-138.34h0s0-.07,0-.07c0-.17,0-.35,0-.52v-329.6c0-3.99-3.24-7.23-7.23-7.23H197.37c-54.75-.14-105.25,17.79-142.42,54.97-89.28,89.28-67.46,255.85,48.74,372.05Z"/>+</svg>\ No newline at end of file
skills/adom-youtube-thumbnails/assets/adom-orbital-white-1024.pngadded
⋯ 1 unchanged line ⋯
skills/adom-youtube-thumbnails/assets/adom-orbital-white-512.pngadded
⋯ 1 unchanged line ⋯
skills/adom-youtube-thumbnails/assets/arrow-curve-left.pngadded
⋯ 1 unchanged line ⋯
skills/adom-youtube-thumbnails/assets/arrow-hook-down.pngadded
⋯ 1 unchanged line ⋯
skills/adom-youtube-thumbnails/assets/arrow-straight-right.pngadded
⋯ 1 unchanged line ⋯
skills/adom-youtube-thumbnails/assets/arrow-swoosh-right.pngadded
⋯ 1 unchanged line ⋯
skills/adom-youtube-thumbnails/assets/fonts/FamiljenGrotesk-OFL.txtadded+93
@@ -0,0 +1,93 @@+Copyright 2021 The Familjen Grotesk Project Authors (https://github.com/Familjen-Sthlm/Familjen-Grotesk)++This Font Software is licensed under the SIL Open Font License, Version 1.1.+This license is copied below, and is also available with a FAQ at:+http://scripts.sil.org/OFL+++-----------------------------------------------------------+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007+-----------------------------------------------------------++PREAMBLE+The goals of the Open Font License (OFL) are to stimulate worldwide+development of collaborative font projects, to support the font creation+efforts of academic and linguistic communities, and to provide a free and+open framework in which fonts may be shared and improved in partnership+with others.++The OFL allows the licensed fonts to be used, studied, modified and+redistributed freely as long as they are not sold by themselves. The+fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved+names are not used by derivative works. The fonts and derivatives,+however, cannot be released under any other type of license. The+requirement for fonts to remain under this license does not apply+to any document created using the fonts or their derivatives.++DEFINITIONS+"Font Software" refers to the set of files released by the Copyright+Holder(s) under this license and clearly marked as such. This may+include source files, build scripts and documentation.++"Reserved Font Name" refers to any names specified as such after the+copyright statement(s).++"Original Version" refers to the collection of Font Software components as+distributed by the Copyright Holder(s).++"Modified Version" refers to any derivative made by adding to, deleting,+or substituting -- in part or in whole -- any of the components of the+Original Version, by changing formats or by porting the Font Software to a+new environment.++"Author" refers to any designer, engineer, programmer, technical+writer or other person who contributed to the Font Software.++PERMISSION & CONDITIONS+Permission is hereby granted, free of charge, to any person obtaining+a copy of the Font Software, to use, study, copy, merge, embed, modify,+redistribute, and sell modified and unmodified copies of the Font+Software, subject to the following conditions:++1) Neither the Font Software nor any of its individual components,+in Original or Modified Versions, may be sold by itself.++2) Original or Modified Versions of the Font Software may be bundled,+redistributed and/or sold with any software, provided that each copy+contains the above copyright notice and this license. These can be+included either as stand-alone text files, human-readable headers or+in the appropriate machine-readable metadata fields within text or+binary files as long as those fields can be easily viewed by the user.++3) No Modified Version of the Font Software may use the Reserved Font+Name(s) unless explicit written permission is granted by the corresponding+Copyright Holder. This restriction only applies to the primary font name as+presented to the users.++4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font+Software shall not be used to promote, endorse or advertise any+Modified Version, except to acknowledge the contribution(s) of the+Copyright Holder(s) and the Author(s) or with their explicit written+permission.++5) The Font Software, modified or unmodified, in part or in whole,+must be distributed entirely under this license, and must not be+distributed under any other license. The requirement for fonts to+remain under this license does not apply to any document created+using the Font Software.++TERMINATION+This license becomes null and void if any of the above conditions are+not met.++DISCLAIMER+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM+OTHER DEALINGS IN THE FONT SOFTWARE.
skills/adom-youtube-thumbnails/assets/fonts/FamiljenGrotesk-Variable.ttfadded
⋯ 1 unchanged line ⋯
skills/adom-youtube-thumbnails/assets/fonts/Satoshi-Bold.ttfadded
⋯ 1 unchanged line ⋯
skills/adom-youtube-thumbnails/assets/fonts/Satoshi-Variable.ttfadded
⋯ 1 unchanged line ⋯
skills/adom-youtube-thumbnails/assets/gradient.jsonadded+18
@@ -0,0 +1,18 @@+{+  "name": "Adom thumbnail headline gradient",+  "use": "Last headline line only, horizontal, spanning the ink width of that line (left edge to the period).",+  "direction": "left-to-right, 0 deg, no angle",+  "stops": [+    {"pos": 0.00, "color": "#00b8b1", "note": "brand primary"},+    {"pos": 0.40, "color": "#2fd0c3", "note": "lifted teal, measured mid-left on the channel thumbnails"},+    {"pos": 0.70, "color": "#5fb0e6", "note": "sky blue, measured"},+    {"pos": 1.00, "color": "#8c6bf7", "note": "brand gradient end (purple)"}+  ],+  "css": "linear-gradient(90deg, #00b8b1 0%, #2fd0c3 40%, #5fb0e6 70%, #8c6bf7 100%)",+  "measured_from": {+    "iMIVb8FOXnI 'Fusion 360.'": ["#2abfb2", "#50cfc0", "#76abd8", "#7d9edf"],+    "k-_wNFG1R3o 'in town.'": ["#13dfd9", "#48bdf4", "#7d85fe", "#8e76fe"],+    "wFHON9yi8Yc 'for a radio.'": ["#22b3a8", "#4ecfbf", "#74add8", "#838fe5"]+  },+  "note": "The brand gradient #0061ef -> #00b8b1 -> #8c6bf7 is the source; thumbnails skip the deep blue start and run teal -> purple so the text stays bright on navy."+}
skills/adom-youtube-thumbnails/examples/altium-A.jsonadded+92
@@ -0,0 +1,92 @@+{+ "_archetype": "presenter + board: 3D buck board in front of a tilted Altium screenshot, John and Colby behind",+ "background": {+  "type": "brand"+ },+ "orbital": "left",+ "layers": [+  {+   "type": "screen",+   "path": "altium_pcb_window.jpg",+   "quad": [+    [+     738,+     92+    ],+    [+     1292,+     58+    ],+    [+     1292,+     430+    ],+    [+     738,+     418+    ]+   ],+   "dim": 0.12+  },+  {+   "type": "image",+   "path": "cut_hosts_chest.png",+   "box": [+    722,+    62,+    580+   ],+   "fade_bottom": 60,+   "name": "hosts",+   "important": true,+   "important_box": [+    800,+    80,+    1180,+    260+   ]+  },+  {+   "type": "image",+   "path": "cut_board.png",+   "box": [+    760,+    302,+    494+   ],+   "name": "board",+   "important": true,+   "important_box": [+    772,+    318,+    1252,+    612+   ]+  },+  {+   "type": "headline",+   "lines": [+    "Less than 2 hours",+    "from prompt to",+    "finished board."+   ],+   "x": 46,+   "y": 196,+   "size": 86,+   "leading": 0.98,+   "max_width": 670+  },+  {+   "type": "arrow",+   "from": [+    410,+    492+   ],+   "to": [+    742,+    452+   ],+   "bend": 0.28+  }+ ]+}\ No newline at end of file
skills/adom-youtube-thumbnails/examples/altium-B.jsonadded+10
@@ -0,0 +1,10 @@+{+ "_archetype": "POV / vlog: the hosts large on the right over the blurred lab, prompt bubble, headline left",+ "background": {"type": "photo", "path": "lab_f18.jpg", "blur": 16, "darken": 0.5, "scrim": "left", "scrim_strength": 0.85},+ "orbital": "left",+ "layers": [+  {"type": "image", "path": "cut_hosts_clean.png", "box": [560, 250, 820], "name": "hosts", "important": true, "important_box": [690, 300, 1200, 470]},+  {"type": "bubble", "text": "Design a 12 V to 5 V buck converter in Altium.", "center": [800, 150], "scale": 0.82},+  {"type": "headline", "lines": ["Less than 2 hours", "from prompt to", "finished board."], "x": 46, "y": 270, "size": 84, "leading": 0.98, "max_width": 650}+ ]+}
skills/adom-youtube-thumbnails/examples/altium-C.jsonadded+73
@@ -0,0 +1,73 @@+{+ "_archetype": "app screenshot: tilted Altium PCB editor with the Altium Designer logo, board in front, no presenter",+ "background": {+  "type": "brand"+ },+ "orbital": "left",+ "layers": [+  {+   "type": "screen",+   "path": "altium_pcb_canvas.jpg",+   "quad": [+    [+     746,+     200+    ],+    [+     1310,+     160+    ],+    [+     1310,+     600+    ],+    [+     746,+     570+    ]+   ],+   "dim": 0.15+  },+  {+   "type": "image",+   "path": "altiumdesigner_cutout_ondark.png",+   "box": [+    806,+    40,+    390+   ],+   "name": "altium logo",+   "important": true+  },+  {+   "type": "image",+   "path": "cut_board.png",+   "box": [+    800,+    330,+    400+   ],+   "name": "board",+   "important": true,+   "important_box": [+    810,+    340,+    1195,+    585+   ]+  },+  {+   "type": "headline",+   "lines": [+    "Less than 2 hours",+    "from prompt to",+    "finished board."+   ],+   "x": 46,+   "y": 196,+   "size": 86,+   "leading": 0.98,+   "max_width": 670+  }+ ]+}\ No newline at end of file
skills/adom-youtube-thumbnails/make_thumbnail.pyadded+711
@@ -0,0 +1,711 @@+#!/usr/bin/env python3+"""Compose an Adom YouTube thumbnail (1280x720) in the house style.++Two ways to call it:++1. Quick mode (background frame + optional cutout + headline + orbital side):+     make_thumbnail.py -o out.jpg --bg frame.png --cutout john.png \+         --headline "AI PCBs in|Altium." --orbital left --check+   Lines are separated by "|". The LAST line gets the teal-to-purple gradient.+   --bg brand (default) draws the navy grid background; a file path uses that+   image as the right-hand visual (or full bleed with --bg-mode photo).++2. Spec mode (full control, layers drawn in order):+     make_thumbnail.py spec.json -o out.jpg --check+   See SKILL.md "Spec file" for every layer type: image, screen, headline,+   arrow, bubble, badge, text.++Extras:+  --check                 also write <out>-xcheck.png with the X / YouTube safe zones+                          and a 320x180 preview, and print any violations.+  --check-only IMG        overlay the safe zones on an existing thumbnail.+  --arrow-assets DIR      render the standard arrow PNGs into DIR.++Needs Pillow (and numpy). Fonts and the white orbital load from ./assets.+"""+import argparse, json, math, os, sys+from PIL import Image, ImageDraw, ImageFilter, ImageFont, ImageChops++HERE = os.path.dirname(os.path.abspath(__file__))+ASSETS = os.path.join(HERE, "assets")+FONTS = os.path.join(ASSETS, "fonts")+W, H = 1280, 720++# ---------------------------------------------------------------- brand tokens+NAVY = (13, 17, 23)            # #0d1117 background+SURFACE = (22, 27, 34)         # #161b22+TEXT = (230, 237, 243)         # #e6edf3 (headline white uses pure white, see SKILL.md)+TEAL = (0, 184, 177)           # #00b8b1 primary+ACCENT = (0, 230, 220)         # #00e6dc accent: arrows, kickers, starburst+PURPLE = (140, 107, 247)       # #8c6bf7+# Headline gradient, measured from the 4 channel thumbnails (left to right across the line)+HEADLINE_GRADIENT = [(0.00, "#00b8b1"), (0.40, "#2fd0c3"), (0.70, "#5fb0e6"), (1.00, "#8c6bf7")]++# ---------------------------------------------------------------- safe zones (px at 1280x720)+X_ZONE = (0, 562, 768, 720)        # X/Twitter title pill: left 60 %, bottom 22 %+YT_ZONE = (1088, 634, 1280, 720)   # YouTube duration badge: right 15 %, bottom 12 %+EDGE_MARGIN = 24                   # keep text and faces 24 px off every edge++# ---------------------------------------------------------------- orbital rule+ORBITAL_SIZE = 58                  # px, measured on the channel thumbnails+ORBITAL_MARGIN_X = 30+ORBITAL_MARGIN_Y = 28++# ---------------------------------------------------------------- arrow spec (1280x720)+ARROW_COLOR = ACCENT               # measured #06e3d9 after JPEG, i.e. brand accent #00e6dc+ARROW_STROKE = 11                  # px+ARROW_HEAD_W = 4.0                 # head base width  = 4.0 x stroke (44 px)+ARROW_HEAD_L = 3.6                 # head length      = 3.6 x stroke (40 px)+ARROW_HEAD_ROUND = 0.15            # corner rounding  = 0.15 x stroke (slightly soft corners)+ARROW_GLOW_SIGMA = 8               # px gaussian+ARROW_GLOW_OPACITY = 0.6++# ---------------------------------------------------------------- prompt bubble spec+BUBBLE_FONT_SIZE = 37              # Satoshi Bold, cap height about 26 px+BUBBLE_PAD_X = 46+BUBBLE_HEIGHT = 1.0                # x font size + 2 * pad_y+BUBBLE_PAD_Y = 31+BUBBLE_RADIUS = 20+BUBBLE_FILL = (11, 14, 19, 240)    # #0b0e13 at 94 %+BUBBLE_BORDER = (230, 237, 243, 90)  # light border, 2 px, 35 %+BUBBLE_BORDER_W = 2+BUBBLE_ROTATE = 3.0                # degrees counter-clockwise (right end higher)++SS = 4  # supersampling factor for vector shapes+++# ================================================================ helpers+def hexrgb(h):+    h = h.lstrip("#")+    return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4))+++def font(role, size):+    """role: display (Familjen Grotesk Bold) | body (Satoshi Medium) | bodybold (Satoshi Bold)."""+    if role == "display":+        f = ImageFont.truetype(os.path.join(FONTS, "FamiljenGrotesk-Variable.ttf"), size)+        f.set_variation_by_name("Bold")+    else:+        f = ImageFont.truetype(os.path.join(FONTS, "Satoshi-Variable.ttf"), size)+        f.set_variation_by_name("Bold" if role == "bodybold" else "Medium")+    return f+++def lerp_stops(stops, t):+    stops = [(p, hexrgb(c) if isinstance(c, str) else c) for p, c in stops]+    for (p0, c0), (p1, c1) in zip(stops, stops[1:]):+        if t <= p1:+            k = 0 if p1 == p0 else (t - p0) / (p1 - p0)+            return tuple(int(round(a + (b - a) * k)) for a, b in zip(c0, c1))+    return stops[-1][1]+++def h_gradient(w, h, stops):+    row = Image.new("RGB", (max(w, 1), 1))+    for x in range(max(w, 1)):+        row.putpixel((x, 0), lerp_stops(stops, x / max(w - 1, 1)))+    return row.resize((max(w, 1), max(h, 1)))+++def radial(size, center, radius, color, opacity):+    """RGBA layer with a soft radial glow."""+    w, h = size+    small = Image.new("L", (w // 8, h // 8), 0)+    d = ImageDraw.Draw(small)+    cx, cy, r = center[0] / 8, center[1] / 8, radius / 8+    steps = 40+    for i in range(steps, 0, -1):+        rr = r * i / steps+        v = int(255 * opacity * (1 - i / steps) ** 1.6)+        d.ellipse((cx - rr, cy - rr, cx + rr, cy + rr), fill=v)+    mask = small.resize((w, h), Image.BICUBIC).filter(ImageFilter.GaussianBlur(6))+    layer = Image.new("RGBA", (w, h), color + (0,))+    layer.putalpha(mask)+    return layer+++def alpha_mul(im, k):+    im = im.copy()+    a = im.getchannel("A").point(lambda v: int(v * k))+    im.putalpha(a)+    return im+++def shadow_of(rgba, blur=18, opacity=0.5, offset=(0, 10), color=(0, 0, 0)):+    a = rgba.getchannel("A")+    pad = blur * 3+    big = Image.new("L", (rgba.width + 2 * pad, rgba.height + 2 * pad), 0)+    big.paste(a, (pad, pad))+    big = big.filter(ImageFilter.GaussianBlur(blur)).point(lambda v: int(v * opacity))+    sh = Image.new("RGBA", big.size, color + (0,))+    sh.putalpha(big)+    return sh, (offset[0] - pad, offset[1] - pad)+++def glow_of(rgba, sigma, opacity, color):+    return shadow_of(rgba, blur=sigma, opacity=opacity, offset=(0, 0), color=color)+++# ================================================================ background+def brand_background(glow_left=0.55, glow_right=0.5, grid=True):+    """Navy base, 40 px grid, teal glow bottom-left, purple glow bottom-right."""+    bg = Image.new("RGBA", (W, H), NAVY + (255,))+    # slight vertical lift toward the top (#0c0e1a measured top-left)+    top = Image.new("RGBA", (W, H), (12, 14, 26, 0))+    m = Image.linear_gradient("L").resize((W, H)).point(lambda v: int((255 - v) * 0.5))+    top.putalpha(m)+    bg = Image.alpha_composite(bg, top)+    if grid:+        g = Image.new("RGBA", (W, H), (0, 0, 0, 0))+        d = ImageDraw.Draw(g)+        for x in range(0, W, 40):+            d.line((x, 0, x, H), fill=(255, 255, 255, 11))+        for y in range(0, H, 40):+            d.line((0, y, W, y), fill=(255, 255, 255, 11))+        bg = Image.alpha_composite(bg, g)+    bg = Image.alpha_composite(bg, radial((W, H), (-60, 800), 700, TEAL, glow_left))+    bg = Image.alpha_composite(bg, radial((W, H), (1340, 820), 640, PURPLE, glow_right))+    return bg+++def photo_background(path, crop=None, blur=0, darken=0.35, scrim="left", scrim_strength=0.85):+    im = Image.open(path).convert("RGB")+    if crop:+        im = im.crop(tuple(crop))+    # cover-fit+    s = max(W / im.width, H / im.height)+    im = im.resize((math.ceil(im.width * s), math.ceil(im.height * s)), Image.LANCZOS)+    l, t = (im.width - W) // 2, (im.height - H) // 2+    im = im.crop((l, t, l + W, t + H))+    if blur:+        im = im.filter(ImageFilter.GaussianBlur(blur))+    im = im.convert("RGBA")+    if darken:+        im = Image.alpha_composite(im, Image.new("RGBA", (W, H), NAVY + (int(255 * darken),)))+    if scrim in ("left", "right"):+        g = Image.linear_gradient("L").rotate(90 if scrim == "left" else -90, expand=True).resize((W, H))+        g = g.point(lambda v: int(v * scrim_strength))+        sc = Image.new("RGBA", (W, H), NAVY + (0,))+        sc.putalpha(g)+        im = Image.alpha_composite(im, sc)+    im = Image.alpha_composite(im, radial((W, H), (-60, 800), 700, TEAL, 0.45))+    im = Image.alpha_composite(im, radial((W, H), (1340, 820), 640, PURPLE, 0.4))+    return im+++# ================================================================ layers+def place_image(canvas, L):+    im = Image.open(L["path"]).convert("RGBA")+    if L.get("crop"):+        im = im.crop(tuple(L["crop"]))+    if L.get("flip"):+        im = im.transpose(Image.FLIP_LEFT_RIGHT)+    x, y, w = L["box"][:3]+    h = round(im.height * w / im.width)+    im = im.resize((round(w), h), Image.LANCZOS)+    if L.get("rotate"):+        im = im.rotate(L["rotate"], resample=Image.BICUBIC, expand=True)+    if L.get("fade_bottom"):+        fb = int(L["fade_bottom"])+        a = im.getchannel("A")+        g = Image.linear_gradient("L").resize((im.width, fb)).point(lambda v: 255 - v)+        mask = Image.new("L", im.size, 255)+        mask.paste(g, (0, im.height - fb))+        im.putalpha(ImageChops.multiply(a, mask))+    if L.get("glow"):+        gl, off = glow_of(im, 14, 0.45, hexrgb(L["glow"]) if isinstance(L["glow"], str) else ACCENT)+        canvas.alpha_composite(gl, (int(x + off[0]), int(y + off[1])))+    if L.get("shadow", True):+        sh, off = shadow_of(im, blur=L.get("shadow_blur", 18), opacity=L.get("shadow_opacity", 0.55))+        canvas.alpha_composite(sh, (int(x + off[0]), int(y + off[1])))+    canvas.alpha_composite(im, (int(x), int(y)))+    return (x, y, x + im.width, y + h)+++def _persp_coeffs(dst, src):+    import numpy as np+    A, B = [], []+    for (x, y), (u, v) in zip(dst, src):+        A.append([x, y, 1, 0, 0, 0, -u * x, -u * y]); B.append(u)+        A.append([0, 0, 0, x, y, 1, -v * x, -v * y]); B.append(v)+    return list(np.linalg.solve(np.array(A, float), np.array(B, float)))+++def place_screen(canvas, L):+    """App screenshot mapped onto a quad (tl, tr, br, bl), rounded, bordered, optional teal edge glow."""+    im = Image.open(L["path"]).convert("RGBA")+    if L.get("crop"):+        im = im.crop(tuple(L["crop"]))+    r = int(L.get("radius", 14) * im.width / 900)+    mask = Image.new("L", im.size, 0)+    ImageDraw.Draw(mask).rounded_rectangle((0, 0, im.width - 1, im.height - 1), r, fill=255)+    im.putalpha(mask)+    bd = ImageDraw.Draw(im)+    bd.rounded_rectangle((0, 0, im.width - 1, im.height - 1), r, outline=(48, 54, 61, 255),+                         width=max(2, im.width // 400))+    if L.get("dim"):+        dim = Image.new("RGBA", im.size, NAVY + (int(255 * L["dim"]),))+        dim.putalpha(ImageChops.multiply(dim.getchannel("A"), mask))+        im = Image.alpha_composite(im, dim)+    quad = [tuple(p) for p in L["quad"]]+    src = [(0, 0), (im.width, 0), (im.width, im.height), (0, im.height)]+    out = im.transform((W, H), Image.PERSPECTIVE, _persp_coeffs(quad, src), Image.BICUBIC)+    if L.get("glow", True):+        gl, off = glow_of(out, 16, 0.55, ACCENT)+        canvas.alpha_composite(gl, off)+    sh, off = shadow_of(out, blur=24, opacity=0.6, offset=(0, 14))+    canvas.alpha_composite(sh, off)+    canvas.alpha_composite(out)+    xs, ys = [p[0] for p in quad], [p[1] for p in quad]+    return (min(xs), min(ys), max(xs), max(ys))+++def draw_headline(canvas, L):+    """Big Familjen Grotesk Bold headline. Last line gradient. Optional kicker above and sub below."""+    lines = L["lines"]+    size = L.get("size", 120)+    maxw = L.get("max_width")+    f = font("display", size)+    if maxw:+        while size > 60 and max(f.getbbox(t)[2] - f.getbbox(t)[0] for t in lines) > maxw:+            size -= 2+            f = font("display", size)+    pitch = round(size * L.get("leading", 0.96))+    x0, y0 = L.get("x", 52), L.get("y", 220)        # y = top of the first line's cap height+    align = L.get("align", "left")+    cap = round(size * 0.65)                         # Familjen cap height = 780/1200 em+    asc = f.getmetrics()[0]+    boxes = []+    layer = Image.new("RGBA", (W, H), (0, 0, 0, 0))+    ky = y0+    if L.get("kicker"):+        kf = font("display", L.get("kicker_size", 40))+        txt = L["kicker"].upper()+        track = round(L.get("kicker_size", 40) * 0.14)+        kx = x0+        widths = [kf.getlength(ch) + track for ch in txt]+        total = sum(widths) - track+        if align == "right":+            kx = x0 - total+        d = ImageDraw.Draw(layer)+        kc = round(L.get("kicker_size", 40) * 0.65)+        kyy = y0 - kc - L.get("kicker_gap", 34)+        cx = kx+        for ch, wch in zip(txt, widths):+            d.text((cx, kyy + kc), ch, font=kf, fill=ACCENT + (255,), anchor="ls")+            cx += wch+        boxes.append((kx, kyy, kx + total, kyy + kc))+    for i, t in enumerate(lines):+        base = y0 + cap + i * pitch+        bb = f.getbbox(t, anchor="ls")+        tw = bb[2] - bb[0]+        lx = x0 - tw if align == "right" else x0+        last = i == len(lines) - 1+        m = Image.new("L", (W, H), 0)+        ImageDraw.Draw(m).text((lx - bb[0], base), t, font=f, fill=255, anchor="ls")+        tb = m.getbbox()+        if not tb:+            continue+        if last and L.get("gradient", True):+            g = Image.new("RGBA", (W, H), (0, 0, 0, 0))+            gw = tb[2] - tb[0]+            g.paste(h_gradient(gw, tb[3] - tb[1], L.get("stops", HEADLINE_GRADIENT)), (tb[0], tb[1]))+            g.putalpha(m)+        else:+            g = Image.new("RGBA", (W, H), (255, 255, 255, 0))+            g.putalpha(m)+        layer = Image.alpha_composite(layer, g)+        boxes.append(tb)+    if L.get("sub"):+        sf = font("display", L.get("sub_size", 46))+        parts = L["sub"]  # list of [text, "white"|"teal"]+        if isinstance(parts, str):+            parts = [[parts, "white"]]+        sy = y0 + cap + (len(lines) - 1) * pitch + L.get("sub_gap", 64)+        sx = x0+        d = ImageDraw.Draw(layer)+        total = sum(sf.getlength(p[0]) for p in parts)+        if align == "right":+            sx = x0 - total+        start = sx+        for txt, col in parts:+            d.text((sx, sy), txt, font=sf, fill=(ACCENT if col == "teal" else (255, 255, 255)) + (255,), anchor="ls")+            sx += sf.getlength(txt)+        boxes.append((start, sy - round(L.get("sub_size", 46) * 0.72), sx, sy + round(L.get("sub_size", 46) * 0.2)))+    if L.get("shadow", True):+        sh, off = shadow_of(layer, blur=14, opacity=L.get("shadow_opacity", 0.45), offset=(0, 4))+        canvas.alpha_composite(sh, off)+    canvas.alpha_composite(layer)+    bx = (min(b[0] for b in boxes), min(b[1] for b in boxes), max(b[2] for b in boxes), max(b[3] for b in boxes))+    return bx+++# ---------------------------------------------------------------- arrow+def _bezier(p0, p1, bend, n=200, control=None):+    (x0, y0), (x1, y1) = p0, p1+    dx, dy = x1 - x0, y1 - y0+    length = math.hypot(dx, dy)+    nx, ny = -dy / length, dx / length          # screen-space normal, right of travel+    cx, cy = (x0 + x1) / 2 + nx * bend * length, (y0 + y1) / 2 + ny * bend * length+    if control is not None:                      # explicit control point (e.g. a hook)+        cx, cy = control+    pts = []+    for i in range(n + 1):+        t = i / n+        pts.append(((1 - t) ** 2 * x0 + 2 * (1 - t) * t * cx + t * t * x1,+                    (1 - t) ** 2 * y0 + 2 * (1 - t) * t * cy + t * t * y1))+    return pts+++def arrow_layer(size, start, end, bend=0.25, scale=1.0, color=ARROW_COLOR, glow=True, control=None):+    """Return an RGBA layer (size) with the house arrow from start to end.++    bend: signed curvature, control point offset as a fraction of the chord length+          (0 = straight; 0.2 to 0.35 is the house range). Positive bulges to the+          RIGHT of the direction of travel: a left-to-right arrow with bend +0.3 sags+          downward like the KiCad thumbnail; a top-to-bottom arrow with bend -0.3+          bulges right like the radio thumbnail's hook.+    scale: 1.0 = 11 px stroke at 1280x720.+    control: optional explicit quadratic control point; overrides bend. For the radio-style+          hook use control = (end_x, start_y): it leaves horizontally and lands vertically.+    """+    w, h = size+    sw = ARROW_STROKE * scale+    hl, hw = ARROW_HEAD_L * sw, ARROW_HEAD_W * sw+    pts = _bezier(start, end, bend, control=control)+    # end tangent from the last segment+    ex, ey = end+    k = len(pts) - 1+    while k > 0 and math.hypot(ex - pts[k][0], ey - pts[k][1]) < hl * 0.9:+        k -= 1+    tx, ty = ex - pts[k][0], ey - pts[k][1]+    tl = math.hypot(tx, ty) or 1+    ux, uy = tx / tl, ty / tl+    bx, by = ex - ux * hl, ey - uy * hl                     # head base center+    stroke_pts = pts[:k + 1] + [(bx + ux * sw * 0.6, by + uy * sw * 0.6)]+    big = Image.new("L", (w * SS, h * SS), 0)+    d = ImageDraw.Draw(big)+    r = sw * SS / 2+    sp = [(x * SS, y * SS) for x, y in stroke_pts]+    # dense discs along the path: uniform width, round caps, smooth edges+    dense = []+    for (xa, ya), (xb, yb) in zip(sp, sp[1:]):+        n = max(1, int(math.hypot(xb - xa, yb - ya) / (r * 0.25)))+        dense += [(xa + (xb - xa) * i / n, ya + (yb - ya) * i / n) for i in range(n)]+    dense.append(sp[-1])+    for x, y in dense:+        d.ellipse((x - r, y - r, x + r, y + r), fill=255)+    px, py = -uy, ux+    tri = [(ex, ey), (bx + px * hw / 2, by + py * hw / 2), (bx - px * hw / 2, by - py * hw / 2)]+    # rounded corners: shrink the triangle toward its centroid, draw, then dilate by the rounding radius+    rr = ARROW_HEAD_ROUND * sw+    cxm, cym = sum(p[0] for p in tri) / 3, sum(p[1] for p in tri) / 3+    tri2 = []+    for x, y in tri:+        vx, vy = x - cxm, y - cym+        vl = math.hypot(vx, vy)+        s = (vl - rr * 2.0) / vl+        tri2.append(((cxm + vx * s) * SS, (cym + vy * s) * SS))+    head = Image.new("L", big.size, 0)+    ImageDraw.Draw(head).polygon(tri2, fill=255)+    head = head.filter(ImageFilter.MaxFilter(int(rr * SS * 2) | 1))+    big = ImageChops.lighter(big, head)+    mask = big.resize((w, h), Image.LANCZOS)+    out = Image.new("RGBA", (w, h), color + (0,))+    out.putalpha(mask)+    if glow:+        g = mask.filter(ImageFilter.GaussianBlur(ARROW_GLOW_SIGMA * scale)).point(lambda v: int(min(255, v * ARROW_GLOW_OPACITY * 1.6)))+        gl = Image.new("RGBA", (w, h), color + (0,))+        gl.putalpha(g)+        out = Image.alpha_composite(gl, out)+    return out+++def draw_arrow(canvas, L):+    lay = arrow_layer((W, H), tuple(L["from"]), tuple(L["to"]), L.get("bend", 0.25), L.get("scale", 1.0),+                      control=tuple(L["control"]) if L.get("control") else None)+    canvas.alpha_composite(lay)+    return lay.getchannel("A").point(lambda v: 255 if v > 60 else 0).getbbox()+++# ---------------------------------------------------------------- prompt bubble+def bubble_layer(text, scale=1.0, rotate=BUBBLE_ROTATE):+    """Dark pill with a thin light border and the prompt in curly quotes. Returns RGBA (with shadow)."""+    t = text.strip().strip('"“”')+    if not t.endswith((".", "?")):+        t += "."+    t = "“" + t + "”"+    fs = round(BUBBLE_FONT_SIZE * scale)+    f = font("bodybold", fs * SS)+    tw = f.getlength(t)+    padx, pady = BUBBLE_PAD_X * scale * SS, BUBBLE_PAD_Y * scale * SS+    bw = int(tw + 2 * padx)+    bh = int(fs * SS * BUBBLE_HEIGHT + 2 * pady)+    m = int(40 * scale * SS)+    im = Image.new("RGBA", (bw + 2 * m, bh + 2 * m), (0, 0, 0, 0))+    d = ImageDraw.Draw(im)+    box = (m, m, m + bw, m + bh)+    rad = BUBBLE_RADIUS * scale * SS+    d.rounded_rectangle(box, rad, fill=BUBBLE_FILL)+    d.rounded_rectangle(box, rad, outline=BUBBLE_BORDER, width=int(BUBBLE_BORDER_W * scale * SS))+    # optical vertical centre on cap height+    cap = fs * SS * 0.70+    d.text((m + padx, m + bh / 2 + cap / 2), t, font=f, fill=(255, 255, 255, 255), anchor="ls")+    im = im.resize((im.width // SS, im.height // SS), Image.LANCZOS)+    if rotate:+        im = im.rotate(rotate, resample=Image.BICUBIC, expand=True)+    sh, off = shadow_of(im, blur=int(16 * scale), opacity=0.55, offset=(0, int(6 * scale)))+    base = Image.new("RGBA", sh.size, (0, 0, 0, 0))+    base.alpha_composite(sh)+    base.alpha_composite(im, (-off[0], -off[1]))+    # return the layer plus the position of the pill's own top-left inside it+    return base, (-off[0], -off[1]), im.size+++def draw_bubble(canvas, L):+    lay, inner, isz = bubble_layer(L["text"], L.get("scale", 1.0), L.get("rotate", BUBBLE_ROTATE))+    cx, cy = L["center"]+    x = int(cx - isz[0] / 2 - inner[0])+    y = int(cy - isz[1] / 2 - inner[1])+    canvas.alpha_composite(lay, (x, y)) if x >= 0 and y >= 0 else _paste_clip(canvas, lay, x, y)+    m = int(36 * L.get("scale", 1.0))+    return (cx - isz[0] / 2 + m, cy - isz[1] / 2 + m, cx + isz[0] / 2 - m, cy + isz[1] / 2 - m)+++def _paste_clip(canvas, lay, x, y):+    tmp = Image.new("RGBA", canvas.size, (0, 0, 0, 0))+    tmp.paste(lay, (x, y))+    canvas.alpha_composite(tmp)+++# ---------------------------------------------------------------- starburst badge+def draw_badge(canvas, L):+    cx, cy = L["center"]+    r = L.get("r", 130)+    pts_n = L.get("points", 22)+    big = Image.new("RGBA", (W * 2, H * 2), (0, 0, 0, 0))+    d = ImageDraw.Draw(big)+    poly = []+    for i in range(pts_n * 2):+        a = math.pi * i / pts_n + math.radians(L.get("rotate", -12))+        rr = r if i % 2 == 0 else r * 0.86+        poly.append(((cx + rr * math.cos(a)) * 2, (cy + rr * math.sin(a)) * 2))+    d.polygon(poly, fill=ACCENT + (255,))+    lay = big.resize((W, H), Image.LANCZOS)+    sh, off = shadow_of(lay, blur=16, opacity=0.5, offset=(0, 8))+    canvas.alpha_composite(sh, off)+    canvas.alpha_composite(lay)+    lines = L.get("lines", ["1st", "EVER"])+    txt = Image.new("RGBA", (W, H), (0, 0, 0, 0))+    td = ImageDraw.Draw(txt)+    f1 = font("display", int(r * 0.78))+    f2 = font("display", int(r * 0.30))+    td.text((cx, cy + r * 0.12), lines[0], font=f1, fill=NAVY + (255,), anchor="ms")+    if len(lines) > 1:+        td.text((cx, cy + r * 0.52), lines[1], font=f2, fill=NAVY + (255,), anchor="ms")+    txt = txt.rotate(L.get("text_rotate", 12), center=(cx, cy), resample=Image.BICUBIC)+    canvas.alpha_composite(txt)+    return (cx - r, cy - r, cx + r, cy + r)+++def draw_text(canvas, L):+    f = font(L.get("font", "display"), L.get("size", 40))+    d = ImageDraw.Draw(canvas)+    col = L.get("color", "#ffffff")+    d.text(tuple(L["at"]), L["text"], font=f, fill=hexrgb(col) + (255,), anchor=L.get("anchor", "ls"))+    return d.textbbox(tuple(L["at"]), L["text"], font=f, anchor=L.get("anchor", "ls"))+++def draw_orbital(canvas, side):+    o = Image.open(os.path.join(ASSETS, "adom-orbital-white-512.png")).convert("RGBA")+    o = o.resize((ORBITAL_SIZE, ORBITAL_SIZE), Image.LANCZOS)+    x = ORBITAL_MARGIN_X if side == "left" else W - ORBITAL_MARGIN_X - ORBITAL_SIZE+    sh, off = shadow_of(o, blur=8, opacity=0.35, offset=(0, 2))+    canvas.alpha_composite(sh, (x + off[0], ORBITAL_MARGIN_Y + off[1]))+    canvas.alpha_composite(o, (x, ORBITAL_MARGIN_Y))+    return (x, ORBITAL_MARGIN_Y, x + ORBITAL_SIZE, ORBITAL_MARGIN_Y + ORBITAL_SIZE)+++DRAW = {"image": place_image, "screen": place_screen, "headline": draw_headline,+        "arrow": draw_arrow, "bubble": draw_bubble, "badge": draw_badge, "text": draw_text}+CHECKED = {"headline", "bubble", "badge", "text"}   # always must stay out of the zones+++# ================================================================ compose / check+def compose(spec):+    bg = spec.get("background", {"type": "brand"})+    if bg.get("type", "brand") == "brand":+        canvas = brand_background(bg.get("glow_left", 0.55), bg.get("glow_right", 0.5), bg.get("grid", True))+    else:+        canvas = photo_background(bg["path"], bg.get("crop"), bg.get("blur", 0), bg.get("darken", 0.35),+                                  bg.get("scrim", "left"), bg.get("scrim_strength", 0.85))+    important = []+    for L in spec.get("layers", []):+        box = DRAW[L["type"]](canvas, L)+        if L["type"] in CHECKED or L.get("important"):+            important.append((L.get("name", L["type"]), L.get("important_box", box)))+    side = spec.get("orbital", "left")+    obox = draw_orbital(canvas, side)+    important.append(("orbital", obox))+    return canvas.convert("RGB"), important+++def _intersects(a, b):+    return a[0] < b[2] and a[2] > b[0] and a[1] < b[3] and a[3] > b[1]+++def violations(important):+    out = []+    for name, b in important:+        if b is None:+            continue+        if _intersects(b, X_ZONE):+            out.append(f"{name} {tuple(int(v) for v in b)} overlaps the X title zone {X_ZONE}")+        if _intersects(b, YT_ZONE):+            out.append(f"{name} {tuple(int(v) for v in b)} overlaps the YouTube duration zone {YT_ZONE}")+        if name != "orbital" and (b[0] < EDGE_MARGIN or b[1] < EDGE_MARGIN or b[2] > W - EDGE_MARGIN or b[3] > H - EDGE_MARGIN):+            out.append(f"{name} {tuple(int(v) for v in b)} is within {EDGE_MARGIN} px of the edge")+    return out+++def check_overlay(img, important=None):+    """1280x720 thumbnail + zone overlay, with a 320x180 preview strip underneath."""+    img = img.convert("RGBA").resize((W, H))+    ov = Image.new("RGBA", (W, H), (0, 0, 0, 0))+    d = ImageDraw.Draw(ov)+    lab = font("bodybold", 22)+    # X zone: red hatch+    d.rectangle(X_ZONE, fill=(255, 40, 60, 70), outline=(255, 40, 60, 255), width=3)+    for k in range(-H, W, 28):+        d.line((X_ZONE[0] + k, X_ZONE[3], X_ZONE[0] + k + (X_ZONE[3] - X_ZONE[1]), X_ZONE[1]), fill=(255, 40, 60, 90), width=2)+    ov2 = Image.new("RGBA", (W, H), (0, 0, 0, 0))+    ov2.paste(ov.crop(X_ZONE), X_ZONE[:2])+    ov = ov2+    d = ImageDraw.Draw(ov)+    d.rectangle(X_ZONE, outline=(255, 40, 60, 255), width=3)+    d.text((X_ZONE[0] + 12, X_ZONE[1] + 10), "X title pill: keep clear (0,562)-(768,720)", font=lab, fill=(255, 255, 255, 255))+    d.rectangle(YT_ZONE, fill=(255, 160, 0, 90), outline=(255, 160, 0, 255), width=3)+    d.text((YT_ZONE[0] + 10, YT_ZONE[1] + 10), "YT 12:34", font=lab, fill=(255, 255, 255, 255))+    d.rectangle((EDGE_MARGIN, EDGE_MARGIN, W - EDGE_MARGIN, H - EDGE_MARGIN), outline=(255, 255, 255, 60), width=1)+    for xo in (ORBITAL_MARGIN_X, W - ORBITAL_MARGIN_X - ORBITAL_SIZE):+        d.rectangle((xo, ORBITAL_MARGIN_Y, xo + ORBITAL_SIZE, ORBITAL_MARGIN_Y + ORBITAL_SIZE), outline=(0, 230, 220, 200), width=2)+    for name, b in (important or []):+        if b:+            d.rectangle(tuple(int(v) for v in b), outline=(255, 255, 0, 200), width=2)+            d.text((b[0] + 4, b[1] + 2), name, font=font("bodybold", 16), fill=(255, 255, 0, 255))+    top = Image.alpha_composite(img, ov).convert("RGB")+    sheet = Image.new("RGB", (W, H + 200), (8, 10, 14))+    sheet.paste(top, (0, 0))+    small = img.convert("RGB").resize((320, 180), Image.LANCZOS)+    sheet.paste(small, (10, H + 10))+    # simulated X card pill over a second small copy+    xs = small.copy()+    xd = ImageDraw.Draw(xs, "RGBA")+    xd.rounded_rectangle((8, 180 - 34, 8 + 190, 180 - 10), 6, fill=(0, 0, 0, 170))+    xd.text((16, 180 - 30), "youtube.com  Video title", font=font("body", 13), fill=(255, 255, 255, 255))+    sheet.paste(xs, (350, H + 10))+    d2 = ImageDraw.Draw(sheet)+    d2.text((700, H + 30), "Left: 320x180 (search size).", font=font("body", 22), fill=(230, 237, 243))+    d2.text((700, H + 62), "Middle: X card with its title pill.", font=font("body", 22), fill=(230, 237, 243))+    d2.text((700, H + 94), "Red: X pill zone. Orange: YT duration.", font=font("body", 22), fill=(230, 237, 243))+    d2.text((700, H + 126), "Yellow: checked elements. Teal: orbital slots.", font=font("body", 22), fill=(230, 237, 243))+    return sheet+++def save_thumb(img, path):+    if path.lower().endswith((".jpg", ".jpeg")):+        q = 92+        while True:+            img.save(path, quality=q, optimize=True, progressive=True)+            if os.path.getsize(path) < 2_000_000 or q <= 70:+                break+            q -= 4+    else:+        img.save(path, optimize=True)+    return os.path.getsize(path)+++def write_arrow_assets(outdir):+    os.makedirs(outdir, exist_ok=True)+    presets = {+        # name: canvas size, start, end, bend  (KiCad-style swoosh, radio-style hook, straight, short)+        "arrow-swoosh-right": ((360, 170), (30, 40), (320, 60), 0.30),+        "arrow-hook-down": ((200, 320), (30, 40), (150, 290), (150, 40)),+        "arrow-curve-left": ((360, 170), (330, 40), (40, 60), -0.30),+        "arrow-straight-right": ((300, 120), (30, 60), (270, 60), 0.0),+    }+    for name, (size, s, e, b) in presets.items():+        lay = arrow_layer(size, s, e, control=b) if isinstance(b, tuple) else arrow_layer(size, s, e, b)+        lay.save(os.path.join(outdir, name + ".png"), optimize=True)+        print("wrote", os.path.join(outdir, name + ".png"))+++def main():+    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)+    ap.add_argument("spec", nargs="?", help="JSON spec file (spec mode)")+    ap.add_argument("-o", "--out", help="output .jpg or .png")+    ap.add_argument("--bg", default="brand", help="'brand' or a background frame image")+    ap.add_argument("--bg-mode", choices=["visual", "photo"], default="visual",+                    help="visual: frame as a tilted screen on the right; photo: full bleed with a scrim")+    ap.add_argument("--cutout", help="transparent PNG of a presenter or board, placed right")+    ap.add_argument("--headline", help="lines separated by |, last line gradient, end with a period")+    ap.add_argument("--kicker", help="small tracked teal caps above the headline")+    ap.add_argument("--orbital", choices=["left", "right"], default="left")+    ap.add_argument("--check", action="store_true", help="also write <out>-xcheck.png")+    ap.add_argument("--check-only", help="write the zone overlay for an existing image")+    ap.add_argument("--arrow-assets", help="render the standard arrow PNGs into this folder")+    a = ap.parse_args()++    if a.arrow_assets:+        write_arrow_assets(a.arrow_assets)+        return+    if a.check_only:+        out = a.out or os.path.splitext(a.check_only)[0] + "-xcheck.png"+        check_overlay(Image.open(a.check_only)).save(out)+        print("wrote", out)+        return+    if a.spec:+        spec = json.load(open(a.spec))+        base = os.path.dirname(os.path.abspath(a.spec))+        for L in spec.get("layers", []):+            if "path" in L and not os.path.isabs(L["path"]):+                L["path"] = os.path.join(base, L["path"])+        bgs = spec.get("background", {})+        if "path" in bgs and not os.path.isabs(bgs["path"]):+            bgs["path"] = os.path.join(base, bgs["path"])+    else:+        if not (a.headline and a.out):+            ap.error("quick mode needs --headline and -o")+        left = a.orbital == "left"+        spec = {"orbital": a.orbital, "layers": []}+        if a.bg != "brand" and a.bg_mode == "photo":+            spec["background"] = {"type": "photo", "path": a.bg, "scrim": "left" if left else "right"}+        else:+            spec["background"] = {"type": "brand"}+            if a.bg != "brand":+                quad = [[690, 90], [1300, 60], [1300, 560], [690, 520]] if left else \+                       [[-20, 60], [590, 90], [590, 520], [-20, 560]]+                spec["layers"].append({"type": "screen", "path": a.bg, "quad": quad, "dim": 0.15})+        if a.cutout:+            spec["layers"].append({"type": "image", "path": a.cutout,+                                   "box": [700, 140, 560] if left else [20, 140, 560]})+        lines = a.headline.split("|")+        spec["layers"].append({"type": "headline", "lines": lines, "kicker": a.kicker,+                               "x": 52 if left else 1228, "align": "left" if left else "right",+                               "y": 250 if len(lines) <= 2 else 200, "size": 120, "max_width": 620})+    img, important = compose(spec)+    size = save_thumb(img, a.out)+    print(f"wrote {a.out} ({size/1e6:.2f} MB, {img.size[0]}x{img.size[1]})")+    probs = violations(important)+    for p in probs:+        print("SAFE-ZONE:", p)+    if not probs:+        print("safe zones: clear")+    if a.check:+        xc = os.path.splitext(a.out)[0] + "-xcheck.png"+        check_overlay(img, important).save(xc, optimize=True)+        print("wrote", xc)+++if __name__ == "__main__":+    main()
skills/adom-youtube-titles/SKILL.mdadded+189
@@ -0,0 +1,189 @@+---+name: adom-youtube-titles+description: >-+  The authority on titles for Adom's YouTube channel (@adominc): the `AI <verb> <what> in <tool>`+  formula, Title Case, 30 to 55 characters, colon and series (Part N) rules, tool-name keyword+  placement, how the title pairs with the thumbnail headline, good and bad examples, a checklist,+  and a "generate 5 options, best first" procedure. Use when writing, choosing, or checking the+  title of an Adom video. Descriptions, chapters, tags and thumbnail style live in+  adom-youtube-descriptions. Trigger words: youtube title, video title, title ideas, name the+  video, title options, rename the video, what should we call the video.+---++Parent skill: **video-editing-skillpack**++# Adom YouTube titles++Channel: https://www.youtube.com/@adominc (Adom Industries, `UCiWS0GITCbr3I-BDRbF4zkg`).+This skill owns the title. The description, chapters, tags and thumbnail style are in+`adom-youtube-descriptions`; posting identity and cadence are in `social-media-skillpack`.++## What the channel shows (October 2026, 4 long-form videos)++Data: `/home/adom/youtube-research/adominc_videos.json`.++| Date | Title | Chars | Views (Oct 21) | Leads with AI |+|---|---|---|---|---|+| 2026-09-29 | Introducing Hydrogen - Adom Industries | 38 | 1,285 | no |+| 2026-10-03 | AI PCB Design for Engineers | 27 | 9,679 | yes |+| 2026-10-09 | AI Designed This Radio Circuit Board | 36 | 7,935 | yes |+| 2026-10-11 | 1st Ever: AI Designs a Complete PCB in Fusion 360 | 49 | 9,451 | yes |++The three titles that lead with "AI" and name a concrete result each drew 6 to 7 times the views+of the product-name title with a brand suffix. That title ("Introducing Hydrogen - Adom+Industries") is the pattern to avoid: it names a product nobody searches for yet and spends 17+characters on the channel name, which YouTube already shows under every title.++## The formula++**`AI <verb> <what> in <tool>`**++- **AI**: first word. It is the search term and the promise. Exception: a series prefix or a short+  factual opener goes before it (see Colons).+- **verb**: present tense for a demo of the process (`Designs`, `Routes`, `Builds`, `Simulates`,+  `Places`, `Fixes`). Past tense with "This" for a finished-object reveal (`AI Designed This Radio+  Circuit Board`). Pick one; never "Is Designing" or "Can Design".+- **what**: the concrete thing made, as a searcher would name it: `a PCB`, `a Buck Converter`,+  `an AM/FM Radio Board`, `a 4-Layer Board`, `a USB-C Power Supply`. Prefer the circuit or board+  over the chip part number (part numbers go in the description and tags, not the title).+- **in &lt;tool&gt;**: the EDA or CAD tool, spelled the way people search it: `KiCad`,+  `Fusion 360`, `Altium Designer`, `LTspice`. Omit only when no single tool is the point (the+  radio video).++Variations that still follow the formula:+- Topic form when the video is a how-to or overview: `AI PCB Design for Engineers`,+  `AI PCB Design in Altium Designer`.+- Reveal form: `AI Designed This <what>`.++## Hard rules++| Rule | Detail |+|---|---|+| Length | 30 to 55 characters, spaces included. Count with `printf '%s' "<title>" \| wc -m`. Under 30 wastes the space; over 55 is cut off in search results and on phones (about 60 shows in full on desktop, less on mobile). |+| Case | Title Case. Small words lowercase unless first: a, an, the, and, or, for, in, on, of, to, with. Capitalize verbs and "Is", "This". |+| Acronyms and names | As written by their owners: AI, PCB, KiCad, Fusion 360, Altium Designer, SPICE, LTspice, ngspice, USB-C, AM/FM, MCP. Never "Kicad", "Fusion360", "Pcb". |+| Separators | A colon only, and only after a short factual opener (see below). Never `\|`, never ` - `, no em or en dashes. |+| Brand | No brand suffix (` - Adom Industries`, `\| Adom`). "Adom" may appear only when the Adom product is itself the subject and people will search for it, and then not as a suffix. |+| Voice | Plain, precise, no hype. No exclamation marks, no emoji, no ALL CAPS words, no question bait ("Can AI Really...?"), no "INSANE", "Game Changer", "Revolutionary", "You Won't Believe", "Mind-Blowing", "Ultimate", "Secret". |+| Claims | "1st Ever", "First" only when checkable (we searched YouTube and found none). Numbers only when true and shown in the video. |+| Punctuation | No period at the end. Numerals for numbers (`3`, `4-Layer`, `2 Hours`), `1st` is fine in an opener. |++## Colons++A colon is allowed only after a short factual opener of 1 to 2 words that a viewer can verify:++- `1st Ever:` (a checkable first)+- `Part 3:` (a series position)+- `Live:` (a live recording)++Not allowed: a colon used to stack a second headline (`AI PCB Design: The Future Is Here`), or a+tool name before the colon (`Altium: AI Designs a PCB`). Put the tool at the end after "in".++## Series naming (Part N)++Use a series when videos rebuild the same thing in different tools or continue one project (the+TPS54202 buck board: KiCad, then Fusion 360, then Altium Designer).++- Format: `Part N: AI <verb> <what> in <tool>`. The title must still make sense alone; a viewer+  from search has not seen Part 1.+- Put the Part number in the title only when the series is the reason to watch. Otherwise leave it+  out of the title and link the other parts in the description's Related lines (that is what the+  descriptions skill does).+- Keep the series wording parallel so the titles line up in a playlist:+  `Part 1: AI Designs a Buck Converter in KiCad`, `Part 2: AI Designs a Buck Converter in Fusion 360`,+  `Part 3: AI Designs a Buck Converter in Altium Designer`.+- When an earlier part was published without "Part 1", do not rename it after it has views; use a+  playlist named after the series instead.++## Keywords and search++- The tool name is the strongest keyword. It goes in the title, near the end, after "in", spelled+  as searched: `in KiCad`, `in Fusion 360`, `in Altium Designer`.+- "AI" first, "PCB" (or the circuit name) second where it fits. These are the terms people type:+  "AI PCB design", "AI KiCad", "AI Fusion 360 PCB", "AI Altium".+- One tool per title. If a video spans tools, name the one the video is about and put the rest in+  the description and tags.+- Do not stuff: `AI PCB Design KiCad Altium Fusion 360 Tutorial` reads as spam and loses the click.++## Title and thumbnail headline++They are seen together, so they split the job. The thumbnail headline is 2 to 4 words of punch;+the title carries the searchable facts. Never repeat the title in the thumbnail.++| Title | Thumbnail headline | Why it works |+|---|---|---|+| `1st Ever: AI Designs a Complete PCB in Fusion 360` | "AI PCBs in Fusion 360." | Same subject, the thumbnail is the short read, the title adds the "first" claim. Close to repeating; acceptable, but a headline like "No mouse. No clicks." would add more. |+| `AI Designed This Radio Circuit Board` | "I asked AI for a radio." | Thumbnail gives the human angle (the ask), the title gives the result. |+| `AI PCB Design for Engineers` | "AI Engineered PCBs." | Title states the audience, thumbnail states the outcome. |+| `AI Designs a PCB in Altium Designer` | "Now in Altium." | Thumbnail tells returning viewers it is the next tool; title serves search. |++Rules: the thumbnail headline may end with a period (house style), the title never does. If the+tool name is large in the thumbnail visual (a logo or screenshot), the headline can skip it, but+the title keeps it.++## Examples++Good:++| Title | Chars | Note |+|---|---|---|+| `AI Designs a PCB in Altium Designer` | 35 | The formula, clean. |+| `1st Ever: AI Designs a Complete PCB in Fusion 360` | 49 | Checkable first, tool at the end. |+| `AI Designed This Radio Circuit Board` | 36 | Reveal form, no tool needed. |+| `AI Designs a Buck Converter in KiCad` | 36 | Names the circuit people search. |+| `Part 3: AI Designs a Buck Converter in Altium Designer` | 54 | Series, still reads alone. |+| `AI Routes a 4-Layer Board in KiCad` | 34 | Specific verb and spec. |+| `AI Simulates a Buck Converter in LTspice` | 40 | Tool is the keyword. |+| `AI Designs a USB-C Power Supply in Fusion 360` | 45 | Concrete object plus tool. |+| `AI PCB Design in Altium Designer` | 32 | Topic form for an overview. |+| `AI Builds a Component Library in KiCad` | 38 | Works for a non-board task. |++Bad, with the fix:++| Bad | Problem | Fix |+|---|---|---|+| `Introducing Hydrogen - Adom Industries` | Product name nobody searches, brand suffix, dash | `AI Designs PCBs in KiCad with Adom Hydrogen` (43) |+| `AI PCB Design in Altium \| Adom` | Pipe, brand suffix | `AI PCB Design in Altium Designer` (32) |+| `This AI Designs PCBs!!! (INSANE)` | Exclamation marks, caps, hype | `AI Designs a PCB in KiCad` (25, so add the object: `AI Designs a Buck Converter PCB in KiCad`, 40) |+| `Can AI Really Design a Circuit Board?` | Question bait | `AI Designed This Circuit Board` (30) |+| `The Future of Electronics Is Here: AI PCB Design` | Hype, colon after a non-factual opener | `AI PCB Design for Electrical Engineers` (38) |+| `Altium: AI Designs a PCB` | Tool before the colon | `AI Designs a PCB in Altium Designer` (35) |+| `ai designs a pcb in kicad` | Not Title Case, acronyms wrong | `AI Designs a PCB in KiCad` |+| `AI Designs a 12 V to 5 V 1 A TPS54202 Buck Converter in Altium Designer` | 71 characters, part number in title | `AI Designs a Buck Converter in Altium Designer` (46) |+| `AI PCB` | 6 characters, says nothing | `AI Designs a Complete PCB in Fusion 360` (39) |+| `KiCad, Fusion 360, Now Altium: AI PCB Design` | Three tools, colon after a non-factual list | `Part 3: AI Designs the Same PCB in Altium Designer` (50) |+| `Revolutionary AI Tool Designs Circuit Boards` | Buzzword, no tool | `AI Designs Circuit Boards in Fusion 360` (39) |++## Procedure: generate 5 options, best first++1. Collect the facts: what was built (circuit or board), the tool, any checkable first, series+   position, presenter. Read the project's wiki page or the video draft if there is one.+2. Write 8 to 10 candidates: at least 3 in the plain formula, 1 reveal form ("AI Designed This"),+   1 topic form, 1 with a factual opener (`Part N:` or `1st Ever:`) if one applies.+3. Run the checklist on each and drop any that fail.+4. Rank: formula fit first, then the strongest search keyword (the tool name), then+   specificity, then shortness. A plain `AI Designs <what> in <tool>` almost always ranks first.+5. Present 5, numbered, best first, each with its character count in brackets, and one line on+   why the first one wins. Suggest a thumbnail headline that complements option 1.++Count characters in one go:++```bash+while IFS= read -r t; do printf '%3d  %s\n' "$(printf '%s' "$t" | wc -m)" "$t"; done <<'EOF'+AI Designs a PCB in Altium Designer+Part 3: AI Designs a Buck Converter in Altium Designer+EOF+```++## Checklist++- [ ] Starts with "AI", or with a short factual opener and a colon followed by "AI".+- [ ] Says what was made and, when there is one, the tool after "in", spelled as searched.+- [ ] 30 to 55 characters (counted, not guessed).+- [ ] Title Case; acronyms and product names as their owners write them.+- [ ] No `|`, no ` - `, no em or en dashes, no brand suffix, no period at the end.+- [ ] No exclamation marks, no emoji, no ALL CAPS words, no question bait, no hype words.+- [ ] Any "first" or number is true and checkable.+- [ ] Series title (if any) uses `Part N:` and reads on its own.+- [ ] Thumbnail headline complements it, does not repeat it.+- [ ] After choosing: the same title goes in the description draft and the wiki page link text.
uninstall.shadded+5
@@ -0,0 +1,5 @@+#!/usr/bin/env bash+set -e+DEST="$HOME/.claude/skills"+for s in video-editing-skillpack adom-youtube-titles adom-youtube-descriptions adom-youtube-thumbnails adom-arrows; do rm -rf "$DEST/$s"; done+echo "OK: Removed video-editing-skillpack and its 4 bundled skills."