.app-shell { display: grid; grid-template-columns: var(--sidebar-width) 1fr; min-height: 100vh; transition: grid-template-columns var(--transition-base); } .app-shell.is-collapsed { grid-template-columns: var(--sidebar-width-collapsed) 1fr; } /* Sidebar */ .sidebar { background: var(--bg-sidebar); border-right: 1px solid var(--sidebar-border); padding: var(--space-5) var(--space-3); display: flex; flex-direction: column; gap: var(--space-4); overflow-y: auto; overflow-x: hidden; /* Duração/curva própria (não var(--transition-base)) só pra entrada em portal.html vinda do login (ver .pid-entering-sidebar abaixo) — um slide precisa de mais tempo e uma curva com "chegada" suave pra ler como um movimento de verdade, não um fade genérico de microinteração. */ transition: opacity 420ms ease, transform 420ms cubic-bezier(0.16, 1, 0.3, 1); } /* Antes de access.js aplicar a visibilidade real (união de permissões, vinda de /api/me/, assíncrona), todo item de menu gated por permissão começa escondido — "fail closed". Sem isso, o menu inteiro (todas as seções/ aplicações) ficava visível por um instante em qualquer carregamento de página — inclusive pra quem não tem acesso a todas —, já que o HTML de cada shell já vem com todos os itens no DOM e só é filtrado depois que o fetch de /api/me/ resolve. `pidApplyAccessVisibility` (access.js) só marca `.sidebar` como `.is-ready` depois de já ter escondido cada item que o usuário não pode ver. */ .sidebar:not(.is-ready) [data-section], .sidebar:not(.is-ready) [data-app] { display: none !important; } .sidebar__brand { position: relative; display: flex; align-items: center; justify-content: center; height: 92px; padding: var(--space-2) 0 var(--space-3); text-decoration: none; cursor: pointer; } .sidebar__brand:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-sm); } /* Duas marcas em cima uma da outra (não uma só redimensionada) — a horizontal tem texto (ilegível encolhida), o ícone é só o símbolo quadrado. O cross-fade (opacity + scale) entre elas, em vez de trocar o `src` na hora, é o que deixa a transição expandir/recolher fluida em vez de brusca. */ .sidebar__logo { position: absolute; top: 50%; left: 50%; object-fit: contain; transition: opacity var(--transition-base), transform var(--transition-base); } .sidebar__logo--full { width: 250px; max-width: 96%; height: auto; opacity: 1; transform: translate(-50%, -50%) scale(1); } .sidebar__logo--icon { width: 40px; height: 40px; opacity: 0; transform: translate(-50%, -50%) scale(0.85); } .sidebar__nav { flex: 1; } .nav-list { display: flex; flex-direction: column; gap: var(--space-1); } .nav-item { display: flex; align-items: center; gap: var(--space-3); width: 100%; padding: var(--space-3); border-radius: var(--radius-sm); color: var(--sidebar-text-secondary); font-size: 0.88rem; font-weight: 500; white-space: nowrap; text-align: left; transition: background var(--transition-fast), color var(--transition-fast); } .nav-item svg { color: var(--sidebar-text-muted); transition: color var(--transition-fast); } .nav-item:hover { background: var(--sidebar-hover-bg); color: var(--sidebar-text-primary); } .nav-item:hover svg { color: var(--accent); } .nav-item.is-active { background: var(--accent); color: var(--accent-contrast); } .nav-item.is-active svg { color: var(--accent-contrast); } .nav-item__label { flex: 1; overflow: hidden; text-overflow: ellipsis; } .nav-item__chevron { transition: transform var(--transition-fast); color: var(--sidebar-text-muted); } .nav-group.is-open > .nav-group__toggle .nav-item__chevron { transform: rotate(90deg); } .nav-group__list { display: flex; flex-direction: column; gap: var(--space-1); padding: var(--space-1) 0 var(--space-2) var(--space-6); max-height: 0; overflow: hidden; visibility: hidden; pointer-events: none; transition: max-height var(--transition-base); } .nav-group.is-open > .nav-group__list { visibility: visible; pointer-events: auto; max-height: 340px; } .nav-subitem { display: block; padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm); color: var(--sidebar-text-muted); font-size: 0.82rem; text-align: left; } .nav-subitem:hover { background: var(--sidebar-hover-bg); color: var(--sidebar-text-primary); } .nav-subitem.is-active { color: var(--accent); font-weight: 700; } /* Subgrupo dentro de um módulo (3º nível: módulo > subgrupo > ferramenta) */ .nav-subgroup__toggle { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); width: 100%; font-weight: 700; border-bottom: 1px solid var(--sidebar-border); border-radius: 0; } .nav-subgroup.is-open > .nav-subgroup__toggle .nav-item__chevron { transform: rotate(90deg); } .nav-subgroup__list { display: flex; flex-direction: column; gap: var(--space-1); padding: var(--space-1) 0 var(--space-2) var(--space-4); max-height: 0; overflow: hidden; visibility: hidden; pointer-events: none; transition: max-height var(--transition-base); } .nav-subgroup.is-open > .nav-subgroup__list { visibility: visible; pointer-events: auto; max-height: 160px; } .nav-subitem--tool { display: flex; align-items: center; gap: var(--space-2); } .nav-subitem--tool svg { color: var(--sidebar-text-muted); flex-shrink: 0; } /* Linha com o item de navegação + botão de favoritar. padding-right aqui repõe o padding-right que o .nav-item perde visualmente ao ser encurtado por flex:1 — sem isso, o .fav-toggle fica encostado na borda da sidebar, enquanto itens sem estrela (ex.: toggles de grupo) mantêm o respiro do próprio padding do .nav-item. */ .nav-item-row { display: flex; align-items: center; gap: var(--space-1); padding-right: var(--space-3); } .nav-item-row > .nav-item, .nav-item-row > .nav-subitem { flex: 1; min-width: 0; } .fav-toggle { flex-shrink: 0; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; color: var(--sidebar-text-muted); border-radius: var(--radius-sm); transition: color var(--transition-fast), background var(--transition-fast); } .fav-toggle:hover { color: var(--gold-400); background: var(--sidebar-hover-bg); } .fav-toggle.is-favorite { color: var(--gold-400); } .fav-toggle.is-favorite svg { fill: currentColor; } .badge-restricted { font-size: 0.62rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: var(--gold-400); border: 1px solid var(--gold-400); border-radius: var(--radius-pill); padding: 1px 6px; } /* Sidebar colapsada: esconde texto, mantém ícones */ .app-shell.is-collapsed .nav-item__label, .app-shell.is-collapsed .nav-item__chevron, .app-shell.is-collapsed .nav-group__list, .app-shell.is-collapsed .badge-restricted, .app-shell.is-collapsed .fav-toggle { display: none; } .app-shell.is-collapsed .nav-item { justify-content: center; } .app-shell.is-collapsed .sidebar__logo--full { opacity: 0; transform: translate(-50%, -50%) scale(0.85); } .app-shell.is-collapsed .sidebar__logo--icon { opacity: 1; transform: translate(-50%, -50%) scale(1); } .app-shell.is-collapsed .nav-item-row { padding-right: 0; } /* Tooltip com o nome da ferramenta, mostrado ao passar o mouse sobre um ícone da sidebar colapsada. Fica em document.body (não dentro de .sidebar, que tem overflow-x:hidden e cortaria um elemento posicionado fora da faixa da sidebar), posicionado via JS com getBoundingClientRect. */ .sidebar-tooltip { position: fixed; transform: translateY(-50%); z-index: 60; padding: var(--space-1) var(--space-3); background: var(--bg-sidebar); color: var(--sidebar-text-primary); border: 1px solid var(--sidebar-border); border-radius: var(--radius-sm); font-size: 0.82rem; font-weight: 500; white-space: nowrap; box-shadow: var(--shadow-md); pointer-events: none; animation: pidFadeIn var(--transition-fast); } /* Topbar */ .topbar { height: var(--topbar-height); background: var(--bg-topbar); border-bottom: 1px solid var(--border-subtle); display: flex; align-items: center; gap: var(--space-4); padding: 0 var(--space-5); position: sticky; top: 0; z-index: 20; } .topbar__title { font-size: 1rem; font-weight: 700; letter-spacing: 0.03em; color: var(--text-primary); } .topbar__actions { margin-left: auto; display: flex; align-items: center; gap: var(--space-2); } .main-content { /* `isolation:isolate` garante um contexto de empilhamento próprio — sem isso, `position:relative` sozinho (z-index auto) não isola nada, e a teia de aranha sazonal (z-index:-1, ver halloween-cobweb.css) escaparia pro contexto raiz da página em vez de ficar só atrás do conteúdo desta área. Mesmo raciocínio de `isolation` em `.login-page` (login.css). */ position: relative; isolation: isolate; min-width: 0; transition: opacity var(--transition-base); } /* Ligado só em portal.html, só quando se chega ali logo depois da animação de intro do login (ver portal-reveal.js) — duas classes, removidas em sequência pelo JS: primeiro a sidebar desliza de fora da tela (esquerda pra direita, `translateX(-100%)` — toda a largura dela, não um deslize sutil de poucos pixels, pra realmente ler como "a barra lateral surgindo" em vez de só um fade) até a posição final; só depois o resto do app aparece num fade (`pid-entering`), em vez das duas coisas surgirem ao mesmo tempo. */ html.pid-entering-sidebar .sidebar { opacity: 0; transform: translateX(-100%); } html.pid-entering .main-content { opacity: 0; } .page-content { padding: var(--space-5); max-width: 1200px; margin: 0 auto; animation: pidFadeSlideUp 180ms ease; } /* Telas de aplicação (tudo exceto o "Principal"/portal.html) aproveitam melhor o espaço entre a sidebar e a borda da tela — a home continua com o limite padrão de 1200px, mais confortável de leitura pra uma grade de favoritos. */ .page-content.page-content--wide { max-width: 1600px; } @media (max-width: 1024px) { .app-shell { grid-template-columns: var(--sidebar-width-collapsed) 1fr; } .app-shell .nav-item__label, .app-shell .nav-item__chevron, .app-shell .nav-group__list, .app-shell .badge-restricted, .app-shell .fav-toggle { display: none; } .app-shell .nav-item { justify-content: center; } .app-shell .nav-item-row { padding-right: 0; } .app-shell .sidebar__logo--full { opacity: 0; transform: translate(-50%, -50%) scale(0.85); } .app-shell .sidebar__logo--icon { opacity: 1; transform: translate(-50%, -50%) scale(1); } .app-shell.is-expanded-mobile { grid-template-columns: var(--sidebar-width) 1fr; } .app-shell.is-expanded-mobile .sidebar__logo--full { opacity: 1; transform: translate(-50%, -50%) scale(1); } .app-shell.is-expanded-mobile .sidebar__logo--icon { opacity: 0; transform: translate(-50%, -50%) scale(0.85); } .app-shell.is-expanded-mobile .nav-item__label, .app-shell.is-expanded-mobile .nav-item__chevron, .app-shell.is-expanded-mobile .badge-restricted { display: inline; } .app-shell.is-expanded-mobile .fav-toggle { display: flex; } .app-shell.is-expanded-mobile .nav-item-row { padding-right: var(--space-3); } .app-shell.is-expanded-mobile .nav-group.is-open .nav-group__list { display: flex; } }