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 | } |