subscriptions.pinned, per person, set by PATCH /api/feeds/{id} {pinned} and
returned as FeedRow.pinned. Kept out of Sub, which the scanner merges into its
policy; set_subscription names its columns, so saving a feed's settings leaves
the pin alone (tested).
Pinned feeds come first in the list, a pin before the name and a rule under the
block: a pinned folder with its feeds under it, a feed from inside one lifted out
of it. The pin button is on both the feed and the folder page.
Closes #33.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
921 lines
44 KiB
HTML
921 lines
44 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="color-scheme" content="dark light">
|
||
<title>iPodderX</title>
|
||
<link rel="icon" type="image/png" sizes="128x128" href="/favicon.png">
|
||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||
<style>
|
||
/* Inter, served by ipx itself (/inter.woff2) rather than a font CDN, so the page asks nothing of
|
||
anyone else. One variable file covers every weight used here; italics are synthesized. Classic
|
||
keeps Lucida Grande, the 2004 app's face. */
|
||
@font-face{font-family:Inter;src:url(/inter.woff2) format("woff2");font-weight:100 900;font-display:swap}
|
||
/* Palette taken from the 2004 iPodderX icon: the silver device body, the blue
|
||
screen, and the amber EQ bars. Hex values in comments are sampled straight from it. */
|
||
:root {
|
||
--bg:#0e131b; /* the screen's navy (#314B74), taken right down */
|
||
--panel:#151c27;
|
||
--panel2:#1c2431;
|
||
--raise:#25303f;
|
||
--line:#2c3849;
|
||
--fg:#f5f5f5; /* #F5F5F5 device highlight */
|
||
--dim:#95a0b1; /* #95A0B1 straight from the icon's blue-grey */
|
||
--faint:#7a8799; /* lifted from the icon ramp until it clears AA at small sizes */
|
||
--accent:#92b2e6; /* #92B2E6 the screen blue */
|
||
--accent2:#f49e2c; /* #F49E2C the EQ bars */
|
||
--ink:#0e131b; /* text on an accent fill */
|
||
--good:#6fbf8b;
|
||
--warn:#f49e2c; /* the amber doubles as the pending colour */
|
||
--bad:#e2705f;
|
||
--shadow:0 8px 28px rgba(6,10,16,.55);
|
||
}
|
||
/* Every other theme overrides the same variables, under data-theme and data-mode. The page's
|
||
script sets data-mode to light or dark, working Auto out from the system, so each theme has
|
||
one block per variant here and none needs repeating under a media query. */
|
||
:root[data-theme="modern"][data-mode="light"] {
|
||
--bg:#f2f4f7;
|
||
--panel:#ffffff; /* #FFFFFF device body */
|
||
--panel2:#e9edf3;
|
||
--raise:#dde3ec;
|
||
--line:#d6d6d6; /* #D6D6D6 device edge */
|
||
--fg:#1a1a1a; /* #1A1A1A icon outline */
|
||
--dim:#606060; /* #606060 */
|
||
--faint:#767676; /* between the icon's #929292 and #606060, to clear AA */
|
||
--accent:#2d5391; /* #2D5391 the deep screen blue reads better on white */
|
||
--accent2:#9a5f0a; /* the EQ amber, taken down until white on it clears AA */
|
||
--ink:#ffffff;
|
||
--good:#2f7d4f;
|
||
--warn:#b06f10;
|
||
--bad:#b3402f;
|
||
--shadow:0 8px 28px rgba(45,83,145,.14);
|
||
}
|
||
/* Dracula, and Alucard, its light half, from draculatheme.com/spec. The spec's comment colour
|
||
(#6272A4, #6C664B) is too faint for small text on its own background, so --faint is lifted. */
|
||
:root[data-theme="dracula"] {
|
||
--bg:#282a36;
|
||
--panel:#21222c;
|
||
--panel2:#343746;
|
||
--raise:#44475a; /* selection */
|
||
--line:#414558;
|
||
--fg:#f8f8f2;
|
||
--dim:#c9cbd6;
|
||
--faint:#9ea8c7;
|
||
--accent:#bd93f9; /* purple */
|
||
--accent2:#ff79c6; /* pink */
|
||
--ink:#282a36;
|
||
--good:#50fa7b;
|
||
--warn:#ffb86c;
|
||
--bad:#ff8080;
|
||
--shadow:0 8px 28px rgba(0,0,0,.45);
|
||
}
|
||
:root[data-theme="dracula"][data-mode="light"] {
|
||
--bg:#fffbeb;
|
||
--panel:#f7f2df;
|
||
--panel2:#efe9d3;
|
||
--raise:#cfcfde; /* selection */
|
||
--line:#ddd6bd;
|
||
--fg:#1f1f1f;
|
||
--dim:#454137;
|
||
--faint:#635d44;
|
||
--accent:#644ac9;
|
||
--accent2:#a3144d;
|
||
--ink:#ffffff;
|
||
--good:#14710a;
|
||
--warn:#a34d14;
|
||
--bad:#b83322;
|
||
--shadow:0 8px 28px rgba(108,102,75,.18);
|
||
}
|
||
/* Material 3's baseline scheme: surface, the surface containers, outline and primary. */
|
||
:root[data-theme="material"] {
|
||
--bg:#141218; /* surface */
|
||
--panel:#1d1b20; /* surface-container-low */
|
||
--panel2:#211f26; /* surface-container */
|
||
--raise:#36343b; /* surface-container-highest */
|
||
--line:#49454f; /* outline-variant */
|
||
--fg:#e6e0e9; /* on-surface */
|
||
--dim:#cac4d0; /* on-surface-variant */
|
||
--faint:#9a95a0; /* outline, a shade up to clear AA on the containers */
|
||
--accent:#d0bcff; /* primary */
|
||
--accent2:#efb8c8; /* tertiary */
|
||
--ink:#381e72; /* on-primary */
|
||
--good:#8fd6a0;
|
||
--warn:#f2c46b;
|
||
--bad:#f2b8b5; /* error */
|
||
--shadow:0 8px 28px rgba(0,0,0,.5);
|
||
}
|
||
:root[data-theme="material"][data-mode="light"] {
|
||
--bg:#fef7ff;
|
||
--panel:#f7f2fa;
|
||
--panel2:#f3edf7;
|
||
--raise:#e6e0e9;
|
||
--line:#cac4d0;
|
||
--fg:#1d1b20;
|
||
--dim:#49454f;
|
||
--faint:#67626c; /* outline (#79747E), a shade down to clear AA on the containers */
|
||
--accent:#6750a4;
|
||
--accent2:#7d5260;
|
||
--ink:#ffffff;
|
||
--good:#2b6c3c;
|
||
--warn:#7c5800;
|
||
--bad:#b3261e;
|
||
--shadow:0 8px 28px rgba(103,80,164,.14);
|
||
}
|
||
/* Adwaita, from libadwaita's own CSS variables: view, window and sidebar backgrounds, the blue
|
||
accent, and its destructive, success and warning colours. */
|
||
:root[data-theme="adwaita"] {
|
||
--bg:#1d1d20; /* view */
|
||
--panel:#2e2e32; /* sidebar, headerbar */
|
||
--panel2:#222226; /* window */
|
||
--raise:#3a3a3f;
|
||
--line:#3f3f45;
|
||
--fg:#ffffff;
|
||
--dim:#c4c4c8;
|
||
--faint:#a0a0a7;
|
||
--accent:#81d0ff;
|
||
--accent2:#3584e4;
|
||
--ink:#1d1d20;
|
||
--good:#78e9ab;
|
||
--warn:#ffc252;
|
||
--bad:#ff938c;
|
||
--shadow:0 8px 28px rgba(0,0,0,.5);
|
||
}
|
||
:root[data-theme="adwaita"][data-mode="light"] {
|
||
--bg:#ffffff;
|
||
--panel:#ebebed;
|
||
--panel2:#fafafb;
|
||
--raise:#dcdce0;
|
||
--line:#d9d9dd;
|
||
--fg:#333338; /* rgb(0 0 6 / 80%) on white */
|
||
--dim:#57575d;
|
||
--faint:#66666c;
|
||
--accent:#0461be;
|
||
--accent2:#3584e4;
|
||
--ink:#ffffff;
|
||
--good:#00753a;
|
||
--warn:#905400;
|
||
--bad:#c30000;
|
||
--shadow:0 8px 28px rgba(0,0,6,.12);
|
||
}
|
||
/* Flat Remix, from its GTK theme's _colors.scss: base and bg, fg, the selection blue, and its
|
||
link colours, which are the selection blue taken down (light) or up (dark) until readable. */
|
||
:root[data-theme="flatremix"] {
|
||
--bg:#272a34; /* base */
|
||
--panel:#23262f; /* bg: base darkened 2% */
|
||
--panel2:#2e323d;
|
||
--raise:#363b48;
|
||
--line:#3a3f4c;
|
||
--fg:#eeeeec;
|
||
--dim:#babec8;
|
||
--faint:#959baa;
|
||
--accent:#8fb6ff; /* link */
|
||
--accent2:#fd7d00; /* warning orange */
|
||
--ink:#1a1c23;
|
||
--good:#4cc28c;
|
||
--warn:#ff9a3c;
|
||
--bad:#ff6b6b;
|
||
--shadow:0 8px 28px rgba(0,0,0,.3);
|
||
}
|
||
:root[data-theme="flatremix"][data-mode="light"] {
|
||
--bg:#fafafa; /* base */
|
||
--panel:#ffffff; /* bg */
|
||
--panel2:#f0f0f1;
|
||
--raise:#e4e5e7;
|
||
--line:#d9d9d9; /* borders: bg darkened 15% */
|
||
--fg:#22252b;
|
||
--dim:#5c616c; /* fg */
|
||
--faint:#686d78;
|
||
--accent:#0060f0; /* link */
|
||
--accent2:#fd7d00;
|
||
--ink:#ffffff;
|
||
--good:#23794f;
|
||
--warn:#a85400;
|
||
--bad:#d41919;
|
||
--shadow:0 8px 28px rgba(0,0,0,.1);
|
||
}
|
||
/* Paper: a single light palette, black on the colour of paper, colour kept for what matters.
|
||
Its blue and red are taken down a shade to clear AA on that background. */
|
||
:root[data-theme="paper"] {
|
||
--bg:#f2eede;
|
||
--panel:#ebe7d7;
|
||
--panel2:#e4e0d0;
|
||
--raise:#d8d5c7; /* highlight */
|
||
--line:#cdc9b9;
|
||
--fg:#000000;
|
||
--dim:#3d3a33;
|
||
--faint:#5a564c;
|
||
--accent:#1a5fae;
|
||
--accent2:#b58900;
|
||
--ink:#ffffff;
|
||
--good:#216609;
|
||
--warn:#795a00;
|
||
--bad:#b1331f;
|
||
--shadow:0 8px 28px rgba(0,0,0,.12);
|
||
}
|
||
/* Nordic: the Nord palette. Polar Night for the dark half, Snow Storm for the light, Frost for
|
||
the accent and Aurora for the rest. Aurora red and Frost blue are shifted until they read. */
|
||
:root[data-theme="nordic"] {
|
||
--bg:#2e3440; /* nord0 */
|
||
--panel:#3b4252; /* nord1 */
|
||
--panel2:#434c5e; /* nord2 */
|
||
--raise:#4c566a; /* nord3 */
|
||
--line:#434c5e;
|
||
--fg:#eceff4; /* nord6 */
|
||
--dim:#d8dee9; /* nord4 */
|
||
--faint:#b4bccb;
|
||
--accent:#93c9d8; /* nord8, a shade up */
|
||
--accent2:#ebcb8b; /* nord13 */
|
||
--ink:#2e3440;
|
||
--good:#b0c99b; /* nord14, a shade up */
|
||
--warn:#ebcb8b;
|
||
--bad:#f2aeb4;
|
||
--shadow:0 8px 28px rgba(0,0,0,.35);
|
||
}
|
||
:root[data-theme="nordic"][data-mode="light"] {
|
||
--bg:#eceff4; /* nord6 */
|
||
--panel:#e5e9f0; /* nord5 */
|
||
--panel2:#d8dee9; /* nord4 */
|
||
--raise:#cdd4e0;
|
||
--line:#c5cedb;
|
||
--fg:#2e3440; /* nord0 */
|
||
--dim:#3b4252; /* nord1 */
|
||
--faint:#4c566a; /* nord3 */
|
||
--accent:#3a5e8a;
|
||
--accent2:#b0674e;
|
||
--ink:#ffffff;
|
||
--good:#446632;
|
||
--warn:#7e590e;
|
||
--bad:#9c3f49;
|
||
--shadow:0 8px 28px rgba(46,52,64,.15);
|
||
}
|
||
/* Classic: the 2004 Mac app. Colours here; the chrome it needs is at the end of the sheet. */
|
||
:root[data-theme="classic"] {
|
||
color-scheme:light;
|
||
--bg:#ffffff;
|
||
--panel:#e7ebf1; /* the source list's pale blue-grey */
|
||
--panel2:#f2f2f2;
|
||
--raise:#d5dce7;
|
||
--line:#a9a9a9;
|
||
--fg:#000000;
|
||
--dim:#444444;
|
||
--faint:#666666;
|
||
--accent:#3875d7; /* Aqua selection blue */
|
||
--accent2:#2a5db0;
|
||
--ink:#ffffff;
|
||
--good:#237a23;
|
||
--warn:#a15f00;
|
||
--bad:#c42b1c;
|
||
--shadow:0 4px 16px rgba(0,0,0,.28);
|
||
}
|
||
*{box-sizing:border-box}
|
||
/* A rule that sets display beats the UA's [hidden], and several below do. */
|
||
[hidden]{display:none!important}
|
||
html,body{height:100%}
|
||
body{
|
||
margin:0;background:var(--bg);color:var(--fg);
|
||
font:14.5px/1.55 Inter,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
|
||
display:grid;grid-template-rows:auto 1fr auto auto;overflow:hidden;
|
||
/* iOS Safari's address bar collapses and expands without firing a resize, so 100vh is
|
||
measured against whichever state happened to be current -- sized too tall while the bar
|
||
is showing, which puts the topbar (the hamburger included) under Safari's own chrome,
|
||
where a tap never reaches the page. Only a hard refresh reset it, forcing 100vh to be
|
||
recomputed. 100dvh tracks the real visible viewport as the bar moves; the 100vh above is
|
||
the fallback for a browser that does not know dvh. */
|
||
height:100vh;height:100dvh;
|
||
}
|
||
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
|
||
a{color:var(--accent)}
|
||
/* Inset wherever a parent clips its overflow, or the toolbar and the feed list cut the ring off. */
|
||
:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
||
.tgroup button:focus-visible,.feed:focus-visible,.place:focus-visible,.chev:focus-visible{outline-offset:-2px}
|
||
@media (prefers-reduced-motion:reduce){
|
||
*,*::before,*::after{animation:none!important;transition:none!important}
|
||
}
|
||
::-webkit-scrollbar{width:10px;height:10px}
|
||
::-webkit-scrollbar-thumb{background:var(--raise);border-radius:6px}
|
||
::-webkit-scrollbar-track{background:transparent}
|
||
|
||
/* ---------- shell ---------- */
|
||
#shell{display:grid;grid-template-columns:290px 1fr;min-height:0;min-width:0;overflow:hidden}
|
||
/* [ hides the feed list. A phone slides it over the page instead, so this is for wider screens. */
|
||
@media (min-width:821px){
|
||
body.nosb #shell{grid-template-columns:minmax(0,1fr)}
|
||
body.nosb #sidebar{display:none}
|
||
}
|
||
#sidebar{
|
||
background:var(--panel);border-right:1px solid var(--line);
|
||
display:flex;flex-direction:column;min-height:0;
|
||
}
|
||
.brand{display:flex;align-items:center;gap:9px;padding:14px 14px 10px}
|
||
.brand .logo{
|
||
width:30px;height:30px;flex:none;object-fit:contain;
|
||
}
|
||
.brand h1{font-size:16px;margin:0;font-weight:650;letter-spacing:-.01em;color:var(--fg);flex:1}
|
||
.iconbtn{
|
||
width:30px;height:30px;border-radius:8px;display:grid;place-items:center;
|
||
color:var(--dim);flex:none;
|
||
}
|
||
.iconbtn:hover{background:var(--raise);color:var(--fg)}
|
||
/* One toolbar across the window, as the original had: grouped buttons, search on the right. */
|
||
#topbar{
|
||
display:flex;align-items:center;gap:10px;padding:7px 12px;min-width:0;overflow:hidden;
|
||
background:var(--panel);border-bottom:1px solid var(--line);
|
||
}
|
||
#topbar .grow{flex:1}
|
||
#topbar #epSearch{width:260px;flex:none}
|
||
.tgroup{display:flex;flex:none;border:1px solid var(--line);border-radius:8px;overflow:hidden;background:var(--panel2)}
|
||
.tgroup button{padding:5px 11px;min-width:34px;color:var(--dim);font-size:14px;line-height:1.3}
|
||
.tgroup button+button{border-left:1px solid var(--line)}
|
||
.tgroup button:hover:not(:disabled){background:var(--raise);color:var(--fg)}
|
||
.tgroup button:disabled{opacity:.35;cursor:default}
|
||
.sidetools button,.sidefoot button{
|
||
flex:1;background:var(--panel2);border:1px solid var(--line);border-radius:8px;
|
||
padding:6px 8px;font-size:12.5px;color:var(--dim);
|
||
}
|
||
.sidetools button:hover,.sidefoot button:hover{border-color:var(--accent);color:var(--fg)}
|
||
/* Settings and the log are housekeeping: they sit under the feeds, out of the way. */
|
||
.sidefoot{
|
||
display:flex;flex-direction:column;gap:7px;padding:8px 12px;
|
||
border-top:1px solid var(--line);flex:none;
|
||
}
|
||
.sidefoot .row{display:flex;gap:6px}
|
||
.who{display:flex;align-items:center;gap:8px;font-size:11.5px;color:var(--faint)}
|
||
.who span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.who button{flex:none;background:none;border:0;padding:2px 4px;color:var(--faint);text-decoration:underline}
|
||
.who button:hover{color:var(--fg)}
|
||
.sidefoot button{display:inline-flex;align-items:center;justify-content:center;gap:6px}
|
||
.sidefoot i{font-style:normal;opacity:.75}
|
||
.searchwrap{padding:0 12px 8px}
|
||
input[type=search],input[type=text],input[type=password],input[type=number],select{
|
||
width:100%;background:var(--bg);border:1px solid var(--line);color:var(--fg);
|
||
border-radius:8px;padding:7px 10px;font:inherit;font-size:13.5px;
|
||
}
|
||
input:focus,select:focus{outline:0;border-color:var(--accent)}
|
||
/* The wider left gutter is the folder triangle's; everything in the list shifts with it, so the
|
||
feeds still line up with the places above. */
|
||
#feedlist{overflow-y:auto;padding:0 8px 12px 16px;flex:1;min-height:0}
|
||
.feed{
|
||
display:flex;gap:10px;align-items:center;padding:7px 8px;border-radius:9px;
|
||
cursor:pointer;margin-bottom:1px;position:relative;
|
||
}
|
||
.feed:hover{background:var(--panel2)}
|
||
.feed.sel{background:var(--raise)}
|
||
/* A show sits under its folder's title, a size down, so an open folder reads as one. */
|
||
.feed.child{margin-left:11px}
|
||
/* Pinned feeds, at the top of the list: a small pin before the name, and a rule under the last. */
|
||
.feed .fpin .i{width:10px;height:10px;margin-right:5px;vertical-align:-1px;color:var(--accent)}
|
||
.feed.lastpin{margin-bottom:9px}
|
||
.feed.lastpin::after{content:"";position:absolute;left:8px;right:8px;bottom:-5px;border-bottom:1px solid var(--line)}
|
||
.feed.child .art{width:28px;height:28px;font-size:11px}
|
||
/* Only a folder has a triangle, hung in the margin so every feed's art lines up with the places
|
||
above it. The button is the row's full height and 24 px wide: a near miss used to open the
|
||
folder's page, and 16 px left the triangle cramped against the art. */
|
||
.chev{
|
||
position:absolute;left:-16px;top:0;bottom:0;width:24px;display:grid;place-items:center;
|
||
border-radius:4px;color:var(--faint);
|
||
}
|
||
.chev:hover{color:var(--fg)}
|
||
.chev.bad,.chev.bad:hover{color:var(--bad)}
|
||
/* A feed's error mark, in the triangle's place: the same column as every folder's triangle,
|
||
a child's included, which is why it moves left by the child's indent. */
|
||
.ferr{position:absolute;left:-16px;top:0;bottom:0;width:24px;display:grid;place-items:center;color:var(--bad)}
|
||
.ferr .i{width:12px;height:12px}
|
||
.feed.child .ferr{left:-27px}
|
||
.chev .i{width:12px;height:12px;transition:transform .12s}
|
||
.chev[aria-expanded="true"] .i{transform:rotate(90deg)}
|
||
.childlist{display:grid;grid-template-columns:minmax(0,1fr);gap:4px;margin-top:10px}
|
||
.childrow{
|
||
display:flex;gap:10px;align-items:center;padding:7px 9px;border:1px solid var(--line);
|
||
border-radius:9px;cursor:pointer;background:var(--panel);
|
||
}
|
||
.childrow:hover{border-color:var(--accent)}
|
||
.childrow .art{width:32px;height:32px;font-size:12px}
|
||
.childrow .txt{flex:1;min-width:0}
|
||
.childrow .txt b{display:block;font-weight:500;font-size:13.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
/* Currently Listening: how far into each episode you are, as a rail along the foot of its row.
|
||
The one in the player is marked as it is everywhere else, by the amber EQ bars, and its rail
|
||
and time left move as it plays. The rest stay neutral, so that one is what stands out. */
|
||
#listening .childrow{position:relative;overflow:hidden;gap:12px;padding:10px 10px 13px}
|
||
#listening .childrow .art{width:44px;height:44px;font-size:13px}
|
||
#listening .txt b{display:flex;align-items:center;gap:7px}
|
||
#listening .txt b span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
#listening .txt small{display:flex;gap:12px;margin-top:3px;color:var(--faint);font-size:11.5px}
|
||
#listening .txt small .fd{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
#listening .txt small .left{flex:none;color:var(--dim)}
|
||
#listening .childrow:not(.now) .eq{display:none}
|
||
#listening .rail{position:absolute;left:0;right:0;bottom:0;height:3px;background:var(--raise)}
|
||
#listening .rail i{display:block;height:100%;width:0;background:var(--faint);transition:width .25s linear}
|
||
#listening .now{border-color:color-mix(in srgb,var(--accent2) 60%,var(--line))}
|
||
#listening .now .eq,#listening .now .left{color:var(--accent2)}
|
||
#listening .now .rail i{background:var(--accent2)}
|
||
/* Quiet buttons: a filled one on every row outshouted the row that is playing. */
|
||
#listening [data-a=play]{color:var(--accent)}
|
||
#listening [data-a=remove]{color:var(--faint)}
|
||
/* Directory's filters: .tabs for what a feed is, as the item filters are, and chips for what it
|
||
is about. A picked chip is underlined in --accent2, as the download bar and the now-playing EQ
|
||
are; a .badge's fill already means unread in the sidebar. */
|
||
.dirbar{display:flex;flex-wrap:wrap;align-items:center;gap:8px 14px;margin-top:4px}
|
||
.dirbar .tabs{flex:none}
|
||
.chips{display:flex;flex-wrap:wrap;gap:2px 6px;flex:1 1 0;min-width:0}
|
||
.chips button{flex:none;padding:4px 6px;font-size:13px;color:var(--dim);white-space:nowrap;border-bottom:2px solid transparent}
|
||
.chips button:hover{color:var(--fg)}
|
||
.chips button[aria-pressed="true"]{color:var(--fg);border-bottom-color:var(--accent2)}
|
||
/* Square cover art, title underneath: podcast art is made to be known at a glance. The subscribe
|
||
button stays visible, since a button that only shows on hover cannot be reached on a phone. */
|
||
.tiles{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:18px 14px;margin-top:14px}
|
||
.tiles>.hint{grid-column:1/-1}
|
||
.tile{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 2px;align-items:start;cursor:pointer}
|
||
.tile .art{grid-column:1/-1;width:100%;height:auto;aspect-ratio:1;font-size:34px}
|
||
.tile .txt b{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;overflow-wrap:anywhere;font-weight:500;font-size:13px;line-height:1.3}
|
||
.tile .txt small{display:block;color:var(--faint);font-size:11.5px}
|
||
.tile:hover .txt b{color:var(--accent)}
|
||
.tag{
|
||
font-size:11px;font-weight:600;
|
||
padding:1px 5px;border-radius:4px;background:var(--raise);color:var(--warn);flex:none;
|
||
}
|
||
.art{
|
||
border-radius:7px;object-fit:cover;background:var(--raise);flex:none;
|
||
display:grid;place-items:center;color:var(--faint);font-weight:600;overflow:hidden;
|
||
}
|
||
/* Initials, tinted by a hash of the name so two "TS" in a row can be told apart. Mixed into the
|
||
page ground, not --raise: that is also the selected row, and a selected tile vanished into it. */
|
||
.art.ini{background:color-mix(in srgb,var(--tint) 24%,var(--bg));color:var(--tint)}
|
||
/* A folder: its first four shows' art, as iTunes built a playlist's from its albums. */
|
||
.art.mosaic{grid-template:1fr 1fr/1fr 1fr;place-items:stretch}
|
||
.art.mosaic img{width:100%;height:100%;min-height:0;object-fit:cover;display:block}
|
||
.feed .art{width:34px;height:34px;font-size:13px}
|
||
.feed .txt{min-width:0;flex:1}
|
||
.feed .txt b{display:block;font-weight:500;font-size:13.5px;line-height:1.35;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.feed .txt small{display:block;color:var(--faint);font-size:11.5px;line-height:1.35;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.badge{
|
||
background:var(--accent2);color:var(--ink);border-radius:20px;padding:1px 7px;
|
||
font-size:11px;font-weight:600;flex:none;min-width:26px;text-align:center;
|
||
}
|
||
.badge.zero{background:var(--raise);color:var(--faint)}
|
||
/* Counts, sizes and times that change in place should not jiggle the text around them. */
|
||
.badge,.feed small,.childrow small,.tile small,.ep,.fhead .sub,.dmeta,#status,#seekrow{font-variant-numeric:tabular-nums}
|
||
|
||
/* ---------- main ---------- */
|
||
#main{overflow:hidden;min-height:0;min-width:0;display:flex;flex-direction:column}
|
||
.wrap{flex:1;min-height:0;min-width:0;display:flex;flex-direction:column}
|
||
.wrap.plain{overflow-y:auto;padding:18px 22px 40px;display:block}
|
||
#content{display:flex;flex-direction:column;min-height:0;min-width:0;flex:1}
|
||
#content>.fhead,#content>.toolbar{padding-left:20px;padding-right:20px;flex:none}
|
||
#content>.fhead{padding-top:16px}
|
||
|
||
/* Three panes, as the original had: feeds beside, items above, the item below. */
|
||
#split{
|
||
display:grid;flex:1;min-height:0;
|
||
grid-template-columns:minmax(0,1fr) 270px;
|
||
grid-template-rows:minmax(90px,var(--listh,60%)) 7px 1fr;
|
||
grid-template-areas:"list files" "grab grab" "detail detail";
|
||
}
|
||
#list{grid-area:list;overflow:auto;padding:0 14px 10px;overscroll-behavior-y:contain}
|
||
/* Pull-to-refresh's note (gestures.ts). overscroll-behavior keeps the browser's own pull, which
|
||
reloads the page, from starting on top of it. */
|
||
#pulltip{display:flex;align-items:flex-end;justify-content:center;padding-bottom:6px;overflow:hidden;
|
||
font-size:12px;color:var(--faint)}
|
||
/* The selected item's files, beside the list, as the original's Files pane was. */
|
||
#files{grid-area:files;overflow-y:auto;padding:8px 12px;border-left:1px solid var(--line);background:var(--panel)}
|
||
/* Nothing selected, or an item with no files: the list takes the width. */
|
||
#split:has(>#files[hidden]){grid-template-columns:minmax(0,1fr) 0}
|
||
#files .encbox{margin-top:8px}
|
||
#files .empty{padding:24px 0}
|
||
.fhd{font-size:12px;color:var(--faint)}
|
||
#grab{grid-area:grab;cursor:row-resize;background:var(--line)}
|
||
#grab:hover{background:var(--accent)}
|
||
#detail{grid-area:detail;overflow-y:auto;padding:16px 20px 28px;background:var(--panel);border-top:1px solid var(--line)}
|
||
.dt{font-size:18px;font-weight:650;margin:0 0 5px;line-height:1.3;overflow-wrap:anywhere}
|
||
.dmeta{color:var(--faint);font-size:12.5px;display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:14px}
|
||
.dmeta .btn{padding:3px 9px;font-size:12px}
|
||
#detail .encbox{margin:0 0 10px}
|
||
.dbody{font-size:14.5px;line-height:1.65;overflow-wrap:anywhere;color:var(--fg)}
|
||
.dbody img{max-width:100%;height:auto;border-radius:6px}
|
||
.dbody a{color:var(--accent)}
|
||
.dbody pre{overflow-x:auto;background:var(--panel2);padding:10px;border-radius:8px}
|
||
.encbox{
|
||
display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:14px;
|
||
padding:10px 12px;border:1px solid var(--line);border-radius:9px;background:var(--panel2);
|
||
}
|
||
.fhead{display:flex;gap:18px;margin-bottom:18px}
|
||
.fhead .art{width:118px;height:118px;font-size:34px;box-shadow:var(--shadow)}
|
||
.fhead .meta{min-width:0;flex:1;display:flex;flex-direction:column}
|
||
.fhead h2{margin:0 0 3px;font-size:23px;line-height:1.2;overflow-wrap:anywhere}
|
||
.fhead .sub{color:var(--dim);font-size:13px;margin-bottom:8px}
|
||
.fhead .sub a{color:var(--dim);text-decoration:none}
|
||
.fhead .sub a:hover{color:var(--accent)}
|
||
.acts{display:flex;gap:7px;flex-wrap:wrap;align-items:center;margin-top:auto}
|
||
.acts .btn{display:inline-flex;align-items:center;gap:6px;padding:7px 13px;border-radius:999px}
|
||
.acts .btn i{font-style:normal;font-size:13px;line-height:1;opacity:.7}
|
||
.acts .btn.primary i{opacity:.9}
|
||
.acts .btn:hover{background:var(--raise)}
|
||
.acts .btn.primary:hover{background:var(--accent)}
|
||
.btn{
|
||
background:var(--panel2);border:1px solid var(--line);border-radius:8px;
|
||
padding:6px 12px;font-size:13px;
|
||
}
|
||
.btn:hover{border-color:var(--accent)}
|
||
a.btn{text-decoration:none;color:inherit}
|
||
.btn.primary{background:var(--accent);border-color:var(--accent);color:var(--ink);font-weight:600}
|
||
.btn.primary:hover{filter:brightness(1.08)}
|
||
.btn.danger:hover{border-color:var(--bad);color:var(--bad)}
|
||
/* An icon in place of a word; the word is in its tooltip. */
|
||
.btn.ico{padding:4px 9px;min-width:32px;font-size:14px;line-height:1.25;text-align:center}
|
||
/* Inline in a sentence, next to a plain-word error explanation. */
|
||
.btn.tiny{padding:2px 7px;font-size:11.5px;border-radius:6px;margin-left:2px}
|
||
/* The keys ? lists. */
|
||
.keys{border-collapse:collapse;width:100%;font-size:13px}
|
||
.keys th{text-align:left;font-weight:600;padding:12px 0 4px;color:var(--fg)}
|
||
.keys td{padding:3px 0;color:var(--dim)}
|
||
.keys td:first-child{width:10em;white-space:nowrap}
|
||
kbd{font:inherit;font-size:12px;color:var(--fg);background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:0 5px}
|
||
/* An icon (Font Awesome, embedded as SVG) in the button's own colour. */
|
||
.i{display:inline-block;width:16px;height:16px;vertical-align:-3px;flex:none;fill:currentColor}
|
||
/* A file's type as an icon, in place of the old DOWNLOADED / PENDING / audio chips: green once
|
||
it is here, red when the download failed. */
|
||
.kind{display:inline-grid;place-items:center;color:var(--dim)}
|
||
.kind.here{color:var(--good)}
|
||
.subbed{display:inline-flex;padding:0 9px;color:var(--good)}
|
||
.subbed .i{width:18px;height:18px}
|
||
.kind.bad{color:var(--bad)}
|
||
.fhead .art .i{width:22px;height:22px}
|
||
.toolbar{
|
||
display:flex;gap:10px;align-items:center;margin-bottom:12px;flex-wrap:wrap;
|
||
position:sticky;top:0;background:var(--bg);padding:6px 0 8px;z-index:3;
|
||
}
|
||
.tabs{display:flex;gap:2px;background:var(--panel2);border-radius:9px;padding:3px}
|
||
.tabs button{padding:5px 12px;border-radius:7px;font-size:13px;color:var(--dim)}
|
||
.tabs button.on{background:var(--raise);color:var(--fg);font-weight:500}
|
||
.toolbar .grow{flex:1;min-width:150px;max-width:320px}
|
||
|
||
/* ---------- items ---------- */
|
||
/* A table, as the original's was: unread, kept, title, feed, file, size, published. */
|
||
.ephead,.ep{
|
||
display:grid;gap:8px;align-items:center;
|
||
grid-template-columns:22px 22px minmax(120px,1fr) minmax(0,160px) 56px minmax(0,72px) minmax(0,92px) 64px;
|
||
}
|
||
/* A pixel more side padding than a row's: a row has a 1px border the heading has not, and without
|
||
it every heading sat a pixel left of its column. */
|
||
.ephead{
|
||
position:sticky;top:0;z-index:2;background:var(--bg);padding:7px 11px 5px;
|
||
border-bottom:1px solid var(--line);margin-bottom:3px;
|
||
font-size:12px;color:var(--faint);
|
||
}
|
||
/* Each heading sorts by its column; the caret says which way. */
|
||
.ephead .hs{display:flex;align-items:center;gap:4px;min-width:0;padding:0;border:0;background:none;
|
||
font:inherit;color:inherit;text-transform:inherit;letter-spacing:inherit;text-align:left;cursor:pointer}
|
||
.ephead .hs:hover,.ephead .hs.on{color:var(--fg)}
|
||
.ephead .hs .arr{display:inline-flex}
|
||
.ephead .hs .arr .i{width:8px;height:8px}
|
||
.ephead .hs .arr.asc{transform:rotate(-90deg)}
|
||
.ephead .hs .arr.desc{transform:rotate(90deg)}
|
||
/* An icon heading is centred over its column, as the icons under it are, and its caret hangs
|
||
outside so sorting by it does not push the icon off line. */
|
||
.ephead .hs.h-ic{justify-content:center;position:relative;width:100%}
|
||
.ephead .hs.h-ic .arr{position:absolute;left:100%}
|
||
.ep{padding:4px 10px;border-radius:7px;border:1px solid transparent;cursor:pointer;margin-bottom:1px}
|
||
.ep>*{min-width:0}
|
||
.ep.sel{background:var(--raise);border-color:var(--line)}
|
||
.ep:hover{background:var(--panel)}
|
||
/* Amber means new: the unread dot, the badges and a download under way. Blue is for the one
|
||
primary action and links, so a count no longer looks like a button. */
|
||
/* No padding: a button keeps the browser's side padding, which left too little room for the 16px
|
||
icon, so it spilled 3px right of centre and off line with its heading. */
|
||
.ep .st,.ep .fl{width:22px;height:22px;padding:0;border-radius:5px;display:grid;place-items:center;font-size:11px;color:var(--accent2)}
|
||
.ep .fl{color:var(--faint);font-size:13px}
|
||
.ep .fl.on{color:var(--fg)}
|
||
/* The icon's EQ bars mark what is playing: standing still, and moving only while it plays. A
|
||
paused animation was tried first; the frames it held were a pixel apart and read as dots. */
|
||
.eq{display:inline-flex;align-items:flex-end;gap:2px;width:13px;height:12px;flex:none}
|
||
.eq i{flex:1;height:100%;background:currentColor;border-radius:1px;transform-origin:bottom}
|
||
.eq i:nth-child(1){height:60%}
|
||
.eq i:nth-child(3){height:40%}
|
||
body.playing .eq i{animation:eq .9s ease-in-out infinite alternate}
|
||
body.playing .eq i:nth-child(1){animation-delay:-.3s}
|
||
body.playing .eq i:nth-child(3){animation-delay:-.6s}
|
||
@keyframes eq{from{transform:scaleY(.35)}}
|
||
.ep .st:hover,.ep .fl:hover{background:var(--raise)}
|
||
.ep .t{font-weight:600;font-size:13.5px;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.ep.read .t{color:var(--dim);font-weight:500}
|
||
.ep .line{display:flex;gap:9px;align-items:center;flex-wrap:wrap;color:var(--faint);font-size:11.5px}
|
||
.ep .line:empty{display:none}
|
||
.ep .fd,.ep .size,.ep .date{color:var(--dim);font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.ep .file{display:flex;gap:6px;align-items:center;position:relative;color:var(--faint);font-size:11.5px}
|
||
/* Under the icon, not beside it or on a line of its own: taking a line, it lifted the icon of
|
||
every file not yet downloaded above the downloaded ones' (issue #31). */
|
||
.ep .file .dlbar{position:absolute;left:0;right:0;bottom:-5px;margin:0}
|
||
.ep .rowacts{justify-content:flex-end}
|
||
/* One feed's own table has no need of a Feed column; All Subscriptions does. */
|
||
#split.one .ephead,#split.one .ep{grid-template-columns:22px 22px minmax(120px,1fr) 56px minmax(0,72px) minmax(0,92px) 64px}
|
||
#split.one .h-fd,#split.one .ep .fd{display:none}
|
||
.dot{width:3px;height:3px;border-radius:50%;background:var(--faint);flex:none}
|
||
.ep .rowacts{display:flex;gap:2px;align-items:flex-start;opacity:0;transition:opacity .12s}
|
||
.ep:hover .rowacts{opacity:1}
|
||
.dlbar{height:3px;background:transparent;border-radius:2px;overflow:hidden;margin-top:7px}
|
||
.dlbar.live{background:var(--raise)}
|
||
.dlbar i{display:block;height:100%;width:0;background:var(--accent2);transition:width .25s}
|
||
.empty{color:var(--faint);text-align:center;padding:50px 0}
|
||
#more{display:block;width:100%;margin-top:10px}
|
||
|
||
/* ---------- player ---------- */
|
||
#player{
|
||
border-top:1px solid var(--line);background:var(--panel);
|
||
display:none;grid-template-columns:auto 1fr auto;gap:14px;align-items:center;
|
||
padding:9px 16px;box-shadow:0 -6px 24px rgba(6,10,16,.4);
|
||
}
|
||
#player.on{display:grid}
|
||
/* #audio is a <video> playing double duty as the audio element (see its tag). Only a video
|
||
file needs to be seen, floated above the bar rather than laid into it, so an audio episode's
|
||
layout is unchanged. */
|
||
#audio{display:none}
|
||
body.has-video #audio{
|
||
display:block;position:fixed;z-index:45;right:16px;bottom:120px;
|
||
width:360px;max-width:calc(100vw - 32px);aspect-ratio:16/9;background:#000;
|
||
border-radius:10px;box-shadow:0 10px 30px rgba(0,0,0,.5);
|
||
}
|
||
#pnow{display:flex;gap:11px;align-items:center;min-width:0;width:250px}
|
||
#pnow .art{width:44px;height:44px;font-size:13px}
|
||
#pnow .txt{min-width:0}
|
||
#pnow b{display:block;font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
#pnow .eq{color:var(--accent2);margin-right:6px}
|
||
#pnow small{color:var(--faint);font-size:11.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block}
|
||
#pmid{display:flex;flex-direction:column;gap:3px;min-width:0}
|
||
#pbtns{display:flex;gap:5px;align-items:center;justify-content:center}
|
||
#pbtns .iconbtn{width:34px;height:34px}
|
||
#pplay{
|
||
width:40px!important;height:40px!important;background:var(--fg);color:var(--bg);
|
||
border-radius:50%;
|
||
}
|
||
#pplay:hover{background:var(--accent);color:var(--ink)}
|
||
#seekrow{display:flex;gap:9px;align-items:center;font-variant-numeric:tabular-nums;font-size:11.5px;color:var(--faint)}
|
||
input[type=range]{
|
||
-webkit-appearance:none;appearance:none;height:4px;border-radius:3px;flex:1;
|
||
background:var(--raise);cursor:pointer;
|
||
}
|
||
input[type=range]::-webkit-slider-thumb{
|
||
-webkit-appearance:none;width:12px;height:12px;border-radius:50%;background:var(--accent);
|
||
}
|
||
input[type=range]::-moz-range-thumb{width:12px;height:12px;border:0;border-radius:50%;background:var(--accent)}
|
||
#pright{display:flex;gap:8px;align-items:center}
|
||
#pright select{width:auto;padding:4px 6px;font-size:12px}
|
||
#vol{width:78px;flex:none}
|
||
|
||
/* ---------- overlays ---------- */
|
||
#modal{
|
||
position:fixed;inset:0;background:rgba(0,0,0,.6);display:none;
|
||
place-items:center;z-index:50;padding:20px;
|
||
}
|
||
#modal.on{display:grid}
|
||
.card{
|
||
background:var(--panel);border:1px solid var(--line);border-radius:14px;
|
||
padding:20px;width:min(460px,100%);box-shadow:var(--shadow);max-height:88vh;overflow:auto;
|
||
}
|
||
.card.wide{width:min(1000px,100%)}
|
||
#logbox{
|
||
background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:10px 12px;
|
||
height:min(60vh,520px);overflow:auto;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;
|
||
}
|
||
#logbox .l{display:flex;gap:9px;white-space:pre-wrap;overflow-wrap:anywhere}
|
||
#logbox time{color:var(--faint);flex:none}
|
||
#logbox .lv{flex:none;width:42px;font-weight:700}
|
||
#logbox .lv.ERROR{color:var(--bad)} #logbox .lv.WARN{color:var(--warn)}
|
||
#logbox .lv.INFO{color:var(--accent)} #logbox .lv.DEBUG,#logbox .lv.TRACE{color:var(--faint)}
|
||
#logbox .tg{color:var(--faint);flex:none}
|
||
.logbar{display:flex;gap:8px;align-items:center;margin-bottom:9px;flex-wrap:wrap}
|
||
.logbar .grow{flex:1;min-width:120px}
|
||
.card h3{margin:0 0 14px;font-size:17px}
|
||
.field{display:grid;gap:4px;margin-bottom:12px}
|
||
.field label{font-size:12px;color:var(--dim)}
|
||
.field .hint{font-size:11.5px;color:var(--faint)}
|
||
.check{display:flex;gap:8px;align-items:center;font-size:13.5px;margin-bottom:9px}
|
||
.inline{display:flex;gap:6px;align-items:stretch}
|
||
.inline input{flex:1;min-width:0}
|
||
.inline .btn{white-space:nowrap;flex:none}
|
||
.inline select{flex:none;width:auto}
|
||
.inline input[type=number]{flex:none;width:90px}
|
||
.check input{width:16px;height:16px;accent-color:var(--accent)}
|
||
.cardacts{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}
|
||
#toasts{position:fixed;bottom:88px;right:18px;display:flex;flex-direction:column;gap:8px;z-index:60}
|
||
.toast{
|
||
background:var(--raise);border:1px solid var(--line);border-radius:9px;
|
||
padding:9px 13px;font-size:13px;box-shadow:var(--shadow);animation:in .18s;max-width:340px;
|
||
}
|
||
.toast.bad{border-color:var(--bad);color:var(--bad)}
|
||
@keyframes in{from{opacity:0;transform:translateY(6px)}}
|
||
#burger,#dback{display:none}
|
||
/* Totals for what is showing, along the bottom, as the original's status bar. */
|
||
#status{
|
||
padding:3px 14px;min-height:22px;font-size:12px;color:var(--faint);background:var(--panel);
|
||
border-top:1px solid var(--line);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
||
}
|
||
/* The feed's own header, kept to one line so the table starts high, as it did. */
|
||
.fhead.slim{align-items:center;gap:8px 12px;margin-bottom:10px;flex-wrap:wrap}
|
||
.fhead.slim .art{width:44px;height:44px;font-size:15px;box-shadow:none}
|
||
.fhead.slim .meta{flex:1 1 260px}
|
||
/* One line each, or a long title wraps to three and pushes the table down. */
|
||
.fhead.slim h2,.fhead.slim .sub.stat{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.fhead.slim h2{font-size:17px}
|
||
.fhead.slim .sub{margin-bottom:0;font-size:12.5px}
|
||
.fhead.slim .acts{margin-top:0;flex:none}
|
||
.fhead.slim .acts .btn{padding:5px 11px;font-size:12.5px}
|
||
/* Places above the feeds: not subscriptions, but where to look. */
|
||
.places{border-bottom:1px solid var(--line);margin:0 0 6px;padding-bottom:6px}
|
||
.place{display:flex;gap:10px;align-items:center;padding:6px 8px;border-radius:9px;cursor:pointer}
|
||
.place:hover{background:var(--panel2)}
|
||
.place.sel{background:var(--raise)}
|
||
.place .ico{width:18px;text-align:center;color:var(--accent);flex:none}
|
||
.place b{flex:1;font-weight:500;font-size:13.5px}
|
||
|
||
@media (max-width:820px){
|
||
#shell{grid-template-columns:1fr}
|
||
#sidebar{position:fixed;inset:0 auto 0 0;width:min(300px,86vw);z-index:42;transform:translateX(-100%);transition:transform .2s;box-shadow:var(--shadow)}
|
||
#sidebar.open{transform:none}
|
||
#scrim{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:41}
|
||
#player{position:relative;z-index:39;padding:7px 10px;gap:8px}
|
||
|
||
/* The feed list is reachable whether or not anything is playing. */
|
||
#burger{display:grid}
|
||
#topbar{gap:6px;padding:6px 8px}
|
||
#topbar .grow,.tgroup.item{display:none}
|
||
#topbar #epSearch{width:auto;flex:1;min-width:0}
|
||
.tgroup button{padding:4px 8px;min-width:32px}
|
||
.fhead.slim .acts{flex:1 1 100%}
|
||
.iconbtn{width:38px;height:38px}
|
||
|
||
/* One pane at a time: the list, then the item over it. */
|
||
#split{grid-template-columns:1fr;grid-template-rows:1fr;grid-template-areas:"list"}
|
||
/* Title and date only; the files sit above the item's text in the reader instead. */
|
||
#files,.ephead,.ep .fd,.ep .file,.ep .size,.ep .rowacts{display:none}
|
||
.ep,#split.one .ep{grid-template-columns:20px 20px minmax(0,1fr) auto}
|
||
#grab{display:none}
|
||
#detail{position:fixed;inset:0;z-index:38;display:none;border-top:0;padding:12px 16px 90px}
|
||
body.reading #detail{display:block}
|
||
#dback{display:inline-block;margin-bottom:10px}
|
||
|
||
#content>.fhead,#content>.toolbar{padding-left:14px;padding-right:14px}
|
||
#content>.fhead{padding-top:12px}
|
||
.fhead{gap:12px;margin-bottom:12px}
|
||
.fhead .art{width:64px;height:64px;font-size:20px}
|
||
.fhead h2{font-size:18px}
|
||
.fhead .sub{font-size:12px;margin-bottom:6px}
|
||
.acts{gap:6px}
|
||
.acts .btn{padding:7px 10px;font-size:12.5px}
|
||
.toolbar{gap:8px}
|
||
#list{padding:0 12px 10px}
|
||
#player{grid-template-columns:1fr auto;grid-template-areas:"now right" "mid mid";gap:2px 8px}
|
||
#pnow{grid-area:now;width:auto}
|
||
#pright{grid-area:right}
|
||
#pmid{grid-area:mid}
|
||
#pnow .art{width:38px;height:38px}
|
||
#pnow .txt small,#pright #vol{display:none}
|
||
.wrap.plain{padding:14px}
|
||
.card{padding:16px}
|
||
/* The chips get a line of their own and scroll sideways rather than wrap, so they never push
|
||
the grid down. */
|
||
.chips{flex:1 1 100%;flex-wrap:nowrap;overflow-x:auto}
|
||
.tiles{grid-template-columns:repeat(auto-fill,minmax(104px,1fr));gap:14px 10px}
|
||
|
||
/* A log line has no room for four columns: keep time and message, wrap as prose. */
|
||
#logbox{font-size:11.5px;padding:8px}
|
||
#logbox .l{flex-wrap:wrap;gap:6px}
|
||
#logbox .lv{width:auto}
|
||
#logbox .tg{display:none}
|
||
}
|
||
|
||
/* ---------- Classic: the 2004 Mac app ---------- */
|
||
/* After the iPodderX screenshots: brushed-metal chrome, a pale blue-grey source list, Aqua blue
|
||
for whatever is selected, red unread badges, a striped table and Lucida Grande. */
|
||
:root[data-theme="classic"] body{
|
||
font-family:"Lucida Grande","Lucida Sans Unicode","Lucida Sans",Geneva,Verdana,sans-serif;font-size:13px;
|
||
}
|
||
:root[data-theme="classic"] #topbar,
|
||
:root[data-theme="classic"] #status,
|
||
:root[data-theme="classic"] #player{background:linear-gradient(#ececec,#c9c9c9);border-color:#8e8e8e;color:#1a1a1a}
|
||
:root[data-theme="classic"] #status{text-align:center}
|
||
:root[data-theme="classic"] #sidebar{background:#e7ebf1;border-right-color:#a3a3a3}
|
||
:root[data-theme="classic"] #files{background:#f4f4f4}
|
||
:root[data-theme="classic"] .tgroup{background:none;border-color:#8e8e8e;border-radius:6px}
|
||
:root[data-theme="classic"] .tgroup button+button{border-left-color:#8e8e8e}
|
||
:root[data-theme="classic"] .tgroup button,
|
||
:root[data-theme="classic"] .btn,
|
||
:root[data-theme="classic"] .tabs{background:linear-gradient(#ffffff,#d9d9d9);border-color:#8e8e8e;color:#1a1a1a}
|
||
:root[data-theme="classic"] .tabs{border:1px solid #8e8e8e;border-radius:6px}
|
||
:root[data-theme="classic"] .tgroup button:hover:not(:disabled),
|
||
:root[data-theme="classic"] .btn:hover{background:linear-gradient(#f5f9ff,#cbdbf4)}
|
||
:root[data-theme="classic"] .btn.primary,
|
||
:root[data-theme="classic"] .tabs button.on,
|
||
:root[data-theme="classic"] .feed.sel,
|
||
:root[data-theme="classic"] .place.sel{background:linear-gradient(#86b0ec,#3a73cf);border-color:#2d5eae;color:#fff}
|
||
:root[data-theme="classic"] .feed.sel .txt small,
|
||
:root[data-theme="classic"] .place.sel .ico{color:#e6eeff}
|
||
:root[data-theme="classic"] #epSearch{border-radius:14px}
|
||
:root[data-theme="classic"] .badge{background:#c9302c;color:#fff}
|
||
:root[data-theme="classic"] .badge.zero{background:#b4bdc9;color:#fff}
|
||
:root[data-theme="classic"] .ephead,
|
||
:root[data-theme="classic"] .fhd{
|
||
background:linear-gradient(#fbfbfb,#dcdcdc);color:#1a1a1a;font-size:11px;
|
||
}
|
||
:root[data-theme="classic"] .fhd{padding:3px 6px;border:1px solid #c2c2c2}
|
||
:root[data-theme="classic"] .ephead{border-bottom-color:#a3a3a3}
|
||
:root[data-theme="classic"] .ep{border-radius:0;margin:0}
|
||
:root[data-theme="classic"] #eps .ep:nth-child(even){background:#edf3fe}
|
||
:root[data-theme="classic"] .ep .t{color:#1c4fa8}
|
||
:root[data-theme="classic"] .ep.read .t{color:#333}
|
||
:root[data-theme="classic"] #eps .ep.sel{background:#3875d7;border-color:#3875d7}
|
||
:root[data-theme="classic"] .ep.sel .t,
|
||
:root[data-theme="classic"] .ep.sel .fd,
|
||
:root[data-theme="classic"] .ep.sel .size,
|
||
:root[data-theme="classic"] .ep.sel .date,
|
||
:root[data-theme="classic"] .ep.sel .line,
|
||
:root[data-theme="classic"] .ep.sel .st,
|
||
:root[data-theme="classic"] .ep.sel .fl,
|
||
:root[data-theme="classic"] .ep.sel .file,
|
||
:root[data-theme="classic"] .ep.sel .kind{color:#fff}
|
||
/* Green and red stay green and red on the blue, just lighter so they read. */
|
||
:root[data-theme="classic"] .ep.sel .kind.here{color:#a6f3a6}
|
||
:root[data-theme="classic"] .ep.sel .kind.bad{color:#ffb8ad}
|
||
/* Lists were white in the original; the pale blue-grey belongs to the source list alone. */
|
||
:root[data-theme="classic"] .childrow{background:#fff}
|
||
:root[data-theme="classic"] .dt{background:linear-gradient(#80aae6,#3f78cf);color:#fff;padding:6px 12px;border-radius:4px}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header id="topbar">
|
||
<button class="iconbtn" id="burger" title="Feeds" aria-label="Feeds" data-icon="menu"></button>
|
||
<!-- One group per thing acted on, in the order the panes read: feeds, then the selected item.
|
||
A phone hides the item group, which it has no table for. -->
|
||
<div class="tgroup">
|
||
<button id="addFeed" title="Add a feed" aria-label="Add a feed" data-icon="plus"></button>
|
||
<button id="tbRemove" title="Unsubscribe from this feed" aria-label="Unsubscribe from this feed" data-icon="circleMinus" disabled></button>
|
||
<button id="scanAll" title="Check every feed for new items" aria-label="Check every feed for new items" data-icon="scan"></button>
|
||
</div>
|
||
<div class="tgroup item">
|
||
<button id="tbPlay" title="Play the selected item" aria-label="Play the selected item" data-icon="play" disabled></button>
|
||
<button id="tbRead" title="Mark the selected item read or unread" aria-label="Mark read or unread" data-icon="check" disabled></button>
|
||
<button id="tbFlag" title="Pin the selected item, so it is never deleted" aria-label="Pin" data-icon="pin" disabled></button>
|
||
</div>
|
||
<span class="grow"></span>
|
||
<input type="search" id="epSearch" placeholder="Search items…">
|
||
<div class="tgroup" id="admintools">
|
||
<button id="prefs" title="Settings" aria-label="Settings" data-icon="settings"></button>
|
||
<button id="logs" title="Daemon and web log" aria-label="Log" data-icon="log"></button>
|
||
</div>
|
||
</header>
|
||
<div id="shell">
|
||
<aside id="sidebar">
|
||
<div class="brand">
|
||
<img class="logo" src="/icon.png" alt="iPodderX" title="The original iPodderX icon, 2004"><h1>iPodderX</h1>
|
||
</div>
|
||
<div class="searchwrap"><input type="search" id="feedFilter" placeholder="Filter feeds…"></div>
|
||
<div id="feedlist"></div>
|
||
<div class="sidefoot">
|
||
<div class="who"><span id="who"></span><button id="signout" title="Sign out" aria-label="Sign out" data-icon="signout"></button></div>
|
||
</div>
|
||
</aside>
|
||
<div id="main">
|
||
<div class="wrap" id="content"></div>
|
||
</div>
|
||
<div id="scrim" hidden></div>
|
||
</div>
|
||
<div id="status"><span id="count"></span></div>
|
||
|
||
<div id="player">
|
||
<div id="pnow">
|
||
<div id="partwrap"></div>
|
||
<div class="txt"><b><span class="eq" aria-hidden="true"><i></i><i></i><i></i></span><span id="ptitle"></span></b><small id="pfeed"></small></div>
|
||
</div>
|
||
<div id="pmid">
|
||
<div id="pbtns">
|
||
<button class="iconbtn" id="pback" title="Back 15 seconds (←)" aria-label="Back 15 seconds" data-icon="back"></button>
|
||
<button class="iconbtn" id="pplay" title="Play or pause (space)" aria-label="Play or pause" data-icon="play"></button>
|
||
<button class="iconbtn" id="pfwd" title="Forward 30 seconds (→)" aria-label="Forward 30 seconds" data-icon="fwd"></button>
|
||
</div>
|
||
<div id="seekrow">
|
||
<span id="pcur">0:00</span>
|
||
<input type="range" id="seek" min="0" max="1000" value="0">
|
||
<span id="pdur">0:00</span>
|
||
</div>
|
||
</div>
|
||
<div id="pright">
|
||
<select id="rate" title="Speed">
|
||
<option value="0.8">0.8×</option><option value="1" selected>1×</option>
|
||
<option value="1.25">1.25×</option><option value="1.5">1.5×</option>
|
||
<option value="1.75">1.75×</option><option value="2">2×</option><option value="2.5">2.5×</option>
|
||
</select>
|
||
<input type="range" id="vol" min="0" max="100" value="100" title="Volume">
|
||
<button class="iconbtn" id="pclose" title="Close the player" aria-label="Close the player" data-icon="close"></button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="modal"><div class="card" id="modalCard"></div></div>
|
||
<div id="toasts"></div>
|
||
<!-- One element for both: a <video> plays an audio-only file exactly like <audio> does (same
|
||
HTMLMediaElement API), and it is the only tag that can also show a picture. Hidden unless
|
||
the current file is video -- see body.has-video below. -->
|
||
<video id="audio" preload="metadata" playsinline></video>
|
||
|
||
<script data-src="web/src"></script>
|
||
</body>
|
||
</html>
|