Documentação de código
Como o Growth Playbook foi construído
Breakdown completo de cada parte: HTML estrutural, sistema de CSS com variáveis, grain overlay, sidebar responsiva, hero animado, cards de setor, modal com animação, JavaScript de renderização e manipulação dinâmica de tema.
1
Arquivo HTML único
3
Linguagens: HTML, CSS, JS
30
Setores com dados
11
Funções JavaScript
4
Breakpoints responsivos
0
Bibliotecas externas
Arquitetura do arquivo Conceito
O arquivo é um único HTML que contém tudo inline — CSS, dados JSON e JavaScript — exceto as fontes do Google. Isso garante que funciona offline após o primeiro carregamento.
<head> + fontes
→
<style> CSS
→
<body> HTML
→
<script> PLAYBOOK data
→
<script> App JS
Mapa dos componentes
ComponenteTécnica principalSeção deste guia
Grain overlaySVG feTurbulence via CSS background-image✦ Grain
Layout raizCSS Flexbox com sidebar sticky📐 Layout
Sidebar responsivaposition:sticky → flex-row em mobile◧ Sidebar
Hero com glowradial-gradient absoluto + CSS vars dinâmicas⬛ Hero
Cards de setorCSS Grid + animação escalonada com delay▦ Cards
Modalposition:fixed + createElement + animação CSS◻ Modal
Troca de tema por empresaJS setCSSVar() muda --acc em runtime✦ Dynamic CSS
Fade de conteúdoClasse .fading + setTimeout(render, 140ms)⟳ JS Render
Score barsdiv com width% calculado em JS⊡ JS Modal
Dados (30 setores)Objeto JS const PLAYBOOK injetado no HTML📦 JS Data
Estrutura HTML HTML
O HTML é deliberadamente minimalista. Quase todo o conteúdo é gerado pelo JavaScript. O HTML só define os containers vazios que o JS vai preencher.
1. Head — Fontes e meta
HTML
<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <title>Growth Playbook 2026</title>

  <!-- Fonte via Google Fonts (requer internet) -->
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,300;12..96,500;12..96,700&family=DM+Mono:wght@400;500&family=Plus+Jakarta+Sans:wght@300;400;500&display=swap" rel="stylesheet">
</head>
1
preconnect
Abre a conexão TCP com o Google Fonts antes de precisar baixar os arquivos — reduz latência de carregamento da fonte.
2
3 fontes em 1 request
Bricolage Grotesque (display/títulos), DM Mono (dados e código), Plus Jakarta Sans (corpo). Um único link carrega as 3 — o Google Fonts agrupa no mesmo request.
3
opsz (optical size)
O parâmetro 12..96 no Bricolage ativa o eixo de tamanho óptico — a fonte ajusta seus detalhes automaticamente conforme o tamanho de renderização.
2. Body — Containers de layout
Apenas os elementos raiz são declarados. O conteúdo de #co-tabs, #metrics, #content etc. é injetado pelo JavaScript.
HTML
<body>
  <div class="layout">

    <!-- SIDEBAR: menu de empresas -->
    <aside class="sidebar">
      <div class="sb-logo"><span>Growth</span> Playbook</div>
      <div id="co-tabs"></div>  <!-- preenchido por renderCompanyTabs() -->
      <div class="sb-foot">6 empresas · 30 setores</div>
    </aside>

    <!-- MAIN: todo o conteúdo principal -->
    <div class="main" id="main-panel">

      <!-- HERO: nome, métricas, estratégia da empresa -->
      <div class="hero" id="hero">
        <div class="hero-glow" id="hero-glow"></div>
        <div class="hero-period" id="hero-period"></div>
        <div class="hero-name" id="hero-name"></div>
        <div class="hero-tag" id="hero-tag"></div>
        <div class="metrics" id="metrics"></div>
        <div class="strat-box">
          <div class="strat-lbl">Estratégia central</div>
          <div class="strat-txt" id="strat"></div>
        </div>
      </div>

      <!-- TAB BAR: alterna entre "setores" e "plano" -->
      <div class="tab-bar">
        <button class="tab" id="tab-steps" onclick="switchTab('steps')">
          Plano da estratégia <span class="tab-badge">8</span>
        </button>
        <button class="tab on" id="tab-sec" onclick="switchTab('sectors')">
          5 setores aplicados <span class="tab-badge">5</span>
        </button>
      </div>

      <!-- CONTENT: área dinâmica preenchida por _render() -->
      <div class="content" id="content"></div>

    </div> <!-- .main -->
  </div> <!-- .layout -->
