mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
* extension: kinpaku UI for the popup, DevTools panel, sidebar and overlays The extension still wore magenta (#d6336c on the toolbar badge, oklch(48% 0.25 350) through the DevTools panel and the Elements sidebar) and a cream popup left over from the old dark system. It now wears the brand the site ships in impeccable-site PR #34: paper and instruments, one light theme. - extension/shared/kinpaku.css is the one token layer the three pages link. Values are copied from site/styles/kinpaku-tokens.css, and the header states the four rules that shape the rest: paper and ink, gold as jewelry, patina carries colored text, a dark surface is a control. - Severity has one language. An AI tell gets the detector's gold tag and a lit gold dot; a quality issue gets patina; a scan that did not run gets vermilion. Selectors and details are patina-deep, which clears 4.5:1 on paper, and gold never carries text anywhere. - Popup: neutral paper, the mark as a carved ink tile, an ink primary button and a paper cap for the secondary, and the count in ink over a rule that lights gold when there is something to report. - Panel and sidebar: paper in both DevTools themes, since the brand has no dark theme any more. The panel handles the seam instead of inverting, with a hard top edge under .theme-dark. Segmented controls and the switch are paper hardware: a recessed track, a raised cap, a lit gold dot. - Overlays in the page: a gold hairline plus a soft outer glow instead of a 2px outline, and the label chip is now the tag, ink on gold in the mono face, matching what live mode draws. - The toolbar badge is gold with dark ink text (about 11.8:1); Chrome's default white badge text does not clear 4.5:1 on gold. Behavior is untouched: every id, class hook and message the popup, panel, sidebar and content script depend on is unchanged, and the one markup change beyond the stylesheet links is a severity class on the sidebar's kind label. The extension smoke suite passes on all nine fixtures with no service worker or offscreen errors, `bun run test` is green with a local engine build, and `web-ext lint` reports the same two Firefox-only errors and the same fifteen warnings as origin/main. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * extension: guard setBadgeTextColor, and make the dark seam paint-only Two review findings from #729. setBadgeTextColor arrived in Chrome 110 and does not exist on Firefox's action API. `chrome.action.setBadgeTextColor?.({...}).catch(...)` still evaluates `.catch` on the undefined the optional call returns, so on a browser without the method updateBadge raised a TypeError that escaped into whatever asked for the badge update. Both the method and its return value are checked now, and tests/extension-build.test.mjs pins the shape: no `?.(...).catch`, an existence check around the call, a promise check before `.catch`, and the gold and ink values with the magenta one gone. The dark-DevTools seam is a fixed 2px line at the top of the sheet, above the sticky toolbar because that is what keeps it in place while the panel scrolls under it. It now sets pointer-events: none, so it is paint and nothing else and the toolbar's top row of pixels stays clickable. extension/shared/ joins the detector suite's trigger list, since the shared stylesheet is part of extension packaging. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * overlay: the page banner is a paper bar, not a gold fill The last place gold was still upholstery. A full-width gold bar across someone else's page is a fill, and the system holds gold to a mark, a line, a lit dot, or a tag. The banner is now paper with ink text, a gold hairline along its bottom edge, and the detector's lit gold dot at its head, the same indicator the panel puts on a section. Each page-level finding wears the tag the panel gives it: gold with ink for an AI tell, a quiet gray chip for a quality issue, with the detail beside it in ink rather than folded into the chip. The bar carries a shadow of its own so it still separates from a dark host page, and the standalone controls take muted ink now that they sit on paper instead of gold. crates/live/assets/detect-antipatterns-browser.js is the regenerated tracked artifact, so live mode and the site's detector get the same banner. Extension smoke: all nine fixtures pass, no service worker or offscreen errors. bun run test green with a local engine build. web-ext lint reports the same two Firefox-only errors and fifteen warnings as origin/main. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY --------- Co-authored-by: Claude Code <noreply@anthropic.com>
653 lines
14 KiB
CSS
653 lines
14 KiB
CSS
/*
|
|
* DevTools panel: a paper sheet laid on the tool.
|
|
*
|
|
* Tokens, the severity language and the theme decision live in
|
|
* ../shared/kinpaku.css. The short version, because it shapes every rule
|
|
* below: one light theme in both DevTools themes; gold is the mark, the
|
|
* tag and the lit dot on a control; patina carries any colored text;
|
|
* selectors and counts are mono, as they are in the agent.
|
|
*
|
|
* How a finding reads here:
|
|
* AI tells gold. A lit gold dot on the section, the count as the
|
|
* detector's tag, the section name in ink.
|
|
* Quality patina. A patina dot, a quiet gray count, a muted name.
|
|
* Selector patina-deep mono in both sections: it is the one piece of
|
|
* colored text, and patina is the color that may carry text.
|
|
*
|
|
* Controls are paper hardware: a recessed track, a raised cap that stands
|
|
* a millimetre proud, and a gold dot that lights when the cap is the one
|
|
* selected.
|
|
*/
|
|
|
|
body {
|
|
height: 100vh;
|
|
overflow-y: auto;
|
|
font-size: var(--ks-type-label);
|
|
}
|
|
|
|
/* ============================================================
|
|
Toolbar
|
|
============================================================ */
|
|
.toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
padding: 7px 10px;
|
|
background: var(--ks-paper);
|
|
border-bottom: 1px solid var(--ks-rule);
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 10;
|
|
}
|
|
|
|
.toolbar-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.toolbar-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 2px;
|
|
}
|
|
|
|
h1 {
|
|
font-family: var(--ks-font-wordmark);
|
|
font-size: var(--ks-type-ui);
|
|
font-weight: 600;
|
|
letter-spacing: 0.16em;
|
|
line-height: 1;
|
|
text-transform: uppercase;
|
|
color: var(--ks-ink);
|
|
}
|
|
|
|
/* The total, as the detector's own tag. */
|
|
.badge {
|
|
display: none;
|
|
align-items: center;
|
|
min-height: 16px;
|
|
padding: 0 6px;
|
|
border-radius: var(--ks-radius-sm);
|
|
background: var(--ks-kinpaku);
|
|
color: var(--ks-on-gold);
|
|
font-family: var(--ks-mono);
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.04em;
|
|
line-height: 1;
|
|
}
|
|
|
|
.badge.visible {
|
|
display: inline-flex;
|
|
}
|
|
|
|
/* Tool buttons: a cap that rises under the pointer. */
|
|
.tool-btn {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 26px;
|
|
height: 26px;
|
|
border: 1px solid transparent;
|
|
border-radius: var(--ks-radius-sm);
|
|
background: transparent;
|
|
color: var(--ks-text-muted);
|
|
cursor: pointer;
|
|
transition:
|
|
background-color var(--ks-quick) var(--ks-ease),
|
|
border-color var(--ks-quick) var(--ks-ease),
|
|
color var(--ks-quick) var(--ks-ease);
|
|
}
|
|
|
|
.tool-btn:hover {
|
|
background: var(--ks-paper-raised);
|
|
border-color: var(--ks-rule);
|
|
box-shadow: var(--ks-cap-lift);
|
|
color: var(--ks-ink);
|
|
}
|
|
|
|
.tool-btn:active {
|
|
box-shadow: var(--ks-cap-press);
|
|
}
|
|
|
|
/* Held down: a raised cap with its indicator lit. */
|
|
.tool-btn.active {
|
|
background: var(--ks-paper-raised);
|
|
border-color: var(--ks-rule);
|
|
box-shadow: var(--ks-cap-lift);
|
|
color: var(--ks-ink);
|
|
}
|
|
|
|
.tool-btn.active::after {
|
|
content: "";
|
|
position: absolute;
|
|
bottom: 2px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
width: 4px;
|
|
height: 4px;
|
|
border-radius: 50%;
|
|
background: var(--ks-kinpaku);
|
|
box-shadow: var(--ks-led);
|
|
}
|
|
|
|
/* Overlays hidden: the eye reads off. */
|
|
.tool-btn.inactive {
|
|
color: var(--ks-text-mute-deep);
|
|
}
|
|
|
|
/* Copied: a state, so patina. */
|
|
.tool-btn.copied {
|
|
color: var(--ks-patina-deep);
|
|
}
|
|
|
|
/* ============================================================
|
|
Findings
|
|
============================================================ */
|
|
#findings-container {
|
|
padding: 6px 8px 16px;
|
|
}
|
|
|
|
.category-section {
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.category-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 10px 6px 4px;
|
|
}
|
|
|
|
.category-dot {
|
|
width: 7px;
|
|
height: 7px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* AI tells: the lit gold dot. Quality: patina. */
|
|
.category-dot-slop {
|
|
background: var(--ks-kinpaku);
|
|
box-shadow: var(--ks-led);
|
|
}
|
|
|
|
.category-dot-quality {
|
|
background: var(--ks-patina);
|
|
}
|
|
|
|
.category-name {
|
|
font-family: var(--ks-mono);
|
|
font-size: var(--ks-type-micro);
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.12em;
|
|
color: var(--ks-text-muted);
|
|
}
|
|
|
|
.category-slop .category-name {
|
|
color: var(--ks-ink);
|
|
}
|
|
|
|
/* The count is the tag in an AI-tell section, a quiet chip in a quality one. */
|
|
.category-count {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
min-height: 15px;
|
|
padding: 0 5px;
|
|
border-radius: var(--ks-radius-sm);
|
|
background: var(--ks-gray);
|
|
color: var(--ks-text-muted);
|
|
font-family: var(--ks-mono);
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
line-height: 1;
|
|
}
|
|
|
|
.category-slop .category-count {
|
|
background: var(--ks-kinpaku);
|
|
color: var(--ks-on-gold);
|
|
}
|
|
|
|
.finding-group {
|
|
margin-bottom: 1px;
|
|
}
|
|
|
|
.group-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 5px 6px;
|
|
border-radius: var(--ks-radius-sm);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
transition: background-color var(--ks-quick) var(--ks-ease);
|
|
}
|
|
|
|
.group-header:hover {
|
|
background: var(--ks-paper-deep);
|
|
}
|
|
|
|
.group-chevron {
|
|
width: 12px;
|
|
flex-shrink: 0;
|
|
font-size: 9px;
|
|
color: var(--ks-text-faint);
|
|
transition: transform var(--ks-quick) var(--ks-ease);
|
|
}
|
|
|
|
.group-header.collapsed .group-chevron {
|
|
transform: rotate(-90deg);
|
|
}
|
|
|
|
.group-name {
|
|
flex: 1;
|
|
min-width: 0;
|
|
font-size: var(--ks-type-label);
|
|
font-weight: 600;
|
|
color: var(--ks-ink);
|
|
}
|
|
|
|
.group-count {
|
|
font-family: var(--ks-mono);
|
|
font-size: 10px;
|
|
color: var(--ks-text-faint);
|
|
}
|
|
|
|
.group-items {
|
|
overflow: hidden;
|
|
}
|
|
|
|
.group-header.collapsed + .group-items {
|
|
display: none;
|
|
}
|
|
|
|
.finding-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1px;
|
|
padding: 4px 6px 5px 26px;
|
|
border-radius: var(--ks-radius-sm);
|
|
cursor: pointer;
|
|
transition: background-color var(--ks-quick) var(--ks-ease);
|
|
}
|
|
|
|
.finding-item:hover {
|
|
background: var(--ks-paper-deep);
|
|
}
|
|
|
|
.finding-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.finding-row .finding-selector {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
/* The one colored text in the list. Patina clears 4.5:1 on paper. */
|
|
.finding-selector {
|
|
font-family: var(--ks-mono);
|
|
font-size: var(--ks-type-micro);
|
|
color: var(--ks-patina-deep);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.finding-detail {
|
|
font-size: var(--ks-type-micro);
|
|
color: var(--ks-text);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.finding-description {
|
|
display: none;
|
|
padding: 3px 0 2px;
|
|
font-size: var(--ks-type-micro);
|
|
line-height: 1.45;
|
|
color: var(--ks-text-muted);
|
|
}
|
|
|
|
.finding-item:hover .finding-description {
|
|
display: block;
|
|
}
|
|
|
|
.finding-copy {
|
|
display: none;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 18px;
|
|
height: 18px;
|
|
flex-shrink: 0;
|
|
border: 1px solid transparent;
|
|
border-radius: var(--ks-radius-sm);
|
|
background: transparent;
|
|
color: var(--ks-text-faint);
|
|
cursor: pointer;
|
|
transition: background-color var(--ks-quick) var(--ks-ease), color var(--ks-quick) var(--ks-ease);
|
|
}
|
|
|
|
.finding-item:hover .finding-copy {
|
|
display: flex;
|
|
}
|
|
|
|
.finding-copy:hover {
|
|
background: var(--ks-paper-raised);
|
|
border-color: var(--ks-rule);
|
|
color: var(--ks-ink);
|
|
}
|
|
|
|
.finding-copy.copied {
|
|
color: var(--ks-patina-deep);
|
|
}
|
|
|
|
/* Tags on a finding: quiet by default. Gold is reserved for the section
|
|
and the total, so a row-level tag never competes with it. */
|
|
.finding-tag {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
align-self: flex-start;
|
|
min-height: 14px;
|
|
margin-bottom: 2px;
|
|
padding: 0 5px;
|
|
border-radius: var(--ks-radius-sm);
|
|
font-family: var(--ks-mono);
|
|
font-size: 9px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.06em;
|
|
line-height: 1;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.tag-page {
|
|
background: var(--ks-patina-pale);
|
|
color: var(--ks-patina-ink);
|
|
}
|
|
|
|
.tag-hidden {
|
|
background: var(--ks-gray);
|
|
color: var(--ks-text-muted);
|
|
}
|
|
|
|
.finding-item.is-hidden {
|
|
opacity: 0.6;
|
|
cursor: default;
|
|
}
|
|
|
|
.finding-item.is-hidden:hover {
|
|
background: transparent;
|
|
}
|
|
|
|
/* ============================================================
|
|
Empty state
|
|
============================================================ */
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 56px 24px;
|
|
text-align: center;
|
|
}
|
|
|
|
.empty-icon {
|
|
width: 26px;
|
|
height: 26px;
|
|
margin-bottom: 14px;
|
|
border-radius: var(--ks-radius-sm);
|
|
background: var(--ks-gray);
|
|
color: var(--ks-text-mute-deep);
|
|
font-family: var(--ks-font-wordmark);
|
|
font-size: 17px;
|
|
font-weight: 600;
|
|
line-height: 26px;
|
|
}
|
|
|
|
.empty-title {
|
|
margin-bottom: 3px;
|
|
font-size: var(--ks-type-ui);
|
|
font-weight: 600;
|
|
color: var(--ks-ink);
|
|
}
|
|
|
|
.empty-hint {
|
|
font-size: var(--ks-type-micro);
|
|
color: var(--ks-text-muted);
|
|
}
|
|
|
|
/* ============================================================
|
|
Settings. Every control here is paper hardware.
|
|
============================================================ */
|
|
#settings-container {
|
|
padding: 0 8px 10px;
|
|
background: var(--ks-paper-deep);
|
|
border-bottom: 1px solid var(--ks-rule);
|
|
}
|
|
|
|
.settings-header {
|
|
padding: 10px 6px 6px;
|
|
font-family: var(--ks-mono);
|
|
font-size: var(--ks-type-micro);
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.12em;
|
|
color: var(--ks-text-muted);
|
|
}
|
|
|
|
#settings-list .settings-header {
|
|
padding: 12px 6px 4px;
|
|
}
|
|
|
|
.settings-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 0 12px;
|
|
padding-bottom: 6px;
|
|
}
|
|
|
|
.setting-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
padding: 4px 6px 8px;
|
|
}
|
|
|
|
.setting-label {
|
|
font-size: var(--ks-type-label);
|
|
color: var(--ks-text);
|
|
}
|
|
|
|
/* Segmented control: a recessed track carrying raised caps. */
|
|
.setting-segmented {
|
|
display: inline-flex;
|
|
gap: 2px;
|
|
padding: 2px;
|
|
border-radius: var(--ks-radius-sm);
|
|
background: var(--ks-gray);
|
|
box-shadow: var(--ks-track-recess);
|
|
}
|
|
|
|
.setting-segmented button {
|
|
position: relative;
|
|
padding: 3px 9px 3px 16px;
|
|
border: none;
|
|
border-radius: 2px;
|
|
background: transparent;
|
|
color: var(--ks-text-muted);
|
|
font-family: inherit;
|
|
font-size: var(--ks-type-micro);
|
|
font-weight: 500;
|
|
line-height: 1.4;
|
|
cursor: pointer;
|
|
transition:
|
|
background-color var(--ks-quick) var(--ks-ease),
|
|
color var(--ks-quick) var(--ks-ease),
|
|
box-shadow var(--ks-quick) var(--ks-ease);
|
|
}
|
|
|
|
/* The key's indicator: dark at rest, lit gold on the selected cap. */
|
|
.setting-segmented button::before {
|
|
content: "";
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 6px;
|
|
transform: translateY(-50%);
|
|
width: 5px;
|
|
height: 5px;
|
|
border-radius: 50%;
|
|
background: var(--ks-gray-2);
|
|
box-shadow: inset 0 1px 1px oklch(13% 0 0 / 0.18);
|
|
transition: background-color var(--ks-quick) var(--ks-ease), box-shadow var(--ks-quick) var(--ks-ease);
|
|
}
|
|
|
|
.setting-segmented button:hover {
|
|
color: var(--ks-ink);
|
|
}
|
|
|
|
.setting-segmented button.active {
|
|
background: var(--ks-paper-raised);
|
|
box-shadow: var(--ks-cap-lift);
|
|
color: var(--ks-ink);
|
|
}
|
|
|
|
.setting-segmented button.active::before {
|
|
background: var(--ks-kinpaku);
|
|
box-shadow: var(--ks-led);
|
|
}
|
|
|
|
.setting-segmented button:active {
|
|
box-shadow: var(--ks-cap-press);
|
|
transform: translateY(1px);
|
|
}
|
|
|
|
/* Switch: a recessed track, a paper knob, a dot that lights when on. */
|
|
.setting-switch {
|
|
position: relative;
|
|
display: inline-block;
|
|
width: 30px;
|
|
height: 18px;
|
|
flex-shrink: 0;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.setting-switch input {
|
|
position: absolute;
|
|
opacity: 0;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
|
|
.setting-switch-track {
|
|
position: absolute;
|
|
inset: 0;
|
|
border-radius: var(--ks-radius-pill);
|
|
background: var(--ks-gray-2);
|
|
box-shadow: var(--ks-track-recess);
|
|
}
|
|
|
|
.setting-switch-track::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 2px;
|
|
left: 2px;
|
|
width: 14px;
|
|
height: 14px;
|
|
border-radius: 50%;
|
|
background: var(--ks-paper-raised);
|
|
box-shadow: var(--ks-cap-lift);
|
|
transition: transform 220ms cubic-bezier(0.3, 0.7, 0.2, 1);
|
|
}
|
|
|
|
.setting-switch-track::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 7px;
|
|
left: 7px;
|
|
width: 4px;
|
|
height: 4px;
|
|
border-radius: 50%;
|
|
background: var(--ks-gray-2);
|
|
box-shadow: inset 0 1px 1px oklch(13% 0 0 / 0.18);
|
|
transition:
|
|
transform 220ms cubic-bezier(0.3, 0.7, 0.2, 1),
|
|
background-color var(--ks-quick) var(--ks-ease),
|
|
box-shadow var(--ks-quick) var(--ks-ease);
|
|
}
|
|
|
|
.setting-switch input:checked + .setting-switch-track::before,
|
|
.setting-switch input:checked + .setting-switch-track::after {
|
|
transform: translateX(12px);
|
|
}
|
|
|
|
.setting-switch input:checked + .setting-switch-track::after {
|
|
background: var(--ks-kinpaku);
|
|
box-shadow: var(--ks-led);
|
|
}
|
|
|
|
.setting-switch input:focus-visible + .setting-switch-track {
|
|
outline: 2px solid var(--ks-patina);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
/* Rule toggles */
|
|
.setting-rule {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 7px;
|
|
padding: 2px 6px;
|
|
border-radius: var(--ks-radius-sm);
|
|
font-size: var(--ks-type-micro);
|
|
cursor: pointer;
|
|
transition: background-color var(--ks-quick) var(--ks-ease);
|
|
}
|
|
|
|
.setting-rule:hover {
|
|
background: var(--ks-gray);
|
|
}
|
|
|
|
.setting-rule input[type="checkbox"] {
|
|
margin: 0;
|
|
accent-color: var(--ks-patina-deep);
|
|
}
|
|
|
|
/* ============================================================
|
|
Scanning
|
|
============================================================ */
|
|
.scanning-indicator {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 14px 12px;
|
|
font-size: var(--ks-type-label);
|
|
color: var(--ks-text-muted);
|
|
}
|
|
|
|
.scanning-dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 50%;
|
|
background: var(--ks-kinpaku);
|
|
box-shadow: var(--ks-led);
|
|
animation: pulse 1s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 0.35; }
|
|
50% { opacity: 1; }
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.scanning-dot { animation: none; }
|
|
}
|