Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/www/css/mobile.css

24.4 KiB, 1 run

created by r2519314175:1113, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1/* ============================================================
2 Daimond — the phone shell
3 ------------------------------------------------------------
4 The mobile paradigm, rotated 90° from the desktop's two-seat
5 stage. On a phone the CONVERSATION is the floor and is never
6 left; a "thing" (Web, Doc, Message, Compose, Tools) rises as a
7 draggable bottom SHEET over it, and at its half detent you see
8 the thing AND the tail of the chat at once — the daimon still
9 beside the thing, only stacked instead of splayed. Diamonds and
10 Chats live in a left DRAWER; the four dwellable destinations
11 (Chat, Email, Files, Agents) sit on the bottom bar.
12
13 This file owns the drawer, the sheet and the hamburger. The
14 plain rearranging (one destination at a time, touch targets)
15 stays in responsive.css. Everything here is confined to the
16 phone breakpoint so desktop is untouched.
17 ============================================================ */
18
19/* The hamburger and the sheet exist only on a phone. */
20#drawer-btn { display: none; }
21#msheet { display: none; }
22#scrim { display: none; }
23
24@media (max-width: 760px) {
25
26 /* ── The hamburger ─────────────────────────────────────────
27 Far left of the top bar, within thumb reach is less important
28 here than convention: the menu lives where a menu lives. */
29 #drawer-btn {
30 display: flex;
31 align-items: center;
32 justify-content: center;
33 width: 38px;
34 height: 38px;
35 margin-right: 2px;
36 padding: 0;
37 background: none;
38 border: none;
39 border-radius: var(--radius);
40 color: var(--text-primary);
41 cursor: pointer;
42 }
43 #drawer-btn .ic { width: 22px; height: 22px; }
44 #drawer-btn:active { background: var(--accent-soft); }
45
46 /* ── The left drawer: Diamonds + Chats + Admin ─────────────────
47 The whole rail, slid in from the left over a scrim. It keeps
48 its own internal split (Diamonds/Chats above, Admin below), so
49 nothing about the rail's contents changes — only where it is. */
50 .panel.rail {
51 position: fixed !important;
52 z-index: 80;
53 top: 0;
54 bottom: 0;
55 left: 0;
56 width: min(86vw, 344px) !important;
57 max-width: 344px;
58 height: 100dvh !important;
59 margin: 0;
60 /* Opaque, or the drawer is see-through: a desktop rail column never
61 needed a background, but a floating drawer does. */
62 background: var(--bg-primary);
63 border-right: 1px solid var(--border);
64 /* The drawer is fixed to the viewport, so it is outside #app's frame and
65 has to hold the hardware off its own contents: installed, its first
66 control sat under the status bar and its last under the home
67 indicator. */
68 padding: calc(8px + var(--safe-t)) 8px calc(8px + var(--safe-b)) calc(8px + var(--safe-l));
69 transform: translateX(-100%);
70 transition: transform .28s cubic-bezier(.32, .72, 0, 1);
71 box-shadow: 0 0 40px rgba(0, 0, 0, .45);
72 will-change: transform;
73 overflow-y: auto;
74 overscroll-behavior: contain;
75 }
76 /* The drawer is the scroller, and nothing inside it may be. Two competing
77 scroll regions inside a 100dvh column meant the lists absorbed every
78 shortfall instead of overflowing: at 320x568 the Diamonds list came out
79 15px tall and the Chats list 2px, with three Diamonds in the store and not
80 one complete row visible in either. In landscape it was worse -- the
81 search box and the two rail buttons were pushed ABOVE the drawer's top
82 edge, unreachable, with scrollHeight equal to clientHeight so there was
83 nothing to scroll back to.
84
85 One column, its natural height, scrolled by the drawer. That is the phone
86 idiom anyway, and it retires the rail-split handle here: there are no
87 longer two regions for it to divide. */
88 /* The Admin panel rises from the identity row, which is right on a desktop
89 where the rail is a full-height column and there is room above it. In the
90 drawer there is not: at 390x844 the identity row sits ~310px down and the
91 panel is `min(70vh, 540px)` tall, so it grew UPWARDS off the screen and its
92 own head -- the title and the × -- landed 214px above the top edge, out of
93 reach with no way to scroll to it. Measured, not guessed.
94
95 Pinned to the top of the drawer instead, and capped to the drawer's own
96 height. The rail carries a `transform`, which makes it the containing block
97 for a fixed descendant, so these insets resolve against the drawer's
98 padding box rather than the viewport -- the panel is the drawer's width and
99 moves in and out with it. */
100 .panel.rail .admin-body {
101 position: fixed;
102 top: 0;
103 bottom: auto;
104 left: 0;
105 right: 0;
106 margin-bottom: 0;
107 max-height: calc(100dvh - 16px - var(--safe-t) - var(--safe-b));
108 }
109 /* ── EACH LIST SCROLLS, AS IT DOES ON A DESKTOP ───────────────────
110 These three rules used to say the opposite -- `overflow: visible` on both
111 lists and `flex: none` on the top half -- which made the drawer one long
112 scroll: a rail of fifteen Diamonds pushed Chats entirely below the fold,
113 and finding a chat meant scrolling past every Diamond first. Reported from
114 a phone.
115
116 Two bounded sections instead, as the desktop has, with the rail itself
117 still scrolling so the Admin block below them stays reachable. The caps are
118 in `dvh` rather than pixels because the drawer is measured against the
119 visible viewport, which on a phone changes as the address bar comes and
120 goes.
121
122 `overscroll-behavior: contain` is what makes two scrollers inside a third
123 bearable on touch: a flick that reaches the end of a list stops there
124 rather than dragging the whole drawer with it. */
125 .panel.rail .rail-top {
126 /* Takes the room that is left rather than a share of the viewport. Capping
127 this in `dvh` was the first attempt and it was wrong twice over: BOTH
128 lists live inside it, so the cap squeezed the pair into the top of the
129 drawer and left a third of the screen empty underneath. */
130 flex: 1 1 auto;
131 min-height: 0;
132 display: flex;
133 flex-direction: column;
134 overflow: hidden;
135 }
136 /* The two lists then split that room between them and scroll inside it, which
137 is exactly what they do on a desktop. The floor stops either one collapsing
138 to nothing when the other is long. */
139 .panel.rail .diamond-list,
140 .panel.rail .session-list {
141 flex: 1 1 0;
142 min-height: 5.5rem;
143 overflow-y: auto;
144 overscroll-behavior: contain;
145 -webkit-overflow-scrolling: touch;
146 }
147 .panel.rail .admin { flex: none; }
148 .panel.rail #handle-rail-split { display: none; }
149
150 body.drawer-open .panel.rail { transform: translateX(0); }
151 @media (prefers-reduced-motion: reduce) {
152 .panel.rail { transition: none; }
153 }
154
155 /* The scrim dims and catches the tap that closes the drawer. The
156 sheet has NO scrim on purpose — the chat above it stays live. */
157 #scrim {
158 display: block;
159 position: fixed;
160 inset: 0;
161 z-index: 70;
162 background: rgba(0, 0, 0, .38);
163 opacity: 0;
164 pointer-events: none;
165 transition: opacity .28s ease;
166 }
167 body.drawer-open #scrim { opacity: 1; pointer-events: auto; }
168
169 /* ── The sheet: a thing, over the chat ─────────────────────
170 Anchored to the bottom, ABOVE the bar, so the bar is always
171 reachable and a destination is one tap away. The detent changes
172 the sheet's HEIGHT (not a translate), so its foot — the "Ask
173 about this" pill — is on screen at every detent, which is the
174 whole point: the thing, and the ear for it, together. No
175 transition WHILE dragging (`.dragging`), so it tracks the finger. */
176 #msheet {
177 display: flex;
178 flex-direction: column;
179 position: fixed;
180 z-index: 40;
181 left: 0;
182 right: 0;
183 bottom: calc(var(--mnav-h) + var(--safe-b));
184 height: 0; /* closed; the controller sets the detent height */
185 background: var(--bg-primary);
186 border-top-left-radius: 16px;
187 border-top-right-radius: 16px;
188 border: 1px solid var(--border);
189 border-bottom: none;
190 box-shadow: 0 -8px 40px rgba(0, 0, 0, .38);
191 overflow: hidden;
192 /* The DETENT sets the height instantly (no relayout animation, which is
193 janky with an iframe inside); the open/close SLIDE is a transform, which
194 the compositor handles smoothly. Closed = slid fully below its own foot. */
195 transform: translateY(110%);
196 transition: transform .3s cubic-bezier(.32, .72, 0, 1);
197 will-change: transform;
198 }
199 #msheet.open { transform: translateY(0); }
200 #msheet.dragging { transition: none; }
201 @media (prefers-reduced-motion: reduce) {
202 #msheet { transition: none; }
203 }
204
205 /* The grabber: the drag handle, and the only place a vertical drag
206 moves the sheet rather than scrolling the thing inside it. */
207 .msheet-grab {
208 flex: none;
209 display: flex;
210 align-items: center;
211 gap: 8px;
212 padding: 8px 12px 6px;
213 cursor: grab;
214 touch-action: none; /* the grabber owns the vertical gesture */
215 border-top-left-radius: 16px;
216 border-top-right-radius: 16px;
217 }
218 .msheet-grab:active { cursor: grabbing; }
219 .msheet-bar {
220 position: absolute;
221 left: 50%;
222 transform: translateX(-50%);
223 top: 7px;
224 width: 38px;
225 height: 4px;
226 border-radius: 3px;
227 background: var(--border);
228 }
229 .msheet-title {
230 flex: 1;
231 min-width: 0;
232 margin-top: 6px;
233 font-size: var(--fs-sm);
234 font-weight: 600;
235 color: var(--text-muted);
236 white-space: nowrap;
237 overflow: hidden;
238 text-overflow: ellipsis;
239 }
240 .msheet-close {
241 margin-top: 0;
242 /* 40px, matching the guest's own lifted controls that now sit beside it
243 (see `#msheet .panel > .railhead`): two icon buttons on one row at two
244 different sizes read as two different kinds of control. It is also the
245 larger thumb target of the two, which on a phone is the right way to
246 settle a tie. */
247 width: 40px;
248 height: 40px;
249 flex: none;
250 display: flex;
251 align-items: center;
252 justify-content: center;
253 background: none;
254 border: none;
255 border-radius: var(--radius);
256 color: var(--text-muted);
257 font-size: var(--fs-3xl);
258 line-height: 1;
259 cursor: pointer;
260 }
261 .msheet-close:active { background: var(--accent-soft); }
262
263 /* The thing itself fills the room between the grabber and the ask
264 pill. The moved panel keeps its own header and controls. */
265 .msheet-scroll {
266 flex: 1;
267 min-height: 0;
268 display: flex;
269 flex-direction: column;
270 overflow: hidden;
271 }
272 #msheet .panel {
273 display: flex !important;
274 flex-direction: column;
275 height: 100% !important;
276 width: 100% !important;
277 min-width: 0 !important;
278 border: none;
279 border-radius: 0;
280 }
281 /* The sheet already names what it is holding, in the grabber. A guest that
282 carries its own head then said it twice, one line apart and in two
283 different cases -- "Spending" over "SPENDING". The guest keeps its
284 controls; the sheet keeps the title. */
285 /* Dropping the name left a row holding nothing but its controls, and it kept
286 a full band of the sheet to itself: 44px directly under the sheet's own
287 closer, with the whole width beside the one refresh button empty. It read
288 as a title that had gone missing rather than one that had moved. Lift the
289 strip into the grabber's row, just left of the closer, where a panel's
290 controls sit everywhere else in the app. Positioned against `#msheet`
291 (the nearest positioned ancestor), so it costs the body nothing: 8px is
292 the grabber's own top padding and 52px clears the 32px closer and its
293 12px of padding. Only Spending is ever raised into a sheet with this kind
294 of head -- the other three are bottom-bar destinations, which keep their
295 names and their own rows. */
296 #msheet .panel > .railhead {
297 position: absolute;
298 top: 8px;
299 /* 12px of grabber padding, the 40px closer, and 8px between them. */
300 right: 60px;
301 z-index: 1;
302 padding: 0;
303 justify-content: flex-end;
304 }
305 /* Beside the sheet's own borderless closer, a bordered box reads as the
306 heavier of two controls that do the same kind of job. Same size, same
307 weight, same ink. */
308 #msheet .panel > .railhead button {
309 width: 40px;
310 height: 40px;
311 background: none;
312 border: none;
313 color: var(--text-muted);
314 }
315 #msheet .panel > .railhead button:active { background: var(--accent-soft); }
316 #msheet .panel > .railhead > span:first-child { display: none; }
317 /* THE LIFT IS FOR A HEAD HOLDING ONE OR TWO ICON BUTTONS, and the comment
318 above it named Spending as the only panel raised with one. That stopped
319 being true when the Improve panel became Social: its head carries FOUR
320 labelled chips, an information mark and a closer. The strip is out of flow
321 with no left bound, so on a 390px screen it ran off the left edge, and the
322 rule above squared every chip to a 40px icon box — "Messages" and "People"
323 drawn one on top of the other, each clipped, and not one of them showing
324 which view was on. Screenshot: dev/shots/social-phone.png, 2026-08-17.
325
326 So this head stays IN FLOW and takes a row of its own, which is where four
327 chips fit and where a fifth would wrap rather than disappear. */
328 #msheet #panel-social > .railhead {
329 position: static;
330 padding: 4px 6px;
331 justify-content: flex-start;
332 flex-wrap: wrap;
333 row-gap: 3px;
334 }
335 /* And a chip keeps its own shape: a word, and a pill saying whether its view
336 is the one showing. The thumb floor is taken as a HEIGHT, which is the half
337 of the rule above that was ever about a finger. */
338 /* The chips carry `margin-left: auto` so they sit right of a heading. The
339 heading is hidden in the sheet, so that auto margin only pushed them into
340 the corner and left the row lopsided. */
341 #msheet #panel-social > .railhead .imp-chips { margin-left: 0; }
342 #msheet #panel-social > .railhead .imp-chip {
343 width: auto;
344 height: auto;
345 min-height: 30px;
346 padding: 0 10px;
347 background: none;
348 border: 1px solid var(--border-strong);
349 color: var(--text-secondary);
350 }
351 #msheet #panel-social > .railhead .imp-chip.on {
352 background: var(--accent-soft);
353 border-color: var(--accent);
354 color: var(--accent-text);
355 }
356 /* The lifted strip is out of flow, so the title's box still runs the full
357 width and a long panel name would ellipsise UNDER the buttons. Give the
358 title back the room the strip is standing in. */
359 #msheet:has(.panel > .railhead) .msheet-title { padding-right: 40px; }
360 /* A guest whose own head says what the sheet has just said. This USED to
361 name Tools as the only such panel; rendering all nine showed Message over
362 Message, Graph over Graph, and a blank 44px band over Doc. The test cannot
363 live here -- a `.ctitle` is the panel's name while it is empty and a live
364 title once it holds a URL, a filename or a subject, and no selector tells
365 those apart -- so `DaimondSheet.open` makes it when the guest is raised
366 and marks the head. See hideRedundantHead in mobile.js. */
367 #msheet .panel > .chead.head-said-twice,
368 #msheet .panel > .railhead.head-said-twice { display: none; }
369 /* The Terminal's head is not droppable -- Start and Stop live in it, and a
370 terminal you cannot start is not a terminal -- so only the duplicated NAME
371 goes. What is left is the program's own title and the two controls, which
372 is what the head is for. */
373 #msheet .panel.termpanel > .chead > .ctitle { display: none; }
374 /* With the name gone and no program running there is no title either, so the
375 Terminal's head drew the same blank band Spending's did: 44px under the
376 sheet's own closer holding two buttons at the far right. Same answer --
377 lift the controls into the grabber's row, and let what remains collapse to
378 the height of whatever the program calls itself (nothing, until one is
379 started) rather than to a header's minimum. */
380 #msheet .panel.termpanel > .chead {
381 min-height: 0;
382 padding: 0 14px;
383 border-bottom: none;
384 }
385 #msheet .panel.termpanel > .chead > .chead-right {
386 position: absolute;
387 top: 8px;
388 right: 60px;
389 z-index: 1;
390 gap: 4px;
391 }
392 #msheet .panel.termpanel > .chead > .chead-right button {
393 width: 40px;
394 height: 40px;
395 background: none;
396 border: none;
397 color: var(--text-muted);
398 }
399 #msheet .panel.termpanel > .chead > .chead-right button:active { background: var(--accent-soft); }
400 #msheet:has(.panel.termpanel) .msheet-title { padding-right: 96px; }
401
402 /* The ask pill: the composer, relocated. "Ask about THIS" is the
403 whole thesis — read the thing, and the daimon is still there to
404 be asked about it. It posts to the one conversation on the floor. */
405 .msheet-ask {
406 flex: none;
407 display: flex;
408 align-items: center;
409 gap: 8px;
410 padding: 8px 10px calc(8px + var(--safe-b));
411 border-top: 1px solid var(--border);
412 background: var(--bg-secondary);
413 }
414 .msheet-ask.hidden { display: none; }
415 #msheet-ask-input {
416 flex: 1;
417 min-width: 0;
418 font-size: var(--fs-xl); /* 16px keeps iOS Safari from zooming on focus */
419 padding: 10px 13px;
420 border: 1px solid var(--border-strong);
421 border-radius: 20px;
422 background: var(--bg-primary);
423 color: var(--text-primary);
424 }
425 #msheet-ask-input:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
426 #msheet-ask-send {
427 flex: none;
428 width: 42px;
429 height: 42px;
430 border: none;
431 border-radius: 50%;
432 background: var(--accent);
433 color: var(--on-fill, #fff);
434 font-size: var(--fs-xl-2xl);
435 cursor: pointer;
436 }
437 #msheet-ask-send:disabled { opacity: .4; }
438
439 /* While a sheet is up, the destination behind it is always the
440 chat floor. Its own input bar is hidden — the ask pill speaks
441 for it — so two composers can never disagree. */
442 body.sheet-open .ai .chat-input-bar { visibility: hidden; }
443
444 /* ── The footer: the panel chip row, scrolled sideways ─────
445 The bar sits ABOVE the sheet, so a panel is always one tap away and the
446 sheet's foot rests just above it (see #msheet bottom).
447
448 It held four hard-wired destinations until 2026-08-28 -- Chat, Email,
449 Files, Agents -- and the owner's verdict was that it "is currently useless
450 to me": four of the seventeen panels named, the other thirteen reachable
451 only through the drawer. It carries the chip row now, which is the same row
452 the desktop header carries, moved rather than copied (`placeChips`,
453 js/mobile.js).
454
455 ITS HEIGHT IS `--mnav-h`, plus the home indicator's inset, and that variable
456 is the only place the number is written. `#msheet { bottom }` below reads
457 it, and js/mobile.js MEASURES the bar rather than carrying a fourth copy:
458 there were three -- 54 in variables.css, 58 in this file and 58 in the shell
459 -- and `dev/verify_sweep_mobile.mjs` exists partly because a sheet placed
460 from one of them lands over a bar drawn from another. */
461 .mnav { position: relative; z-index: 60; }
462
463 /* The strip itself. It SCROLLS rather than fitting: "A chip is its label.
464 Below that width it is not a shorter chip, it is an unreadable one" --
465 css/improve.css, on the Social panel's chips, and it is the same rule here.
466 So every chip takes the width its label needs (`flex: none` below) and the
467 strip carries the rest of them past the edge.
468 THE SAFE INSET IS ON THE SCROLLER, not on the bar, so the last chip scrolls
469 clear of the home indicator instead of ending under it. A chip you cannot
470 reach is the same class of fault as a chip that moves while you reach. */
471 #mnav .panel-tags {
472 display: flex !important;
473 flex: 1 1 auto;
474 align-items: center;
475 min-width: 0;
476 gap: 8px;
477 margin: 0;
478 /* 8 + 44 + 9 + the bar's 1px rule = `--mnav-h`, which is where the number
479 lives and what the sheet's foot is placed from. */
480 padding: 8px 14px calc(9px + var(--safe-b));
481 overflow-x: auto;
482 overflow-y: hidden;
483 overscroll-behavior-x: contain;
484 -webkit-overflow-scrolling: touch;
485 /* The scrollbar would eat the height the chips need and, on a phone, is
486 drawn over them for a second and then gone -- so the fact that the row
487 scrolls is carried by the fade at its ends instead (`data-more` below). */
488 scrollbar-width: none;
489 /* A chip scrolled to is scrolled clear of the fade rather than under it. */
490 scroll-padding-inline: 22px;
491 }
492 #mnav .panel-tags::-webkit-scrollbar { display: none; }
493 /* A THUMB'S TARGET. 44px is the AAA figure and it is what the four buttons
494 this row replaced already met, so the row that replaced them may not be
495 smaller: `dev/verify_sweep_mobile.mjs` reports anything under it, and it
496 reported these chips at 40 before this line said 44. */
497 #mnav .ptag,
498 #mnav .ptag-more {
499 flex: none;
500 min-height: 44px;
501 /* AND 44 ACROSS. A two-letter label -- "AI" -- came to 43px with the
502 padding, which `dev/verify_sweep_mobile.mjs` reported at every phone
503 width. It is a floor and not a width: a chip is still its label, and a
504 longer one takes the room it needs. */
505 min-width: 44px;
506 justify-content: center;
507 display: inline-flex;
508 align-items: center;
509 padding: 0 14px;
510 font-size: var(--fs-xs);
511 }
512 #mnav .ptag-group { flex: none; gap: 8px; }
513 #mnav .ptag-div { margin: 0 2px; }
514
515 /* SCROLLABLE MUST LOOK SCROLLABLE. A strip that runs to the edge and stops
516 reads as a truncated list, and a scrollbar would cost the height the chips
517 are using. A soft fade at whichever end has more beyond it says the same
518 thing and costs nothing: `data-more` is set by `markScroll` in js/mobile.js
519 from the strip's own scroll position, so it is never a decoration that
520 claims something untrue. Half a chip showing through the fade is the other
521 half of the signal. */
522 #mnav .panel-tags[data-more="end"] { mask-image: linear-gradient(to right, #000 calc(100% - 34px), transparent 100%); }
523 #mnav .panel-tags[data-more="start"] { mask-image: linear-gradient(to right, transparent 0, #000 34px); }
524 #mnav .panel-tags[data-more="both"] { mask-image: linear-gradient(to right, transparent 0, #000 34px, #000 calc(100% - 34px), transparent 100%); }
525 #mnav .panel-tags[data-more="end"] { -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 34px), transparent 100%); }
526 #mnav .panel-tags[data-more="start"] { -webkit-mask-image: linear-gradient(to right, transparent 0, #000 34px); }
527 #mnav .panel-tags[data-more="both"] { -webkit-mask-image: linear-gradient(to right, transparent 0, #000 34px, #000 calc(100% - 34px), transparent 100%); }
528
529 /* ── No iOS auto-zoom ───────────────────────────────────────
530 Safari zooms IN (and never back out) when a focused control is
531 under 16px. Every form control is 16px on a phone so focusing a
532 name box, a filter, or a dialog field never jerks the view. */
533 input, textarea, select,
534 .settings-select, .dlg-input, .dlg-select, .files-filter-input {
535 font-size: var(--fs-xl) !important;
536 }
537
538 /* ── Modals fit the phone ──────────────────────────────────
539 The settings modal hosts an admin view (Models / Credits) that
540 already carries its OWN header — the right title and a top-right
541 ×. Show that (it is hidden on desktop) and drop the modal's own
542 generic "Settings" heading and bottom Close, which duplicated it
543 with the wrong title and no corner close. And size to the DYNAMIC
544 viewport so a tall form scrolls inside instead of dragging. */
545 .modal { padding: 12px; align-items: flex-start; }
546 .modal-card {
547 width: 100%;
548 max-width: 520px;
549 margin: 0 auto;
550 max-height: calc(100dvh - 24px);
551 padding: 16px;
552 }
553 /* The About card's two bleeding children cancel the card's padding out, so
554 they have to be told it changed here. Without this the splash hangs 8px
555 past the card on each side and the rule above the signature does the same. */
556 .about-card { --about-pad: 16px; }
557 /* The create screen asks for more room than the shared 80vh (see app.css) and
558 takes 92vh -- but `vh` counts the strip behind the address bar, which a
559 phone does not show, so on a phone the card ran past the fold and cut
560 "Create account" in half. The dynamic viewport is the honest measure, and
561 the card scrolls inside it. */
562 #identity-modal .modal-card { max-height: calc(100dvh - 24px); }
563 /* The hosted view's own head is the card's heading everywhere now, and the
564 bottom Close is hidden everywhere with it (both in app.css), so nothing
565 about the settings card needs saying twice here. */
566 /* The view's own × sits top-right and is the close. Make it a clear target. */
567 #settings-slot .admin-back { font-size: var(--fs-4xl); padding: 4px 10px; }
568}
569
570/* A wider phone / small tablet in landscape gets a roomier sheet and
571 drawer, but the same paradigm. The single 760 cliff is deliberately
572 the only structural switch for now; this is a comfort tweak. */
573@media (max-width: 760px) and (min-width: 560px) and (orientation: landscape) {
574 .panel.rail { width: 380px !important; }
575}