</body>
1
HTML é só o esqueleto
Todos os id="" são alvos do JavaScript. O conteúdo real (nomes, textos, cards, steps) não existe no HTML — é gerado dinamicamente por renderHero(), _render(), renderCompanyTabs().
2
aside vs div
Usamos <aside> para a sidebar porque semanticamente ela é conteúdo complementar ao conteúdo principal — melhor para acessibilidade e SEO.
3
onclick inline vs addEventListener
Usamos onclick inline nas tabs e botões por simplicidade — o projeto tem escopo pequeno. Para projetos maiores, addEventListener separado é melhor prática.
CSS Custom Properties CSS
Todas as cores, espaçamentos e fontes são definidas como variáveis no :root. Isso permite que o JavaScript mude o tema inteiro mudando apenas --acc.
CSS
/* RESET: aplica box-sizing em tudo */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* DESIGN TOKENS: toda a paleta em um lugar só */
:root {
  /* Backgrounds */
  --bg:    #050c18;   /* fundo da página (navy mais escuro) */
  --surf:  #080f1e;   /* superfície de sidebar e tab bar */
  --card:  #0d1626;   /* cards e steps */
  --card2: #111e33;   /* hover dos cards, badges */

  /* Borders: usam rgba para transparência */
  --b0: rgba(255,255,255,0.05);  /* borda sutil */
  --b1: rgba(255,255,255,0.09);  /* borda visível */
  --b2: rgba(255,255,255,0.15);  /* borda de hover */

  /* Texto */
  --t1: #eef2ff;  /* texto principal (quase branco) */
  --t2: #8892a8;  /* texto secundário (cinza médio) */
  --t3: #3d4a60;  /* texto terciário (cinza escuro) */

  /* Accent: esta variável é TROCADA pelo JS a cada empresa */
  --acc:   #f59e0b;                  /* cor de acento ativa */
  --acc-d: rgba(245,158,11,0.12);   /* acento com transparência */
  --acc-g: rgba(245,158,11,0.05);   /* acento muito sutil (fundo) */

  /* Pros e Cons */
  --pro:   #10b981;                  /* verde para vantagens */
  --pro-d: rgba(16,185,129,0.10);   /* fundo verde */
  --con:   #ef4444;                  /* vermelho para desvantagens */
  --con-d: rgba(239,68,68,0.10);    /* fundo vermelho */

  /* Bordas arredondadas */
  --r:  10px;  /* radius padrão (cards grandes) */
  --rs: 6px;   /* radius pequeno (badges, inputs) */

  /* Sidebar width: usada em calc() e media queries */
  --sb: 230px;

  /* Fontes */
  --font-d: 'Bricolage Grotesque', system-ui, sans-serif;
  --font-b: 'Plus Jakarta Sans', system-ui, sans-serif;
  --font-m: 'DM Mono', monospace;
}
1
Por que --acc é separada?
Porque o JavaScript muda --acc, --acc-d e --acc-g toda vez que o usuário troca de empresa. Isso propaga automaticamente para todos os elementos que usam essa variável — hero glow, tab ativa, badges, botões ativos, score bars.
2
Cores com rgba para bordas
Em vez de #ffffff10 (hexadecimal com alpha), usamos rgba(255,255,255,0.05). Ambos funcionam, mas rgba é mais legível para quem está aprendendo — deixa explícito que é branco com 5% de opacidade.
Grain Overlay (textura de película) CSS
O efeito de grain (grão/ruído visual) é gerado por um filtro SVG embutido como base64 no CSS. Nenhuma imagem externa é necessária.
CSS
/* O grain é um pseudo-elemento :before no body */
/* Ele fica fixed (cobre a tela inteira) com z-index alto */
/* pointer-events:none para não bloquear cliques */
body::before {
  content: '';
  position: fixed;
  inset: 0;           /* top:0; right:0; bottom:0; left:0 */
  pointer-events: none;
  z-index: 9998;

  /* SVG inline como data URL — sem arquivo externo */
  background-image: url(
    "data:image/svg+xml,
    %3Csvg xmlns='http://www.w3.org/2000/svg'
           width='300' height='300'%3E
      %3Cfilter id='n'%3E
        %3CfeTurbulence
          type='fractalNoise'
          baseFrequency='.75'
          numOctaves='4'
          stitchTiles='stitch'/%3E
      %3C/filter%3E
      %3Crect width='300' height='300'
             filter='url(%23n)'
             opacity='.06'/%3E
    %3C/svg%3E"
  );
}
1
feTurbulence
feTurbulence é um filtro SVG nativo que gera ruído fractal. baseFrequency='.75' controla a granularidade — valores maiores = grão mais fino. numOctaves='4' controla a complexidade do padrão.
2
%3C e %23 são URL encoding
%3C = <, %3E = >, %23 = #. O SVG precisa ser URL-encoded para funcionar dentro de uma string CSS. Você pode usar ferramentas online de URL encode para SVG.
3
opacity='.06'
A opacidade do grain. .06 é muito sutil — visível mas não distrai. Valores entre .04 e .12 são os mais usados. Acima de .15 começa a parecer sujo.
4
stitchTiles='stitch'
Faz o padrão se repetir de forma contínua (sem borda visível na repetição). Sem isso, haveria uma grade visível no background.
Layout raiz — Sidebar + Main CSS
O layout usa Flexbox. A sidebar tem largura fixa e fica sticky (grudada no scroll). O main ocupa o resto (flex:1). Em mobile, vira coluna com sidebar horizontal no topo.
CSS
/* Container raiz: flex row em desktop */
.layout {
  display: flex;
  min-height: 100vh;
}

