diff --git a/publish_episode.py b/publish_episode.py
index 1510bfb..f314890 100755
--- a/publish_episode.py
+++ b/publish_episode.py
@@ -439,7 +439,7 @@ def _create_episode_direct(audio_path: str, metadata: dict, episode_number: int,
# Docker cp into Castopod container
print(" Copying into Castopod container...")
- media_path = f"/var/www/castopod/public/media/{file_key}"
+ media_path = f"/app/public/media/{file_key}"
cp_cmd = f'{DOCKER_PATH} cp {nas_tmp} {CASTOPOD_CONTAINER}:{media_path}'
success, output = run_ssh_command(cp_cmd, timeout=120)
if not success:
@@ -673,7 +673,7 @@ def upload_transcript_to_castopod(episode_slug: str, episode_id: int, transcript
print(f" Warning: SCP transcript failed: {result.stderr}")
return False
- media_path = f"/var/www/castopod/public/media/{remote_path}"
+ media_path = f"/app/public/media/{remote_path}"
run_ssh_command(f'{DOCKER_PATH} cp {nas_tmp} {CASTOPOD_CONTAINER}:{media_path}', timeout=60)
run_ssh_command(f'{DOCKER_PATH} exec {CASTOPOD_CONTAINER} chown www-data:www-data {media_path}')
run_ssh_command(f'rm -f {nas_tmp}')
@@ -690,7 +690,7 @@ def upload_transcript_to_castopod(episode_slug: str, episode_id: int, transcript
subprocess.run(scp_json, capture_output=True, text=True, timeout=60)
os.remove(json_tmp_local.name)
- json_media_path = f"/var/www/castopod/public/media/{json_key}"
+ json_media_path = f"/app/public/media/{json_key}"
run_ssh_command(f'{DOCKER_PATH} cp {nas_json_tmp} {CASTOPOD_CONTAINER}:{json_media_path}', timeout=60)
run_ssh_command(f'{DOCKER_PATH} exec {CASTOPOD_CONTAINER} chown www-data:www-data {json_media_path}')
run_ssh_command(f'rm -f {nas_json_tmp}')
@@ -750,7 +750,7 @@ def upload_chapters_to_castopod(episode_slug: str, episode_id: int, chapters_pat
chapters_b64 = base64.b64encode(chapters_content.encode()).decode()
# Upload file to container using base64 decode
- upload_cmd = f'echo "{chapters_b64}" | base64 -d | {DOCKER_PATH} exec -i {CASTOPOD_CONTAINER} tee /var/www/castopod/public/media/{remote_path} > /dev/null'
+ upload_cmd = f'echo "{chapters_b64}" | base64 -d | {DOCKER_PATH} exec -i {CASTOPOD_CONTAINER} tee /app/public/media/{remote_path} > /dev/null'
success, output = run_ssh_command(upload_cmd)
if not success:
print(f" Warning: Failed to upload chapters file: {output}")
@@ -817,7 +817,7 @@ def download_from_castopod(file_key: str, local_path: str) -> bool:
"""Download a file from Castopod's container storage to local filesystem."""
remote_filename = Path(file_key).name
remote_tmp = f"/share/CACHEDEV1_DATA/tmp/castopod_{remote_filename}"
- cp_cmd = f'{DOCKER_PATH} cp {CASTOPOD_CONTAINER}:/var/www/castopod/public/media/{file_key} {remote_tmp}'
+ cp_cmd = f'{DOCKER_PATH} cp {CASTOPOD_CONTAINER}:/app/public/media/{file_key} {remote_tmp}'
success, _ = run_ssh_command(cp_cmd, timeout=120)
if not success:
return False
diff --git a/reaper/dialog_regions.lua b/reaper/dialog_regions.lua
new file mode 100644
index 0000000..7db1be8
--- /dev/null
+++ b/reaper/dialog_regions.lua
@@ -0,0 +1,115 @@
+-- Show Region Marker — background script for REAPER
+-- Polls /tmp/reaper_state.txt for state changes and creates colored regions.
+-- Backend writes "dialog", "ad", or "ident" to the file.
+-- Run via Actions → Run ReaScript before or during recording.
+
+local STATE_FILE = "/tmp/reaper_state.txt"
+
+local COLORS = {
+ dialog = reaper.ColorToNative(50, 180, 50) + 0x1000000, -- green
+ ad = reaper.ColorToNative(200, 80, 80) + 0x1000000, -- red
+ ident = reaper.ColorToNative(80, 120, 200) + 0x1000000, -- blue
+}
+local LABELS = {
+ dialog = "DIALOG",
+ ad = "AD",
+ ident = "IDENT",
+}
+
+local counts = { dialog = 0, ad = 0, ident = 0 }
+local current_type = nil -- which region type is currently open
+local current_start = 0
+local last_pos = 0 -- last known transport position (while running)
+local last_state = ""
+local transport_active = false
+
+local function log(msg)
+ reaper.ShowConsoleMsg("[Regions] " .. msg .. "\n")
+end
+
+local function is_playing_or_recording()
+ local state = reaper.GetPlayState()
+ return state > 0 and state ~= 2
+end
+
+local function open_region(rtype)
+ if current_type then return end
+ current_type = rtype
+ current_start = reaper.GetPlayPosition()
+ log("OPEN " .. rtype .. " at " .. string.format("%.2f", current_start))
+end
+
+local function close_region(pos_override)
+ if not current_type then return end
+ local pos = pos_override or reaper.GetPlayPosition()
+ local len = pos - current_start
+ local rtype = current_type
+ current_type = nil
+ log("CLOSE " .. rtype .. " at " .. string.format("%.2f", pos) .. " (len=" .. string.format("%.2f", len) .. ")")
+ if len > 0.1 then
+ counts[rtype] = counts[rtype] + 1
+ local name = LABELS[rtype] .. " " .. counts[rtype]
+ reaper.AddProjectMarker2(0, true, current_start, pos, name, -1, COLORS[rtype])
+ log(" -> Created '" .. name .. "'")
+ else
+ log(" -> Skipped (too short)")
+ end
+end
+
+local function poll()
+ if not transport_active then
+ if is_playing_or_recording() then
+ transport_active = true
+ log("Transport started at " .. string.format("%.2f", reaper.GetPlayPosition()))
+ local f = io.open(STATE_FILE, "r")
+ if f then
+ last_state = f:read("*l") or "dialog"
+ f:close()
+ else
+ last_state = "dialog"
+ end
+ log("Initial state: '" .. last_state .. "'")
+ open_region(last_state)
+ end
+ reaper.defer(poll)
+ return
+ end
+
+ -- Track position while transport is running
+ last_pos = reaper.GetPlayPosition()
+
+ -- Detect transport stop (recording ended) — use last known good position
+ if not is_playing_or_recording() then
+ log("Transport stopped at last known pos " .. string.format("%.2f", last_pos))
+ close_region(last_pos)
+ transport_active = false
+ reaper.defer(poll)
+ return
+ end
+
+ local f = io.open(STATE_FILE, "r")
+ if f then
+ local state = f:read("*l") or "dialog"
+ f:close()
+
+ if state ~= last_state then
+ log("State change: '" .. last_state .. "' -> '" .. state .. "'")
+ close_region()
+ open_region(state)
+ last_state = state
+ end
+ end
+
+ reaper.defer(poll)
+end
+
+log("Script loaded — waiting for transport to start...")
+
+reaper.atexit(function()
+ log("Script stopping (atexit)")
+ close_region()
+ local total = counts.dialog + counts.ad + counts.ident
+ log("Done. " .. total .. " regions (" .. counts.dialog .. " dialog, " .. counts.ad .. " ad, " .. counts.ident .. " ident)")
+end)
+
+poll()
diff --git a/website/css/style.css b/website/css/style.css
index 34f177d..f053e2c 100644
--- a/website/css/style.css
+++ b/website/css/style.css
@@ -139,51 +139,23 @@ a:hover {
padding: 3rem 1.5rem 2.5rem;
max-width: 900px;
margin: 0 auto;
- text-align: center;
}
-.hero-inner {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 1.5rem;
-}
-
-.hero-inner--full {
- max-width: 700px;
- margin: 0 auto;
-}
-
-.hero-info {
+.hero-grid {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
-.hero-info--centered {
+.hero-text {
+ display: flex;
+ flex-direction: column;
align-items: center;
+ gap: 0.5rem;
text-align: center;
}
-.hero-about {
- font-size: 1.05rem;
- color: var(--text-muted);
- line-height: 1.7;
- max-width: 600px;
- margin: 0.75rem auto 0;
-}
-
-.hero-about--bold {
- color: var(--text);
- font-weight: 600;
- font-size: 1.1rem;
-}
-
-.hero-cta {
- margin-top: 0.5rem;
-}
-
.hero h1 {
font-size: 2.8rem;
font-weight: 800;
@@ -193,17 +165,187 @@ a:hover {
.tagline {
font-size: 1.2rem;
color: var(--text-muted);
- max-width: 500px;
line-height: 1.5;
}
.tagline--hero {
- font-size: 1.4rem;
+ font-size: 1.5rem;
color: var(--text);
- font-weight: 600;
+ font-weight: 700;
+ line-height: 1.3;
max-width: 600px;
}
+.tagline--sub {
+ font-size: 1.05rem;
+ color: var(--text-muted);
+ margin-top: 0.25rem;
+}
+
+/* Social Proof Strip */
+.proof-strip {
+ max-width: 900px;
+ margin: 0 auto;
+ padding: 1.5rem 1.5rem;
+ border-top: 1px solid #2a2015;
+ border-bottom: 1px solid #2a2015;
+}
+
+.hero-links {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: 0.9rem;
+ margin-top: 0.25rem;
+}
+
+.hero-links a {
+ color: var(--text-muted);
+ transition: color 0.2s;
+}
+
+.hero-links a:hover {
+ color: var(--accent);
+}
+
+.hero-links .support-link {
+ color: var(--accent);
+}
+
+.hero-links-sep {
+ color: var(--text-dim);
+}
+
+.proof-strip-inner {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 2rem;
+}
+
+.proof-quote {
+ font-size: 0.85rem;
+ color: var(--text-dim);
+ font-style: italic;
+ line-height: 1.5;
+ text-align: center;
+ margin: 0;
+}
+
+.proof-quote cite {
+ display: block;
+ font-style: normal;
+ font-size: 0.75rem;
+ color: var(--text-dim);
+ margin-top: 0.35rem;
+ font-weight: 600;
+}
+
+/* About Q&A */
+.about-qa {
+ max-width: 900px;
+ margin: 0 auto;
+ padding: 2rem 1.5rem;
+}
+
+.about-qa-inner {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 2rem;
+ margin: 0 auto;
+}
+
+.qa-item {
+ text-align: center;
+}
+
+.qa-q {
+ font-size: 1rem;
+ font-weight: 700;
+ color: var(--accent);
+ margin-bottom: 0.4rem;
+}
+
+.qa-a {
+ font-size: 1rem;
+ color: var(--text-muted);
+ line-height: 1.6;
+}
+
+.qa-stat {
+ font-size: 1.1rem;
+ font-weight: 700;
+ color: var(--text);
+ text-align: center;
+ margin-top: 1rem;
+ grid-column: 1 / -1;
+}
+
+/* Featured Episode */
+.featured-episode-section {
+ max-width: 900px;
+ margin: 0 auto;
+ padding: 1rem 1.5rem 2rem;
+}
+
+.featured-episode-section h2 {
+ font-size: 1.5rem;
+ font-weight: 700;
+ margin-bottom: 1rem;
+}
+
+.featured-episode-card {
+ background: #252015;
+ border-radius: var(--radius);
+ border-left: 3px solid var(--accent);
+ padding: 1.5rem 1.5rem 1.5rem 2rem;
+}
+
+.featured-episode-meta {
+ font-size: 0.85rem;
+ color: var(--text-dim);
+ margin-bottom: 0.5rem;
+}
+
+.featured-episode-title {
+ font-size: 1.4rem;
+ font-weight: 700;
+ color: var(--text);
+ margin-bottom: 0.75rem;
+ line-height: 1.3;
+}
+
+.featured-episode-desc {
+ font-size: 0.95rem;
+ color: var(--text-muted);
+ line-height: 1.6;
+ margin-bottom: 1rem;
+}
+
+.featured-episode-actions {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.featured-play-btn {
+ width: 48px;
+ height: 48px;
+}
+
+.featured-play-btn svg {
+ width: 22px;
+ height: 22px;
+}
+
+.featured-episode-actions .episode-transcript-link {
+ font-size: 0.85rem;
+ color: var(--accent);
+}
+
+.featured-episode-actions .episode-share-btn {
+ margin-left: auto;
+}
+
.phone {
display: flex;
align-items: center;
@@ -341,39 +483,7 @@ a:hover {
}
/* Secondary links — How It Works, Discord, Support */
-.secondary-links {
- display: flex;
- flex-wrap: wrap;
- justify-content: center;
- align-items: center;
- gap: 0.5rem;
- margin-top: 0.25rem;
-}
-
-.secondary-link {
- font-size: 0.85rem;
- color: var(--text-dim);
- transition: color 0.2s;
-}
-
-.secondary-link:hover {
- color: var(--accent);
-}
-
-.secondary-sep {
- color: var(--text-dim);
- font-size: 0.85rem;
-}
-
-.support-link {
- color: var(--accent);
- opacity: 1;
- font-weight: 600;
-}
-
-.support-link:hover {
- color: var(--accent-hover);
-}
+/* Secondary links removed — nav handles these now */
/* Episodes */
.episodes-section {
@@ -1795,10 +1905,6 @@ a:hover {
max-width: 1000px;
}
- .hero-inner {
- gap: 2rem;
- }
-
.hero h1 {
font-size: 2.8rem;
}
@@ -1810,6 +1916,18 @@ a:hover {
gap: 0.5rem;
}
+ .proof-strip {
+ padding: 1.5rem 2rem;
+ }
+
+ .about-qa {
+ padding: 3rem 2rem;
+ }
+
+ .featured-episode-section {
+ padding: 0 2rem 2rem;
+ }
+
.episodes-section {
padding: 2rem 2rem 3rem;
}
@@ -1899,6 +2017,19 @@ a:hover {
font-size: 2rem;
}
+ .tagline--hero {
+ font-size: 1.2rem;
+ }
+
+ .proof-strip-inner {
+ grid-template-columns: 1fr;
+ gap: 1.25rem;
+ }
+
+ .about-qa-inner {
+ grid-template-columns: 1fr;
+ gap: 1.5rem;
+ }
.page-header h1 {
font-size: 2rem;
diff --git a/website/index.html b/website/index.html
index e19012d..f3eee86 100644
--- a/website/index.html
+++ b/website/index.html
@@ -31,7 +31,7 @@
-
+
-
+