The Red Demon
YouTube
Statement Begins
A thumbnail, a channel, a runtime, a link. Not much, until the card builds the strip you would see under a player: the still up top, the channel as a byline, the runtime where it belongs.
It keeps the road back to the source, the url and the id, so the note is always one click from the thing itself.
A bookmark that grew a face, out of the few lines I saved when I filed it.
Custom Views Code

<div class="video-card" data-name="{{file.name}}" data-url="{{url}}" data-vid="{{video_id}}">
<!-- Hero: thumbnail + gradient -->
<div class="hero">
<img id="bgImg" class="hero-img" src="{{banner}}" crossorigin="anonymous" alt="" />
<div class="hero-gradient" id="gradientOverlay"></div>
</div>
<!-- Content overlaps hero -->
<div class="content-area">
<div class="info-row">
<div class="movie-info">
<p class="movie-title">{{file.basename}}</p>
<p class="movie-genre vid-channel"></p>
<p class="movie-desc vid-lead"></p>
<div class="meta-row">
<p class="movie-meta vid-meta"></p>
</div>
<a class="vid-watch" target="_blank" rel="noopener">
<span class="vid-play" aria-hidden="true"></span>
<span class="vid-watch-label">Watch<br>on YouTube</span>
</a>
</div>
</div>
<div class="bottom-section">
<div class="file-content-wrap">
<button class="movie-more" type="button">SHOW MORE</button>
<div class="movie-notes"></div>
</div>
</div>
</div>
</div>
/* ============================================================
Harker — shared Movies/Shows card CSS
Used by the Custom Views "Movies" (harker-movies) and "Shows"
(harker-shows) views. Both render .movie-card; this is loaded
once here instead of duplicated in each view's css field.
(Games/Books/Albums have their own gv-/bk-/alb- styles.)
============================================================ */
.obsidian-custom-view-editable .obsidian-custom-view-render, .obsidian-custom-view-render {
padding: 0 !important;
--line-width: 100%;
--max-width: none;
}
.file-content-wrap .markdown-rendered-content.markdown-preview-view.markdown-rendered {
margin: 0;
padding: 0;
}
blockquote {
color: var(--blockquote-color);
font-style: var(--blockquote-font-style);
background-color: var(--blockquote-background-color);
border-inline-start: var(--blockquote-border-thickness) solid var(--blockquote-border-color);
padding-top: 0;
padding-bottom: 0;
padding-inline-start: var(--size-4-6);
margin-inline-start: 0;
margin-inline-end: 0;
}
.cm-content.cm-lineWrapping {
padding-bottom: 40px !important;
}
/* ================================================================
Movie Card — Obsidian Custom View
Faithful translation of Figma node 1:2
================================================================ */
.obsidian-custom-view-render, .obsidian-custom-view-editable .obsidian-custom-view-render {
padding: 0;
}
.movie-card {
background-color: var(--color-bg, #7baaba);
position: relative;
width: 100%;
padding-bottom: 145px;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 10px;
align-items: flex-start;
caret-color: pink;
}
/* ── Hero ──────────────────────────────────────────────────────── */
.hero {
position: absolute;
width: 100%;
flex-shrink: 0;
line-height: 0;
}
.hero-img {
display: block;
width: 100%;
aspect-ratio: 3840 / 2160;
object-fit: cover;
max-width: none;
}
/* Gradient overlay.
Figma: div starting at mt-372px, h-600px, inside 972px hero.
Stop 1 (transparent): 372 + 600×23.5% = 513px from hero top = 52.77% of 972
Stop 2 (solid): 372 + 600×53.08% = 690.5px = 71.04% of 972
Applied as inset-0 so % are of the image height at any width. */
.hero-gradient {
position: absolute;
inset: 0;
background: linear-gradient(to bottom,
transparent 52.77%,
var(--color-bg, #7baaba) 71.04%
);
pointer-events: none;
}
/* ── Content area ──────────────────────────────────────────────── */
/* Figma: absolute top-583px over a 972px hero.
HTML: negative margin = 583px - (image height as % of width).
Image aspect 3840/2160 → height = width × 56.25%.
So margin-top: calc(583px - 56.25%) replicates the overlap at any width. */
.content-area {
margin-top: calc(100% * (6/16));
position: relative;
z-index: 1;
width: 100%;
box-sizing: border-box;
padding: 0 100px 10px;
display: flex;
flex-direction: column;
gap: 20px;
align-items: flex-start;
}
/* ── Info row ──────────────────────────────────────────────────── */
.info-row {
display: flex;
flex-wrap: wrap;
row-gap: 20px;
column-gap: 30px;
align-items: flex-end;
justify-content: space-between;
flex-shrink: 0;
width: 100%;
}
/* Left: title / genre / description / meta */
.movie-info {
display: flex;
flex: 1 0 0;
flex-direction: column;
gap: 12px;
align-items: flex-start;
max-width: 900px;
min-width: 800px;
overflow: clip;
}
.movie-title {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro', sans-serif;
font-size: calc(36px + 1.5vw);
font-weight: 500;
line-height: normal;
color: var(--text-normal);
letter-spacing: -1.28px;
width: 100%;
margin: 0;
word-break: break-word;
}
.movie-genre {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro', sans-serif;
font-size: 24px;
font-weight: 510;
line-height: normal;
color: var(--text-muted);
letter-spacing: -0.48px;
width: 100%;
margin: 0;
word-break: break-word;
}
.movie-genre a {
text-decoration: none;
color: var(--text-muted);
}
.movie-genre a:hover {
text-decoration: none;
color: var(--color-base-80);
}
.movie-desc {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro', sans-serif;
font-size: 18px;
font-weight: 400;
line-height: 1.6;
color: var(--text-muted);
letter-spacing: -0.36px;
width: 100%;
margin: 0;
word-break: break-word;
}
.meta-row {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 100%;
}
.movie-meta {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro', sans-serif;
font-size: 18px;
font-weight: 400;
line-height: normal;
color: var(--text-faint);
letter-spacing: -0.36px;
flex: 1 0 0;
min-width: 1px;
margin: 0;
word-break: break-word;
}
/* Right: Starring / Director */
.starring-col {
display: flex;
flex: 1 0 0;
flex-direction: column;
align-items: flex-start;
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro', sans-serif;
font-size: 16px;
font-weight: 400;
line-height: 1.6;
letter-spacing: -0.32px;
max-width: 400px;
min-width: 400px;
overflow: clip;
padding-top: 29px;
}
.starring-row {
display: flex;
gap: 10px;
align-items: flex-start;
flex-shrink: 0;
width: 100%;
}
.starring-label {
color: var(--text-faint);
white-space: nowrap;
flex-shrink: 0;
margin: 0;
}
.starring-value {
flex: 1 0 0;
min-width: 1px;
color: var(--text-muted);
margin: 0;
}
.starring-value a {
text-decoration: none;
color: var(--text-normal);
}
/* ── Bottom section ────────────────────────────────────────────── */
.bottom-section {
display: flex;
flex-direction: column;
gap: 20px;
align-items: flex-start;
padding-top: 30px;
flex-shrink: 0;
width: 100%;
}
/* Content + Watch Now row */
.content-watch-row {
display: flex;
gap: 30px;
align-items: flex-start;
flex-shrink: 0;
width: 100%;
justify-content: space-between;
}
.file-content-wrap {
flex-shrink: 0;
flex-grow: 1;
width: auto;
max-width: 700px;
}
.file-content-text {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro', sans-serif;
font-size: 18px;
font-weight: 400;
line-height: 1.6;
color: var(--text-muted);
letter-spacing: -0.36px;
flex: 1 0 0;
min-width: 1px;
margin: 0;
word-break: break-word;
}
.watch-col {
display: flex;
flex: 1 0 0;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 1px;
max-width: 400px;
}
.watch-row {
display: flex;
gap: 10px;
align-items: center;
flex-shrink: 0;
width: 100%;
}
.watch-label {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro', sans-serif;
font-size: 22px;
font-weight: 400;
line-height: 1.6;
color: var(--text-normal);
letter-spacing: -0.44px;
flex-shrink: 0;
white-space: nowrap;
margin: 0;
}
/* 4K badge: CSS grid stack (bg rect + text overlapping) */
.badge-4k {
display: inline-grid;
grid-template-columns: max-content;
grid-template-rows: max-content;
line-height: 0;
place-items: start;
flex-shrink: 0;
}
.badge-4k-bg {
grid-column: 1;
grid-row: 1;
background-color: var(--color-base-30);
height: 27px;
width: 44px;
border-radius: 7px;
}
.badge-4k-text {
grid-column: 1;
grid-row: 1;
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro', sans-serif;
font-size: 14px;
font-weight: 510;
line-height: 1.6;
color: var(--text-muted);
letter-spacing: -0.28px;
white-space: nowrap;
margin-left: 13px;
margin-top: 3px;
}
/* ── Trailers ──────────────────────────────────────────────────── */
.trailers-section {
display: flex;
flex-direction: column;
gap: 10px;
align-items: flex-start;
flex-shrink: 0;
width: 100%;
}
.section-heading {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro', sans-serif;
font-size: 24px;
font-weight: 400;
line-height: 1.6;
color: var(--text-normal);
letter-spacing: -0.48px;
white-space: nowrap;
flex-shrink: 0;
margin: 0;
}
.trailers-row {
display: flex;
gap: 20px;
width: 100%;
overflow: scroll;
align-items: flex-start;
flex-shrink: 0;
}
.trailer-card {
position: relative;
height: 159px;
width: 282px;
border-radius: 8px;
flex-shrink: 0;
overflow: hidden;
cursor: pointer;
}
.trailer-thumb {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
max-width: none;
}
.trailer-dark-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.2);
pointer-events: none;
}
/* ── Cast and Crew ─────────────────────────────────────────────── */
.cast-section {
display: flex;
flex-direction: column;
gap: 10px;
align-items: flex-start;
flex-shrink: 0;
width: 100%;
}
.cast-row {
display: flex;
align-items: flex-start;
flex-shrink: 0;
width: 100%;
overflow: scroll;
}
.cast-inner {
display: flex;
flex: 1 0 0;
gap: 20px;
align-items: flex-start;
min-width: 1px;
}
.cast-card {
display: flex;
flex-direction: column;
gap: 16px;
align-items: flex-start;
flex-shrink: 0;
}
.cast-portrait {
height: 179px;
width: 120px;
border-radius: 8px;
object-fit: cover;
display: block;
flex-shrink: 0;
}
.cast-avatar {
height: 179px;
width: 120px;
border-radius: 8px;
background-color: var(--color-base-20, #2a2a2a);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-faint);
font-size: 36px;
font-weight: 300;
flex-shrink: 0;
}
.cast-info {
display: flex;
flex-direction: column;
align-items: flex-start;
line-height: 1.6;
flex-shrink: 0;
width: 120px;
word-break: break-word;
}
.cast-name {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro', sans-serif;
font-size: 16px;
font-weight: 400;
letter-spacing: -0.32px;
color: var(--text-normal);
width: 100%;
margin: 0;
}
.cast-role {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro', sans-serif;
font-size: 14px;
font-weight: 400;
font-style: italic;
letter-spacing: -0.28px;
color: var(--text-muted);
width: 100%;
margin: 0;
}
.tmdb-loading {
font-family: -apple-system, sans-serif;
font-size: 14px;
color: var(--text-faint);
}
/* ===== RESPONSIVE (added 2026-05-29) — narrow panes / mobile =====
Gated >=1000px so desktop is unchanged; container queries + @media fallback. */
.movie-card { container-type: inline-size; container-name: mv; }
@container mv (max-width: 1000px) {
.content-area { padding-left: clamp(18px, 5cqw, 100px); padding-right: clamp(18px, 5cqw, 100px); }
.info-row { flex-direction: column; align-items: stretch; flex-wrap: nowrap; }
.movie-info { min-width: 0; max-width: none; }
.starring-col { min-width: 0; max-width: none; padding-top: 0; }
.content-watch-row { flex-direction: column; gap: 20px; }
.watch-col { max-width: none; align-items: flex-start; }
.file-content-wrap { max-width: none; }
}
@container mv (max-width: 560px) {
.content-area { padding-left: 18px; padding-right: 18px; }
.movie-title { font-size: clamp(28px, 9cqw, 40px); }
}
@media (max-width: 1000px) {
.content-area { padding-left: clamp(18px, 5vw, 100px); padding-right: clamp(18px, 5vw, 100px); }
.info-row { flex-direction: column; align-items: stretch; flex-wrap: nowrap; }
.movie-info { min-width: 0; max-width: none; }
.starring-col { min-width: 0; max-width: none; padding-top: 0; }
.content-watch-row { flex-direction: column; gap: 20px; }
.watch-col { max-width: none; align-items: flex-start; }
.file-content-wrap { max-width: none; }
}
@media (max-width: 560px) {
.content-area { padding-left: 18px; padding-right: 18px; }
.movie-title { font-size: clamp(28px, 9vw, 40px); }
}
/* ── SHOW MORE button + collapsible notes (added 2026-05-30) ──────
Mirrors the Albums card: body is hidden until the pill is pressed. */
.movie-more {
display: inline-block;
align-self: flex-start;
font: 600 12px/1 -apple-system, BlinkMacSystemFont, sans-serif;
letter-spacing: .12em;
color: var(--text-normal);
background: var(--background-modifier-hover);
border: 1px solid var(--background-modifier-border);
padding: 10px 18px;
border-radius: 999px;
cursor: pointer;
transition: background .15s ease;
}
.movie-more:hover { background: var(--background-modifier-active-hover); }
.movie-notes {
display: none;
margin-top: 16px;
font-size: 18px;
line-height: 1.6;
color: var(--text-muted);
}
.movie-card.notes-open .movie-notes,
.video-card.notes-open .movie-notes { display: block; }
.movie-notes > :first-child { margin-top: 0; }
/* ── Computed 0–100 score + verdict (shared by movies/shows/books/games cards;
the Albums card has its own .alb-score). Added 2026-05-30. ────────── */
.media-score {
display: flex;
flex-direction: column;
gap: 3px;
margin: 4px 0;
}
.media-score-num {
font: 600 clamp(30px, 3vw, 42px)/1 -apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif;
letter-spacing: -1.2px;
color: var(--text-normal);
}
.media-score-num[data-empty] {
font-size: 17px;
font-weight: 500;
letter-spacing: 0;
color: var(--text-faint);
}
.media-verdict {
font-size: 14px;
line-height: 1.45;
color: var(--text-muted);
}
/* ============================================================
Videos card (harker-videos) — YouTube hero.
Reuses the Movies hero/info/notes classes above; this block
only adds the .video-card shell, the channel byline link and
the round "Watch on YouTube" play button. Added 2026-06-02.
============================================================ */
.video-card {
background-color: var(--color-bg, #7baaba);
position: relative;
width: 100%;
padding-bottom: 145px;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 10px;
align-items: flex-start;
container-type: inline-size;
container-name: mv;
}
/* channel byline reuses .movie-genre sizing; make its link inherit */
.vid-channel a { text-decoration: none; color: var(--text-muted); }
.vid-channel a:hover { color: var(--color-base-80); text-decoration: none; }
/* lead paragraph (TL;DW) — slightly tighter than a full .movie-desc */
.vid-lead { max-width: 760px; }
/* meta row: left-aligned for videos (movies centre it) */
.video-card .meta-row { justify-content: flex-start; }
/* ── Watch on YouTube — round red play button + label ─────────── */
.vid-watch {
display: inline-flex;
align-items: center;
gap: 16px;
margin-top: 8px;
text-decoration: none;
cursor: pointer;
}
.vid-watch:hover { text-decoration: none; }
.vid-play {
position: relative;
width: 58px;
height: 58px;
border-radius: 999px;
background: #f5000f;
flex-shrink: 0;
box-shadow: 0 6px 24px rgba(245, 0, 15, 0.35);
transition: transform .15s ease, box-shadow .15s ease;
}
.vid-play::after {
content: "";
position: absolute;
top: 50%;
left: 54%;
transform: translate(-50%, -50%);
border-style: solid;
border-width: 11px 0 11px 18px;
border-color: transparent transparent transparent #fff;
}
.vid-watch:hover .vid-play {
transform: scale(1.06);
box-shadow: 0 8px 30px rgba(245, 0, 15, 0.5);
}
.vid-watch-label {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro', sans-serif;
font-size: 20px;
font-weight: 600;
line-height: 1.15;
letter-spacing: -0.4px;
color: var(--text-normal);
}
@container mv (max-width: 1000px) {
.vid-lead { max-width: none; }
}
/* ================================================================
Harker — YouTube "Videos" custom view
Reuses the Movies Flexoki palette engine (samples the thumbnail's
dominant colour and repaints the card), minus TMDB/cast/score.
Idea-capture, not a review.
================================================================ */
const _container = this;
/* ===== COLOR MATH — sRGB / OKLCH / HSL ===== */
function hexToRGB(hex) {
const h = hex.replace("#", "");
return [parseInt(h.slice(0,2),16)/255, parseInt(h.slice(2,4),16)/255, parseInt(h.slice(4,6),16)/255];
}
function rgbToHex(r, g, b) {
return "#" + [r,g,b].map(v => Math.round(Math.max(0,Math.min(255,v*255))).toString(16).padStart(2,"0")).join("");
}
function rgbToHSL(r, g, b) {
const max = Math.max(r,g,b), min = Math.min(r,g,b);
const l = (max+min)/2;
if (max === min) return [0,0,l*100];
const d = max-min;
const s = l > 0.5 ? d/(2-max-min) : d/(max+min);
let h;
switch (max) { case r: h = ((g-b)/d+(g<b?6:0))/6; break; case g: h = ((b-r)/d+2)/6; break; default: h = ((r-g)/d+4)/6; }
return [h*360, s*100, l*100];
}
function sRGBToLinear(v) { return v <= 0.04045 ? v/12.92 : Math.pow((v+0.055)/1.055, 2.4); }
function linearToSRGB(v) { return v <= 0.0031308 ? 12.92*v : 1.055*Math.pow(v,1/2.4)-0.055; }
function relativeLuminance(hex) { return hexToRGB(hex).map(sRGBToLinear).reduce((s,v,i)=>s+v*[0.2126,0.7152,0.0722][i],0); }
function hexToOKLCH(hex) {
let [r,g,b] = hexToRGB(hex).map(sRGBToLinear);
const l = 0.4122214708*r+0.5363325363*g+0.0514459929*b;
const m = 0.2119034982*r+0.6806995451*g+0.1073969566*b;
const s = 0.0883024619*r+0.2817188376*g+0.6299787005*b;
const l_=Math.cbrt(l), m_=Math.cbrt(m), s_=Math.cbrt(s);
const L = 0.2104542553*l_+0.793617785*m_-0.0040720468*s_;
const a = 1.9779984951*l_-2.428592205*m_+0.4505937099*s_;
const bk = 0.0259040371*l_+0.7827717662*m_-0.808675766*s_;
const C = Math.sqrt(a*a+bk*bk);
const H = ((Math.atan2(bk,a)*180)/Math.PI+360)%360;
return [L,C,H];
}
function OKLCHToHex(L, C, H) {
const a = C*Math.cos((H*Math.PI)/180);
const bk = C*Math.sin((H*Math.PI)/180);
const l_ = L+0.3963377774*a+0.2158037573*bk;
const m_ = L-0.1055613458*a-0.0638541728*bk;
const s_ = L-0.0894841775*a-1.291485548*bk;
const lv=l_*l_*l_, mv=m_*m_*m_, sv=s_*s_*s_;
const r = linearToSRGB(4.0767416621*lv-3.3077115913*mv+0.2309699292*sv);
const g = linearToSRGB(-1.2684380046*lv+2.6097574011*mv-0.3413193965*sv);
const b = linearToSRGB(-0.0041960863*lv-0.7034186147*mv+1.707614701*sv);
return rgbToHex(r,g,b);
}
function multiplyColors(hex1, hex2) {
const [r1,g1,b1] = hexToRGB(hex1); const [r2,g2,b2] = hexToRGB(hex2);
return rgbToHex(r1*r2,g1*g2,b1*b2);
}
function buildScale(hex) {
const PAPER = "#FFFCF0", BLACK = "#100F0F";
const [L0,C0,H0] = hexToOKLCH(hex);
const [Lp] = hexToOKLCH(PAPER); const [Lb] = hexToOKLCH(BLACK);
const L400 = Lp+(L0-Lp)*0.88, C400 = C0*0.82;
const L600 = L0+(Lb-L0)*0.18, C600 = C0*0.92;
const scale = { paper: PAPER, black: BLACK };
for (const step of [50,100,150,200,300,400,500,600,700,800,850,875,900,950]) {
if (step < 400) { const t=step/400, te=t*t*(3-2*t); scale[step]=multiplyColors(OKLCHToHex(Lp+(L400-Lp)*te,C400*te,H0),PAPER); }
else if (step === 400) scale[step]=multiplyColors(OKLCHToHex(L400,C400,H0),PAPER);
else if (step === 500) scale[step]=hex;
else if (step === 600) scale[step]=OKLCHToHex(L600,C600,H0);
else { const t=(step-600)/400, te=t*t*(3-2*t); scale[step]=OKLCHToHex(L600+(Lb-L600)*te,C600*(1-te),H0); }
}
return scale;
}
function sampleDominantColor(img) {
try {
const canvas = document.createElement("canvas");
canvas.width = 200; canvas.height = 20;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, img.naturalHeight*0.88, img.naturalWidth, img.naturalHeight*0.12, 0, 0, 200, 20);
const d = ctx.getImageData(0,0,200,20).data;
let R=0,G=0,B=0,n=0;
for (let i=0;i<d.length;i+=4){R+=d[i];G+=d[i+1];B+=d[i+2];n++;}
return "#" + [Math.round(R/n),Math.round(G/n),Math.round(B/n)].map(v=>v.toString(16).padStart(2,"0")).join("");
} catch { return null; }
}
function applyPalette(sampledHex) {
const scale = buildScale(sampledHex);
const lum = relativeLuminance(sampledHex);
const isLight = lum > 0.179;
const bg = sampledHex;
const [r,g,b] = hexToRGB(bg).map(v=>Math.round(v*255));
const ordered = [scale.paper,scale[50],scale[100],scale[150],scale[200],scale[300],scale[400],scale[500],scale[600],scale[700],scale[800],scale[850],scale[875],scale[900],scale.black];
const baseOrder = isLight ? ordered : [...ordered].reverse();
const baseMap = {"00":baseOrder[0],"05":baseOrder[1],"10":baseOrder[2],"20":baseOrder[3],"25":baseOrder[4],"30":baseOrder[5],"35":baseOrder[6],"40":baseOrder[7],"50":baseOrder[8],"60":baseOrder[9],"70":baseOrder[10],"80":baseOrder[11],"85":baseOrder[12],"90":baseOrder[13],"100":baseOrder[14]};
const [aR,aG,aB] = hexToRGB(scale[500]);
const [accentH,accentS,accentL] = rgbToHSL(aR,aG,aB);
const textNormal=baseMap["100"], textMuted=baseMap["85"], textFaint=baseMap["70"];
const bgPrimary=baseMap["00"], bgSecond=baseMap["05"], bgHover=baseMap["10"];
const bgModifierBorder = isLight ? baseMap["30"] : baseMap["70"];
const bgModifierBorderHover = isLight ? baseMap["35"] : baseMap["65"];
const bgModifierBorderFocus = isLight ? baseMap["40"] : baseMap["60"];
const S = ".obsidian-custom-view-render, .obsidian-custom-view-editable .obsidian-custom-view-render";
const baseVars = Object.entries(baseMap).map(([k,v])=>` --color-base-${k}: ${v.toUpperCase()};`).join("\n");
const css = `
${S} {
--color-bg: ${bg};
--color-gradient-start: rgba(${r},${g},${b},0);
--accent-h: ${accentH.toFixed(1)};
--accent-s: ${accentS.toFixed(1)}%;
--accent-l: ${accentL.toFixed(1)}%;
${baseVars}
--background-primary: ${bg};
--background-secondary: ${bgSecond};
--background-modifier-border: ${bgModifierBorder};
--background-modifier-border-hover: ${bgModifierBorderHover};
--background-modifier-border-focus: ${bgModifierBorderFocus};
--background-modifier-hover: ${bgHover};
--background-modifier-form-field: ${bgHover};
--table-border-color: ${bgModifierBorder};
--text-normal: ${textNormal};
--text-muted: ${textMuted};
--text-faint: ${textFaint};
--blockquote-border-color: ${textFaint};
--interactive-accent: ${textFaint};
--text-on-accent: ${isLight ? scale.black : scale.paper};
--text-on-accent-inverted: ${isLight ? scale.paper : scale.black};
color: ${textNormal};
background-color: ${bgPrimary};
}
${S} .theme-light, ${S} .theme-dark {
${baseVars}
--accent-h: ${accentH.toFixed(1)};
--accent-s: ${accentS.toFixed(1)}%;
--accent-l: ${accentL.toFixed(1)}%;
--text-normal: ${textNormal};
--text-muted: ${textMuted};
--text-faint: ${textFaint};
--link-color: ${textNormal};
--background-primary: ${bgPrimary};
--background-secondary: ${bgSecond};
--bases-table-header-background: ${bg};
--bases-table-header-color: ${textMuted};
--bases-embed-border-color: ${textMuted};
--icon-color-active: ${textNormal};
--caret-color: ${textMuted};
}`;
let styleEl = _container.querySelector("#flexoki-palette");
if (!styleEl) { styleEl = document.createElement("style"); styleEl.id = "flexoki-palette"; _container.appendChild(styleEl); }
styleEl.textContent = css;
const gradientEl = _container.querySelector("#gradientOverlay");
if (gradientEl) gradientEl.style.background = `linear-gradient(to bottom, transparent 32.77%, ${bg} 83.04%)`;
}
/* ===== IMAGE -> PALETTE ===== */
const bgImg = _container.querySelector("#bgImg");
function onBgLoad() { applyPalette(sampleDominantColor(bgImg) || "#7baaba"); }
if (bgImg) {
if (bgImg.complete && bgImg.naturalWidth > 0) onBgLoad();
else { bgImg.addEventListener("load", onBgLoad); bgImg.addEventListener("error", () => applyPalette("#7baaba")); }
}
/* ===== resolve this note's frontmatter ===== */
const _vcard = _container.querySelector(".video-card");
function resolveFile() {
const want = _vcard?.dataset.name || "";
let f = app.workspace.getActiveFile();
if (want && (!f || f.name !== want)) { const b = app.vault.getFiles().find(x => x.name === want); if (b) f = b; }
return f;
}
const _file = resolveFile();
const _fm = _file ? ((app.metadataCache.getFileCache(_file) || {}).frontmatter || {}) : {};
const stripLink = s => String(s || "").replace(/^\[\[|\]\]$/g, "").replace(/\|.*$/, "").trim();
/* ===== channel byline (linked to channel_url when present) ===== */
(() => {
const el = _vcard?.querySelector(".vid-channel"); if (!el) return;
const name = stripLink(_fm.channel);
if (!name) { el.remove(); return; }
const url = _fm.channel_url || "";
if (url) { const a = document.createElement("a"); a.href = url; a.target = "_blank"; a.rel = "noopener"; a.textContent = name; el.appendChild(a); }
else el.textContent = name;
})();
/* ===== meta row: year · channel · duration ===== */
(() => {
const el = _vcard?.querySelector(".vid-meta"); if (!el) return;
const dateStr = String(_fm.last || _fm.created || "");
const year = (dateStr.match(/^\d{4}/) || [""])[0];
const dur = _fm.duration ? String(_fm.duration).trim() : "";
const parts = [year, stripLink(_fm.channel), dur].filter(Boolean);
const SEP = " <span style='color:var(--text-faint)'>·</span> ";
el.innerHTML = parts.join(SEP);
})();
/* ===== watch button -> the video URL ===== */
(() => {
const a = _vcard?.querySelector(".vid-watch"); if (!a) return;
const url = _fm.url || _vcard.dataset.url || (_fm.video_id ? `https://www.youtube.com/watch?v=${_fm.video_id}` : "");
if (!url) { a.remove(); return; }
a.href = url;
})();
/* ===== lead paragraph: pull the TL;DW (or first body paragraph) ===== */
/* and SHOW MORE: render the full body, score-callout/frontmatter stripped ===== */
(async () => {
try {
const moreBtn = _vcard?.querySelector(".movie-more");
const notesEl = _vcard?.querySelector(".movie-notes");
const leadEl = _vcard?.querySelector(".vid-lead");
if (!_file) { moreBtn?.remove(); notesEl?.remove(); leadEl?.remove(); return; }
let raw = await app.vault.read(_file);
let body = raw.replace(/^---[\s\S]*?\n---\n?/, "");
body = body.replace(/^\s*((?:>.*\n?)+)\n?/, m => /dataviewjs|\[!abstract\]|\[!tldr\]/.test(m) ? "" : m);
body = body.trim();
// lead = first paragraph under "## TL;DW", else first non-heading paragraph
if (leadEl) {
let lead = "";
const m = body.match(/##\s*TL;DW\s*\n+([^\n][\s\S]*?)(?:\n\s*\n|\n##|$)/i);
if (m) lead = m[1].trim();
else { const m2 = body.match(/(?:^|\n)([^#>\n][^\n]+)/); if (m2) lead = m2[1].trim(); }
lead = lead.replace(/\[\[([^\]|]+)\|([^\]]+)\]\]/g, "$2").replace(/\[\[([^\]]+)\]\]/g, "$1").replace(/\*\*?([^*]+)\*\*?/g, "$1");
if (lead) leadEl.textContent = lead; else leadEl.remove();
}
if (!notesEl) return;
const meaningful = body.replace(/^#{1,6}\s.*$/gm, "").replace(/<!--[\s\S]*?-->/g, "").replace(/\s+/g, "");
if (!meaningful) { moreBtn?.remove(); notesEl.remove(); return; }
const obs = require("obsidian");
notesEl.classList.add("markdown-rendered");
notesEl.innerHTML = "";
const comp = new obs.Component();
if (obs.MarkdownRenderer && obs.MarkdownRenderer.render) await obs.MarkdownRenderer.render(app, body, notesEl, _file.path, comp);
else if (obs.MarkdownRenderer) await obs.MarkdownRenderer.renderMarkdown(body, notesEl, _file.path, comp);
if (moreBtn) moreBtn.addEventListener("click", () => {
const open = _vcard.classList.toggle("notes-open");
moreBtn.textContent = open ? "SHOW LESS" : "SHOW MORE";
});
} catch (e) { console.error("[Custom Views video]", e); }
})();
Templater Template
<%*
/**
* YouTube → Video Frontmatter (Obsidian Templater)
* No API key required. Reads a YouTube URL, pulls the title, channel and
* thumbnail from YouTube's public oEmbed endpoint, writes idea-capture
* frontmatter, and renames the note "Videos - Title". The thumbnail becomes
* the `banner`, so the generic Custom Views banner card renders it. There is
* no 0–10 score — this is a knowledge note, not a review: the body scaffolds
* for takeaways and for promoting ideas into their own Concept notes.
*/
const OEMBED = "https://www.youtube.com/oembed";
const today = tp.date.now("YYYY-MM-DD");
const fileTitle = tp.file.title;
// 1) Ask for the video URL ----------------------------------------------
let url = await tp.system.prompt("Paste the YouTube URL (leave empty to fill by hand):");
url = (url || "").trim();
// 2) Pull the 11-char video id out of any common YouTube URL shape ------
function videoId(u) {
const m = u.match(/(?:youtu\.be\/|youtube\.com\/(?:watch\?(?:.*&)?v=|embed\/|shorts\/|live\/|v\/))([\w-]{11})/);
return m ? m[1] : "";
}
const id = videoId(url);
// Canonicalise the link so the note always stores a clean watch URL.
const canonUrl = id ? `https://www.youtube.com/watch?v=${id}` : url;
// 3) Fetch title / channel / thumbnail via oEmbed (keyless) -------------
let title = "", channel = "", channelUrl = "", oThumb = "";
if (url) {
try {
let resp = await tp.web.request(`${OEMBED}?format=json&url=${encodeURIComponent(canonUrl)}`);
// tp.web.request may hand back a parsed object or a JSON string.
if (typeof resp === "string") { try { resp = JSON.parse(resp); } catch (e) { resp = {}; } }
title = (resp && resp.title) || "";
channel = (resp && resp.author_name) || "";
channelUrl = (resp && resp.author_url) || "";
oThumb = (resp && resp.thumbnail_url) || "";
} catch (e) {
tR += `oEmbed fetch failed (private/age-restricted?): ${e}\n`;
}
}
// 4) Fall back to a hand-typed title when there was no URL / fetch ------
if (!title) {
let typed = await tp.system.prompt("Video title:", fileTitle);
title = (typed && typed.trim()) || fileTitle;
}
// 5) Thumbnail — prefer YouTube's maxres, fall back to the oEmbed one ---
const thumbnail = id ? `https://i.ytimg.com/vi/${id}/maxresdefault.jpg` : oThumb;
// 6) Link the channel to an existing note when one exists (host pattern)-
async function noteExists(name) {
for (const folder of ["References", ""]) {
const path = folder ? `${folder}/${name}.md` : `${name}.md`;
try { if (await tp.file.exists(path)) return true; } catch (e) {}
}
return false;
}
const channelField = channel
? ((await noteExists(channel)) ? `"[[${channel}]]"` : `"${channel.replace(/"/g, '\\"')}"`)
: "[]";
// 7) Watched date --------------------------------------------------------
let lastDate = await tp.system.prompt("Watched date (YYYY-MM-DD)", today);
if (!lastDate || !/^\d{4}-\d{2}-\d{2}$/.test(lastDate.trim())) lastDate = today;
lastDate = lastDate.trim();
// 8) Rename to "Videos - Title" (colon→dash, strip illegal chars) -------
const colonFixed = title.replace(/:/g, " - ");
let sanitized = colonFixed.replace(/[\/#%&{}<>*?$!'"@+`|=]/g, "").trim();
if (!sanitized) sanitized = fileTitle;
const newName = sanitized;
if (newName !== fileTitle) await tp.file.rename(newName);
// 9) Build YAML ----------------------------------------------------------
const L = [];
L.push("---");
L.push("connections:");
L.push(thumbnail ? `banner: ${thumbnail}` : 'banner: "[[32.jpg]]"');
L.push("categories:");
L.push(' - "[[Videos]]"');
L.push(`channel: ${channelField}`);
L.push(channelUrl ? `channel_url: "${channelUrl}"` : "channel_url: []");
L.push(canonUrl ? `url: "${canonUrl}"` : 'url: ""');
L.push(id ? `video_id: ${id}` : "video_id: []");
L.push("duration: ");
L.push("published: ");
L.push(`created: ${today}`);
L.push(`last: ${lastDate}`);
L.push("topics: []");
L.push("---");
// 10) Body scaffold — built for ideas, not scoring ----------------------
const body = [
"",
canonUrl ? `> [!tldr] [Watch on YouTube](${canonUrl})${channel ? ` · ${channel}` : ""}` : "> [!tldr] Watch",
"",
"## TL;DW",
"",
"## Key Ideas",
"",
"## Concepts",
"<!-- Promote the load-bearing ones to their own bare-named concept notes. -->",
"",
"## Why It Matters",
"",
"## Quotes & Timestamps",
""
];
tR += L.join("\n") + "\n" + body.join("\n");
-%>
Field Assembly
Two plugins build this strip in your vault.
- Custom Views: the renderer. Make a view, build its rule in the menu to match
categoriescontainsVideos(the screenshot above), then paste the template and JS blocks into their fields. - Templater: the note-maker. Paste the Templater block into a template file and bind it to a hotkey or a folder template.
The template fills the note from a public database when you create it, no key needed. After that the card runs on what is in the note.

Statement Ends