/* SIDEBAR: coluna esquerda fixa */
.sidebar {
  width: var(--sb);       /* 230px */
  min-width: var(--sb);   /* não encolhe quando o main precisar de espaço */
  background: var(--surf);
  border-right: 1px solid var(--b0);
  display: flex;
  flex-direction: column;
  height: 100vh;
  position: sticky;  /* gruda no topo do viewport ao rolar */
  top: 0;
  overflow-y: auto;  /* scroll interno se houver muitos itens */
  padding: 28px 14px 20px;
  gap: 6px;
}

/* MAIN: ocupa todo o espaço restante */
.main {
  flex: 1;          /* cresce para preencher o espaço */
  min-width: 0;    /* impede overflow em flex items */
  height: 100vh;
  overflow-y: auto;  /* scroll independente do sidebar */
  display: flex;
  flex-direction: column;
}
1
position: sticky na sidebar
Com position:sticky; top:0; height:100vh, a sidebar fica visível enquanto o usuário rola o conteúdo principal. O overflow-y:auto no main permite que ele role independentemente.
2
min-width:0 no .main
Sem isso, um flex item com flex:1 pode causar overflow horizontal quando o conteúdo interno for muito largo. min-width:0 corrige esse bug clássico do Flexbox.
3
Dois scrolls independentes
A sidebar tem overflow-y:auto próprio e o main tem o seu. Isso permite que a sidebar role separadamente do conteúdo — útil quando há muitas empresas no menu.
Sidebar — Botões de empresa CSS
Os botões da sidebar têm três estados: default, hover e active. O estado active usa uma borda esquerda colorida via ::before.
CSS
.co-btn {
  background: transparent;
  border: none;
  border-radius: var(--rs);
  padding: 10px 10px;
  text-align: left;
  cursor: pointer;
  color: var(--t2);
  font-family: var(--font-b);
  font-size: 13px;
  transition: all .18s;
  display: flex;
  align-items: center;
  gap: 10px;
  position: relative;  /* necessário para o ::before funcionar */
  width: 100%;
}

.co-btn:hover {
  background: var(--card);
  color: var(--t1);
}

.co-btn.active {
  background: var(--acc-d);   /* fundo com cor da empresa */
  color: var(--acc);           /* texto com cor da empresa */
  font-weight: 500;
}

/* LINHA VERTICAL ESQUERDA: o indicador de item ativo */
.co-btn.active::before {
  content: '';
  position: absolute;
  left: 0;
  top: 25%;       /* não vai do topo ao fundo — fica centrada */
  bottom: 25%;
  width: 2.5px;
  background: var(--acc);
  border-radius: 2px;
}

/* DOT: indicador visual pequeno no início de cada item */
.co-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--t3);  /* cinza escuro no default */
  flex-shrink: 0;
  transition: background .18s;
}
.co-btn.active .co-dot {
  background: var(--acc);  /* muda de cor junto com o estado ativo */
}
1
Indicador via ::before
A linha vertical esquerda do item ativo não é um elemento HTML — é o pseudo-elemento ::before. Isso mantém o HTML limpo e a lógica de estilo toda no CSS. Só funciona porque o botão tem position:relative.
2
top:25% bottom:25%
Em vez de top:0; bottom:0 (linha do topo ao fundo), usamos 25%–25% para que a linha fique centrada e mais elegante, sem tocar as bordas do botão.
3
transition: all .18s
A transição cobre todas as propriedades que mudam no hover e no active. Mais eficiente especificar as propriedades exatas (background .18s, color .18s), mas all é mais rápido de escrever para protótipos.
Hero — Glow e tipografia CSS
O glow é um div absoluto com radial-gradient e border-radius:50%. O JS muda sua cor a cada empresa. A tipografia usa clamp() para escalar automaticamente.
CSS
.hero {
  padding: 40px 44px 32px;
  border-bottom: 1px solid var(--b0);
  position: relative;   /* necessário para o glow funcionar com absolute */
  overflow: hidden;     /* corta o glow que sai da borda */
}

/* GLOW: círculo radial com a cor da empresa */
.hero-glow {
  position: absolute;
  top: -80px;   /* parcialmente fora da hero */
  right: -80px;
  width: 380px;
  height: 380px;
  border-radius: 50%;
  /* background é definido pelo JS: */
  /* background: radial-gradient(circle, COR 0%, transparent 70%) */
  opacity: .05;
  pointer-events: none;
  transition: background .4s;  /* transição suave ao trocar empresa */
}

/* EYEBROW: label de período acima do nome */
.hero-period {
  font-family: var(--font-m);
  font-size: 10px;
  color: var(--acc);
  letter-spacing: .12em;
  text-transform: uppercase;
  margin-bottom: 10px;
  display: flex;
  align-items: center;
  gap: 7px;
}
.hero-period::before {
  content: '▸';  /* símbolo decorativo via CSS puro */
  opacity: .7;
}

/* NOME: tamanho responsivo com clamp() */
.hero-name {
  font-family: var(--font-d);

  /* clamp(min, preferred, max)               */
  /* mínimo: 36px (mobile)                     */
  /* preferido: 5vw (5% da largura da tela)    */
  /* máximo: 58px (desktop grande)             */
  font-size: clamp(36px, 5vw, 58px);

  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -.02em;  /* tracking negativo em títulos grandes */
  margin-bottom: 6px;
  color: var(--t1);
}

