Design
Faculdade Impacta
Comecei pelo design
Estudei na Impacta e desenhei interface por dez anos. Aprendi a decidir o que a pessoa vê primeiro e o que nem precisa aparecer.
[build]
command = "npm run build"
publish = "dist"
[build.environment]
NODE_VERSION = "22"
[[headers]]
for = "/*"
[headers.values]
Content-Security-Policy = "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; manifest-src 'self'; frame-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; object-src 'none'; upgrade-insecure-requests"
Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload"
X-Frame-Options = "DENY"
X-Content-Type-Options = "nosniff"
Referrer-Policy = "strict-origin-when-cross-origin"
Permissions-Policy = "accelerometer=(), autoplay=(), camera=(), display-capture=(), encrypted-media=(), fullscreen=(self), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), publickey-credentials-get=(), screen-wake-lock=(), usb=(), xr-spatial-tracking=()"
Cross-Origin-Opener-Policy = "same-origin"
Cross-Origin-Resource-Policy = "same-origin"
Cross-Origin-Embedder-Policy = "require-corp"
X-Permitted-Cross-Domain-Policies = "none"
X-DNS-Prefetch-Control = "off"
[[headers]]
for = "/_astro/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
[[headers]]
for = "/.well-known/security.txt"
[headers.values]
Content-Type = "text/plain; charset=utf-8"
[[redirects]]
from = "/labs/"
to = "/sobre/"
status = 301
force = true
[[redirects]]
from = "/projetos/*"
to = "/trabalhos/:splat"
status = 301
force = true
export const EU = {
nome: 'LUCAS NEGRELLI',
papel: 'Design, conversão e segurança da informação',
posicao: 'Design, conversão e segurança da informação.',
cidade: 'São Paulo, SP',
email: 'lucas.negrelli82@gmail.com',
telefone: '(11) 9 5293 0145',
linkedin: 'https://linkedin.com/in/lucasnegrelli',
github: 'https://github.com/lucasnegrelli',
dominio: 'lucasnegrelli.com',
cnpj: '59.288.879/0001-61',
disponivel: 'Procurando estágio ou vaga N1 em SOC, NOC ou suporte técnico. Disponível no período diurno (curso a FATEC à noite).',
};
export const PILARES = [
{
nome: 'Design',
origem: 'Faculdade Impacta',
titulo: 'Comecei pelo design',
texto:
'Estudei na Impacta e desenhei interface por dez anos. Aprendi a decidir o que a pessoa vê primeiro e o que nem precisa aparecer.',
},
{
nome: 'Conversão',
origem: 'V4 Company',
titulo: 'Depois fui olhar o número',
texto:
'Foram quase três anos fazendo landing page, anúncio e e-mail na V4, sempre olhando o resultado depois que a página ia ao ar. Hoje eu já desenho pensando em quem vai clicar e em quem não vai.',
},
{
nome: 'Segurança',
origem: 'FATEC',
titulo: 'Hoje estudo segurança',
texto:
'Faço Segurança da Informação na FATEC. Antes disso já tinha entrado num site invadido pra limpar: webshell, usuário falso, banco com dado plantado. Depois dessa, parei de construir do jeito mais fácil.',
},
];
export const NAV = [
{ href: '/', rotulo: 'Início' },
{ href: '/trabalhos/', rotulo: 'Trabalhos' },
{ href: '/sobre/', rotulo: 'Sobre' },
];
export const CABECALHOS = [
{
chave: 'content-security-policy',
nome: 'Content-Security-Policy',
curto: 'CSP',
previne: 'que um script injetado na página consiga rodar',
peso: 25,
},
{
chave: 'strict-transport-security',
nome: 'Strict-Transport-Security',
curto: 'HSTS',
previne: 'que a conexão caia pra HTTP e alguém leia no meio do caminho',
peso: 20,
},
{
chave: 'x-frame-options',
nome: 'X-Frame-Options',
curto: 'XFO',
previne: 'que outro site abra este por dentro pra roubar o seu clique',
peso: 15,
},
{
chave: 'x-content-type-options',
nome: 'X-Content-Type-Options',
curto: 'nosniff',
previne: 'que o navegador adivinhe o tipo do arquivo e execute sem querer',
peso: 10,
},
{
chave: 'referrer-policy',
nome: 'Referrer-Policy',
curto: 'Referrer',
previne: 'que o endereço da página vaze pro site que você clicou',
peso: 10,
},
{
chave: 'permissions-policy',
nome: 'Permissions-Policy',
curto: 'Permissions',
previne: 'que câmera, microfone e localização fiquem disponíveis à toa',
peso: 10,
},
{
chave: 'cross-origin-opener-policy',
nome: 'Cross-Origin-Opener-Policy',
curto: 'COOP',
previne: 'que outra aba consiga bisbilhotar a memória desta',
peso: 5,
},
{
chave: 'cross-origin-resource-policy',
nome: 'Cross-Origin-Resource-Policy',
curto: 'CORP',
previne: 'que outro site carregue arquivos daqui sem permissão',
peso: 5,
},
];
([\s\S]*?)<\/script>/)?.[1] ?? '';
const codigoOculto = [netlifyToml, dadosSrc, scriptDaPagina, globalCss]
.map(semComentarios)
.join('\n\n\n');
---
<Base
titulo="Início"
descricao="Lucas Negrelli, São Paulo. Estudante de Segurança da Informação procurando estágio ou N1 em SOC, NOC e suporte técnico. Já atuei em resposta a incidente real. Este site mede a própria configuração de segurança em tempo real."
atual="/"
>
<section class="capa" data-gatilho data-secao="Início">
<pre class="capa__codigo" aria-hidden="true">{codigoOculto}</pre>
<div class="capa__interno wrap">
<div class="capa__texto">
<p class="capa__assina micro">
<Cifra texto="LUCAS NEGRELLI" auto={true} />
<span class="capa__sep" aria-hidden="true">/</span>
{EU.cidade}
</p>
<div class="capa__marca">
<h1 class="capa__titulo display-1" aria-label="O lado que aparece. E o que ninguém vê.">
<span class="capa__l capa__l--a" data-cinetica aria-hidden="true">O lado que aparece.</span>
<span class="capa__l capa__l--b" data-cinetica aria-hidden="true">E o que ninguém vê.</span>
</h1>
</div>
<p class="capa__frase">
Sou designer e estudo segurança da informação. Já desenhei muito site e
já limpei um que foi invadido. Esta página testa a própria segurança
enquanto você lê, e o resultado está ao lado.
</p>
<p class="capa__busca peq"><span class="capa__ponto" aria-hidden="true"></span>{EU.disponivel}</p>
<p class="capa__acoes">
<a class="botao botao--forte" href="/curriculo.pdf" download="Lucas-Negrelli-Curriculo.pdf">Baixar currículo</a>
<a class="botao" href="/trabalhos/">Ver os trabalhos</a>
</p>
</div>
<aside class="capa__prova" data-prova aria-label="Nota de segurança desta página, medida agora">
<p class="micro">Medido agora, neste navegador</p>
<p class="capa__nota num" data-nota-hero aria-live="polite">··</p>
<p class="capa__pontos peq fraco num" data-nota-hero-pontos>medindo</p>
<div class="capa__barra" aria-hidden="true"><span data-nota-hero-barra></span></div>
<a class="capa__medicao peq" href="#placar">Ver a medição, cabeçalho por cabeçalho <span aria-hidden="true">↓</span></a>
</aside>
<p class="capa__design">
<span class="micro fraco">Precisa de design ou de um site novo?</span>
<a class="capa__design-link" href="https://design.lucasnegrelli.com/" rel="noopener">
Veja meu trabalho de direção de arte e sites <span aria-hidden="true">→</span>
</a>
</p>
</div>
</section>
<div class="capa__halo-cursor" aria-hidden="true"></div>
<section class="secao wrap" data-secao="Os três lados">
<p class="rotulo-secao micro">01. Os três lados</p>
<h2 class="titulo-2">Três coisas que eu faço, e de onde cada uma veio.</h2>
<div class="pilares">
{PILARES.map((p) => (
<article class="pilar">
<p class="pilar__nome micro">{p.nome}</p>
<p class="pilar__origem micro">{p.origem}</p>
<h3 class="pilar__titulo">{p.titulo}</h3>
<p class="peq fraco">{p.texto}</p>
</article>
))}
</div>
<p class="pilar__fecho">
Cada uma dessas três é comum sozinha. O que me interessa é fazer as
três no mesmo projeto.
</p>
</section>
<section class="secao wrap" id="placar" data-secao="Medição">
<p class="rotulo-secao micro">02. Medição em tempo real</p>
<div class="placar">
<div class="nota cartao" data-nota>
<p class="micro">Nota</p>
<p class="nota__valor" data-nota-letra>··</p>
<p class="nota__pontos peq fraco" data-nota-pontos>medindo</p>
<div class="nota__barra"><span data-nota-barra></span></div>
</div>
<div class="placar__texto">
<h2 class="titulo-2">Cabeçalhos de resposta HTTP</h2>
<p class="fraco">
Quando esta página carregou, o navegador pediu o arquivo pro
servidor e recebeu junto uma lista de instruções de segurança.
Isso se chama cabeçalho de resposta. A lista abaixo é essa resposta,
lida agora, no seu navegador. Não é print nem texto que eu digitei.
</p>
<p class="peq fraco" data-aviso-local hidden>
<span class="ciano">Você está rodando local.</span> Os cabeçalhos são
aplicados pelo servidor de produção, então aqui eles aparecem como
ausentes. Em <code>{EU.dominio}</code> a lista fecha completa.
</p>
<p class="peq">
<a class="link-ext" href="https://securityheaders.com/?q=lucasnegrelli.com&followRedirects=on" rel="noopener" target="_blank">Conferir no securityheaders.com</a>
<a class="link-ext" href="https://developer.mozilla.org/en-US/observatory/analyze?host=lucasnegrelli.com" rel="noopener" target="_blank">Conferir no Mozilla Observatory</a>
</p>
</div>
</div>
<noscript>
<div class="cartao sem-js">
<p class="micro ciano">JavaScript desligado</p>
<p class="peq">
A medição abaixo é feita pelo seu navegador em tempo real, então ela
precisa de JavaScript pra rodar. Sem ele os itens ficam parados em
"medindo".
</p>
<p class="peq">
Se você prefere não ligar, o resultado é o mesmo nestes dois
verificadores independentes, que não dependem de nada rodando aqui:
</p>
<p class="peq">
<a class="link-ext" href="https://securityheaders.com/?q=lucasnegrelli.com&followRedirects=on" rel="noopener" target="_blank">securityheaders.com</a>
<a class="link-ext" href="https://developer.mozilla.org/en-US/observatory/analyze?host=lucasnegrelli.com" rel="noopener" target="_blank">Mozilla Observatory</a>
</p>
</div>
</noscript>
<ul class="cabs" data-cabs>
{CABECALHOS.map((c) => (
<li class="cab" data-cab={c.chave} data-estado="medindo">
<div class="cab__topo">
<span class="cab__sinal" aria-hidden="true"></span>
<span class="cab__nome">{c.nome}</span>
<span class="cab__curto micro">{c.curto}</span>
<span class="cab__estado micro" data-cab-estado>medindo</span>
</div>
<p class="cab__previne peq fraco">Serve pra evitar: {c.previne}</p>
<p class="cab__valor peq" data-cab-valor hidden></p>
</li>
))}
</ul>
</section>
<section class="secao wrap" data-secao="Prova">
<p class="rotulo-secao micro">03. Prova prática</p>
<h2 class="titulo-2">Três ataques rodando de verdade nesta página</h2>
<p class="fraco intro-demo">
Configurar um cabeçalho é fácil. Difícil é saber se ele funciona. Cada
botão abaixo tenta um ataque de verdade contra esta página, e o texto que
aparece depois é o navegador respondendo, não uma animação minha.
</p>
<div class="demos">
<article class="demo cartao">
<p class="micro">Ataque 01</p>
<h3 class="demo__titulo">Injeção de script (XSS)</h3>
<p class="peq fraco">
Vou colocar um script dentro desta página e mandar ele rodar. É o que
um XSS faz depois de achar um campo que não filtra o que a pessoa
digita.
</p>
<button class="botao" type="button" data-demo="xss">Tentar</button>
<output class="demo__saida peq" data-saida="xss" aria-live="polite"></output>
</article>
<article class="demo cartao">
<p class="micro">Ataque 02</p>
<h3 class="demo__titulo">Clickjacking</h3>
<p class="peq fraco">
Vou tentar abrir este site dentro de um quadro invisível. É assim que
funciona o golpe: o atacante põe o site verdadeiro por cima de uma
página falsa e você clica achando que está em outro lugar.
</p>
<button class="botao" type="button" data-demo="frame">Tentar</button>
<output class="demo__saida peq" data-saida="frame" aria-live="polite"></output>
<div class="demo__moldura" data-moldura hidden></div>
</article>
<article class="demo cartao">
<p class="micro">Ataque 03</p>
<h3 class="demo__titulo">Adivinhação de tipo de arquivo</h3>
<p class="peq fraco">
Vou baixar um arquivo de texto e ver se o servidor mandou o navegador
respeitar o tipo declarado. Sem essa ordem, o navegador tenta
adivinhar o que o arquivo é e pode acabar executando ele.
</p>
<button class="botao" type="button" data-demo="sniff">Tentar</button>
<output class="demo__saida peq" data-saida="sniff" aria-live="polite"></output>
</article>
</div>
<p class="peq fraco nota-console">
Se você abrir o console do navegador antes de clicar, vai ver as mesmas
violações registradas lá.
</p>
</section>
<section class="secao wrap" data-secao="Continua em">
<p class="rotulo-secao micro">04. Continua em</p>
<div class="proximos">
<a class="proximo cartao" href="/trabalhos/">
<h3 class="demo__titulo">Trabalhos</h3>
<p class="peq fraco">O que eu já fiz e está no ar, com a história de cada trabalho.</p>
<span class="peq ciano">Ver</span>
</a>
<a class="proximo cartao" href="/sobre/">
<h3 class="demo__titulo">Sobre</h3>
<p class="peq fraco">Quem eu sou, de onde vim e o que estou estudando agora.</p>
<span class="peq ciano">Ver</span>
</a>
<a class="proximo cartao" href="/hardening/">
<h3 class="demo__titulo">Como configurei este site</h3>
<p class="peq fraco">Cada proteção deste site explicada do zero, com o ataque que ela evita.</p>
<span class="peq ciano">Ler</span>
</a>
</div>
</section>
</Base>
<script>
import { CABECALHOS } from '../dados';
const local = ['localhost', '127.0.0.1', '[::1]'].includes(location.hostname);
const aviso = document.querySelector<HTMLElement>('[data-aviso-local]');
function pinta(chave: string, presente: boolean, valor: string | null) {
const li = document.querySelector<HTMLElement>(`[data-cab="${chave}"]`);
if (!li) return;
li.dataset.estado = presente ? 'ok' : 'falha';
const est = li.querySelector<HTMLElement>('[data-cab-estado]');
if (est) est.textContent = presente ? 'presente' : 'ausente';
const cel = li.querySelector<HTMLElement>('[data-cab-valor]');
if (cel && valor) {
cel.textContent = valor.length > 240 ? valor.slice(0, 240) + '...' : valor;
cel.hidden = false;
}
}
function nota(pct: number) {
if (pct >= 97) return 'A+';
if (pct >= 85) return 'A';
if (pct >= 70) return 'B';
if (pct >= 55) return 'C';
if (pct >= 40) return 'D';
return 'F';
}
async function auditar() {
let headers: Headers | null = null;
try {
const r = await fetch(location.href, { method: 'HEAD', cache: 'no-store' });
headers = r.headers;
} catch {
headers = null;
}
const total = CABECALHOS.reduce((s, c) => s + c.peso, 0);
let pontos = 0;
for (const c of CABECALHOS) {
const v = headers ? headers.get(c.chave) : null;
if (v) pontos += c.peso;
pinta(c.chave, Boolean(v), v);
}
const pct = Math.round((pontos / total) * 100);
const letra = document.querySelector<HTMLElement>('[data-nota-letra]');
const cont = document.querySelector<HTMLElement>('[data-nota-pontos]');
const barra = document.querySelector<HTMLElement>('[data-nota-barra]');
const caixa = document.querySelector<HTMLElement>('[data-nota]');
const escreve = (el: HTMLElement | null, txt: string) => {
if (!el) return;
el.textContent = '';
el.append(txt.replace('+', ''));
if (txt.endsWith('+')) {
const sup = document.createElement('sup');
sup.textContent = '+';
el.append(sup);
}
};
escreve(letra, nota(pct));
escreve(document.querySelector<HTMLElement>('[data-nota-hero]'), nota(pct));
const hp = document.querySelector<HTMLElement>('[data-nota-hero-pontos]');
const hb = document.querySelector<HTMLElement>('[data-nota-hero-barra]');
const hc = document.querySelector<HTMLElement>('[data-prova]');
if (hp) hp.textContent = `${pontos} de ${total} pontos, ${pct}%`;
if (hb) hb.style.width = pct + '%';
if (hc) hc.dataset.faixa = pct >= 85 ? 'ok' : pct >= 55 ? 'alerta' : 'falha';
if (cont) cont.textContent = `${pontos} de ${total} pontos, ${pct}%`;
if (barra) barra.style.width = pct + '%';
if (caixa) caixa.dataset.faixa = pct >= 85 ? 'ok' : pct >= 55 ? 'alerta' : 'falha';
if (aviso && local) aviso.hidden = false;
}
auditar();
const saida = (k: string) => document.querySelector<HTMLElement>(`[data-saida="${k}"]`);
function diz(k: string, estado: 'ok' | 'falha' | 'neutro', txt: string) {
const el = saida(k);
if (!el) return;
el.dataset.estado = estado;
el.textContent = txt;
}
let violacao: SecurityPolicyViolationEvent | null = null;
document.addEventListener('securitypolicyviolation', (e) => { violacao = e; });
document.querySelector('[data-demo="xss"]')?.addEventListener('click', () => {
violacao = null;
diz('xss', 'neutro', 'Colocando um script na página e mandando rodar.');
(window as any).__xss = false;
const s = document.createElement('script');
s.textContent = 'window.__xss = true;';
document.body.appendChild(s);
setTimeout(() => {
s.remove();
if ((window as any).__xss) {
diz('xss', 'falha', 'Deu certo, o script rodou. Isso significa que a política de segurança não está ativa aqui.');
} else {
const d = violacao?.violatedDirective ?? 'script-src';
diz('xss', 'ok', `Bloqueado. O script entrou na página, mas o navegador se recusou a executar por causa da Content-Security-Policy. Regra violada: ${d}.`);
}
}, 120);
});
document.querySelector('[data-demo="frame"]')?.addEventListener('click', () => {
const cx = document.querySelector<HTMLElement>('[data-moldura]');
if (!cx) return;
cx.hidden = false;
cx.textContent = '';
diz('frame', 'neutro', 'Tentando abrir o site dentro de um quadro.');
const f = document.createElement('iframe');
f.src = location.origin + '/';
f.title = 'Tentativa de abrir o site em um quadro';
f.className = 'demo__iframe';
cx.appendChild(f);
setTimeout(() => {
let bloqueado = true;
try {
const doc = f.contentDocument;
if (doc && doc.body && doc.body.childElementCount > 0) bloqueado = false;
} catch {
bloqueado = true;
}
if (bloqueado) {
diz('frame', 'ok', 'Bloqueado. O quadro abaixo ficou vazio. As regras frame-ancestors none e X-Frame-Options DENY impedem qualquer página de abrir este site por dentro, inclusive ela mesma.');
} else {
diz('frame', 'falha', 'O site abriu dentro do quadro. Falta configurar X-Frame-Options ou frame-ancestors.');
}
}, 900);
});
document.querySelector('[data-demo="sniff"]')?.addEventListener('click', async () => {
diz('sniff', 'neutro', 'Baixando /.well-known/security.txt e olhando a resposta.');
try {
const r = await fetch('/.well-known/security.txt', { cache: 'no-store' });
const tipo = r.headers.get('content-type') ?? 'não declarado';
const sniff = r.headers.get('x-content-type-options');
if (sniff && sniff.toLowerCase() === 'nosniff') {
diz('sniff', 'ok', `Bloqueado. O servidor declarou o tipo como ${tipo} e mandou nosniff junto. Com isso o navegador fica proibido de reinterpretar o conteúdo, então um arquivo enviado como texto nunca vira script executável.`);
} else {
diz('sniff', 'falha', `Cabeçalho nosniff ausente. Tipo declarado: ${tipo}. Sem ele o navegador pode adivinhar o tipo real e executar o conteúdo.`);
}
} catch {
diz('sniff', 'falha', 'Não consegui completar a requisição.');
}
});
import('../scripts/tipografia').then(({ ligarCinetica }) => ligarCinetica());
import('../scripts/revelacao').then(({ ligarRevelacao }) => {
const capa = document.querySelector<HTMLElement>('.capa');
const halo = document.querySelector<HTMLElement>('.capa__halo-cursor');
if (capa && halo) ligarRevelacao(capa, halo);
});
@font-face {
font-family: "Instrument Serif";
font-style: normal; font-weight: 400; font-display: swap;
src: url("/fonts/InstrumentSerif-Regular.woff2") format("woff2");
}
@font-face {
font-family: "Instrument Serif";
font-style: italic; font-weight: 400; font-display: swap;
src: url("/fonts/InstrumentSerif-Italic.woff2") format("woff2");
}
@font-face {
font-family: "Inter Tight";
font-style: normal; font-weight: 100 900; font-display: swap;
src: url("/fonts/InterTight-latin-ext.woff2") format("woff2");
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: "Inter Tight";
font-style: normal; font-weight: 100 900; font-display: swap;
src: url("/fonts/InterTight-latin.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "JetBrains Mono";
font-style: normal; font-weight: 100 800; font-display: swap;
src: url("/fonts/JetBrainsMono-latin-ext.woff2") format("woff2");
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: "JetBrains Mono";
font-style: normal; font-weight: 100 800; font-display: swap;
src: url("/fonts/JetBrainsMono-latin.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
:root {
--lilas: #b8a6ff;
--rosa: #ff9ccb;
--preto: #08090a;
--carvao: #0d0f10;
--superficie: #131617;
--osso-rgb: 236 233 226;
--linha: rgb(var(--osso-rgb) / .09);
--linha-forte: rgb(var(--osso-rgb) / .2);
--branco: #ece9e2;
--cinza: #a09d95;
--cinza-fraco: #66645e;
--ciano: #8fcde6;
--ciano-fraco: #3d6b7d;
--ciano-rgb: 143 205 230;
--ciano-halo: rgb(var(--ciano-rgb) / 0.45);
--ok: #7fcf9c;
--alerta: #e8b860;
--falha: #e57a6e;
--serif: "Instrument Serif", "Iowan Old Style", "Palatino Linotype", Georgia, serif;
--sans: "Inter Tight", "Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
--mono: "JetBrains Mono", ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;
--condensada: var(--serif);
--margem: clamp(1.1rem, 4vw, 4rem);
--max: 1180px;
--t-micro: 0.6875rem;
--t-peq: 0.875rem;
--t-corpo: clamp(1rem, .95rem + .25vw, 1.125rem);
--t-d1: clamp(2.6rem, 1.4rem + 4.6vw, 5.4rem);
--t-d2: clamp(1.9rem, 1.3rem + 2.2vw, 3.1rem);
--t-d3: clamp(1.3rem, 1.1rem + .8vw, 1.7rem);
--ease: cubic-bezier(.2, .8, .2, 1);
}
[data-tema='verde'] { --ciano: #8fd9ad; --ciano-rgb: 143 217 173; --ciano-fraco: #3f7d5a; }
[data-tema='lilas'] { --ciano: #b8a6ff; --ciano-rgb: 184 166 255; --ciano-fraco: #5b4aa6; }
[data-tema='rosa'] { --ciano: #ff9ccb; --ciano-rgb: 255 156 203; --ciano-fraco: #9c4c73; }
*,
*::before,
*::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; color-scheme: dark; scrollbar-width: thin; scrollbar-color: var(--linha-forte) transparent; }
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
*, *::before, *::after {
animation-duration: .001ms !important;
animation-iteration-count: 1 !important;
transition-duration: .001ms !important;
}
}
::selection { background: var(--branco); color: var(--preto); }
body {
margin: 0;
background: var(--preto);
color: var(--branco);
font-family: var(--sans);
font-size: var(--t-corpo);
line-height: 1.7;
letter-spacing: .004em;
font-kerning: normal;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
overflow-x: hidden;
}
body::before {
content: "";
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
background:
radial-gradient(90% 60% at 50% -10%, rgb(var(--osso-rgb) / .07), transparent 62%),
radial-gradient(60% 50% at 100% 100%, rgb(var(--ciano-rgb) / .05), transparent 70%);
}
main, header, footer { position: relative; z-index: 1; }
h1, h2, h3 {
margin: 0;
text-wrap: balance;
font-family: var(--serif);
font-weight: 400;
letter-spacing: -.02em;
line-height: 1.06;
}
p { margin: 0 0 1em; text-wrap: pretty; hanging-punctuation: first; }
a { color: inherit; text-decoration: none; }
img, iframe { display: block; max-width: 100%; }
:focus-visible {
outline: 1px solid var(--ciano);
outline-offset: 4px;
border-radius: 2px;
}
.wrap { width: 100%; max-width: var(--max); margin-inline: auto; padding-inline: var(--margem); }
.micro {
font-family: var(--mono);
font-size: var(--t-micro); letter-spacing: .16em; text-transform: uppercase;
color: var(--cinza); font-weight: 450;
}
.peq { font-size: var(--t-peq); letter-spacing: 0; text-wrap: pretty; line-height: 1.65; }
.brilho { text-shadow: 0 0 30px rgb(var(--osso-rgb) / .1); }
.ciano { color: var(--ciano); }
.fraco { color: var(--cinza); }
.num { font-variant-numeric: tabular-nums; }
.sr-only {
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.regua {
height: 7px; border: 0; margin: 0;
background:
linear-gradient(var(--linha-forte), var(--linha-forte)) top / 100% 1px no-repeat,
repeating-linear-gradient(90deg, var(--linha-forte) 0 1px, transparent 1px 12px) top / 100% 6px no-repeat;
-webkit-mask-image: linear-gradient(90deg, #000, transparent 70%);
mask-image: linear-gradient(90deg, #000, transparent 70%);
}
.rotulo-secao {
display: flex; align-items: center; gap: 1.1rem;
margin-bottom: 2.25rem;
}
.rotulo-secao::after {
content: ""; flex: 1; height: 7px;
background:
linear-gradient(var(--linha-forte), var(--linha-forte)) top / 100% 1px no-repeat,
repeating-linear-gradient(90deg, var(--linha-forte) 0 1px, transparent 1px 12px) top / 100% 5px no-repeat;
-webkit-mask-image: linear-gradient(90deg, #000, transparent);
mask-image: linear-gradient(90deg, #000, transparent);
}
.botao {
display: inline-flex; align-items: center; gap: .65rem;
padding: .85em 1.45em;
border: 1px solid var(--linha-forte);
border-radius: 999px;
background: transparent;
color: var(--branco);
font-family: var(--sans);
font-size: var(--t-peq);
font-weight: 500;
letter-spacing: .01em;
cursor: pointer;
transition: border-color .25s var(--ease), color .25s var(--ease), background-color .25s var(--ease), transform .25s var(--ease);
}
.botao:hover { border-color: var(--branco); background: rgb(var(--osso-rgb) / .06); }
.botao:active { transform: translateY(1px); }
.botao--forte { background: var(--branco); border-color: var(--branco); color: var(--preto); }
.botao--forte:hover { background: transparent; color: var(--branco); border-color: var(--branco); }
.cartao {
border: 0;
border-top: 1px solid var(--linha-forte);
background: transparent;
padding: 1.4rem 0 .4rem;
}
code, .cod {
font-family: var(--mono);
font-size: .86em;
color: var(--ciano);
word-break: break-word;
}
pre {
margin: 0;
padding: 1rem 1.1rem;
border: 0;
border-left: 1px solid var(--linha-forte);
background: rgb(var(--osso-rgb) / .03);
overflow-x: auto;
font-family: var(--mono);
font-size: var(--t-peq);
line-height: 1.7;
color: var(--cinza);
}
pre code { color: inherit; }
.mira {
position: fixed; top: 0; left: 0; z-index: 90;
width: 22px; height: 22px; margin: -11px 0 0 -11px;
pointer-events: none; mix-blend-mode: difference;
opacity: 0; transition: opacity .3s ease, width .18s ease, height .18s ease, margin .18s ease;
}
.mira::before, .mira::after { content: ""; position: absolute; background: #fff; }
.mira::before { left: 50%; top: 0; bottom: 0; width: 1px; transform: translateX(-.5px); }
.mira::after { top: 50%; left: 0; right: 0; height: 1px; transform: translateY(-.5px); }
.mira i { position: absolute; inset: 0; border: 1px solid #fff; border-radius: 50%; opacity: .4; }
.mira[data-ativo] { opacity: 1; }
.mira[data-alvo] { width: 44px; height: 44px; margin: -22px 0 0 -22px; }
@media (hover: none), (pointer: coarse) { .mira { display: none; } }
@media (prefers-reduced-motion: reduce) { .mira { display: none; } }
.trilho {
position: fixed; top: 0; bottom: 0; z-index: 40;
width: var(--margem); display: flex; align-items: center; justify-content: center;
pointer-events: none;
}
.trilho span {
font-family: var(--mono); font-size: 10px; letter-spacing: .24em;
color: var(--cinza-fraco); white-space: nowrap; text-transform: uppercase;
}
.trilho--e { left: 0; } .trilho--e span { transform: rotate(-90deg); }
.trilho--d { right: 0; } .trilho--d span { transform: rotate(90deg); }
@media (max-width: 900px) { .trilho { display: none; } }
.progresso {
position: fixed; top: 0; left: 0; height: 2px; width: 100%;
background: var(--ciano); box-shadow: 0 0 10px var(--ciano-halo);
transform: scaleX(0); transform-origin: left; z-index: 95; opacity: .85;
}
@media (prefers-reduced-motion: reduce) { .progresso { display: none; } }
.display-1 {
font-family: var(--serif); font-weight: 400;
font-size: var(--t-d1); line-height: 1.02; letter-spacing: -.025em;
text-wrap: balance;
}
.display-2 {
font-family: var(--serif); font-weight: 400;
font-size: var(--t-d2); line-height: 1.08; letter-spacing: -.02em;
}
.titulo-linha { display: flex; align-items: center; gap: clamp(.6rem, 2vw, 1.1rem); }
.selo {
height: 1em; width: auto; flex: none;
opacity: .35; filter: grayscale(1) brightness(1.5);
transition: opacity .45s ease, filter .45s ease;
}
.titulo-linha:hover .selo,
.titulo-linha:focus-within .selo {
opacity: 1;
filter: drop-shadow(0 0 10px var(--ciano-halo)) drop-shadow(0 0 26px rgb(var(--ciano-rgb) / .3));
}
[data-tema='verde'] .topo-pag { position: relative; }
[data-tema='verde'] .topo-pag::before {
content: ''; position: absolute; z-index: -1; pointer-events: none;
right: clamp(-6rem, -4vw, 0rem); top: 50%; width: clamp(220px, 34vw, 420px); aspect-ratio: 1;
translate: 0 -50%; border-radius: 50%;
background:
radial-gradient(circle, transparent 38%, rgb(var(--ciano-rgb) / .22) 40%, transparent 62%),
conic-gradient(from 0deg, transparent, rgb(var(--ciano-rgb) / .5), transparent 40%, rgb(var(--ciano-rgb) / .25), transparent 80%);
mask-image: radial-gradient(circle, transparent 30%, #000 36%, #000 62%, transparent 70%);
-webkit-mask-image: radial-gradient(circle, transparent 30%, #000 36%, #000 62%, transparent 70%);
filter: blur(.3px) drop-shadow(0 0 22px rgb(var(--ciano-rgb) / .45));
opacity: .55;
animation: portal 14s linear infinite;
}
@keyframes portal { to { rotate: 360deg; } }
.ceu {
position: fixed; inset: 0; z-index: 0; overflow: hidden; pointer-events: none;
mask-image: linear-gradient(to right, #000 0%, rgba(0,0,0,.24) 14%, rgba(0,0,0,.24) 86%, #000 100%);
-webkit-mask-image: linear-gradient(to right, #000 0%, rgba(0,0,0,.24) 14%, rgba(0,0,0,.24) 86%, #000 100%);
}
.ceu__camada {
position: absolute; left: 0; top: 0; border-radius: 50%; opacity: .75;
translate: calc(var(--mx, 0) * var(--prof) * -1px) calc(var(--my, 0) * var(--prof) * -1px);
animation: deriva var(--dur) linear infinite;
}
@keyframes deriva { to { transform: translateY(-100vh); } }
.palco { position: fixed; inset: 0; z-index: 3; pointer-events: none; overflow: hidden; }
.ev { position: absolute; display: block; pointer-events: auto; cursor: crosshair; }
.ev--pego { filter: brightness(2.2) drop-shadow(0 0 10px var(--ciano)); }
.ev--meteoro {
left: var(--x); top: var(--y); width: 150px; height: 2px; padding-block: 8px; background-clip: content-box;
background-color: transparent; background-image: linear-gradient(90deg, transparent, #fff 85%, var(--rosa));
rotate: 24deg; opacity: 0; animation: meteoro 1.5s ease-in forwards;
}
@keyframes meteoro {
0% { opacity: 0; transform: translate(0, 0); }
12% { opacity: 1; }
100% { opacity: 0; transform: translate(-60vw, 28vh); }
}
.ev--satelite {
top: var(--y); left: -10px; width: 5px; height: 5px; padding: 8px; background-clip: content-box;
background-color: #fff; border-radius: 50%; box-shadow: 0 0 6px 1px rgb(255 255 255 / .6);
animation: satelite 24s linear forwards, pisca-sat 1.6s steps(1) infinite;
}
@keyframes satelite { to { transform: translate(calc(100vw + 30px), -12vh); } }
@keyframes pisca-sat { 50% { opacity: .25; } }
.ev--ovni { top: var(--y); left: -80px; width: 56px; height: 22px; animation: ovni 15s ease-in-out forwards; }
.ev--ovni[data-lado='d'] { left: auto; right: -80px; animation-name: ovni-d; }
@keyframes ovni { 0% { transform: translate(0, 0); } 35% { transform: translate(40vw, 6vh); } 65% { transform: translate(62vw, -4vh); } 100% { transform: translate(calc(100vw + 120px), 8vh); } }
@keyframes ovni-d { 0% { transform: translate(0, 0); } 35% { transform: translate(-40vw, 6vh); } 65% { transform: translate(-62vw, -4vh); } 100% { transform: translate(calc(-100vw - 120px), 8vh); } }
.ev--ovni::before {
content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 11px; border-radius: 50%;
background: linear-gradient(#cfc4ff, #6a55c9); box-shadow: 0 0 14px rgb(var(--ciano-rgb) / .45);
}
.ev--ovni::after {
content: ''; position: absolute; left: 18px; top: 1px; width: 20px; height: 13px; border-radius: 50% 50% 0 0;
background: rgb(var(--ciano-rgb) / .55);
}
.ev--ovni { animation-play-state: running; }
.ev--ovni.ev--feixe { animation-play-state: paused; filter: drop-shadow(0 0 14px var(--rosa)); }
.ev--ovni.ev--feixe::before { animation: luzes .35s steps(2) infinite; }
@keyframes luzes { 50% { background: linear-gradient(#ffd1ea, #c24d8c); } }
.ev--ovni .ev__feixe { display: none; }
.ev--ovni.ev--feixe { background: linear-gradient(to bottom, transparent 55%, rgb(255 111 181 / .0) 55%); }
.ev--ovni.ev--feixe::after { background: rgb(255 111 181 / .7); }
.ev--ovni.ev--some { opacity: 0; transition: opacity .8s ease; }
.palco__leitura {
position: fixed; z-index: 4; left: var(--margem); bottom: 1.1rem; margin: 0; max-width: min(70ch, calc(100vw - 2 * var(--margem)));
font-size: var(--t-micro); letter-spacing: .14em; text-transform: uppercase; color: var(--ciano);
opacity: 0; transition: opacity .4s ease; pointer-events: none;
}
.palco__leitura--on { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .ceu__camada { animation: none; } .palco { display: none; } }
.digita__cursor {
display: inline-block; width: .55ch; height: 1.05em; margin-left: .15ch;
background: var(--ciano); vertical-align: -.16em; animation: pisca 1s steps(1) infinite;
}
.digita__cursor--livre { opacity: .7; }
@keyframes pisca { 50% { opacity: 0; } } LUCAS NEGRELLI LUCAS NEGRELLI / São Paulo, SP
Sou designer e estudo segurança da informação. Já desenhei muito site e já limpei um que foi invadido. Esta página testa a própria segurança enquanto você lê, e o resultado está ao lado.
Procurando estágio ou vaga N1 em SOC, NOC ou suporte técnico. Disponível no período diurno (curso a FATEC à noite).
Precisa de design ou de um site novo? Veja meu trabalho de direção de arte e sites →
01. Os três lados
Design
Faculdade Impacta
Estudei na Impacta e desenhei interface por dez anos. Aprendi a decidir o que a pessoa vê primeiro e o que nem precisa aparecer.
Conversão
V4 Company
Foram quase três anos fazendo landing page, anúncio e e-mail na V4, sempre olhando o resultado depois que a página ia ao ar. Hoje eu já desenho pensando em quem vai clicar e em quem não vai.
Segurança
FATEC
Faço Segurança da Informação na FATEC. Antes disso já tinha entrado num site invadido pra limpar: webshell, usuário falso, banco com dado plantado. Depois dessa, parei de construir do jeito mais fácil.
Cada uma dessas três é comum sozinha. O que me interessa é fazer as três no mesmo projeto.
02. Medição em tempo real
Nota
··
medindo
Quando esta página carregou, o navegador pediu o arquivo pro servidor e recebeu junto uma lista de instruções de segurança. Isso se chama cabeçalho de resposta. A lista abaixo é essa resposta, lida agora, no seu navegador. Não é print nem texto que eu digitei.
Você está rodando local. Os cabeçalhos são
aplicados pelo servidor de produção, então aqui eles aparecem como
ausentes. Em lucasnegrelli.com a lista fecha completa.
Conferir no securityheaders.com Conferir no Mozilla Observatory
Serve pra evitar: que um script injetado na página consiga rodar
Serve pra evitar: que a conexão caia pra HTTP e alguém leia no meio do caminho
Serve pra evitar: que outro site abra este por dentro pra roubar o seu clique
Serve pra evitar: que o navegador adivinhe o tipo do arquivo e execute sem querer
Serve pra evitar: que o endereço da página vaze pro site que você clicou
Serve pra evitar: que câmera, microfone e localização fiquem disponíveis à toa
Serve pra evitar: que outra aba consiga bisbilhotar a memória desta
Serve pra evitar: que outro site carregue arquivos daqui sem permissão
03. Prova prática
Configurar um cabeçalho é fácil. Difícil é saber se ele funciona. Cada botão abaixo tenta um ataque de verdade contra esta página, e o texto que aparece depois é o navegador respondendo, não uma animação minha.
Ataque 01
Vou colocar um script dentro desta página e mandar ele rodar. É o que um XSS faz depois de achar um campo que não filtra o que a pessoa digita.
Ataque 02
Vou tentar abrir este site dentro de um quadro invisível. É assim que funciona o golpe: o atacante põe o site verdadeiro por cima de uma página falsa e você clica achando que está em outro lugar.
Ataque 03
Vou baixar um arquivo de texto e ver se o servidor mandou o navegador respeitar o tipo declarado. Sem essa ordem, o navegador tenta adivinhar o que o arquivo é e pode acabar executando ele.
Se você abrir o console do navegador antes de clicar, vai ver as mesmas violações registradas lá.
04. Continua em