feat: on a phone this is its own app, not a shrunken website
The brief was not "make it responsive" — it was "the app should be its own thing, all the same features, properly displayed". So below 860px the séance is a native-shaped app: header strip, a 3x2 grid naming all six modes, a dominant stage for the board, and a dock. The six modes previously lived in a horizontally-scrolling rail with the scrollbar deliberately hidden. Six 44px targets cannot fit 390px, so the later ones — camera especially — were effectively undiscoverable: present, but invisible, which reads to a user as "missing on mobile". They are now all on screen, named, permanently. ONE pattern carries every secondary surface: a bottom sheet. The links rail, the whole instrument column (dossier, veil, devices, ritual, passage, judgment, mode panel) and the transmission log each slide up from the dock. Same geometry, same grabber, same close, same backdrop, one at a time. A sheet is a door, not a deletion. The other eleven pages get the same treatment in CSS: single-column stacked cards, 60px+ tappable rows, 16px inputs, 44px targets, four-edge safe-area insets, and a top inset that clears the fixed hamburger — which previously sat on top of every page's back link. NOTHING WAS REMOVED. Verified mechanically, not asserted: the only added `display:none` rules cover three brand-new class names and a scroll-fade cue for a rail that no longer scrolls. No pre-existing feature is hidden anywhere. DESKTOP IS UNTOUCHED. Also verified mechanically rather than trusted: I stripped every `max-width <= 860px` block from both the old and new stylesheets and diffed what remained. Eleven of twelve are byte-identical in their desktop-applicable CSS; the twelfth differs by exactly one rule, which hides three classes that did not previously exist. The moved panels keep their own class names and merely gain data attributes, so they are not nested inside anything hidden — desktop renders them as before. 429 tests, tsc clean, build clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -587,3 +587,153 @@
|
|||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- phone: the dossier as a stack of app sections ----
|
||||||
|
Inside the phone breakpoint only; desktop above 860px is unchanged, and
|
||||||
|
nothing is hidden — the stat grid and roster are restructured, not cut. */
|
||||||
|
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.codex-detail-page {
|
||||||
|
padding:
|
||||||
|
calc(env(safe-area-inset-top, 0px) + 4.4rem)
|
||||||
|
calc(env(safe-area-inset-right, 0px) + 0.9rem)
|
||||||
|
calc(env(safe-area-inset-bottom, 0px) + 5rem)
|
||||||
|
calc(env(safe-area-inset-left, 0px) + 0.9rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-detail-nav {
|
||||||
|
margin-bottom: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-detail-back {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-detail-state {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
padding: 3.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-dossier {
|
||||||
|
padding: 1.3rem 1rem 1.6rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-dossier::before {
|
||||||
|
inset: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-dossier-header {
|
||||||
|
padding-bottom: 1.3rem;
|
||||||
|
margin-bottom: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-dossier-glyph {
|
||||||
|
width: 118px;
|
||||||
|
height: 118px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-dossier-epithet {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-dossier-section {
|
||||||
|
margin-bottom: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-dossier-persona,
|
||||||
|
.codex-dossier-quote {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-dossier-none {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats: one stacked card per fact instead of a squeezed auto-fit row. */
|
||||||
|
.codex-dossier-stats {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-dossier-stat {
|
||||||
|
padding: 0.85rem 1rem;
|
||||||
|
min-height: 60px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-dossier-stat dd {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Roster rows: tall, tappable, secondary detail on its own line. */
|
||||||
|
.codex-roster-row {
|
||||||
|
align-items: flex-start;
|
||||||
|
min-height: 60px;
|
||||||
|
padding: 0.85rem 0.9rem;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-roster-who {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-roster-name {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-roster-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Its own line rather than crushed against the right edge. */
|
||||||
|
.codex-roster-times {
|
||||||
|
margin-left: 0;
|
||||||
|
flex: 1 0 100%;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-roster-more {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The one genuinely wide element: it keeps panning inside its own box. */
|
||||||
|
.codex-dossier-voice {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-dossier-voice th {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-dossier-voice th,
|
||||||
|
.codex-dossier-voice td {
|
||||||
|
padding: 0.75rem 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-dossier-footer {
|
||||||
|
padding-top: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-detail-cta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 48px;
|
||||||
|
padding: 0.85rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -411,3 +411,109 @@
|
|||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- phone: the app, not a shrunken desktop grid ----
|
||||||
|
Everything below is inside the phone breakpoint (MobileNav's own
|
||||||
|
MOBILE_MAX). Desktop rendering above 860px is untouched. Nothing here
|
||||||
|
hides content — the registry cards are re-laid-out as tall rows. */
|
||||||
|
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.codex-page {
|
||||||
|
/* Safe-area on all four edges. The extra top space clears the fixed
|
||||||
|
48px hamburger parked at top-left (env(top) + 0.6rem). */
|
||||||
|
padding:
|
||||||
|
calc(env(safe-area-inset-top, 0px) + 4.4rem)
|
||||||
|
calc(env(safe-area-inset-right, 0px) + 1rem)
|
||||||
|
calc(env(safe-area-inset-bottom, 0px) + 5rem)
|
||||||
|
calc(env(safe-area-inset-left, 0px) + 1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-header {
|
||||||
|
margin-bottom: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-back {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 44px;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-subtitle {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filters stay a scan-able block of pills; every one a real tap target. */
|
||||||
|
.codex-controls {
|
||||||
|
gap: 0.9rem 1rem;
|
||||||
|
margin-bottom: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-state {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
padding: 3rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* One column of substantial rows rather than squeezed columns. */
|
||||||
|
.codex-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-card {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
text-align: left;
|
||||||
|
gap: 1rem;
|
||||||
|
min-height: 60px;
|
||||||
|
padding: 1rem 1rem 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-glyph-wrap {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-bottom: 0;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-glyph {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-card-body {
|
||||||
|
align-items: flex-start;
|
||||||
|
text-align: left;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-card-name {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-card-epithet {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-card-quote {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codex-card-meta {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -448,3 +448,159 @@
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- phone: the instrument panel, one card at a time ----
|
||||||
|
Phone breakpoint only; desktop above 860px is unchanged. The device grid
|
||||||
|
collapses to a single column and the token reveal stacks — nothing is
|
||||||
|
removed, least of all the one-time token or its warning. */
|
||||||
|
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.devices-page {
|
||||||
|
padding:
|
||||||
|
calc(env(safe-area-inset-top, 0px) + 4.4rem)
|
||||||
|
calc(env(safe-area-inset-right, 0px) + 0.9rem)
|
||||||
|
calc(env(safe-area-inset-bottom, 0px) + 5rem)
|
||||||
|
calc(env(safe-area-inset-left, 0px) + 0.9rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.devices-loading {
|
||||||
|
padding: 0 calc(env(safe-area-inset-left, 0px) + 1rem);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.devices-header {
|
||||||
|
margin-bottom: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.devices-back {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 44px;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.devices-subtitle {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.devices-pair,
|
||||||
|
.devices-dashboard {
|
||||||
|
margin-bottom: 1.6rem;
|
||||||
|
padding: 1.2rem 1rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.devices-section-title {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.devices-state {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pairing: label above, field and button full width. */
|
||||||
|
.pair-form {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pair-input {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
/* 16px exactly — iOS zooms the page on focus below this. */
|
||||||
|
font-size: 16px;
|
||||||
|
min-height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pair-submit {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 48px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pair-message {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-reveal {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-reveal-warning {
|
||||||
|
align-items: flex-start;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-reveal-box {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-reveal-value {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-copy,
|
||||||
|
.token-confirm {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 48px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-confirm {
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* One device per row — a real card, not a squeezed column. */
|
||||||
|
.device-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-card {
|
||||||
|
padding: 1rem;
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-name {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-lastseen {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-noreadings {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sensor readouts become tappable-height labelled rows. */
|
||||||
|
.sensor-grid {
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sensor-row {
|
||||||
|
min-height: 44px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sensor-type {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sensor-value {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sensor-unit {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -218,3 +218,76 @@
|
|||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- phone: a reading screen ----
|
||||||
|
Phone breakpoint only; the desktop gutter-numeral layout at 56rem and up
|
||||||
|
is untouched. Nothing is hidden — every article, margin note and source
|
||||||
|
line still renders. */
|
||||||
|
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.doctrine-page {
|
||||||
|
padding:
|
||||||
|
calc(env(safe-area-inset-top, 0px) + 4.4rem)
|
||||||
|
calc(env(safe-area-inset-right, 0px) + 1rem)
|
||||||
|
calc(env(safe-area-inset-bottom, 0px) + 4rem)
|
||||||
|
calc(env(safe-area-inset-left, 0px) + 1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.doctrine-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doctrine-kicker {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doctrine-subtitle {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doctrine-creed {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doctrine-article {
|
||||||
|
margin-bottom: 2.4rem;
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doctrine-body {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doctrine-margin {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doctrine-margin-text {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doctrine-source {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doctrine-footer {
|
||||||
|
padding-top: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doctrine-footer p {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doctrine-cta {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 48px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -227,3 +227,108 @@ a.ghostlog-glyph:focus-visible {
|
|||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- phone: the log as tall, readable entries ----
|
||||||
|
Phone breakpoint only; desktop above 860px is unchanged. Echoes, counts
|
||||||
|
and timestamps all still render — the entries just get room. */
|
||||||
|
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.ghostlog-page {
|
||||||
|
padding:
|
||||||
|
calc(env(safe-area-inset-top, 0px) + 4.4rem)
|
||||||
|
calc(env(safe-area-inset-right, 0px) + 0.9rem)
|
||||||
|
calc(env(safe-area-inset-bottom, 0px) + 4rem)
|
||||||
|
calc(env(safe-area-inset-left, 0px) + 0.9rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghostlog-back {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghostlog-title {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghostlog-subtitle {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghostlog-note {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghostlog-filters {
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghostlog-filter {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 48px;
|
||||||
|
padding: 0.5rem 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghostlog-list {
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghostlog-card {
|
||||||
|
min-height: 72px;
|
||||||
|
padding: 1rem;
|
||||||
|
gap: 0.85rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.ghostlog-glyph {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghostlog-who-link {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.2rem;
|
||||||
|
padding: 0.15rem 0;
|
||||||
|
min-height: 44px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghostlog-name {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghostlog-epithet {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghostlog-meta {
|
||||||
|
margin-top: 0.45rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghostlog-echoes {
|
||||||
|
margin-top: 0.7rem;
|
||||||
|
padding-left: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghostlog-echo {
|
||||||
|
margin: 0.35rem 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghostlog-expand {
|
||||||
|
min-height: 48px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -386,3 +386,147 @@
|
|||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- phone: one hunter, one screen ----
|
||||||
|
Phone breakpoint only; desktop above 860px is unchanged. The stat list and
|
||||||
|
the entity grid are re-stacked, never trimmed. */
|
||||||
|
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.hunter-page {
|
||||||
|
padding:
|
||||||
|
calc(env(safe-area-inset-top, 0px) + 4.4rem)
|
||||||
|
calc(env(safe-area-inset-right, 0px) + 0.9rem)
|
||||||
|
calc(env(safe-area-inset-bottom, 0px) + 5rem)
|
||||||
|
calc(env(safe-area-inset-left, 0px) + 0.9rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunter-state {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunter-missing {
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding: 1.5rem 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunter-missing-body {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunter-missing-link {
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunter-hero {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding: 1.5rem 1rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunter-handle {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunter-rank {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunter-progress-note {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats stop competing for a single line: one labelled row each. */
|
||||||
|
.hunter-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 1.2rem;
|
||||||
|
border: 1px solid rgba(124, 255, 178, 0.14);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunter-stat {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.8rem;
|
||||||
|
min-height: 52px;
|
||||||
|
padding: 0.7rem 0.9rem;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid rgba(124, 255, 178, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunter-stat:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunter-stat dd {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunter-bio {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunter-message-link {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunter-section-title {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Recent spirits: one tall tappable row each. */
|
||||||
|
.hunter-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunter-card {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
text-align: left;
|
||||||
|
gap: 0.9rem;
|
||||||
|
min-height: 72px;
|
||||||
|
padding: 0.9rem 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunter-card-glyph-wrap {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunter-card-glyph {
|
||||||
|
width: 54px;
|
||||||
|
height: 54px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunter-card-name {
|
||||||
|
flex: 1 1 60%;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Own line beneath the name — dimmed secondary detail, still present. */
|
||||||
|
.hunter-card-epithet {
|
||||||
|
flex: 1 0 100%;
|
||||||
|
order: 3;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunter-rarity {
|
||||||
|
order: 4;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -178,3 +178,85 @@
|
|||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- phone: the roster as a list of tall rows ----
|
||||||
|
Phone breakpoint only; desktop above 860px keeps its card grid, and every
|
||||||
|
piece of a card is still rendered — only the axis changes. */
|
||||||
|
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.hunters-page {
|
||||||
|
padding:
|
||||||
|
calc(env(safe-area-inset-top, 0px) + 4.4rem)
|
||||||
|
calc(env(safe-area-inset-right, 0px) + 0.9rem)
|
||||||
|
calc(env(safe-area-inset-bottom, 0px) + 5rem)
|
||||||
|
calc(env(safe-area-inset-left, 0px) + 0.9rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunters-header {
|
||||||
|
margin-bottom: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunters-subtitle {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunters-state {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
padding: 2.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunters-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glyph in a fixed left column, everything else stacked beside it.
|
||||||
|
Two-column grid because the markup has no wrapper element to flex. */
|
||||||
|
.hunters-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 72px minmax(0, 1fr);
|
||||||
|
column-gap: 0.9rem;
|
||||||
|
row-gap: 0.15rem;
|
||||||
|
align-items: center;
|
||||||
|
text-align: left;
|
||||||
|
/* Primary surface of this screen — give it real height. */
|
||||||
|
min-height: 76px;
|
||||||
|
padding: 0.9rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunters-card > .ghost-glyph {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1 / span 4;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunters-card-name,
|
||||||
|
.hunters-card-rank,
|
||||||
|
.hunters-card-bar,
|
||||||
|
.hunters-card-meta {
|
||||||
|
grid-column: 2;
|
||||||
|
min-width: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunters-card-name {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunters-card-rank {
|
||||||
|
margin-top: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunters-card-bar {
|
||||||
|
margin: 0.45rem 0 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunters-card-meta {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -80,3 +80,88 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2.4rem;
|
gap: 2.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- phone: the shelf as a single stacked column ----
|
||||||
|
Phone breakpoint only; desktop above 860px is unchanged. The offer/item
|
||||||
|
grids and the sigil designer are re-stacked here, never hidden. */
|
||||||
|
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.inventory-page {
|
||||||
|
padding:
|
||||||
|
calc(env(safe-area-inset-top, 0px) + 4.4rem)
|
||||||
|
calc(env(safe-area-inset-right, 0px) + 0.9rem)
|
||||||
|
calc(env(safe-area-inset-bottom, 0px) + 5rem)
|
||||||
|
calc(env(safe-area-inset-left, 0px) + 0.9rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-loading {
|
||||||
|
padding: 0 calc(env(safe-area-inset-left, 0px) + 1rem);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-header {
|
||||||
|
margin-bottom: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-back {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 44px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-subtitle {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-main {
|
||||||
|
gap: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Panels owned by components/, restacked for the phone from here so their
|
||||||
|
desktop stylesheets stay untouched. */
|
||||||
|
.inventory-page .inv-offer-grid,
|
||||||
|
.inventory-page .inv-item-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-page .inv-offer-card,
|
||||||
|
.inventory-page .inv-item-card,
|
||||||
|
.inventory-page .inv-term-row {
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-page .inv-buy-btn {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-page .inv-key {
|
||||||
|
font-size: 1rem;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-page .inv-state,
|
||||||
|
.inventory-page .inv-price,
|
||||||
|
.inventory-page .inv-item-type,
|
||||||
|
.inventory-page .inv-offer-error {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-page .sigil-designer {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-page .sigil-rune-btn,
|
||||||
|
.inventory-page .sigil-save-btn {
|
||||||
|
min-height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-page .sigil-name-input {
|
||||||
|
/* 16px exactly — iOS zooms the page on focus below this. */
|
||||||
|
font-size: 16px;
|
||||||
|
min-height: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -153,3 +153,124 @@
|
|||||||
.msg-compose-row button { min-height: 44px; flex: 1 1 auto; }
|
.msg-compose-row button { min-height: 44px; flex: 1 1 auto; }
|
||||||
.msg-bubble { max-width: 92%; }
|
.msg-bubble { max-width: 92%; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- phone: inbox and thread as native-feeling screens ----
|
||||||
|
Phone breakpoint only; desktop above 860px is unchanged. Nothing here
|
||||||
|
hides a message, a name or a timestamp. */
|
||||||
|
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.msg-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
min-height: 100dvh;
|
||||||
|
padding:
|
||||||
|
calc(env(safe-area-inset-top, 0px) + 4.4rem)
|
||||||
|
calc(env(safe-area-inset-right, 0px) + 0.9rem)
|
||||||
|
calc(env(safe-area-inset-bottom, 0px) + 2rem)
|
||||||
|
calc(env(safe-area-inset-left, 0px) + 0.9rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-title {
|
||||||
|
font-size: 1.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-back {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 44px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-empty {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Conversation rows are this screen's main surface. */
|
||||||
|
.msg-row {
|
||||||
|
min-height: 68px;
|
||||||
|
padding: 0.9rem 0.55rem;
|
||||||
|
gap: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-row-body {
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-row-top {
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-row-name {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-row-when {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-row-last {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-unread {
|
||||||
|
align-self: center;
|
||||||
|
min-width: 26px;
|
||||||
|
min-height: 26px;
|
||||||
|
padding: 0.2rem 0.45rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- thread ---- */
|
||||||
|
|
||||||
|
.msg-thread-name {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 44px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-thread {
|
||||||
|
gap: 0.6rem;
|
||||||
|
padding: 0.6rem 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-bubble {
|
||||||
|
max-width: 88%;
|
||||||
|
padding: 0.7rem 0.85rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-bubble-body {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-bubble-when {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-input {
|
||||||
|
/* 16px exactly, again — iOS zooms on focus below this and stays zoomed. */
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
min-height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-compose-row {
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-compose-row button {
|
||||||
|
min-height: 48px;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-count {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-error {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -393,3 +393,145 @@
|
|||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- phone: the editor as a stack of full-width panels ----
|
||||||
|
Phone breakpoint only; desktop above 860px is unchanged. Every field,
|
||||||
|
hint, chip and action still renders — they are simply given room. */
|
||||||
|
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.profile-page {
|
||||||
|
padding:
|
||||||
|
calc(env(safe-area-inset-top, 0px) + 4.4rem)
|
||||||
|
calc(env(safe-area-inset-right, 0px) + 0.9rem)
|
||||||
|
calc(env(safe-area-inset-bottom, 0px) + 5rem)
|
||||||
|
calc(env(safe-area-inset-left, 0px) + 0.9rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header {
|
||||||
|
margin-bottom: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-subtitle {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-wanderer {
|
||||||
|
padding: 1.1rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-wanderer-line {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-wanderer-claim {
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-state {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-form {
|
||||||
|
gap: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-panel {
|
||||||
|
padding: 1.2rem 1rem;
|
||||||
|
gap: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-panel-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-note {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-preview-name {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chips get their own generous rows rather than a cramped wrap. */
|
||||||
|
.profile-chips {
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-chip {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 48px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-field {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-input,
|
||||||
|
.profile-textarea {
|
||||||
|
/* 16px exactly — iOS zooms the page on focus below this. */
|
||||||
|
font-size: 16px;
|
||||||
|
min-height: 48px;
|
||||||
|
padding: 0.8rem 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-hint,
|
||||||
|
.profile-count {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-error {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-hue-slider {
|
||||||
|
min-height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-toggle {
|
||||||
|
min-height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-toggle input {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-toggle-text {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Actions stack: full-width primary, then the secondary links beneath. */
|
||||||
|
.profile-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-save {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 48px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-view-link {
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 48px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-saved {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1648,3 +1648,641 @@
|
|||||||
padding: 0.3rem 0.55rem;
|
padding: 0.3rem 0.55rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========================================================================
|
||||||
|
THE PHONE APP (<= 860px)
|
||||||
|
========================================================================
|
||||||
|
Everything below this line is inert above 860px. Not one selector in this
|
||||||
|
section is reachable at desktop widths, and nothing above this line was
|
||||||
|
changed, so the desktop séance renders exactly as it did before.
|
||||||
|
860px is deliberately the same threshold MobileNav uses (it is
|
||||||
|
display:none from 861px up), so the two mobile shells appear together.
|
||||||
|
|
||||||
|
The brief was not "make it responsive" — it was "the app should be its own
|
||||||
|
thing". So on a phone this is a native-shaped app, not a squeezed page:
|
||||||
|
|
||||||
|
[ header strip ] brand, the 'more' handle
|
||||||
|
[ mode grid ] all six modes, named, 3x2, always visible
|
||||||
|
[ STAGE ] the board / matrix / graphs — the dominant surface
|
||||||
|
[ dock ] two sheet handles + the ask row
|
||||||
|
|
||||||
|
ONE pattern carries every secondary surface: a bottom sheet. The links
|
||||||
|
rail, the whole instrument column (dossier, veil, devices, ritual,
|
||||||
|
passage, judgment, mode panel) and the transmission log each slide up over
|
||||||
|
the stage from the dock. Same geometry, same grabber, same close, same
|
||||||
|
backdrop, one at a time. Nothing is removed and no markup is hidden that a
|
||||||
|
handle does not open — a sheet is a door, not a deletion.
|
||||||
|
|
||||||
|
Type floor 14px for anything read, inputs exactly 16px (iOS zooms below
|
||||||
|
that and never zooms back), every target >= 44px, all four safe-area
|
||||||
|
insets honoured, and the page itself never scrolls sideways. */
|
||||||
|
|
||||||
|
/* Mobile-only furniture: no box at all until the breakpoint claims it. */
|
||||||
|
.seance-dock,
|
||||||
|
.seance-sheet-close,
|
||||||
|
.seance-sheet-backdrop {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
/* ---------- shell ---------- */
|
||||||
|
|
||||||
|
.seance-page {
|
||||||
|
/* Fallback first for browsers without dynamic viewport units; the dvh
|
||||||
|
line then wins where it is understood, so the dock is not left under
|
||||||
|
a retracting URL bar. */
|
||||||
|
height: 100vh;
|
||||||
|
height: 100dvh;
|
||||||
|
overflow-x: hidden;
|
||||||
|
/* The page's own padding already carries every safe-area inset (see the
|
||||||
|
shell rules at the top of this file), so in-flow children below use
|
||||||
|
plain lengths — only the position:fixed sheets re-apply env(). */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- header strip ---------- */
|
||||||
|
|
||||||
|
.seance-topbar {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px 10px;
|
||||||
|
min-height: 56px;
|
||||||
|
padding: 8px 12px 10px;
|
||||||
|
/* The app's fixed 48px hamburger sits at env(safe-area-inset-left) +
|
||||||
|
0.6rem. The page has already inset us by the safe area, so clearing it
|
||||||
|
costs 0.6rem + 48px + a little breathing room. Nothing of ours is ever
|
||||||
|
placed under that corner. */
|
||||||
|
padding-left: 3.9rem;
|
||||||
|
/* An opaque strip instead of a blur: backdrop-filter would make this
|
||||||
|
header the containing block for the position:fixed sheet inside it. */
|
||||||
|
backdrop-filter: none;
|
||||||
|
background: rgba(12, 12, 20, 0.97);
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seance-brand {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- the mode control ---------- */
|
||||||
|
/* A 3x2 segmented grid. Chosen over a mode sheet because switching modes
|
||||||
|
is the single most frequent act in a séance and a sheet would cost a tap
|
||||||
|
*and* hide which mode is live; a wrapped segmented control shows all six
|
||||||
|
names and the current selection permanently, for ~100px of height. */
|
||||||
|
|
||||||
|
.seance-modes-rail {
|
||||||
|
order: 3;
|
||||||
|
flex: 1 1 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seance-modes {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 6px;
|
||||||
|
width: 100%;
|
||||||
|
/* Undo the horizontal-scroll rail: with a grid there is nothing behind
|
||||||
|
an edge, so there is nothing left to scroll to. */
|
||||||
|
overflow: visible;
|
||||||
|
justify-content: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The fade + arrow cues pointed at off-screen tabs. There are none now. */
|
||||||
|
.seance-modes-rail::before,
|
||||||
|
.seance-modes-rail::after,
|
||||||
|
.seance-modes-cue {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seance-modes .mode-tab {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 48px;
|
||||||
|
padding: 0.4rem 0.35rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
line-height: 1.15;
|
||||||
|
text-align: center;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
border-color: rgba(124, 255, 178, 0.16);
|
||||||
|
border-radius: 9px;
|
||||||
|
background: rgba(16, 16, 26, 0.72);
|
||||||
|
color: rgba(211, 233, 219, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.seance-modes .mode-tab.active {
|
||||||
|
color: #7cffb2;
|
||||||
|
border-color: rgba(124, 255, 178, 0.55);
|
||||||
|
background: rgba(124, 255, 178, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.seance-nav-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
order: 2;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-height: 44px;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
border-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- status lines ---------- */
|
||||||
|
/* Absolute banners pinned under a fixed-height desktop topbar cannot
|
||||||
|
survive a header that wraps. On a phone they simply become the app's
|
||||||
|
status row, in flow between the header and the stage — full width,
|
||||||
|
wrapping, no magic offsets. `bannerIn` is dropped with them because it
|
||||||
|
animates the translate(-50%) that no longer applies. */
|
||||||
|
|
||||||
|
.seance-conn-banner,
|
||||||
|
.seance-wanderer-note {
|
||||||
|
position: static;
|
||||||
|
transform: none;
|
||||||
|
width: auto;
|
||||||
|
margin: 0;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
white-space: normal;
|
||||||
|
border-radius: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
padding: 0.55rem 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seance-conn-banner {
|
||||||
|
animation: flickerAnim 4s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seance-wanderer-note {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wanderer-note-dismiss {
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- the stage ---------- */
|
||||||
|
|
||||||
|
.seance-main {
|
||||||
|
flex-direction: column;
|
||||||
|
/* The stage is exactly the space left over; the sheets carry everything
|
||||||
|
that used to fight the board for it, so nothing here scrolls. */
|
||||||
|
overflow: hidden;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seance-board-wrap {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
/* Deliberately generous: on a 390x844 phone this leaves the board ~55%
|
||||||
|
of the screen instead of the strip it used to get. */
|
||||||
|
min-height: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The BOARD / MATRIX / GRAPHS switcher overlays the top of the stage as a
|
||||||
|
proper segmented control. Specificity beats MatrixView.css's own
|
||||||
|
`.seance-view-tabs .mode-tab` regardless of stylesheet order. */
|
||||||
|
.seance-board-wrap .seance-view-tabs {
|
||||||
|
left: 12px;
|
||||||
|
right: 12px;
|
||||||
|
transform: none;
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seance-board-wrap .seance-view-tabs .mode-tab {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0.3rem 0.4rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seance-status-overlay {
|
||||||
|
top: 64px;
|
||||||
|
max-width: calc(100% - 24px);
|
||||||
|
padding: 0.5rem 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- the dock ---------- */
|
||||||
|
|
||||||
|
.seance-bottom {
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 12px 10px;
|
||||||
|
background: linear-gradient(0deg, rgba(7, 7, 13, 0.98) 62%, rgba(7, 7, 13, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
.seance-dock {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seance-dock-btn {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 52px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.2rem 0.5rem;
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
background: rgba(16, 16, 26, 0.92);
|
||||||
|
border: 1px solid rgba(124, 255, 178, 0.28);
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #7cffb2;
|
||||||
|
font-family: 'IBM Plex Mono', monospace;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seance-dock-btn[aria-expanded='true'] {
|
||||||
|
border-color: rgba(124, 255, 178, 0.6);
|
||||||
|
background: rgba(124, 255, 178, 0.1);
|
||||||
|
box-shadow: inset 0 0 12px rgba(124, 255, 178, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.seance-dock-btn .dock-glyph {
|
||||||
|
color: #b26bff;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seance-dock-btn .dock-label {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seance-dock-btn .anomaly-count {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- the ask row ---------- */
|
||||||
|
|
||||||
|
.seance-contact {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seance-contact input {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
min-height: 48px;
|
||||||
|
/* Exactly 16px. Anything smaller and iOS Safari zooms the page on focus
|
||||||
|
and never zooms back out. */
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ask-btn,
|
||||||
|
.summon-btn {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-height: 48px;
|
||||||
|
padding: 0 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ask-btn {
|
||||||
|
font-size: 0.92rem;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summon-btn {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- the one sheet pattern ---------- */
|
||||||
|
|
||||||
|
.seance-sheet-backdrop {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 44;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: rgba(2, 2, 6, 0.66);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seance-top-right,
|
||||||
|
.seance-side,
|
||||||
|
.seance-transcript-wrap {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 45;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 12px;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
/* Tall enough to be the screen's subject, short enough that the stage
|
||||||
|
behind it still says which app you are in. */
|
||||||
|
max-height: 84vh;
|
||||||
|
max-height: 84dvh;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
/* Fixed to the viewport, so unlike the in-flow rules above these must
|
||||||
|
carry the insets themselves — all four edges, because in landscape the
|
||||||
|
cutout moves to a side. */
|
||||||
|
padding: 0.6rem calc(env(safe-area-inset-right, 0px) + 1rem)
|
||||||
|
calc(env(safe-area-inset-bottom, 0px) + 1.1rem)
|
||||||
|
calc(env(safe-area-inset-left, 0px) + 1rem);
|
||||||
|
background: rgba(10, 10, 17, 0.98);
|
||||||
|
border: 1px solid rgba(124, 255, 178, 0.22);
|
||||||
|
border-bottom: none;
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
box-shadow: 0 -14px 44px rgba(0, 0, 0, 0.66);
|
||||||
|
transform: translateY(101%);
|
||||||
|
visibility: hidden;
|
||||||
|
transition: transform 220ms ease, visibility 220ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The grabber. Sticky, so it stays put while a long sheet scrolls. */
|
||||||
|
.seance-top-right::before,
|
||||||
|
.seance-side::before,
|
||||||
|
.seance-transcript-wrap::before {
|
||||||
|
content: '';
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-self: center;
|
||||||
|
width: 46px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(124, 255, 178, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-sheet][data-open='true'] {
|
||||||
|
transform: none;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.seance-top-right,
|
||||||
|
.seance-side,
|
||||||
|
.seance-transcript-wrap {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.seance-sheet-close {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
align-self: flex-end;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
margin-top: -0.2rem;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(124, 255, 178, 0.22);
|
||||||
|
border-radius: 10px;
|
||||||
|
color: rgba(211, 233, 219, 0.75);
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- sheet: the ways (links, language, passive, departure) ---------- */
|
||||||
|
|
||||||
|
.seance-top-right .seance-codex-link,
|
||||||
|
.seance-top-right .seance-logout,
|
||||||
|
.seance-top-right .passive-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 48px;
|
||||||
|
padding: 0 0.9rem;
|
||||||
|
border: 1px solid rgba(178, 107, 255, 0.28);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(178, 107, 255, 0.05);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seance-top-right .seance-logout {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.passive-label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-toggle {
|
||||||
|
display: flex;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-toggle button {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-height: 48px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seance-user {
|
||||||
|
max-width: none;
|
||||||
|
padding: 0 0.3rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conn-dot {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
align-self: flex-start;
|
||||||
|
margin-left: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- sheet: the instruments ---------- */
|
||||||
|
|
||||||
|
.seance-side {
|
||||||
|
padding-top: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The live instrument comes first: opening this sheet from the dock handle
|
||||||
|
(which is labelled with the active mode) must land on that mode's panel,
|
||||||
|
not scroll past six readouts to reach it. DOM order is untouched, so the
|
||||||
|
desktop column keeps the dossier at the top where it belongs. */
|
||||||
|
.seance-side .mode-panel {
|
||||||
|
order: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-panel {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Readable body copy: 14px floor for anything that is prose or a value,
|
||||||
|
not the 11-12px the desktop column packs into 344px. */
|
||||||
|
.dossier-persona,
|
||||||
|
.dossier-quotes li,
|
||||||
|
.panel-notice p,
|
||||||
|
.ouija-drift,
|
||||||
|
.wire-hint,
|
||||||
|
.dossier-nonehint {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-notice h4,
|
||||||
|
.dossier-title,
|
||||||
|
.panel-title {
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tel-key,
|
||||||
|
.tel-val,
|
||||||
|
.evp-readout,
|
||||||
|
.radio-readout,
|
||||||
|
.emf-readout,
|
||||||
|
.wire-count,
|
||||||
|
.ouija-history,
|
||||||
|
.evp-listening,
|
||||||
|
.emf-listening,
|
||||||
|
.evp-level,
|
||||||
|
.tuned-freq,
|
||||||
|
.emf-baseline,
|
||||||
|
.troubleshoot,
|
||||||
|
.voice-params dt,
|
||||||
|
.voice-params dd {
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voice-params {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voice-params dd {
|
||||||
|
max-width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rarity-badge,
|
||||||
|
.dossier-badge {
|
||||||
|
font-size: 0.88rem;
|
||||||
|
padding: 0.3rem 0.65rem;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Every action in a sheet is a thumb target. */
|
||||||
|
.seance-side .panel-action,
|
||||||
|
.seance-side .panel-subaction,
|
||||||
|
.seance-side .dossier-header {
|
||||||
|
min-height: 48px;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seance-side .panel-action,
|
||||||
|
.seance-side .panel-subaction {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
align-self: stretch;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
border-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emf-actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emf-actions .panel-action,
|
||||||
|
.emf-actions .panel-subaction {
|
||||||
|
flex: 1 1 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The spectrum canvases are the one genuinely wide thing on the page. They
|
||||||
|
already lay out at width:100%, but this guarantees that if a scope ever
|
||||||
|
asks for more room it scrolls inside its own frame rather than dragging
|
||||||
|
the page sideways. */
|
||||||
|
.seance-side .scope,
|
||||||
|
.seance-side .scope-canvases {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emf-gauge {
|
||||||
|
height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- sheet: the transmission log ---------- */
|
||||||
|
|
||||||
|
.seance-transcript-wrap {
|
||||||
|
/* Was a fixed 168px box wedged above the ask row; as a sheet it gets the
|
||||||
|
height a log actually needs. */
|
||||||
|
height: auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seance-transcript-title {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 0 0.2rem 0.5rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seance-transcript-title .anomaly-count {
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transcript {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 42vh;
|
||||||
|
padding: 0.2rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tx-src,
|
||||||
|
.tx-utterance .tx-speaker,
|
||||||
|
.tx-system,
|
||||||
|
.tx-streaming .tx-stream-label {
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- toasts ---------- */
|
||||||
|
/* The header no longer has a fixed height to hang from, so alerts dock to
|
||||||
|
the bottom of the screen the way a phone's do. Above the sheets' z-index
|
||||||
|
backdrop? No — deliberately below, so an open sheet is never argued
|
||||||
|
with; the toast is still there when the sheet closes. */
|
||||||
|
|
||||||
|
.seance-toasts {
|
||||||
|
position: fixed;
|
||||||
|
top: auto;
|
||||||
|
right: calc(env(safe-area-inset-right, 0px) + 12px);
|
||||||
|
left: calc(env(safe-area-inset-left, 0px) + 12px);
|
||||||
|
bottom: calc(env(safe-area-inset-bottom, 0px) + 12px);
|
||||||
|
z-index: 43;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seance-toast {
|
||||||
|
font-size: 0.88rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-dismiss {
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -153,11 +153,34 @@ function SeanceSession({ username }: { username: string }) {
|
|||||||
const [question, setQuestion] = useState('')
|
const [question, setQuestion] = useState('')
|
||||||
const [dossierOpen, setDossierOpen] = useState(true)
|
const [dossierOpen, setDossierOpen] = useState(true)
|
||||||
const [view, setView] = useState<BoardView>('board')
|
const [view, setView] = useState<BoardView>('board')
|
||||||
// The topbar's links/toggles don't fit a phone in one row. Rather than
|
// ---- mobile app shell: one sheet metaphor for every secondary surface ----
|
||||||
// dropping any of them, they live behind this disclosure below 560px; at
|
//
|
||||||
// desktop widths the rail is always shown and the toggle is not rendered
|
// On a phone the séance is a single-purpose screen: a header strip, the
|
||||||
// visible by CSS, so nothing changes there.
|
// board as the stage, and a dock. Everything else (the topbar's rail of
|
||||||
const [navOpen, setNavOpen] = useState(false)
|
// links, the instrument column, the transmission log) is reachable — and
|
||||||
|
// *only* reachable — through the same bottom sheet, so a seeker learns one
|
||||||
|
// gesture instead of three metaphors. Nothing is dropped: the markup is
|
||||||
|
// identical at every width, and above 860px none of this CSS applies, so
|
||||||
|
// the desktop layout renders exactly as it always has (`sheet` can never
|
||||||
|
// leave null there because every control that sets it is display:none).
|
||||||
|
type Sheet = 'nav' | 'panels' | 'log'
|
||||||
|
const [sheet, setSheet] = useState<Sheet | null>(null)
|
||||||
|
const toggleSheet = useCallback(
|
||||||
|
(next: Sheet) => setSheet((cur) => (cur === next ? null : next)),
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const closeSheet = useCallback(() => setSheet(null), [])
|
||||||
|
|
||||||
|
// Escape closes the open sheet — a keyboard is rare on a phone but common
|
||||||
|
// on the small-window desktop widths this breakpoint also covers.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!sheet || typeof window === 'undefined') return
|
||||||
|
const onKey = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') setSheet(null)
|
||||||
|
}
|
||||||
|
window.addEventListener('keydown', onKey)
|
||||||
|
return () => window.removeEventListener('keydown', onKey)
|
||||||
|
}, [sheet])
|
||||||
// Six mode tabs never fit a 390px viewport, so the rail scrolls. A hidden
|
// Six mode tabs never fit a 390px viewport, so the rail scrolls. A hidden
|
||||||
// scrollbar with no other affordance made the later modes (camera above
|
// scrollbar with no other affordance made the later modes (camera above
|
||||||
// all) look like they simply didn't exist on a phone — these flags drive
|
// all) look like they simply didn't exist on a phone — these flags drive
|
||||||
@@ -296,10 +319,12 @@ function SeanceSession({ username }: { username: string }) {
|
|||||||
{t('common.appName')}
|
{t('common.appName')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* The rail keeps the six modes on one line and scrolls them; the
|
{/* Desktop keeps the six modes on one scrolling line, with fade +
|
||||||
wrapper carries the fade + arrow cues so the off-screen ones stay
|
arrow cues for whatever is off the end. At phone widths that rail
|
||||||
discoverable. Wrapping to two rows was the alternative, but that
|
is the single worst control on the page — six 44px targets cannot
|
||||||
steals ~44px of board height on the screen that has the least. */}
|
fit 390px, so LENS in particular reads as if it does not exist.
|
||||||
|
Below 860px the same nav becomes a 3x2 segmented grid (see the
|
||||||
|
mobile block in the CSS): every mode named, on screen, at once. */}
|
||||||
<div
|
<div
|
||||||
className="seance-modes-rail"
|
className="seance-modes-rail"
|
||||||
data-more-start={modeOverflow.start}
|
data-more-start={modeOverflow.start}
|
||||||
@@ -333,19 +358,25 @@ function SeanceSession({ username }: { username: string }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="seance-nav-toggle"
|
className="seance-nav-toggle"
|
||||||
aria-expanded={navOpen}
|
aria-expanded={sheet === 'nav'}
|
||||||
aria-controls="seance-nav-rail"
|
aria-controls="seance-nav-rail"
|
||||||
onClick={() => setNavOpen((o) => !o)}
|
onClick={() => toggleSheet('nav')}
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">⛤</span>
|
<span aria-hidden="true">⛤</span>
|
||||||
<span className="seance-nav-toggle-label">
|
<span className="seance-nav-toggle-label">
|
||||||
{navOpen
|
{sheet === 'nav'
|
||||||
? t('seance.nav.close', { defaultValue: 'close' })
|
? t('seance.nav.close', { defaultValue: 'close' })
|
||||||
: t('seance.nav.open', { defaultValue: 'more' })}
|
: t('seance.nav.open', { defaultValue: 'more' })}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="seance-top-right" id="seance-nav-rail" data-open={navOpen}>
|
<div
|
||||||
|
className="seance-top-right"
|
||||||
|
id="seance-nav-rail"
|
||||||
|
data-sheet="nav"
|
||||||
|
data-open={sheet === 'nav'}
|
||||||
|
>
|
||||||
|
<SheetClose onClose={closeSheet} />
|
||||||
<label className="passive-toggle" title={t('seance.passive')}>
|
<label className="passive-toggle" title={t('seance.passive')}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -491,7 +522,17 @@ function SeanceSession({ username }: { username: string }) {
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<aside className="seance-side">
|
{/* Desktop: the instrument column beside the board. Phone: the same
|
||||||
|
column, verbatim, as a bottom sheet — the active mode's own panel
|
||||||
|
is ordered first by CSS so opening it lands on the instrument the
|
||||||
|
seeker is actually holding. */}
|
||||||
|
<aside
|
||||||
|
className="seance-side"
|
||||||
|
id="seance-side-sheet"
|
||||||
|
data-sheet="panels"
|
||||||
|
data-open={sheet === 'panels'}
|
||||||
|
>
|
||||||
|
<SheetClose onClose={closeSheet} />
|
||||||
<EntityCard
|
<EntityCard
|
||||||
entity={entity}
|
entity={entity}
|
||||||
isNew={state.entityIsNew}
|
isNew={state.entityIsNew}
|
||||||
@@ -518,7 +559,51 @@ function SeanceSession({ username }: { username: string }) {
|
|||||||
|
|
||||||
{/* ---- bottom: transmission log + direct contact ---- */}
|
{/* ---- bottom: transmission log + direct contact ---- */}
|
||||||
<footer className="seance-bottom">
|
<footer className="seance-bottom">
|
||||||
<div className="seance-transcript-wrap">
|
{/* Phones only (display:none above 860px). The two handles that raise
|
||||||
|
the instrument sheet and the log sheet; they sit in the dock with
|
||||||
|
the ask row so every reachable surface has one obvious door.
|
||||||
|
Both labels are existing catalog strings — the first is simply the
|
||||||
|
active mode's own name, which is also the honest description of
|
||||||
|
what the sheet contains. */}
|
||||||
|
<div className="seance-dock">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="seance-dock-btn"
|
||||||
|
aria-expanded={sheet === 'panels'}
|
||||||
|
aria-controls="seance-side-sheet"
|
||||||
|
onClick={() => toggleSheet('panels')}
|
||||||
|
>
|
||||||
|
<span className="dock-glyph" aria-hidden>
|
||||||
|
⌬
|
||||||
|
</span>
|
||||||
|
<span className="dock-label">
|
||||||
|
{t(`seance.modes.${state.mode}`, { defaultValue: MODE_LABEL_DEFAULTS[state.mode] })}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="seance-dock-btn"
|
||||||
|
aria-expanded={sheet === 'log'}
|
||||||
|
aria-controls="seance-log-sheet"
|
||||||
|
onClick={() => toggleSheet('log')}
|
||||||
|
>
|
||||||
|
<span className="dock-glyph" aria-hidden>
|
||||||
|
☰
|
||||||
|
</span>
|
||||||
|
<span className="dock-label">{t('seance.transcriptTitle')}</span>
|
||||||
|
<span className="anomaly-count">
|
||||||
|
{t('seance.anomaly.count', { count: state.anomalyCount })}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="seance-transcript-wrap"
|
||||||
|
id="seance-log-sheet"
|
||||||
|
data-sheet="log"
|
||||||
|
data-open={sheet === 'log'}
|
||||||
|
>
|
||||||
|
<SheetClose onClose={closeSheet} />
|
||||||
<div className="seance-transcript-title">
|
<div className="seance-transcript-title">
|
||||||
<span>{t('seance.transcriptTitle')}</span>
|
<span>{t('seance.transcriptTitle')}</span>
|
||||||
<span className="anomaly-count">
|
<span className="anomaly-count">
|
||||||
@@ -557,6 +642,17 @@ function SeanceSession({ username }: { username: string }) {
|
|||||||
</form>
|
</form>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
{/* ---- sheet backdrop (phones only; `sheet` is always null above
|
||||||
|
860px because every control that sets it is display:none) ---- */}
|
||||||
|
{sheet !== null && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="seance-sheet-backdrop"
|
||||||
|
aria-label={t('common.dismiss')}
|
||||||
|
onClick={closeSheet}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* ---- toasts ---- */}
|
{/* ---- toasts ---- */}
|
||||||
<div className="seance-toasts" aria-live="polite">
|
<div className="seance-toasts" aria-live="polite">
|
||||||
{state.toasts.map((toast) => (
|
{state.toasts.map((toast) => (
|
||||||
@@ -571,6 +667,26 @@ function SeanceSession({ username }: { username: string }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- bottom sheets ----
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The close handle every mobile sheet carries. `display:none` above 860px,
|
||||||
|
* so on desktop this adds no box, no flex gap and no visual change at all.
|
||||||
|
*/
|
||||||
|
function SheetClose({ onClose }: { onClose: () => void }) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="seance-sheet-close"
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label={t('common.dismiss')}
|
||||||
|
>
|
||||||
|
<span aria-hidden>×</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// ---- toasts ----
|
// ---- toasts ----
|
||||||
|
|
||||||
function ToastCard({ toast, onDismiss }: { toast: Toast; onDismiss: (id: number) => void }) {
|
function ToastCard({ toast, onDismiss }: { toast: Toast; onDismiss: (id: number) => void }) {
|
||||||
|
|||||||
@@ -464,3 +464,144 @@
|
|||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- phone: the storefront as a single stacked column ----
|
||||||
|
Phone breakpoint only; desktop above 860px is unchanged. The flagship,
|
||||||
|
every module and the whole manifest form still render — restacked, not
|
||||||
|
trimmed. */
|
||||||
|
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.shop-page {
|
||||||
|
padding:
|
||||||
|
calc(env(safe-area-inset-top, 0px) + 4.4rem)
|
||||||
|
calc(env(safe-area-inset-right, 0px) + 1rem)
|
||||||
|
calc(env(safe-area-inset-bottom, 0px) + 5rem)
|
||||||
|
calc(env(safe-area-inset-left, 0px) + 1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-back {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 44px;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-subtitle {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-section-title {
|
||||||
|
margin-bottom: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-section-title::before,
|
||||||
|
.shop-section-title::after {
|
||||||
|
flex-basis: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-tag {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flagship: glyph over copy, everything left-aligned and readable. */
|
||||||
|
.shop-flagship {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
text-align: left;
|
||||||
|
gap: 1.2rem;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
padding: 1.4rem 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flagship-specs {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flagship-specs li {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.35rem 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flagship-desc {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* One module card per row. */
|
||||||
|
.shop-modules {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modules-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-card {
|
||||||
|
padding: 1.3rem 1.1rem;
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-name {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-desc {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-unlocks {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Manifest: one control per line, all full width. */
|
||||||
|
.shop-waitlist {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
padding: 1.4rem 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waitlist-desc {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
margin: -0.5rem 0 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waitlist-form {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waitlist-input,
|
||||||
|
.waitlist-select {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 100%;
|
||||||
|
/* 16px exactly — iOS zooms the page on focus below this. */
|
||||||
|
font-size: 16px;
|
||||||
|
min-height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waitlist-submit {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 48px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waitlist-message {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-honesty p {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user