/* MÉTRICAS: container flex com wrap */
.metrics { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 20px; }
.met {
  background: var(--card);
  border: 1px solid var(--b0);
  border-radius: var(--rs);
  padding: 11px 16px;
  min-width: 100px;
}
.met-l { font-size: 10px; color: var(--t3); font-family: var(--font-m); margin-bottom: 4px; }
.met-v { font-size: 17px; font-weight: 500; font-family: var(--font-m); }
1
clamp() é tipografia responsiva
clamp(36px, 5vw, 58px) significa: nunca menor que 36px, nunca maior que 58px, mas tenta ser 5% da largura da tela. Elimina media queries só para ajustar tamanho de texto.
2
overflow:hidden no .hero
O glow tem top:-80px e right:-80px, então vai além das bordas do hero. overflow:hidden garante que esse excesso não cause scroll horizontal na página.
3
letter-spacing:-.02em
Tracking negativo em títulos grandes melhora a legibilidade e dá aparência mais profissional. Só funciona bem em fontes com kerning bem feito — Bricolage Grotesque suporta isso.
Cards de Setor — Grid + hover lift CSS
O grid usa auto-fill com minmax para ser responsivo sem media queries. O hover lift usa transform:translateY + box-shadow. A animação de entrada é escalonada via animation-delay.
CSS
/* GRID: auto-fill com minmax = responsivo sem media query */
.sec-grid {
  display: grid;
  /* auto-fill: preenche com quantas colunas couberem    */
  /* minmax(240px, 1fr): mínimo 240px, máximo proporcional */
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 14px;
}

/* CARD BASE */
.sec-card {
  background: var(--card);
  border: 1px solid var(--b0);
  border-radius: var(--r);
  padding: 20px;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 8px;

  /* Animação: ativa no load com delay escalonado */
  animation: fadeIn .35s ease both;

  /* Transições suaves para hover */
  transition:
    border-color .2s,
    transform .2s,
    background .2s,
    box-shadow .2s;
}

/* HOVER LIFT EFFECT */
.sec-card:hover {
  border-color: var(--b2);          /* borda mais visível */
  background: var(--card2);         /* fundo mais claro */
  transform: translateY(-3px);      /* "levantamento" */
  box-shadow: 0 8px 32px rgba(0,0,0,.4);  /* sombra embaixo */
}

/* ANIMAÇÃO DE ENTRADA ESCALONADA */
/* Cada card entra com delay diferente, criando efeito cascata */
.sec-card:nth-child(1) { animation-delay: .04s; }
.sec-card:nth-child(2) { animation-delay: .09s; }
.sec-card:nth-child(3) { animation-delay: .14s; }
.sec-card:nth-child(4) { animation-delay: .19s; }
.sec-card:nth-child(5) { animation-delay: .24s; }

