Phone layout drawn the way iOS draws its apps (#53), clear of the Dynamic Island (#54)

On a phone every control was outlined, hairlines ruled off the bars, corners were 7-9px and
dialogs were centred cards: a desktop page, shrunk. Now, below 820px and in every theme, controls
are filled and round (40px circles and capsules, pills for groups), the feed's name is a 26px
large title, the tabs are a segmented control, rows are 44px with 17px text and an inset hairline,
the reader's type is 17px, and dialogs are sheets from the bottom. --edge is the one switch for the
outlines, left on for the high-contrast theme and for prefers-contrast:more. Glass's desktop radii
moved into a min-width query, because at their specificity they outranked the phone's shapes, and
on a phone its #51 rim only catches the top edge: all round a small capsule it read as an outline.

Text boxes were 13.5px, and Safari zooms the page in on focus below 16px, so tapping search zoomed
it; they are 16-17px on a phone now.

Nothing read safe-area-inset-top, so opened from the home screen the top bar, the reader's back
button and the drawer's head sat under the Dynamic Island. --safe-t pads them, and the phone's own
top-bar rule, a shorthand, had also been discarding the side insets for the notch in landscape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 13:48:15 +00:00
parent f2fde8cc75
commit 162665b010
2 changed files with 120 additions and 12 deletions

View File

@@ -19,11 +19,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- On a phone, iPodderX looks like an iOS app: filled round buttons and capsules instead of
outlined boxes, no rules between the bars and the page, the feed's name as a large title, a
segmented control for the tabs, taller rows with 17px text, and dialogs as sheets from the
bottom. Tapping a text box no longer zooms the page in. High contrast, and Increase Contrast,
keep their outlines.
- The Glass theme's panels catch the light: a bright rim along the top and left edges, a fainter - The Glass theme's panels catch the light: a bright rim along the top and left edges, a fainter
one along the far edges, and a soft sheen from the top-left corner. one along the far edges, and a soft sheen from the top-left corner.
### Fixed ### Fixed
- Opened from the home screen on an iPhone, the top bar, the reader's back button and the top
of the feed list keep clear of the Dynamic Island and the status bar instead of sitting under
them.
- Swiping to the next or previous item takes a longer swipe, a quarter of the screen, so a - Swiping to the next or previous item takes a longer swipe, a quarter of the screen, so a
diagonal scroll no longer jumps ahead by accident. The item follows your finger and slides diagonal scroll no longer jumps ahead by accident. The item follows your finger and slides
across as it changes. across as it changes.

View File

@@ -504,11 +504,12 @@ a{color:var(--accent)}
} }
.iconbtn:hover{background:var(--raise);color:var(--fg)} .iconbtn:hover{background:var(--raise);color:var(--fg)}
/* One toolbar across the window, as the original had: grouped buttons, search on the right. */ /* One toolbar across the window, as the original had: grouped buttons, search on the right. */
/* The display's own intrusions -- the home indicator along the bottom, the notch at one side in /* The display's own intrusions -- the Dynamic Island and status bar along the top (#54), the
landscape. viewport-fit=cover hands the page the whole screen, which is what a standalone home indicator along the bottom, the notch at one side in landscape. viewport-fit=cover hands the page the whole screen, which is what a standalone
shell and an iOS home-screen app both give it, so the bars along the edges pay for them in shell and an iOS home-screen app both give it, so the bars along the edges pay for them in
padding. A browser with its own chrome reports nought and nothing moves. */ padding. A browser with its own chrome reports nought and nothing moves. */
:root{ :root{
--safe-t:env(safe-area-inset-top,0px);
--safe-b:env(safe-area-inset-bottom,0px); --safe-b:env(safe-area-inset-bottom,0px);
--safe-l:env(safe-area-inset-left,0px); --safe-l:env(safe-area-inset-left,0px);
--safe-r:env(safe-area-inset-right,0px); --safe-r:env(safe-area-inset-right,0px);
@@ -518,7 +519,7 @@ a{color:var(--accent)}
/* Longhand, and it has to stay longhand: the minifier runs a calc() in a padding shorthand /* Longhand, and it has to stay longhand: the minifier runs a calc() in a padding shorthand
into the value after it -- `calc(12px + var(--safe-r))7px` -- and the browser then throws into the value after it -- `calc(12px + var(--safe-r))7px` -- and the browser then throws
the whole declaration away, leaving the bar with no padding at all. */ the whole declaration away, leaving the bar with no padding at all. */
padding-top:7px;padding-bottom:7px; padding-top:calc(7px + var(--safe-t));padding-bottom:7px;
padding-left:calc(12px + var(--safe-l));padding-right:calc(12px + var(--safe-r)); padding-left:calc(12px + var(--safe-l));padding-right:calc(12px + var(--safe-r));
background:var(--panel);border-bottom:1px solid var(--line); background:var(--panel);border-bottom:1px solid var(--line);
} }
@@ -966,7 +967,7 @@ body:has(#player.on) #status{padding-bottom:3px}
@media (max-width:820px){ @media (max-width:820px){
#shell{grid-template-columns:1fr} #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{position:fixed;inset:0 auto 0 0;padding-top:var(--safe-t);width:min(300px,86vw);z-index:42;transform:translateX(-100%);transition:transform .2s;box-shadow:var(--shadow)}
#sidebar.open{transform:none} #sidebar.open{transform:none}
#scrim{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:41} #scrim{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:41}
#player{ #player{
@@ -977,7 +978,10 @@ body:has(#player.on) #status{padding-bottom:3px}
/* The feed list is reachable whether or not anything is playing. */ /* The feed list is reachable whether or not anything is playing. */
#burger{display:grid} #burger{display:grid}
#topbar{gap:6px;padding:6px 8px} #topbar{
gap:6px;padding-top:calc(6px + var(--safe-t));padding-bottom:6px;
padding-left:calc(8px + var(--safe-l));padding-right:calc(8px + var(--safe-r));
}
#topbar .grow,.tgroup.item{display:none} #topbar .grow,.tgroup.item{display:none}
#topbar #epSearch{width:auto;flex:1;min-width:0} #topbar #epSearch{width:auto;flex:1;min-width:0}
.tgroup button,.tgroup a{padding:4px 8px;min-width:32px} .tgroup button,.tgroup a{padding:4px 8px;min-width:32px}
@@ -990,13 +994,13 @@ body:has(#player.on) #status{padding-bottom:3px}
#files,.ephead,.ep .fd,.ep .file,.ep .size,.ep .rowacts{display:none} #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} .ep,#split.one .ep{grid-template-columns:20px 20px minmax(0,1fr) auto}
#grab{display:none} #grab{display:none}
#detail{position:fixed;inset:0;z-index:38;display:none;border-top:0;padding:12px 16px 90px} #detail{position:fixed;inset:0;z-index:38;display:none;border-top:0;padding:12px 16px 90px;padding-top:calc(12px + var(--safe-t))}
body.reading #detail{display:block} body.reading #detail{display:block}
#dback{display:inline-block;margin-bottom:10px} #dback{display:inline-block;margin-bottom:10px}
/* Beside the reader while it is swiped (gestures.ts): the item next to it, with a strip of the /* Beside the reader while it is swiped (gestures.ts): the item next to it, with a strip of the
page's own background between them, so the list under the reader never shows (#52). */ page's own background between them, so the list under the reader never shows (#52). */
#dpeek{position:fixed;top:0;bottom:0;left:0;width:calc(100% + 16px);z-index:38;background:var(--bg);pointer-events:none} #dpeek{position:fixed;top:0;bottom:0;left:0;width:calc(100% + 16px);z-index:38;background:var(--bg);pointer-events:none}
#dpeek>div{width:calc(100% - 16px);height:100%;overflow:hidden;padding:12px 16px 90px;background:var(--panel)} #dpeek>div{width:calc(100% - 16px);height:100%;overflow:hidden;padding:12px 16px 90px;padding-top:calc(12px + var(--safe-t));background:var(--panel)}
#dpeek.next>div{margin-left:16px} #dpeek.next>div{margin-left:16px}
#dpeek .encbox{margin:0 0 10px} #dpeek .encbox{margin:0 0 10px}
#dpeek.end{display:flex;align-items:center;padding-left:28px;color:var(--faint);font-size:13px} #dpeek.end{display:flex;align-items:center;padding-left:28px;color:var(--faint);font-size:13px}
@@ -1036,6 +1040,90 @@ body:has(#player.on) #status{padding-bottom:3px}
#logbox .tg{display:none} #logbox .tg{display:none}
} }
/* ---------- A phone, drawn the way iOS draws its own apps (#53) ---------- */
/* Filled shapes rather than outlines, corners that grow rounder as the shape grows and nest inside
each other, 44px to tap and 17px to read. The outlines stay where they are what makes a control
visible at all: the high-contrast theme, and anyone who has asked the system for more contrast.
--edge is the one switch for that. */
@media (max-width:820px){
:root{--edge:transparent}
:root[data-theme="contrast"]{--edge:var(--line)}
/* Glass rules its panels off with an edge of its own; the light along them (--glass-rim) stays. */
:root[data-theme="glass"],:root[data-theme="glass"][data-mode="light"]{--glass-edge:transparent}
}
@media (max-width:820px) and (prefers-contrast:more){
:root{--edge:var(--line)}
}
@media (max-width:820px){
/* The bars run into the page instead of being ruled off from it. */
#topbar{background:var(--bg);border-bottom-color:var(--edge);gap:8px}
#status{background:var(--bg);border-top-color:var(--edge)}
#player{border-top-color:var(--edge)}
.sidefoot{border-top-color:var(--edge)}
/* Buttons: filled and round. Hover is reset because Safari keeps it on whatever was last
tapped, which left an accent outline on it. */
.btn,.sidetools button,.sidefoot button,.tgroup,.toast,kbd,#logbox{border-color:var(--edge)}
.btn:hover,.sidetools button:hover,.sidefoot button:hover{border-color:var(--edge)}
.btn,.sidetools button,.sidefoot button{border-radius:999px;min-height:40px;padding-left:14px;padding-right:14px}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px}
.btn.ico,#dback{min-width:40px;height:40px;padding:0 10px;display:inline-grid;place-items:center}
.btn.tiny{min-height:0;padding:2px 9px}
.iconbtn{width:40px;height:40px;border-radius:999px;background:var(--panel2)}
.tgroup{border-radius:999px}
.tgroup button+button,.tgroup button+a{border-left-color:var(--edge)}
.tgroup button,.tgroup a{min-width:36px;height:40px;padding:0 8px}
/* Text boxes filled, and at 16px or more: Safari zooms the page in on one any smaller. */
input[type=search],input[type=text],input[type=password],input[type=number],select{
font-size:17px;background:var(--panel2);border-color:var(--edge);border-radius:10px;padding:9px 12px;
}
#topbar #epSearch{font-size:16px;border-radius:999px;padding:9px 14px}
input:focus,select:focus{border-color:var(--accent)}
/* The feed's name as the page's large title, the way Podcasts heads a show. */
.fhead h2,.fhead.slim h2{font-size:26px;font-weight:700;letter-spacing:-.01em;line-height:1.2}
.fhead .sub{font-size:15px}
.acts{gap:8px}
.acts .btn{min-width:44px;height:44px;padding:0 13px;justify-content:center;font-size:15px}
/* Tabs as a segmented control: equal segments on one track, the chosen one lifted. */
.tabs{flex:1 1 100%;border-radius:10px;padding:2px}
.tabs button,.tabs a{flex:1;text-align:center;padding:7px 4px;border-radius:8px;font-size:14px}
.tabs button.on,.tabs a.on{box-shadow:0 1px 3px rgba(0,0,0,.22)}
/* Rows: taller, larger type, and a hairline between them that starts where the text does. */
.ep{padding:10px 8px;border-radius:10px;min-height:44px}
.ep.sel{border-color:var(--edge)}
.ep:hover:not(.sel){background-color:transparent}
.ep .t{font-size:17px}
.ep .line,.ep .date{font-size:15px}
.ep+.ep{background-image:linear-gradient(var(--line),var(--line));background-size:calc(100% - 64px) 1px;background-position:64px 0;background-repeat:no-repeat}
.ep.sel,.ep.sel+.ep{background-image:none}
.feed{padding:9px 10px;min-height:44px;border-radius:10px}
.places{border-bottom-color:var(--edge);padding-bottom:10px;margin-bottom:10px}
.feed .txt b{font-size:17px}
.feed .txt small{font-size:13px}
/* The reader. */
.dt{font-size:26px;font-weight:700;line-height:1.2;letter-spacing:-.01em;margin-bottom:8px}
.dmeta{font-size:15px;gap:10px}
.encbox{border-color:var(--edge);border-radius:14px;padding:8px 8px 8px 14px}
.dbody{font-size:17px;line-height:1.55}
/* Dialogs are sheets, up from the bottom. */
#modal{place-items:end stretch;padding:0}
.card,.card.wide{
width:100%;max-height:calc(100dvh - var(--safe-t) - 12px);border-color:var(--edge);border-radius:20px 20px 0 0;
padding:20px 16px calc(20px + var(--safe-b));animation:sheet .3s cubic-bezier(.2,.8,.2,1);
}
.card h3{font-size:22px}
.field label,.field .hint{font-size:13px}
.check{font-size:17px}
.toast{border-radius:14px}
}
@keyframes sheet{from{transform:translateY(100%)}}
/* ---------- Classic: the 2004 Mac app ---------- */ /* ---------- Classic: the 2004 Mac app ---------- */
/* After the iPodderX screenshots: brushed-metal chrome, a pale blue-grey source list, Aqua blue /* 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. */ for whatever is selected, red unread badges, a striped table and Lucida Grande. */
@@ -1137,7 +1225,6 @@ body:has(#player.on) #status{padding-bottom:3px}
} }
:root[data-theme="glass"] .card, :root[data-theme="glass"] .card,
:root[data-theme="glass"] .toast{box-shadow:var(--glass-rim),var(--shadow)} :root[data-theme="glass"] .toast{box-shadow:var(--glass-rim),var(--shadow)}
:root[data-theme="glass"] .card{border-radius:20px}
:root[data-theme="glass"] .toast{border-radius:14px} :root[data-theme="glass"] .toast{border-radius:14px}
:root[data-theme="glass"] .toast.bad{border-color:var(--bad)} :root[data-theme="glass"] .toast.bad{border-color:var(--bad)}
/* A narrow screen slides the feed list over the page, so it keeps its shadow. */ /* A narrow screen slides the feed list over the page, so it keeps its shadow. */
@@ -1149,12 +1236,25 @@ body:has(#player.on) #status{padding-bottom:3px}
:root[data-theme="glass"] .tgroup, :root[data-theme="glass"] .tgroup,
:root[data-theme="glass"] .tabs{ :root[data-theme="glass"] .tabs{
background:color-mix(in srgb,var(--panel2) 60%,transparent); background:color-mix(in srgb,var(--panel2) 60%,transparent);
border-color:var(--glass-edge);border-radius:12px;box-shadow:var(--glass-rim); border-color:var(--glass-edge);box-shadow:var(--glass-rim);
}
/* Rounder than the other themes; a phone has shapes of its own (#53), which these would outrank. */
@media (min-width:821px){
:root[data-theme="glass"] .card{border-radius:20px}
:root[data-theme="glass"] .tgroup,
:root[data-theme="glass"] .tabs{border-radius:12px}
:root[data-theme="glass"] .btn,
:root[data-theme="glass"] .sidetools button,
:root[data-theme="glass"] .sidefoot button{border-radius:10px}
} }
:root[data-theme="glass"] .btn,
:root[data-theme="glass"] .sidetools button,
:root[data-theme="glass"] .sidefoot button{border-radius:10px}
:root[data-theme="glass"] .btn.primary{box-shadow:inset 0 1px 0 rgba(255,255,255,.35)} :root[data-theme="glass"] .btn.primary{box-shadow:inset 0 1px 0 rgba(255,255,255,.35)}
/* On a phone the rim all round a small capsule or bar read as the outline #53 took away, so the
light only catches the top edge there, and the bars at the top and bottom have none. */
@media (max-width:820px){
:root[data-theme="glass"]{--glass-rim:inset 0 1px 0 var(--glass-lo)}
:root[data-theme="glass"] #topbar,
:root[data-theme="glass"] #status{box-shadow:none}
}
/* Someone who has asked the system for less transparency, or more contrast, gets solid panels. */ /* Someone who has asked the system for less transparency, or more contrast, gets solid panels. */
@media (prefers-reduced-transparency:reduce),(prefers-contrast:more){ @media (prefers-reduced-transparency:reduce),(prefers-contrast:more){
:root[data-theme="glass"] body{background:var(--bg)} :root[data-theme="glass"] body{background:var(--bg)}