// ─── Seat tray ────────────────────────────────────────────────────────────── // // Structure: // #id_tray_wrap — fixed right edge, flex row, slides on :has(.open) // #id_tray_handle — $handle-exposed wide; contains grip + button // #id_tray_grip — position:absolute; ::before/::after = concentric rects // #id_tray_btn — circle button (z-index:1, paints above grip) // #id_tray — 280px panel; covers grip's rightward extension when open // // Closed: wrap translateX($tray-w) → only button circle visible at right edge. // Open: translateX(0) → full tray panel slides in; grip rects visible as handle. // // Grid layout (portrait): // 8 explicit rows; columns auto-added as items arrive (grid-auto-flow: column). // --tray-cell-size set by JS from tray.clientHeight / 8 → always square cells. // // Grid layout (landscape): // 8 explicit columns; rows auto-added as items arrive (grid-auto-flow: row). // --tray-cell-size set by JS from tray.clientWidth / 8 → always square cells. $tray-w: 280px; $handle-rect-w: 10000px; $handle-rect-h: 72px; $handle-exposed: 48px; $handle-r: 1rem; #id_tray_wrap { position: fixed; // left set by JS: closed = vw - handleW; open = vw - wrapW // top/bottom set by JS from nav/footer measurements // right intentionally absent — wrap has fixed CSS width (handle + tray) // so the open edge only reaches the viewport boundary when fully open. top: 0; bottom: 0; z-index: 310; pointer-events: none; display: flex; flex-direction: row; align-items: stretch; transition: left 0.35s cubic-bezier(0.4, 0, 0.2, 1); &.tray-dragging { transition: none; } &.wobble { animation: tray-wobble 0.45s ease; } &.snap { animation: tray-snap 0.30s ease; } } #id_tray_handle { flex-shrink: 0; position: relative; width: $handle-exposed; display: flex; align-items: center; justify-content: center; } #id_tray_grip { position: absolute; top: 50%; left: calc(#{$handle-exposed} / 2 - 0.125rem); transform: translateY(-50%); width: $handle-rect-w; height: $handle-rect-h; pointer-events: none; // Border + overflow:hidden on the grip itself clips ::before's shadow with correct radius border-radius: $handle-r; border: 0.15rem solid rgba(var(--secUser), 1); overflow: hidden; // Inset inner window: box-shadow spills outward to fill the opaque frame area, // clipped to grip's rounded edge by overflow:hidden. background:transparent = see-through hole. &::before { content: ''; position: absolute; inset: 0.4rem; border-radius: calc(#{$handle-r} - 0.35rem); border: 0.15rem solid rgba(var(--secUser), 1); background: transparent; box-shadow: 0 0 0 200px rgba(var(--priUser), 1); } &::after { content: none; } } #id_tray_btn { pointer-events: auto; position: relative; z-index: 1; // above #id_tray_grip width: 3rem; height: 3rem; border-radius: 50%; background-color: rgba(var(--priUser), 1); border: 0.15rem solid rgba(var(--secUser), 1); cursor: grab; display: inline-flex; align-items: center; justify-content: center; i { font-size: 1.75rem; color: rgba(var(--secUser), 1); pointer-events: none; } &:active { cursor: grabbing; } &.open { cursor: pointer; border-color: rgba(var(--quaUser), 1); i { color: rgba(var(--quaUser), 1); } } } // Grip borders → --quaUser when tray is open (btn.open precedes grip in DOM so :has() needed) #id_tray_wrap:has(#id_tray_btn.open) #id_tray_grip { border-color: rgba(var(--quaUser), 1); &::before { border-color: rgba(var(--quaUser), 1); } } @keyframes tray-wobble { 0%, 100% { transform: translateX(0); } 20% { transform: translateX(-8px); } 40% { transform: translateX(6px); } 60% { transform: translateX(-5px); } 80% { transform: translateX(3px); } } // Inverted wobble — handle overshoots past the wall on close, then bounces back. @keyframes tray-snap { 0%, 100% { transform: translateX(0); } 20% { transform: translateX(8px); } 40% { transform: translateX(-6px); } 60% { transform: translateX(5px); } 80% { transform: translateX(-3px); } } #id_tray { flex: 1; min-width: 0; margin-left: 0.5rem; // small gap so tray appears slightly off-screen on drag start pointer-events: auto; position: relative; z-index: 1; // above #id_tray_grip pseudo-elements background: rgba(var(--duoUser), 1); border-left:2.5rem solid rgba(var(--quaUser), 1); border-top: 2.5rem solid rgba(var(--quaUser), 1); border-bottom: 2.5rem solid rgba(var(--quaUser), 1); box-shadow: -0.25rem 0 0.5rem rgba(0, 0, 0, 0.55), inset 0 0 0 0.3rem rgba(var(--quiUser), 0.45), // prominent bevel ring at wall edge inset 0.6rem 0 1.5rem -0.5rem rgba(0, 0, 0, 1), // left wall depth inset 0.6rem 0 1.5rem -0.5rem rgba(var(--quiUser), 0.5), // left wall depth (hue) inset 0 0.6rem 1.5rem -0.5rem rgba(0, 0, 0, 1), // top wall depth inset 0 0.6rem 1.5rem -0.5rem rgba(var(--quiUser), 0.5), // top wall depth (hue) inset 0 -0.6rem 1.5rem -0.5rem rgba(0, 0, 0, 1), // bottom wall depth inset 0 -0.6rem 1.5rem -0.5rem rgba(var(--quiUser), 0.5) // bottom wall depth (hue) ; overflow: hidden; // clip #id_tray_grid to the felt interior } #id_tray_grid { display: grid; // Portrait: 8 explicit rows; columns auto-added as items arrive. // --tray-cell-size set by JS from tray.clientHeight / 8 → always square cells. grid-template-rows: repeat(8, var(--tray-cell-size, 48px)); grid-auto-flow: column; grid-auto-columns: var(--tray-cell-size, 48px); } .tray-cell { border-right: 2px dotted rgba(var(--quaUser), 0.35); border-bottom: 2px dotted rgba(var(--quaUser), 0.35); position: relative; } // ─── Tray: landscape reorientation ───────────────────────────────────────── // // Must come AFTER the portrait tray rules above to win the cascade // (same specificity — later declaration wins). // // In landscape the tray slides DOWN from the top instead of in from the right. // Structure (column-reverse): tray panel above, handle below. // JS controls style.top for the Y-axis slide: // Closed: top = -(trayH) → only handle visible at y = 0 // Open: top = gearBtnTop - wrapH → handle bottom at gear btn top // // The wrap fits horizontally between the fixed left-nav and right-footer sidebars. @media (orientation: landscape) { $sidebar-w: 4rem; $tray-landscape-max-w: 960px; // cap tray width on very wide screens #id_tray_wrap { flex-direction: column-reverse; // tray panel above, handle below left: $sidebar-w; right: $sidebar-w; top: auto; // JS controls style.top for the Y-axis slide bottom: auto; transition: top 0.35s cubic-bezier(0.4, 0, 0.2, 1); &.tray-dragging { transition: none; } &.wobble { animation: tray-wobble-landscape 0.45s ease; } &.snap { animation: tray-snap-landscape 0.30s ease; } } #id_tray_handle { width: auto; // full width of wrap height: 48px; // $handle-exposed — same exposed dimension as portrait } #id_tray_grip { // Rotate 90°: centred horizontally, extends vertically. // bottom mirrors portrait's left: grip starts at handle centre and extends // toward the tray (upward in column-reverse layout). bottom: calc(48px / 2 - 0.125rem); // $handle-exposed / 2 from handle bottom top: auto; left: 50%; transform: translateX(-50%); width: 72px; // $handle-rect-h — narrow visible dimension height: 10000px; // $handle-rect-w — extends upward into tray area } #id_tray { // Borders: left/right/bottom are visible walls; top edge is open. // Bottom faces the handle (same logic as portrait's left border facing handle). border-left: 2.5rem solid rgba(var(--quaUser), 1); border-right: 2.5rem solid rgba(var(--quaUser), 1); border-bottom: 2.5rem solid rgba(var(--quaUser), 1); border-top: none; margin-left: 0; // portrait horizontal gap no longer needed margin-bottom: 0.5rem; // gap between tray bottom and handle top // Cap width on ultra-wide screens; center within the handle shelf. width: 100%; max-width: $tray-landscape-max-w; align-self: center; box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.55), inset 0 0 0 0.3rem rgba(var(--quiUser), 0.45), // prominent bevel ring inset 0 -0.6rem 1.5rem -0.5rem rgba(0, 0, 0, 1), // bottom wall depth inset 0 -0.6rem 1.5rem -0.5rem rgba(var(--quaUser), 0.5), // bottom wall depth (hue) inset 0.6rem 0 1.5rem -0.5rem rgba(0, 0, 0, 1), // left wall depth inset 0.6rem 0 1.5rem -0.5rem rgba(var(--quaUser), 0.5), // left wall depth (hue) inset -0.6rem 0 1.5rem -0.5rem rgba(0, 0, 0, 1), // right wall depth inset -0.6rem 0 1.5rem -0.5rem rgba(var(--quaUser), 0.5) // right wall depth (hue) ; flex: 1; // fill wrap height (JS sets wrap height = gearBtnTop) height: auto; min-height: unset; overflow: hidden; // clip #id_tray_grid to the felt interior } #id_tray_grid { // Landscape: 8 explicit columns; rows auto-added as items arrive. // --tray-cell-size set by JS from tray.clientWidth / 8 → always square cells. grid-template-columns: repeat(8, var(--tray-cell-size, 48px)); grid-template-rows: none; // clear portrait's 8-row template grid-auto-flow: row; grid-auto-rows: var(--tray-cell-size, 48px); // Anchor grid to the handle-side (bottom) of the tray so the first row // is visible when partially open; additional rows grow upward. position: absolute; bottom: 0; left: 0; } // In landscape the first row sits at the bottom; border-top divides it from // the felt above. border-bottom would face the wall — swap it out. .tray-cell { border-color: rgba(var(--priUser), 0.35); border-top: 2px dotted rgba(var(--priUser), 0.35); border-bottom: none; } @keyframes tray-wobble-landscape { 0%, 100% { transform: translateY(0); } 20% { transform: translateY(-8px); } 40% { transform: translateY(6px); } 60% { transform: translateY(-5px); } 80% { transform: translateY(3px); } } // Inverted wobble — wrap overshoots upward on close, then bounces back. @keyframes tray-snap-landscape { 0%, 100% { transform: translateY(0); } 20% { transform: translateY(8px); } 40% { transform: translateY(-6px); } 60% { transform: translateY(5px); } 80% { transform: translateY(-3px); } } }