/* KEYFRAME: fadeIn usado em cards E nos steps do modal */
@keyframes fadeIn {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* CTA dentro do card: seta que move no hover */
.sc-cta::after {
  content: '→';
  transition: transform .2s;
}
.sec-card:hover .sc-cta::after {
  transform: translateX(4px);  /* seta se move para a direita no hover */
}

/* FADE DO CONTEÚDO na troca de empresa */
.content.fading {
  animation: fadeOut .14s ease forwards;
}
@keyframes fadeOut {
  to { opacity: 0; transform: translateY(-5px); }
}
1
auto-fill vs auto-fit
auto-fill mantém colunas vazias se houver espaço — o grid preserva o tamanho das colunas. auto-fit colapsa colunas vazias e expande as preenchidas. Para cards, auto-fill é mais previsível.
2
animation: fadeIn .35s ease both
both aplica o estado inicial do keyframe antes da animação começar (prevenindo flash) e mantém o estado final depois (prevenindo resetar para invisível).
3
Efeito de cascata com nth-child
Cada card tem um delay diferente via nth-child. O incremento de .05s entre cards é intencional — pequeno o suficiente para ser sutil, grande o suficiente para ser percebido.
Modal — Sistema de overlay CSS
O modal usa position:fixed com inset:0 para cobrir toda a tela. A animação de entrada usa cubic-bezier com overshoot (spring effect). Em mobile, o modal sobe de baixo como um bottom sheet.
CSS
/* OVERLAY: cobre toda a tela */
.modal-bg {
  position: fixed;
  inset: 0;                          /* top/right/bottom/left: 0 */
  background: rgba(5,12,24,.88);    /* semi-transparente */
  z-index: 200;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 20px;
  overflow-y: auto;
  animation: bgIn .2s ease;
}

@keyframes bgIn { from { opacity: 0; } to { opacity: 1; } }

/* Classe adicionada pelo JS ao fechar — anima para fora */
.modal-bg.out {
  animation: bgOut .18s ease forwards;
}
@keyframes bgOut { to { opacity: 0; } }

/* MODAL: o card branco/escuro dentro do overlay */
.modal {
  background: var(--surf);
  border: 1px solid var(--b1);
  border-radius: var(--r);
  width: 100%;
  max-width: 860px;
  margin: auto;           /* centraliza verticalmente no flex */
  position: relative;
  overflow: hidden;       /* corta o glow interno */

  /* SPRING ANIMATION: cubic-bezier com overshoot */
  animation: modalIn .25s cubic-bezier(.34, 1.56, .64, 1);
}

@keyframes modalIn {
  from { opacity: 0; transform: translateY(20px) scale(.97); }
  to   { opacity: 1; transform: none; }
}

/* SCORE BARS: barras de progresso dentro do modal */
.score-bar {
  height: 4px;
  background: var(--b1);  /* trilho cinza */
  border-radius: 2px;
  overflow: hidden;
  margin-bottom: 4px;
}
.score-fill {
  height: 100%;
  border-radius: 2px;
  transition: width .6s ease;  /* animação da barra ao abrir */
  /* width e background são definidos inline pelo JS */
  /* Ex: style="width:80%;background:#10b981"       */
}

/* MOBILE: modal vira bottom sheet */
@media(max-width:639px) {
  .modal-bg {
    align-items: flex-end;  /* empurra para baixo */
    padding: 0;
  }
  .modal {
    /* arredondamento só no topo — sem bordas embaixo */
    border-radius: var(--r) var(--r) 0 0;
    margin-top: auto;
    margin-bottom: 0;
  }
}
1
cubic-bezier(.34, 1.56, .64, 1)
Esse é um spring bezier. O valor 1.56 ultrapassa 1.0 no eixo Y — isso cria o "overshoot" (o modal vai um pouco além e volta). É o efeito de mola que dá personalidade à animação. Sem isso, seria linear e sem graça.
2
Por que usar .out em vez de remover direto?
Se removermos o elemento imediatamente, a animação de saída nunca acontece. A solução: adicionar a classe .out (que anima para opacidade 0), aguardar 180ms com setTimeout, e só então remover o elemento do DOM.
3
score-fill com inline style
A largura da barra (width:80%) e a cor (background:#10b981) são definidas pelo JavaScript via style inline no HTML gerado. CSS não consegue calcular isso com dados dinâmicos sem JS.
Breakpoints responsivos CSS
4 breakpoints cobrem todos os tamanhos: TV (>1920px), Notebook (1024–1920px), Tablet (640–1023px) e Mobile (<640px). Em tablet e mobile, a sidebar vira uma barra horizontal no topo.
CSS
/* TV (>1920px): mais espaço, mais colunas */
@media(min-width:1921px) {
  :root { --sb: 280px; }         /* sidebar mais larga */
  body  { font-size: 16px; }
  .hero { padding: 50px 56px 40px; }
  .hero-name { font-size: 70px; }
  .sec-grid { grid-template-columns: repeat(4, 1fr); }
  .modal { max-width: 1100px; }
  .modal-body { grid-template-columns: 1fr 1fr; }
}

/* Notebook/Desktop: modal body em 2 colunas */
@media(min-width:1024px) and (max-width:1920px) {
  .modal-body { grid-template-columns: 1fr 1fr; }
}

/* Tablet (<1024px): sidebar vira barra horizontal */
@media(max-width:1023px) {

  /* Layout: coluna em vez de linha */
  .layout { flex-direction: column; }

  /* Sidebar: vira barra horizontal */
  .sidebar {
    width: 100%;
    min-width: 0;
    height: auto;
    flex-direction: row;       /* botões em linha */
    flex-wrap: nowrap;
    padding: 10px 12px;
    border-right: none;
    border-bottom: 1px solid var(--b0);
    overflow-x: auto;          /* scroll horizontal nos botões */
    overflow-y: hidden;
    position: sticky;
    top: 0;
    z-index: 100;
  }

  /* Esconde elementos que não cabem na barra horizontal */
  .sb-logo, .sb-foot { display: none; }
  .co-yr { display: none; }   /* remove anos dos botões */

  /* Indicador ativo: muda de linha vertical para underline */
  .co-btn.active::before {
    top: auto;
    bottom: 0;
    left: 20%;
    right: 20%;
    width: auto;
    height: 2.5px;   /* vira linha embaixo do botão */
  }

  .main { height: auto; }     /* main não tem mais height fixo */
  .hero { padding: 24px 20px 20px; }
  .tab-bar, .content { padding-left: 20px; padding-right: 20px; }
}

/* Mobile (<640px): compactar tudo */
@media(max-width:639px) {
  .hero-tag  { font-size: 13px; }
  .met-v    { font-size: 14px; }
  .metrics  { gap: 8px; }
  .met      { min-width: 90px; padding: 9px 12px; }
  .sec-grid { grid-template-columns: 1fr; }  /* uma coluna */
  .pc-grid  { grid-template-columns: 1fr; }  /* pros/cons: uma coluna */
}
1
Mobile-first vs Desktop-first
Este projeto usa desktop-first (min-width para TV/notebook, max-width para tablet/mobile). Para novos projetos, a recomendação moderna é mobile-first (min-width em todos os breakpoints).
2
Sidebar horizontal no tablet
Em vez de esconder a sidebar, ela vira uma barra horizontal com scroll interno (overflow-x:auto). O indicador ativo muda de linha vertical para underline — a mesma lógica CSS, só as dimensões mudam.
3
:root dentro de @media
Você pode redefinir variáveis CSS dentro de media queries. No breakpoint de TV, mudamos --sb: 280px no :root — todos os lugares que usam essa variável automaticamente atualizam.
JavaScript — Estrutura de dados JS
Os dados de todas as 30 aplicações são um objeto JavaScript declarado na primeira tag <script>. A segunda tag JS (app) lê esse objeto via a variável global PLAYBOOK.
JS — Estrutura do objeto de dados
// PRIMEIRO <script>: só dados
const PLAYBOOK = {
  companies: [

    // Cada empresa é um objeto com esses campos:
    {
      name: "Instagram",
      strategy: "Foco Radical — Um recurso perfeito",
      color: "#e1306c",   // cor de acento usada no glow e theme
      emoji: "📸",

      // Array de setores (5 por empresa = 30 total)
      sectors: [
        {
          icon: "🩺",
          setor: "Saúde",
          title: "App de dermatologia só por foto",
          desc: "Descrição curta para o card",
          difficulty: 4,   // 1–5, usado nas score bars
          potential: 5,    // 1–5, usado nas score bars
          time: "12–18 meses",

          // 8 passos de execução
          steps: [
            { t: "Título do passo", d: "Descrição detalhada..." },
            // ... mais 7 passos
          ],

          // 4 vantagens
          pros: [
            { t: "Título", d: "Descrição..." },
            // ... mais 3
          ],

          // 4 desvantagens
          cons: [
            { t: "Título", d: "Descrição..." },
            // ... mais 3
          ]
        },
        // ... mais 4 setores
      ]
    },
    // ... mais 5 empresas (WhatsApp, Uber, Airbnb, ChatGPT, Lovable)
  ]
};
1
Por que separar dados do app JS?
O objeto PLAYBOOK está em uma tag <script> separada. Isso facilita substituir os dados (trocar o JSON) sem tocar na lógica do app. É a separação de dados e lógica no JavaScript.
2
Chaves curtas: t e d
Os objetos de steps, pros e cons usam t (title) e d (description) em vez de nomes completos. Com 240 steps × 2 chaves, isso economiza ~2KB no arquivo final.
3
color por empresa
Cada empresa tem uma cor hexadecimal. O JS usa essa cor em três lugares: no glow do hero (radial-gradient), na variável CSS --acc e na variável derivada --acc-d (com rgba).
JavaScript — Funções de render JS
O app JS tem 11 funções. As principais são: renderCompanyTabs(), renderHero(), renderContent() e _render(). Cada mudança de empresa chama essas 3 em sequência.
JS — SEGUNDO <script>: toda a lógica
// Referências aos elementos DOM — só buscados uma vez
const CO          = PLAYBOOK.companies;   // array de empresas
let   ci          = 0;    // índice da empresa ativa
let   tab         = 'sectors';   // tab ativa: 'sectors' ou 'steps'

const co_tabs     = document.getElementById('co-tabs');
const hero_glow   = document.getElementById('hero-glow');
const hero_name   = document.getElementById('hero-name');
const metrics_el  = document.getElementById('metrics');
const content_el  = document.getElementById('content');

// ---- HELPERS ----

// Muda uma CSS Custom Property no :root via JS
function setCSSVar(k, v) {
  document.documentElement.style.setProperty(k, v);
}

// Converte #e1306c → "225,48,108" para usar em rgba()
function hexToRGB(hex) {
  const r = parseInt(hex.slice(1, 3), 16);
  const g = parseInt(hex.slice(3, 5), 16);
  const b = parseInt(hex.slice(5, 7), 16);
  return `${r},${g},${b}`;
}

// ---- RENDERIZAÇÃO ----

function renderCompanyTabs() {
  // Gera os botões da sidebar com template literal
  co_tabs.innerHTML = CO.map((c, i) => `
    <button
      class="co-btn ${i === ci ? 'active' : ''}"
      onclick="selectCo(${i})">
      <span class="co-dot"></span>
      ${c.name}
      <span class="co-yr">${YEARS[c.name] || ''}</span>
    </button>`).join('');
}

function renderHero() {
  const c = CO[ci];

  // 1. Muda o glow visual
  hero_glow.style.background =
    `radial-gradient(circle, ${c.color} 0%, transparent 70%)`;

  // 2. Muda o tema global via CSS vars
  setCSSVar('--acc', c.color);
  const rgb = hexToRGB(c.color);
  setCSSVar('--acc-d', `rgba(${rgb},.12)`);
  setCSSVar('--acc-g', `rgba(${rgb},.05)`);

  // 3. Preenche os textos
  hero_name.textContent = c.name;
  metrics_el.innerHTML  = METRICS[c.name].map(m => `
    <div class="met">
      <div class="met-l">${m.l}</div>
      <div class="met-v">${m.v}</div>
    </div>`).join('');
}

// Fade out → 140ms → render novo conteúdo
function renderContent(animate = true) {
  if (animate) {
    content_el.classList.add('fading');
    setTimeout(() => {
      content_el.classList.remove('fading');
      _render();
    }, 140);
  } else {
    _render();   // sem animação no load inicial
  }
}

// Render real do conteúdo (chamado após o fade)
function _render() {
  const c = CO[ci];
  if (tab === 'sectors') {
    // Grid de cards de setor
    content_el.innerHTML = `<div class="sec-grid">
      ${c.sectors.map((s, i) => `
        <div class="sec-card" onclick="openModal(${i})">
          <div class="sc-icon">${s.icon}</div>
          <div class="sc-label">${s.setor}</div>
          <div class="sc-title">${s.title}</div>
          <div class="sc-desc">${s.desc}</div>
          <div class="sc-cta">Ver plano completo</div>
        </div>
      `).join('')}
    </div>`;
  } else {
    // Lista de 8 steps genéricos da estratégia
    content_el.innerHTML = `<div class="steps-list">...</div>`;
  }
}

// Troca de tab (sectors ↔ steps)
function switchTab(t) {
  tab = t;
  document.getElementById('tab-sec').classList.toggle('on', t === 'sectors');
  document.getElementById('tab-steps').classList.toggle('on', t === 'steps');
  renderContent();
}

// Seleciona empresa: atualiza estado, re-renderiza tudo
function selectCo(i) {
  ci = i;
  renderCompanyTabs();
  renderHero();
  renderContent();
  document.querySelector('.main').scrollTo({ top: 0, behavior: 'smooth' });
}

// INIT: carrega sem animação (a página já aparece pronta)
renderCompanyTabs();
renderHero();
renderContent(false);
1
Template literals para HTML dinâmico
A sintaxe `string ${expressão}` permite criar HTML com dados embutidos. .map().join('') transforma um array de objetos em uma string de HTML — esse é o padrão central do projeto.
2
Fade com setTimeout de 140ms
O CSS anima o fade out em 140ms. O JS aguarda exatamente 140ms com setTimeout antes de injetar o novo conteúdo. Se o JS renderizar antes do fade terminar, o usuário vê o conteúdo novo aparecer enquanto ainda está saindo.
3
classList.toggle com valor booleano
classList.toggle('on', condicao) é mais elegante que if/else com add/remove. Se a condição for true, adiciona a classe. Se false, remove. Um método substitui dois.
JavaScript — Sistema de Modal JS
O modal é criado dinamicamente via createElement e innerHTML. Ao fechar, a classe .out é adicionada primeiro (animação de saída), e o elemento só é removido após 180ms.
JS
function openModal(si) {  // si = índice do setor
  const c = CO[ci];   // empresa atual
  const s = c.sectors[si];  // setor clicado

  // Calcula as porcentagens das score bars
  const diffPct   = (s.difficulty / 5 * 100) + '%';
  const potPct    = (s.potential  / 5 * 100) + '%';

  // Cor da barra baseada no valor
  const diffColor = s.difficulty >= 4 ? '#ef4444'   // difícil = vermelho
                  : s.difficulty >= 3 ? '#f59e0b'   // médio = laranja
                  : '#10b981';                        // fácil = verde

  // Gera HTML dos steps com animação escalonada
  const stepsHtml = s.steps.map((st, i) => `
    <div class="step" style="animation:fadeIn .3s ease ${i*0.05}s both">
      <div class="step-n">${String(i+1).padStart(2,'0')}</div>
      <div>
        <div class="step-t">${st.t}</div>
        <div class="step-d">${st.d}</div>
      </div>
    </div>`).join('');

  // Gera HTML dos pros (vantagens)
  const prosHtml = s.pros.map(p => `
    <div class="pc-card pro">
      <div class="pc-icon">✓</div>
      <div class="pc-t">${p.t}</div>
      <div class="pc-d">${p.d}</div>
    </div>`).join('');

  // Gera HTML dos cons (desvantagens)
  const consHtml = s.cons.map(p => `
    <div class="pc-card con">
      <div class="pc-icon">✕</div>
      <div class="pc-t">${p.t}</div>
      <div class="pc-d">${p.d}</div>
    </div>`).join('');

  // Monta o HTML completo do modal
  const html = `
    <div class="modal-bg" id="mbg" onclick="closeModalBg(event)">
      <div class="modal">
        <div class="modal-head">
          <div class="modal-glow" style="background:radial-gradient(circle,${c.color} 0%,transparent 70%)"></div>
          <div class="modal-icon">${s.icon}</div>
          <div>
            <div class="modal-setor">${c.name} → ${s.setor}</div>
            <div class="modal-title">${s.title}</div>
          </div>
          <button class="modal-close" onclick="closeModal()">✕</button>
        </div>
        <div class="scores">
          <div class="score-item">
            <div class="score-lbl">Dificuldade</div>
            <div class="score-bar">
              <div class="score-fill" style="width:${diffPct};background:${diffColor}"></div>
            </div>
          </div>
        </div>
        <div class="modal-body">
          <div>${stepsHtml}</div>
          <div>${prosHtml}${consHtml}</div>
        </div>
      </div>
    </div>`;

  // Injeta no DOM via createElement
  const div = document.createElement('div');
  div.id = 'modal-root';
  div.innerHTML = html;
  document.body.appendChild(div);

  // Impede scroll da página enquanto modal está aberto
  document.body.style.overflow = 'hidden';
}

// Fecha se clicou no fundo (não no modal em si)
function closeModalBg(e) {
  if (e.target.id === 'mbg') closeModal();
}

function closeModal() {
  const bg = document.getElementById('mbg');
  if (bg) {
    bg.classList.add('out');   // trigger da animação CSS de saída
    setTimeout(() => {
      document.getElementById('modal-root')?.remove();  // remove do DOM
      document.body.style.overflow = '';               // restaura scroll
    }, 180);  // aguarda a animação .out terminar (180ms)
  }
}

// ESC fecha o modal
document.addEventListener('keydown', e => {
  if (e.key === 'Escape') closeModal();
});
1
String(i+1).padStart(2,'0')
Transforma 1 em 01, 2 em 02 etc. O argumento 2 é o comprimento mínimo e '0' é o caractere de preenchimento. Cria os números dos steps com zero à esquerda.
2
e.target.id === 'mbg'
O overlay tem id="mbg". Quando o usuário clica no modal em si (não no overlay), o event.target é um elemento interno. Essa verificação garante que fechar só acontece ao clicar fora do modal.
3
?. (optional chaining)
element?.remove() só chama remove() se o elemento existir. Sem o ?., se o usuário clicar ESC duas vezes rapidamente, o segundo clique tentaria remover um elemento já removido e causaria erro.
JavaScript + CSS dinâmico JSCSS
A técnica mais avançada do projeto: o JavaScript muda variáveis CSS em runtime. Isso propaga automaticamente para todos os elementos que usam essas variáveis — sem tocar em nenhum elemento individualmente.
JS — Como funciona a troca de tema
// Função auxiliar: muda uma CSS var no :root
function setCSSVar(name, value) {
  document.documentElement.style.setProperty(name, value);
  // document.documentElement = o elemento <html>
  // setProperty('--acc', '#e1306c') é equivalente a:
  // :root { --acc: #e1306c } no CSS
}

// Função auxiliar: converte hex para string RGB
function hexToRGB(hex) {
  const r = parseInt(hex.slice(1, 3), 16);  // pega os 2 chars de R
  const g = parseInt(hex.slice(3, 5), 16);  // pega os 2 chars de G
  const b = parseInt(hex.slice(5, 7), 16);  // pega os 2 chars de B
  return `${r},${g},${b}`;
  // '#e1306c' → '225,48,108'
}

// Quando o usuário muda de empresa:
function renderHero() {
  const c = CO[ci];
  const rgb = hexToRGB(c.color);

  // Instagram → '#e1306c'
  // WhatsApp  → '#25d366'
  // Uber      → '#1d4ed8'
  // Airbnb    → '#ff5a5f'
  // ChatGPT   → '#10a37f'
  // Lovable   → '#7c3aed'

  setCSSVar('--acc',   c.color);
  setCSSVar('--acc-d', `rgba(${rgb},.12)`);
  setCSSVar('--acc-g', `rgba(${rgb},.05)`);
}

// Resultado: ao mudar para WhatsApp, o CSS vira:
// :root {
//   --acc:   #25d366;
//   --acc-d: rgba(37,211,102,.12);
//   --acc-g: rgba(37,211,102,.05);
// }
//
// Todos esses elementos atualizam automaticamente:
// .co-btn.active  → fundo verde
// .co-btn.active::before → linha verde
// .tab.on  → underline verde
// .hero-period → texto verde
// .strat-box → borda verde
// .acc-d em badges → fundo verde suave
// score-fill (via inline style) → usa a cor diretamente
1
document.documentElement
É o elemento raiz <html> — o mesmo alvo do seletor :root no CSS. Ao fazer setProperty nele, é como editar o :root em tempo real.
2
Por que 3 variáveis e não 1?
A cor de acento é usada em três situações: pura (--acc para texto e bordas), com 12% de opacidade (--acc-d para fundos de cards) e com 5% (--acc-g para o fundo do bloco de estratégia). Separar permite usar a mesma cor com transparência diferente sem recalcular toda vez.
3
parseInt(hex, 16)
parseInt('e1', 16) converte o string hexadecimal 'e1' para o número decimal 225. O segundo argumento 16 é a base (hexadecimal). Isso é como você converte cor hex para RGB em JavaScript puro.
Resumo de todas as funções JS
FunçãoO que fazChamada quando
setCSSVar(k,v)Muda CSS var no :root via JSrenderHero()
hexToRGB(hex)Converte #rrggbb → 'r,g,b'renderHero()
renderCompanyTabs()Gera os botões da sidebarselectCo(), init
renderHero()Atualiza tema, nome, métricasselectCo(), init
renderContent(animate)Fade out → chama _render()selectCo(), switchTab()
_render()Injeta HTML do tab ativo no DOMrenderContent()
switchTab(t)Alterna sectors ↔ stepsclick nos botões de tab
selectCo(i)Muda empresa, re-renderiza tudoclick nos botões da sidebar
openModal(si)Cria e injeta o modal no DOMclick nos cards de setor
closeModalBg(e)Fecha se clicou no fundoonclick no .modal-bg
closeModal()Anima saída e remove do DOMbotão ✕, ESC, closeModalBg