908f7ea973
Participation privacy (decision_2026-10-29_participation-privacy-mechanism): a sign-up is a private Join naming the meeting point, addressed to nobody, handled by the backend on emission. Attendee lists hold member URNs hashed from (WebID, event, a secret kept in the participant's profile), so counts stay public and only contacts recognise anyone; the creator has no read right and is notified anonymously. Each person's participations collection, created by the backend at the first sign-up, is readable by their /contacts WAC group only, never world-readable. The member URN is sticky per (meeting point, participant) and every collection write is verified by reading back, so a refused write leaves the act unrecorded and retryable. The EventRef mechanism and its browser-side provisioning are gone. The backend now runs with WebACL on, as the reference apps do, on its own secure datasets festipod / settings-festipod instead of the pod provider's settings dataset; pod-notifications needs it. The compose header and the deployment leaf say how to reset a stack whose datasets predate this. The rights matrix covers the new mechanism live (148 hard lines), including a forged contact Accept between throwaway accounts: the stranger enters the contacts list (platform defect, reported upstream) but is refused the profile and the participations. Tests may attack as long as they never break the dev environment (rule_tests-never-break-the-dev-environment). Also lands the coherence-pass work that was still uncommitted: contacts as the Pod's social graph and invite links, image upload to the Pod, the meeting-point form, profile completion gate, and the doctrine for them. Superseded briefs and decisions are deleted rather than annotated.
334 lines
6.0 KiB
CSS
334 lines
6.0 KiB
CSS
@import "tailwindcss";
|
|
|
|
/* Modern clean theme - DM Sans */
|
|
:root {
|
|
--app-black: #1a1a1a;
|
|
--app-gray: #888;
|
|
--app-light-gray: #f0f0f0;
|
|
--app-bg: #ffffff;
|
|
--app-white: #ffffff;
|
|
--app-accent: #E8590C;
|
|
--app-accent-light: #FFF7ED;
|
|
--app-accent-border: #FDDCB5;
|
|
--app-accent-dark: #C05621;
|
|
--app-green: #22543D;
|
|
--app-green-light: #f7fff7;
|
|
--app-green-border: #c6f6d5;
|
|
--app-green-text: #68D391;
|
|
--app-radius: 16px;
|
|
--app-radius-sm: 12px;
|
|
--app-radius-xs: 8px;
|
|
--font-app: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
html, body, #root {
|
|
height: 100%;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body {
|
|
font-family: var(--font-app);
|
|
background-color: var(--app-bg);
|
|
color: var(--app-black);
|
|
}
|
|
|
|
/* Modern button styles */
|
|
.app-btn {
|
|
font-family: var(--font-app);
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
padding: 12px 20px;
|
|
background: var(--app-white);
|
|
border: 1.5px solid #e0e0e0;
|
|
border-radius: var(--app-radius-sm);
|
|
cursor: pointer;
|
|
transition: all 0.15s ease;
|
|
color: var(--app-black);
|
|
}
|
|
|
|
.app-btn:hover {
|
|
background: #f9f9f9;
|
|
border-color: #ccc;
|
|
}
|
|
|
|
.app-btn:active {
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
.app-btn-primary {
|
|
background: var(--app-accent);
|
|
color: var(--app-white);
|
|
border-color: var(--app-accent);
|
|
}
|
|
|
|
.app-btn-primary:hover {
|
|
background: #d14e0a;
|
|
border-color: #d14e0a;
|
|
}
|
|
|
|
.app-btn-green {
|
|
background: var(--app-green);
|
|
color: var(--app-white);
|
|
border-color: var(--app-green);
|
|
}
|
|
|
|
/* Modern input styles */
|
|
.app-input {
|
|
font-family: var(--font-app);
|
|
font-size: 14px;
|
|
padding: 12px 14px;
|
|
background: var(--app-white);
|
|
border: 1.5px solid #e0e0e0;
|
|
border-radius: var(--app-radius-sm);
|
|
outline: none;
|
|
width: 100%;
|
|
color: var(--app-black);
|
|
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
|
}
|
|
|
|
.app-input:focus {
|
|
border-color: var(--app-accent);
|
|
box-shadow: 0 0 0 3px rgba(232, 89, 12, 0.1);
|
|
}
|
|
|
|
.app-input::placeholder {
|
|
color: #bbb;
|
|
}
|
|
|
|
/* Modern card */
|
|
.app-card {
|
|
background: var(--app-white);
|
|
border: 1px solid #eee;
|
|
border-radius: var(--app-radius);
|
|
padding: 16px;
|
|
position: relative;
|
|
transition: box-shadow 0.15s ease;
|
|
}
|
|
|
|
.app-card:hover {
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
}
|
|
|
|
/* Text styles */
|
|
.app-title {
|
|
font-family: var(--font-app);
|
|
font-size: 22px;
|
|
font-weight: 700;
|
|
margin: 0 0 10px 0;
|
|
color: var(--app-black);
|
|
letter-spacing: -0.5px;
|
|
}
|
|
|
|
.app-subtitle {
|
|
font-family: var(--font-app);
|
|
font-size: 17px;
|
|
font-weight: 700;
|
|
color: var(--app-black);
|
|
margin: 0 0 8px 0;
|
|
}
|
|
|
|
.app-text {
|
|
font-family: var(--font-app);
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
color: var(--app-black);
|
|
}
|
|
|
|
/* Divider */
|
|
.app-divider {
|
|
height: 1px;
|
|
background: #f0f0f0;
|
|
margin: 16px 0;
|
|
}
|
|
|
|
/* Toggle */
|
|
.app-toggle {
|
|
width: 50px;
|
|
height: 28px;
|
|
border: none;
|
|
border-radius: 14px;
|
|
position: relative;
|
|
cursor: pointer;
|
|
background: #e0e0e0;
|
|
transition: background 0.2s ease;
|
|
}
|
|
|
|
.app-toggle::after {
|
|
content: '';
|
|
position: absolute;
|
|
width: 22px;
|
|
height: 22px;
|
|
background: var(--app-white);
|
|
border-radius: 50%;
|
|
top: 3px;
|
|
left: 3px;
|
|
transition: left 0.2s ease;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
|
}
|
|
|
|
.app-toggle.on {
|
|
background: var(--app-accent);
|
|
}
|
|
|
|
.app-toggle.on::after {
|
|
left: 25px;
|
|
}
|
|
|
|
/* Checkbox */
|
|
.app-checkbox {
|
|
width: 22px;
|
|
height: 22px;
|
|
border: 2px solid #ddd;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--app-white);
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.app-checkbox.checked {
|
|
background: var(--app-accent);
|
|
border-color: var(--app-accent);
|
|
}
|
|
|
|
.app-checkbox.checked::after {
|
|
content: '✓';
|
|
font-size: 13px;
|
|
font-weight: bold;
|
|
color: white;
|
|
}
|
|
|
|
/* Nav bar */
|
|
.app-navbar {
|
|
display: flex;
|
|
justify-content: space-around;
|
|
align-items: center;
|
|
padding: 10px 0 20px;
|
|
background: var(--app-white);
|
|
border-top: 1px solid #eee;
|
|
}
|
|
|
|
/* Header */
|
|
.app-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 8px 16px;
|
|
background: var(--app-white);
|
|
}
|
|
|
|
/* List item */
|
|
.app-list-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 12px 16px;
|
|
border-bottom: 1px solid #f5f5f5;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.app-list-item:hover {
|
|
background: #fafafa;
|
|
}
|
|
|
|
/* Avatar */
|
|
.app-avatar {
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
color: #fff;
|
|
flex-shrink: 0;
|
|
letter-spacing: -0.3px;
|
|
position: relative;
|
|
}
|
|
|
|
/* Badge */
|
|
.app-badge {
|
|
display: inline-block;
|
|
padding: 3px 10px;
|
|
font-size: 11.5px;
|
|
font-weight: 500;
|
|
border-radius: 20px;
|
|
background: #f0f0f0;
|
|
color: #666;
|
|
}
|
|
|
|
/* Section label */
|
|
.app-section-label {
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.8px;
|
|
color: #999;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
/* Tab bar */
|
|
.app-tab {
|
|
flex: 1;
|
|
padding: 10px 0;
|
|
background: none;
|
|
border: none;
|
|
border-bottom: 2.5px solid transparent;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: #999;
|
|
cursor: pointer;
|
|
text-transform: capitalize;
|
|
font-family: var(--font-app);
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.app-tab.active {
|
|
color: var(--app-accent);
|
|
border-bottom-color: var(--app-accent);
|
|
}
|
|
|
|
/* App layout — max-width for tablet portrait */
|
|
.app-container {
|
|
max-width: 768px;
|
|
margin: 0 auto;
|
|
height: 100dvh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--app-white);
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Make the screen's outer div fill the container so BottomNav sticks at bottom */
|
|
.app-container > div:first-child {
|
|
flex: 1;
|
|
min-height: 0;
|
|
}
|
|
|
|
/* Global data-query spinner (near the "Festipod" title) */
|
|
@keyframes app-spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.app-spinner {
|
|
animation: app-spin 0.8s linear infinite;
|
|
flex-shrink: 0;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
/* Online indicator on avatar */
|
|
.app-avatar .online-dot {
|
|
position: absolute;
|
|
bottom: -1px;
|
|
right: -1px;
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
background: #34C759;
|
|
border: 2.5px solid #fff;
|
|
}
|