${escapeHtml(layout.name)}${layout.routes.length
? escapeHtml(layout.routes.map((route) => tr(DISCOVERY_ROUTE_LABELS[route])).join(' · '))
: escapeHtml(tr('Applied only when you press it'))}${active ? ` · ${escapeHtml(tr('Currently applied'))}` : ''}
`;
}).join('')
: `
No saved views yet. Set the page up the way you want it, name it, and save.
`;
return `
Saved views
Keep the density, thumbnail size, rails, and content filters you like as a named view, and have it applied when you open the pages you chose. It is your own settings, applied to what Kick already sent. It changes nothing about what Kick recommends or the order anything appears in.
Save this page as a view
Pick the pages it should apply to, or none to keep it manual.
${routeChips}
${saved}
`;
}
function renderLayoutPage() {
const value = state.settings.layout;
return `
${pageHeader('Layout', 'Control how Kick is arranged across your desktop.', 'Current setup', `${value.sidebar} sidebar · ${value.chat} chat`)}
${row('Sidebar mode', 'Choose how the left discovery rail behaves. Dropdown collapses it to a tab that expands on hover, giving the grid full width. Desktop widths only.', segmented('layout.sidebar', value.sidebar, [['auto','Auto'],['compact','Compact'],['dropdown','Dropdown'],['hidden','Hidden']]))}
${row('Chat layout', 'Place chat on either side, float it as a dock, or hide it.', segmented('layout.chat', value.chat, [['right','Right'],['left','Left'],['docked','Docked'],['hidden','Hidden']]))}
${row('Chat width', 'Set the width of the live chat column.', range('layout.chatWidth', value.chatWidth, 320, 520, '320 px', '520 px', ' px'), { wide: true })}
${row('Content density', 'Adjust spacing and padding across discovery pages.', segmented('layout.density', value.density, [['comfortable','Comfortable'],['compact','Compact']]))}
${row('Stream start behavior', 'Choose how each channel opens.', segmented('layout.streamStart', value.streamStart, [['standard','Standard'],['theater','Theater'],['focus','Focus']]))}
${row('Remember per-channel layout', 'Keep the last runtime layout for each channel.', toggle('layout.rememberPerChannel', value.rememberPerChannel, { label: 'Remember per-channel layout' }))}
${row('Widen browse grids', 'Use reclaimed sidebar space for larger, calmer stream cards.', toggle('layout.wideGrid', value.wideGrid, { label: 'Widen browse grids' }))}
${row('Show Following rail', 'Keep the Following discovery rail visible when Kick provides it.', toggle('layout.showFollowingRail', value.showFollowingRail, { label: 'Show Following rail' }))}
${row('Show Recommended rail', 'Keep recommended stream rows visible in the main content.', toggle('layout.showRecommendedRail', value.showRecommendedRail, { label: 'Show Recommended rail' }))}
${row('Hide Kick’s own controls', 'Switch off the player buttons and sidebar entries you never use. Each one is hidden with styling only. Nothing is clicked or removed, and turning it back on restores it immediately.', hideElementGrid(value.hidden), { wide: true })}
${row('Sticky compact top bar', 'Keep search and account controls available while browsing.', toggle('layout.stickyTopbar', value.stickyTopbar, { label: 'Sticky compact top bar' }))}
${row('Show quick command button', 'Keep the Focus control beside Get KICKs in Kick’s top header.', toggle('layout.quickButton', value.quickButton, { label: 'Show quick command button' }))}
${row('Move mini-player clear of controls', 'Raise Kick’s embedded mini-player only when the Focus control has to use its floating fallback.', toggle('layout.miniPlayerCollision', value.miniPlayerCollision, { label: 'Move mini-player clear of controls' }))}
${row('Recover player after resize', 'Re-apply player geometry after a window or monitor change.', toggle('layout.playerResizeRecovery', value.playerResizeRecovery, { label: 'Recover player after resize' }))}
${row('Keep ultrawide video uncropped', 'Prefer contained video geometry on wide or moved displays.', toggle('layout.playerContainVideo', value.playerContainVideo, { label: 'Keep ultrawide video uncropped' }))}
${renderDiscoveryLayouts()}`;
}
function renderAppearancePage() {
const value = state.settings.appearance;
const themes = [
['studio', 'Studio', 'Layered graphite', 'Balanced depth with a quiet green undertone.'],
['oled', 'OLED', 'True black', 'Minimal lift and maximum contrast for dark rooms.'],
['slate', 'Slate', 'Cool graphite', 'Blue-toned surfaces with stronger separation.'],
];
const accents = [['kick','Kick Green'],['cyan','Cyan'],['violet','Violet'],['gold','Gold'],['custom','Custom']];
const presets = [
['calm', 'Calm', 'Roomier cards, quieter live color, and a compact rail.'],
['cinema', 'Cinema', 'OLED surfaces with the player first and chrome tucked away.'],
['chat', 'Chat First', 'A wider docked chat, compact density, and a clearer accent.'],
['discovery', 'Discovery', 'More stream cards, vivid thumbnails, and both discovery rails.'],
];
return `
Kick Focus settings
Appearance
Choose a clear visual direction, then tune only what matters to you.
Comma-separated words are highlighted locally in chat. They never leave this browser.
Private channel note
Keep a local reminder for this channel. It is not sent to Kick.
Save local channel tools
Only this channel path and the values above are stored.
`;
}
function remoteBlocklistControls() {
const value = state.settings.content;
return `
Optional data-only blocklist
Fetch a user-supplied JSON list of channels, categories, and keywords. No code is accepted or executed.
${row('Enable subscription', 'Off by default. When enabled, refreshes only over HTTPS with credentials omitted.', toggle('content.blocklistSubscription', value.blocklistSubscription, { label: 'Enable optional blocklist subscription' }))}
HTTPS JSON URL
Expected fields: channels, categories, and keywords. Unknown fields are rejected.
${row('Refresh interval', 'Keep the last valid payload if a later request fails.', segmented('content.blocklistRefreshHours', value.blocklistRefreshHours, [[6,'6 h'],[12,'12 h'],[24,'24 h'],[72,'72 h']]))}
${escapeHtml(remoteBlocklistSummary())}
`;
}
function emoteInventorySummary() {
const account = state.live.catalog?.account;
if (!account?.authenticated) return '';
const sets = account.ownedSets.length;
if (!account.ownedEmotes) return tr('Kick reports no emotes this account can send anywhere.');
const from = sets
? `${sets} ${plural(sets, 'subscribed channel', 'subscribed channels')}`
: tr('your global sets');
return `${trf('{count} emotes usable in any chat', { count: account.ownedEmotes })} · ${from}`;
}
function stickerLibrarySummary() {
const library = [...state.stickerPreferences.library.values()];
const locked = library.filter((sticker) => sticker.access === 'locked').length;
const channel = library.filter((sticker) => sticker.access === 'channel').length;
const observed = library.filter((sticker) => sticker.access === 'observed').length;
const changed = countChangedStickers(library);
const atCapacity = library.length >= STICKER_LIBRARY_LIMIT;
return `${library.length} recorded · ${favoriteCount()} favorites · ${state.stickerPreferences.hidden.size} removed · ${state.stickerPreferences.groups.length} custom groups${channel ? ` · ${channel} channel-only` : ''}${observed ? ` · ${observed} seen in chat` : ''}${locked ? ` · ${locked} subscriber-only` : ''}${changed ? ` · ${changed} changed by Kick` : ''}${atCapacity ? ` · full (${STICKER_LIBRARY_LIMIT}); oldest chat-only emotes drop first` : ''}`;
}
function stickerSeenSummary(sticker) {
if (!sticker.firstSeen) return '';
const day = (time) => new Date(time).toISOString().slice(0, 10);
const first = day(sticker.firstSeen);
const last = sticker.lastSeen ? day(sticker.lastSeen) : '';
return last && last !== first ? `First seen ${first} · last ${last}` : `First seen ${first}`;
}
function stickerLibraryFilterMatches(sticker, filter) {
if (filter === 'mine') return sticker.access === 'available' && sticker.usableEverywhere === true;
if (filter === 'favorites') return isFavorited(sticker.key);
if (filter === 'removed') return state.stickerPreferences.hidden.has(sticker.key);
if (filter === 'changed') return stickerChangedSinceCapture(sticker);
if (filter === 'observed') return sticker.access === 'observed';
if (filter === 'channel') return sticker.access === 'channel';
if (filter === 'locked') return sticker.access === 'locked';
if (filter === 'ungrouped') return !state.stickerPreferences.assignments.has(sticker.key);
if (filter.startsWith('group:')) return state.stickerPreferences.assignments.get(sticker.key) === filter.slice(6);
return true;
}
function stickerGroupOptions(selectedGroup = '') {
return `${state.stickerPreferences.groups.map((group) => ``).join('')}`;
}
function stickerLibraryCard(sticker) {
const favorite = isFavorited(sticker.key);
const removed = state.stickerPreferences.hidden.has(sticker.key);
const groupId = state.stickerPreferences.assignments.get(sticker.key) || '';
const chosen = state.runtime.stickerLibrarySelection.has(sticker.key);
const nativeGroups = sticker.nativeGroups.length ? sticker.nativeGroups.join(', ') : 'Unknown Kick group';
const searchText = `${sticker.name} ${nativeGroups} ${sticker.sourceSlug || ''}`.toLowerCase();
const accessLabel = emoteAccessLabel(sticker.access);
const reach = emoteReach(sticker);
const reachNote = reach.text ? trf(reach.text, { channel: reach.channel }) : '';
const changeNote = describeStickerChange(sticker);
const seenNote = stickerSeenSummary(sticker);
const lock = sticker.access === 'locked'
? emoteLockState({ ...sticker, locked: true }, sticker.nativeGroups[0] || '')
: { locked: false, reason: '', unlockUrl: '' };
return `
`).join('');
const inventory = emoteInventorySummary();
const accountKnown = Boolean(state.live.catalog?.account?.authenticated);
const myEmotesEmpty = accountKnown
? 'Kick reports no emotes this account can use in every chat.'
: 'Sign in to Kick and open any channel once to load your owned emotes. Nothing is sent or changed.';
const disabled = chosen.size ? '' : ' disabled';
return `
${filter === 'mine' ? 'My emotes' : 'Library'}
${escapeHtml(stickerLibrarySummary())}
${inventory ? `
${escapeHtml(inventory)}
` : ''}
Add emotes from a channel
Paste a channel name or Kick URL. Access rules still apply.
${escapeHtml(state.runtime.emoteCatalogStatus)}
${chosen.size} selected
${library.length} shownNew emotes save automatically.
${filter === 'mine' ? (groupedCards || `
${myEmotesEmpty}
`) : filter === 'removed' ? `
${state.stickerPreferences.hidden.size} removed. These emotes can return after you restore them and Kick shows them again.${state.stickerPreferences.hidden.size ? ' ' : ''}
` : cards ? `
${cards}
` : `
${state.stickerPreferences.library.size ? 'No emotes match this view.' : 'Open Kick’s emote picker or watch chat to start the library.'}
`}
`;
}
function renderEmotesPage() {
return `${pageHeader('Emotes', 'Find, favorite, remove, and group every emote you have recorded.', 'Library', `${state.stickerPreferences.library.size}/${STICKER_LIBRARY_LIMIT}`)}${renderStickerLibraryManager()}`;
}
function renderCollectiblePanel() {
const inventory = state.live.inventory;
const changed = countChangedStickers(state.stickerPreferences.library);
const observed = inventory
? (inventory.quantityKnown
? trf('Your inventory holds {copies} {copiesWord} across {distinct} distinct {distinctWord}. That is {duplicates} {duplicatesWord}, or {rate}% of what you have pulled.', {
copies: inventory.copies,
copiesWord: plural(inventory.copies, 'collectible', 'collectibles'),
distinct: inventory.distinct,
distinctWord: plural(inventory.distinct, 'item', 'items'),
duplicates: inventory.duplicates,
duplicatesWord: plural(inventory.duplicates, 'duplicate', 'duplicates'),
rate: Math.round(inventory.duplicateRate * 100),
})
: trf('Your inventory holds {distinct} distinct {distinctWord}. Kick’s response carries no per-item quantity, so a duplicate rate cannot be measured from it. That number is unavailable rather than zero.', {
distinct: inventory.distinct,
distinctWord: plural(inventory.distinct, 'collectible', 'collectibles'),
}))
: 'Open a channel with collectibles while signed in to read your own inventory. Nothing is fetched otherwise.';
return `
What Kick does not explain
${escapeHtml(observed)}${changed ? ` ${changed} ${plural(changed, 'recorded emote has been changed by Kick since first capture. See the Changed by Kick filter in the library below.', 'recorded emotes have been changed by Kick since first capture. See the Changed by Kick filter in the library below.')}` : ''}
Read Kick’s own endpoints instead of scraping the page. Same-origin, read-only, using the session you are already signed into. Nothing is sent anywhere.
${escapeHtml(liveStatusSummary())}
${row('Load the emote catalog from Kick', 'Read the full channel, global, and emoji sets without treating public artwork as account access. Falls back to the picker if the response changes shape.', toggle('content.liveEmoteCatalog', value.liveEmoteCatalog, { label: 'Load the emote catalog from Kick' }))}
${row('Follow live chat events', 'Subscribe to the same realtime chat feed Kick’s own client uses. The provider is read from Kick rather than hardcoded.', toggle('content.liveChatEvents', value.liveChatEvents, { label: 'Follow live chat events' }))}
${row('Explain removed messages', 'Kick’s automatic moderation removes messages without saying why. The realtime event carries the reason; the page does not.', toggle('content.showModerationReasons', value.showModerationReasons, { label: 'Explain removed messages' }))}
${row('Show badges Kick leaves out', 'Kick’s chat payload carries collectible and global badges its own markup omits, leaving a gap where other clients show a badge. A badge image that fails to load is replaced by its name.', toggle('content.showChatBadges', value.showChatBadges, { label: 'Show badges Kick leaves out' }))}
${row('Count emote usage', 'Kick’s own “Frequently Used” never counts anything, so no real ranking exists. This one is yours, stored locally and exported with your library.', toggle('content.countEmoteUsage', value.countEmoteUsage, { label: 'Count emote usage' }))}
${row('Show collectible rarity', 'Kick publishes rarity on card art and identity in the picker, with no key joining them. Rarity is shown only where the match is confident.', toggle('content.showEmoteRarity', value.showEmoteRarity, { label: 'Show collectible rarity' }))}
${row('Warn about shadowed emote names', 'Subscriber emotes work in every chat and Kick resolves typed names through one map, so two channels sharing a name means one silently sends the other’s.', toggle('content.warnShadowedEmotes', value.warnShadowedEmotes, { label: 'Warn about shadowed emote names' }))}
${row('Freeze animated emotes', 'Render animated emotes and collectibles as a single static frame, in chat and in the picker. Applied automatically when your system asks for reduced motion.', toggle('content.staticEmotes', value.staticEmotes, { label: 'Freeze animated emotes' }))}
${renderCollectiblePanel()}
${rarity ? `
Collectible rarity
Resolved ${rarity.matched.length} of ${rarity.total} collectible emotes. ${rarity.unmatched.length ? `${rarity.unmatched.length} could not be matched confidently and are shown without a rarity. A wrong label is worse than none.` : 'Every collectible in this channel was matched.'}
` : ''}
${collisions.length ? `
Shadowed emote names
These names exist in more than one of your sets. Kick sends the last one loaded, so typing the name may not send what you expect.
${state.filter.suspended
? `Filtering is suspended on this page. It would have hidden ${state.filter.wouldHide} of ${state.filter.total} cards, which usually means Kick changed its labels rather than that the page is really that promotional. Everything is shown.`
: ''}
Filtering & ad defense
Requests, promotional modules, and sensitive content.
${row('Block separable ad requests', 'Intercept known ad hosts at the earliest userscript-supported page layer.', toggle('content.blockAds', true, { locked: true, label: 'Core ad protection is on' }), { locked: true })}
${row('Remove ad containers', 'Remove empty ad containers and reinjected ad frames.', toggle('content.removeAdContainers', value.removeAdContainers, { label: 'Remove ad containers' }))}
${row('Suppress sponsored and promoted cards', 'Hide clearly labeled promotional cards and modules.', toggle('content.suppressPromoted', value.suppressPromoted, { label: 'Suppress promoted cards' }))}
${row('Pause home-page autoplay', 'Keep background Home previews silent and paused; deliberate playback remains available.', toggle('content.pauseHomeAutoplay', value.pauseHomeAutoplay, { label: 'Pause home-page autoplay' }))}
${row('Hide Slots & Casino content', 'Hide cards and sidebar entries clearly labeled as casino content.', toggle('content.hideCasino', value.hideCasino, { label: 'Hide Slots and Casino content' }))}
${row('Blur mature thumbnails', 'Blur marked mature cards until hover or keyboard focus.', toggle('content.blurMature', value.blurMature, { label: 'Blur mature thumbnails' }))}
${row('Hide Drops and gambling promotions', 'Hide clearly labeled Drops and gambling promotion modules.', toggle('content.hideDropsPromotions', value.hideDropsPromotions, { label: 'Hide Drops and gambling promotions' }))}
${row('Poor mode', 'Hide Subscribe, Gift Subs/Dubs, Get KICKs, gift-shop controls, and spend-based leaderboards. Follow, chat, and free daily rewards stay available.', toggle('content.hideMonetization', value.hideMonetization, { label: 'Poor mode' }))}
${row('Reduce tracking telemetry', 'Block observed third-party video and error telemetry hosts.', toggle('content.reduceTelemetry', value.reduceTelemetry, { label: 'Reduce tracking telemetry' }))}
Hidden channels
Hide specific channels from Home, Browse, Following, and Search.
${value.hiddenChannels.length ? `
${value.hiddenChannels.map((channel) => `
${escapeHtml(channel.replace(/^\//, ''))}
`).join('')}
` : '
No channels hidden. Use the input above or the ✕ action on a card.
'}
${value.hiddenChannels.length} ${plural(value.hiddenChannels.length, 'channel hidden. These count toward the fail-open ceiling.', 'channels hidden. These count toward the fail-open ceiling.')}
Playback & chat
Local playback memory, chat control, emotes, and diagnostics.
${row('Remember volume locally', 'Restore each channel’s volume and mute state from local storage.', toggle('content.rememberVolume', value.rememberVolume, { label: 'Remember volume locally' }))}
${row('Remember quality locally', 'Restore a matching quality control when Kick exposes one.', toggle('content.rememberQuality', value.rememberQuality, { label: 'Remember quality locally' }))}
${row('Always start at the highest quality', 'Open every stream at the best rung Kick offers, taking precedence over remembered quality. The rungs are learned from Kick’s own quality menu, so this does nothing until that menu has been opened once. It will not open it for you.', toggle('content.preferBestQuality', value.preferBestQuality, { label: 'Always start at the highest quality' }))}
${row('Remember VOD position locally', 'Resume finite VODs from the last local playback position.', toggle('content.rememberVodPosition', value.rememberVodPosition, { label: 'Remember VOD position locally' }))}
${row('Show how long the stream has been live', 'Kick sends the start time with every channel and shows it nowhere. This reads that field and counts from it in the player corner, with no extra request and no polling.', toggle('content.showUptime', value.showUptime, { label: 'Show stream uptime' }))}
${row('Show how long Kick keeps this recording', 'Kick deletes recordings after 7 days, or 30 for a verified channel, and shows that deadline nowhere. On a VOD page this reads the recording date from Kick’s own video list and counts down to it. It says nothing at all when the recording is older than the list Kick returns, or when the tier cannot be established. A guess between 7 and 30 days would be a confident wrong date.', toggle('content.showVodExpiry', value.showVodExpiry, { label: 'Show VOD expiry' }))}
${row('Pause chat updates', 'Scrolling the transcript up freezes it, as does the button. Resume is always one control away.', toggle('content.stickyChatPause', value.stickyChatPause, { label: 'Pause chat updates' }))}
${row('Show message times', 'Reveals the timestamp Kick already renders on every message and keeps hidden. It is Kick’s own value, so scrolling back shows when a message was sent rather than when this build first saw it.', toggle('content.chatTimestamps', value.chatTimestamps, { label: 'Show message times' }))}
${row('People worth noticing', 'Names you want to catch in a fast chat. Their messages get a marker of their own, separate from keyword highlights. Comma separated, and stored only in your settings.', ``)}
${row('Sound on a mention', 'A short tone when a message matches your highlights, comes from someone you listed, or says your name. Synthesised in the browser, so nothing is downloaded. Silent while the tab is in the background, silent for your own messages, and never more than once every few seconds.', toggle('content.chatMentionSound', value.chatMentionSound, { label: 'Sound on a mention' }))}
${row('Hide a message for yourself', 'Adds a small dismiss control to each message. It hides that message in your own browser for this session only, changes nothing for anyone else, and offers an undo.', toggle('content.chatHideMessages', value.chatHideMessages, { label: 'Hide a message for yourself' }))}
${row('Recall my sent messages', 'Keep the last five messages sent from this tab in memory. Shift+Up cycles them. Whispers are skipped, reload clears them, and ordinary Arrow Up stays with Kick.', toggle('content.chatComposerRecall', value.chatComposerRecall, { label: 'Recall my sent messages' }))}
${row('Search this session’s chat', 'Keeps what this tab has seen so you can find it again. It stays in memory, never reaches storage, and is gone on reload. Whispers are never recorded, and a message a moderator removes leaves the log the moment the deletion arrives.', toggle('content.chatHistory', value.chatHistory, { label: 'Search this session’s chat' }))}
${value.chatHistory ? `
Session chat log
${state.chatComfort.rows.length} ${plural(state.chatComfort.rows.length, 'message held. Capped at 400 messages, 200 KB, and one hour.', 'messages held. Capped at 400 messages, 200 KB, and one hour.')}
` : ''}
${row('Organize chat emotes', 'Continuously record emotes from live chat and Kick’s picker, then add favorites, removals, search, and custom groups.', toggle('content.organizeChatStickers', value.organizeChatStickers, { label: 'Organize chat emotes' }))}
${row('Click chat emotes to save', 'Click any emote in chat to add it to your favorites. If Kick explicitly marks it as follow-gated, the same click follows its source channel; subscriber access is never bypassed.', toggle('content.clickChatEmotes', value.clickChatEmotes, { label: 'Click chat emotes to save' }))}
${row('Type an emote name into chat', 'Adds a Type in chat action beside Copy name in the emote library. It types the plain name at your cursor and stops. Never the wire token, never an id, and it never sends the message.', toggle('content.insertEmoteName', value.insertEmoteName, { label: 'Type an emote name into chat' }))}
${row('Suggest emotes as you type', 'Typing a colon and two or more letters in chat offers matching emotes from your library, ranked by what you actually send here. Click one to put its plain name at your cursor. Suggestions are clicked, never accepted with a key, so nothing you type is ever captured, and it never sends the message.', toggle('content.emoteAutocomplete', value.emoteAutocomplete, { label: 'Suggest emotes as you type' }))}
${row('Claim the daily reward automatically', 'Opens Kick’s own reward dialog when one is waiting and clicks its claim button for you. It clicks nothing else: a reward Kick has not unlocked yet shows a disabled button, and this leaves it alone rather than trying. It waits until you are not typing, checks at most every ten minutes, and stops for the day once it claims. Signed-in only, because the reward button does not exist otherwise.', toggle('content.autoClaimRewards', value.autoClaimRewards, { label: 'Claim the daily reward automatically' }))}
${escapeHtml(rewardStatusSummary())}
${row('New favorites apply to', 'Global favorites follow you everywhere. Per-channel favorites appear only on the channel you saved them from, above your global ones. Existing favorites are global and are not moved.', segmented('content.favoriteScope', value.favoriteScope, [['global', 'Everywhere'], ['channel', 'This channel']]))}
${row('Highlight chat keywords', 'Use the per-channel keyword list below without sending it anywhere.', toggle('content.chatHighlights', value.chatHighlights, { label: 'Highlight chat keywords' }))}
${row('Show playback diagnostics', 'Show ready state, buffered seconds, and dropped-frame counts on a channel.', toggle('content.playbackDiagnostics', value.playbackDiagnostics, { label: 'Show playback diagnostics' }))}
${row('Start playback without waiting for blocked ad scripts', 'Kick waits on Google PAL, Datazoom, and OM before requesting playback. Blocking them, which this build does, leaves the dead script in the page and the player waits out the full timeout. Removing it lets playback start immediately.', toggle('content.fixPlayerLoading', value.fixPlayerLoading, { label: 'Start playback without waiting for blocked ad scripts' }))}
${renderLiveDataSection(value)}
Emote library
${escapeHtml(stickerLibrarySummary())}
Local discovery choices
Favorites and not-interested choices stay on this device.
Local channel tools
Channel keywords and private notes stay on this device.
Sanitized in-memory diagnostics; query strings are never retained.
Time
Layer
Match
Action
${protectionRows()}
Error log
${escapeHtml(lastCrashSummary())}
Time
Where
Message
${errorLogRows()}
${companion.active
? 'The companion extension blocks known ad hosts at the browser network layer. Server-side stitched media is still delivered inside the stream itself.'
: 'Userscript interception is best-effort and can be bypassed by browser or server-side delivery. Browser-level request guarantees require an extension ruleset.'}
`;
}
function renderAccessibilityPage() {
const value = state.settings.accessibility;
const shortcuts = state.settings.shortcuts;
const rows = [
['command','Open command menu'],['focus','Toggle focus mode'],['chat','Toggle chat'],
['sidebar','Toggle sidebar'],['settings','Open settings'],['mature','Reveal mature thumbnails'],
];
return `
${pageHeader('Accessibility & Shortcuts', 'Improve comfort and keep core actions within reach.', 'Text scale', `${value.textSize}%`)}
${row('Reduce motion', 'Minimize non-essential animations and transitions.', toggle('accessibility.reduceMotion', value.reduceMotion, { label: 'Reduce motion' }))}
${row('High-contrast controls', 'Increase separation for controls, borders, and surfaces.', toggle('accessibility.highContrast', value.highContrast, { label: 'High-contrast controls' }))}
${row('Always show keyboard focus', 'Keep a strong outline for keyboard navigation.', toggle('accessibility.focusVisible', value.focusVisible, { label: 'Always show keyboard focus' }))}
${row('Larger pointer targets', 'Increase the minimum height of interactive controls.', toggle('accessibility.largeTargets', value.largeTargets, { label: 'Larger pointer targets' }))}
${row('Announce layout changes', 'Report view changes to assistive technology.', toggle('accessibility.announceChanges', value.announceChanges, { label: 'Announce layout changes' }))}
${row('Text size', 'Scale text in the main Kick content area.', segmented('accessibility.textSize', value.textSize, [[90,'90%'],[100,'100%'],[110,'110%'],[120,'120%']]))}
${row('Caption background opacity', 'Set the preferred caption background strength.', range('accessibility.captionOpacity', value.captionOpacity, 0, 100, '0%', '100%', '%'), { wide: true })}
${failures
? `${escapeHtml(failureMessage)}${storageHealth.lastError ? ` ${escapeHtml(tr('The browser reported'))} ${escapeHtml(storageHealth.lastError)}.` : ''} ${escapeHtml(tr('Exporting now is the only way to keep these changes.'))}`
: `Kick Focus is using about ${escapeHtml(formatBytes(report.total))} of browser storage. Nothing has failed to save this session.`}
${failures ? '' : ''}
${rows ? `
${rows}
` : ''}
`;
}
function hubNumber(value) {
return Number(value).toLocaleString();
}
function hubCardValue(card) {
if (card.id === 'reward') return tr(VIEWER_HUB_REWARD_WORDS[card.value] || VIEWER_HUB_REWARD_WORDS.available);
if (card.id === 'watch') return formatSessionWatchTime(card.value);
if (card.id === 'collectibles' && Number.isFinite(card.copies) && card.copies > card.value) {
return `${hubNumber(card.value)} (${hubNumber(card.copies)})`;
}
return hubNumber(card.value);
}
function hubCardSource(card, now) {
if (card.state !== 'ready') return '';
if (card.source === 'local') return tr('This browser session only');
const source = card.source === 'api' ? tr('From Kick’s API') : tr('Read from the page');
if (!card.stale) return source;
const minutes = Math.max(1, Math.round((now - card.observedAt) / 60_000));
return `${source} · ${trf('{n} min ago', { n: minutes })}`;
}
function renderViewerHubCards() {
const now = Date.now();
const cards = viewerHubCards(collectViewerFacts(), now);
return cards.map((card) => `
`).join('');
}
function renderViewerPage() {
const summary = viewerHubSummary(viewerHubCards(collectViewerFacts(), Date.now()));
return `
${pageHeader('Viewer', 'Account readings and this browser session, in one place. Nothing here is claimed or sent anywhere.', 'Reading', `${summary.ready}/${summary.total}`)}
${renderViewerHubCards()}
Where these come from
${escapeHtml(hubSourceSummary(summary))}
Nothing is claimed for you here
This page reads. The daily reward is still claimed by Kick’s own dialog, and only when you have turned that on under Content & Ads. A card with no reading says so rather than showing a zero, because an empty balance and an unreadable one are not the same thing.
`;
}
function hubSourceSummary(summary) {
if (!summary.ready) return 'Nothing has been read yet. Each card above says why.';
const parts = [];
const list = (ids) => new Intl.ListFormat(activeLocale(), { style: 'long', type: 'conjunction' })
.format(ids.map((id) => tr(VIEWER_HUB_TITLES[id])));
if (summary.fromDom.length) parts.push(trf('{items} read from the page', { items: list(summary.fromDom) }));
if (summary.fromApi.length) parts.push(trf('{items} from Kick’s API', { items: list(summary.fromApi) }));
if (summary.fromLocal.length) parts.push(trf('{items} kept in this browser session', { items: list(summary.fromLocal) }));
const stale = summary.stale ? ` ${trf('{n} showing an older reading.', { n: summary.stale })}` : '';
const errors = summary.errors ? ` ${trf('{n} could not be built.', { n: summary.errors })}` : '';
return `${parts.join('; ')}.${stale}${errors}`;
}
function renderAboutPage() {
return `
${pageHeader('About', 'A desktop-first layout and control layer for Kick.', 'Version', VERSION)}
This started ${escapeHtml(INJECTION.summary)}. On Chromium 138 and later a userscript manager needs its own Allow user scripts toggle enabled on the browser's extensions page, and its instant-injection mode turned on. Installing the companion extension removes the question entirely.
`}
Multi-stream
Watch up to ${MULTISTREAM_MAX} Kick channels in one grid, with audio and chat following whichever you focus. Uses Kick’s own embedded player, so subscriptions and entitlements are unchanged.${state.multistream.streams.length ? ` Currently holding ${state.multistream.streams.length}.` : ''}
Panic switch
Temporarily restore Kick’s native layout and pause Kick Focus hooks without reloading. Restore it from the Focus button or with Ctrl+Shift+F.
If Kick sign-in, sign-up, or Follow stops working
Since Kick began serving ads on 2026-08-06, some ad-blocker filter lists have been reported to break those actions, which fail with a generic error until the blocker is disabled and the browser restarted. Kick Focus is not involved: it blocks ${AD_HOSTS.length + TELEMETRY_HOSTS.length} third-party ad and telemetry hosts and no kick.com host at all, so pausing Kick Focus will not change that behavior. Check your ad blocker’s filters for kick.com before blaming an extension.
Diagnostics
Copy a sanitized summary or run a local self-check.
Compatibility self-test
${escapeHtml(state.compatibility ? `${compatibilitySummary(state.compatibility)} Probes are checked after every route update.` : 'The shell probes will run after the page mounts.')}
${escapeHtml(tr('Kick groups'))}${escapeHtml(tr('Kick’s original emote groups are shown below.'))}
`);
return;
}
if (!visible.length) {
const searching = Boolean(String(stickerSearchInput(stickerPicker())?.value || '').trim());
const message = searching
? [tr('No emotes found'), tr('Try a different search or clear the search field.')]
: view === 'pinned'
? [tr('No favorites yet'), tr('Open All and use the star on any emote to keep it here.')]
: view === 'recent'
? [tr('No recent emotes yet'), tr('Emotes you send in this channel will appear here.')]
: view === 'group' && !state.stickerPreferences.groups.length
? [tr('Create your first group'), tr('Use New group above, then select emotes and move them into it.')]
: view === 'group'
? [tr('This group is empty'), tr('Choose Organize, select emotes, then move them into this group.')]
: [tr('No emotes found'), tr('Try a different search or return to Kick groups.')];
setStickerGridHost(gridHost, `empty:${view}:${message[0]}`, `
`);
const next = gridHost.querySelector('[data-kf-sticker-grid]');
if (next && scrollTop !== null) next.scrollTop = scrollTop;
measureEmoteAspect(gridHost);
}
function setStickerGridHost(gridHost, signature, markup) {
if (gridHost.dataset.kfStickerGridSignature === signature) return;
gridHost.dataset.kfStickerGridSignature = signature;
delete gridHost.dataset.kfStickerWindow;
setMarkup(gridHost, markup);
}
function patchStickerSelection(organizer) {
if (!organizer) return;
const selected = state.runtime.stickerPickerSelection;
const organizing = state.runtime.stickerPickerOrganizing;
for (const tile of organizer.querySelectorAll('[data-kf-sticker-item]')) {
const key = tile.dataset.kfStickerKey;
const active = selected.has(key);
tile.dataset.kfStickerSelected = String(active);
const proxy = tile.querySelector('[data-kf-sticker-proxy]');
if (!proxy) continue;
const name = proxy.querySelector('img')?.getAttribute('alt') || 'emote';
if (organizing) proxy.setAttribute('aria-pressed', String(active));
else proxy.removeAttribute('aria-pressed');
proxy.setAttribute('aria-label', trf(organizing ? 'Select emote {name}' : 'Use emote {name}', { name }));
proxy.title = trf(organizing ? 'Select {name}' : 'Use {name}', { name });
}
const count = selected.size;
const status = organizer.querySelector('[data-kf-sticker-selected-count]');
if (status) status.textContent = trf(count === 1 ? '{count} selected emote' : '{count} selected emotes', { count });
for (const control of organizer.querySelectorAll('[data-kf-sticker-clear-selection], [data-kf-sticker-batch-move], [data-kf-sticker-batch-remove]')) {
control.disabled = count === 0;
}
for (const control of organizer.querySelectorAll('[data-kf-sticker-batch-reorder]')) control.disabled = count !== 1;
const visible = state.runtime.stickerPickerVisibleKeys;
const selectShown = organizer.querySelector('[data-kf-sticker-select-shown]');
if (selectShown) selectShown.setAttribute('aria-pressed', String(visible.length > 0 && visible.every((key) => selected.has(key))));
}
function patchStickerTileStates(gridHost) {
for (const tile of gridHost.querySelectorAll('[data-kf-sticker-item]')) {
const key = tile.dataset.kfStickerKey;
const pinned = isFavorited(key);
const hidden = state.stickerPreferences.hidden.has(key);
const stamp = `${pinned}:${hidden}`;
if (tile.dataset.kfStickerState === stamp) continue;
tile.dataset.kfStickerState = stamp;
tile.dataset.kfStickerHidden = String(hidden);
tile.dataset.kfStickerPinned = String(pinned);
const name = tile.querySelector('img')?.getAttribute('alt') || 'emote';
const pin = tile.querySelector('[data-kf-sticker-action="pin"]');
if (pin) {
pin.setAttribute('aria-pressed', String(pinned));
setMarkup(pin, uiIcon('star'));
const scope = pinned ? favoriteScopeOf(key) : '';
pin.setAttribute('aria-label', trf(
pinned ? (scope ? 'Remove this-channel favorite {name}' : 'Remove favorite {name}') : 'Favorite {name}',
{ name },
));
pin.title = tr(pinned ? (scope ? 'Remove favorite (this channel)' : 'Remove favorite') : 'Favorite');
tile.dataset.kfStickerScoped = scope ? 'true' : '';
if (!scope) delete tile.dataset.kfStickerScoped;
}
const hide = tile.querySelector('[data-kf-sticker-action="hide"]');
if (hide) {
setMarkup(hide, uiIcon(hidden ? 'reset' : 'trash'));
hide.setAttribute('aria-label', trf(hidden ? 'Restore {name}' : 'Remove {name}', { name }));
hide.title = tr(hidden ? 'Restore' : 'Remove');
}
}
}
function bindStickerGridScroll(gridHost) {
gridHost.addEventListener('scroll', (event) => {
const grid = event.target;
if (!grid?.dataset || grid.dataset.kfStickerTotal === undefined) return;
const total = Number(grid.dataset.kfStickerTotal) || 0;
const [start, end] = String(gridHost.dataset.kfStickerWindow || '0-0').split('-').map(Number);
if (end - start >= total) return;
const columns = stickerGridColumns(grid);
const rowHeight = STICKER_TILE_HEIGHT + STICKER_GRID_GAP;
const first = Math.floor(grid.scrollTop / rowHeight) * columns;
const last = first + (Math.ceil(grid.clientHeight / rowHeight) + 1) * columns;
const guard = STICKER_WINDOW_GUARD_ROWS * columns;
if (first >= start + guard && last <= end - guard) return;
state.runtime.stickerGridAnchor = first;
renderStickerOrganizer();
}, { capture: true, passive: true });
}
function resetStickerPreferences(options = {}) {
const keepLibrary = options.keepLibrary === true;
const library = keepLibrary ? state.stickerPreferences.library : new Map();
state.runtime.stickerLibraryFilter = 'all';
state.runtime.stickerLibraryQuery = '';
state.runtime.stickerLibrarySelection.clear();
state.runtime.stickerLibraryBulkGroup = '';
state.runtime.stickerPickerSelection.clear();
state.runtime.stickerPickerVisibleKeys = [];
state.runtime.stickerPickerBulkGroup = '';
state.runtime.stickerPickerOrganizing = false;
state.runtime.stickerPickerGroupEditor = '';
state.stickerPreferences = {
favorites: [],
hidden: new Set(),
view: 'all',
showHidden: false,
activeGroup: '',
groups: [],
assignments: new Map(),
library,
};
state.runtime.stickerCatalogDirty = true;
if (keepLibrary) persistStickerPreferences();
else {
gmDelete(STICKER_PREFERENCES_KEY);
libraryStore.clear().catch((error) => logAppError('library reset', error));
}
}
function clearStickerPreferences() {
resetStickerPreferences({ keepLibrary: true });
renderSettingsPage();
scheduleApply(0);
showToast('Emote favorites, removals, and custom groups reset.');
}
function commitPickerStickerChange() {
for (const [key, descriptor] of state.stickerCatalog) {
for (const original of descriptor.originals || []) {
original.dataset.kfStickerHidden = String(state.stickerPreferences.hidden.has(key));
original.dataset.kfStickerPinned = String(isFavorited(key));
}
}
persistStickerPreferences();
applySettingsAttributes();
renderStickerOrganizer();
scheduleApply(0);
}
function focusPickerStickerGroupInput(organizer) {
requestAnimationFrame(() => organizer.querySelector('[data-kf-sticker-group-editor-input]')?.focus?.());
}
function savePickerStickerGroup(organizer, editor) {
const input = organizer.querySelector('[data-kf-sticker-group-editor-input]');
const name = cleanCustomStickerGroupName(input?.value);
if (!name) {
showToast('Enter a custom emote group name.', true);
input?.focus?.();
return;
}
if (state.stickerPreferences.groups.some((group) => group.id !== editor && group.name.toLowerCase() === name.toLowerCase())) {
showToast('That emote group already exists.', true);
input?.focus?.();
return;
}
if (editor === 'new') {
if (state.stickerPreferences.groups.length >= 40) {
showToast('The emote group limit is 40.', true);
return;
}
const id = `group_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
state.stickerPreferences.groups.push({ id, name });
state.stickerPreferences.activeGroup = id;
state.stickerPreferences.view = 'all';
state.runtime.stickerPickerBulkGroup = id;
state.runtime.stickerPickerOrganizing = true;
state.runtime.stickerPickerGroupEditor = '';
commitPickerStickerChange();
showToast(trf('Created {name}. Select emotes, then move them into it.', { name }));
return;
}
const group = stickerGroupById(editor);
if (!group) return;
const previous = group.name;
group.name = name;
state.runtime.stickerPickerGroupEditor = '';
commitPickerStickerChange();
showToast('Emote group renamed.', false, [{
label: 'Undo',
onClick: () => {
const current = stickerGroupById(editor);
if (!current) return;
current.name = previous;
commitPickerStickerChange();
showToast('Group name restored.');
},
}]);
}
function deletePickerStickerGroup(id) {
const index = state.stickerPreferences.groups.findIndex((group) => group.id === id);
if (index < 0) return;
const group = state.stickerPreferences.groups[index];
const assignments = [...state.stickerPreferences.assignments].filter(([, groupId]) => groupId === id);
const previous = {
activeGroup: state.stickerPreferences.activeGroup,
view: state.stickerPreferences.view,
bulkGroup: state.runtime.stickerPickerBulkGroup,
};
state.stickerPreferences.groups.splice(index, 1);
state.stickerPreferences.assignments = new Map([...state.stickerPreferences.assignments].filter(([, groupId]) => groupId !== id));
if (state.stickerPreferences.activeGroup === id) {
state.stickerPreferences.activeGroup = state.stickerPreferences.groups[0]?.id || '';
state.stickerPreferences.view = state.stickerPreferences.activeGroup ? 'group' : 'all';
}
if (state.runtime.stickerPickerBulkGroup === id) state.runtime.stickerPickerBulkGroup = '';
state.runtime.stickerPickerGroupEditor = '';
commitPickerStickerChange();
showToast(trf('Deleted emote group {name}.', { name: group.name }), false, [{
label: 'Undo',
onClick: () => {
state.stickerPreferences.groups.splice(Math.min(index, state.stickerPreferences.groups.length), 0, group);
for (const [key, groupId] of assignments) state.stickerPreferences.assignments.set(key, groupId);
state.stickerPreferences.activeGroup = previous.activeGroup;
state.stickerPreferences.view = previous.view;
state.runtime.stickerPickerBulkGroup = previous.bulkGroup;
commitPickerStickerChange();
showToast('Emote group restored.');
},
}]);
}
function editPickerStickerSelection(action) {
const selected = state.runtime.stickerPickerSelection;
if (action === 'shown') {
const keys = state.runtime.stickerPickerVisibleKeys;
const remove = keys.length > 0 && keys.every((key) => selected.has(key));
for (const key of keys) if (remove) selected.delete(key); else selected.add(key);
patchStickerSelection(stickerPicker()?.querySelector('[data-kf-sticker-organizer]'));
return;
}
if (action === 'clear') {
selected.clear();
patchStickerSelection(stickerPicker()?.querySelector('[data-kf-sticker-organizer]'));
return;
}
const keys = [...selected].filter((key) => state.stickerPreferences.library.has(key));
if (!keys.length) return;
if ((action === 'earlier' || action === 'later') && keys.length === 1) {
const previous = [...state.stickerPreferences.favorites];
const key = keys[0];
state.stickerPreferences.favorites = moveStickerFavorite(
state.stickerPreferences.favorites,
key,
favoriteScopeOf(key),
action === 'earlier' ? -1 : 1,
);
commitPickerStickerChange();
showToast(action === 'earlier' ? 'Favorite moved earlier.' : 'Favorite moved later.', false, [{
label: 'Undo',
onClick: () => {
state.stickerPreferences.favorites = previous;
commitPickerStickerChange();
showToast('Favorite order restored.');
},
}]);
return;
}
if (action === 'move') {
const previous = new Map(state.stickerPreferences.assignments);
const groupId = stickerGroupById(state.runtime.stickerPickerBulkGroup)
? state.runtime.stickerPickerBulkGroup
: '';
for (const key of keys) {
if (groupId) state.stickerPreferences.assignments.set(key, groupId);
else state.stickerPreferences.assignments.delete(key);
}
selected.clear();
commitPickerStickerChange();
showToast(`${keys.length} ${plural(keys.length, 'emote moved.', 'emotes moved.')}`, false, [{
label: 'Undo',
onClick: () => {
state.stickerPreferences.assignments = previous;
commitPickerStickerChange();
showToast('Emote group changes restored.');
},
}]);
return;
}
const previous = {
hidden: new Set(state.stickerPreferences.hidden),
favorites: [...state.stickerPreferences.favorites],
assignments: new Map(state.stickerPreferences.assignments),
};
for (const key of keys) {
state.stickerPreferences.hidden.add(key);
state.stickerPreferences.assignments.delete(key);
}
state.stickerPreferences.favorites = state.stickerPreferences.favorites.filter((favorite) => !selected.has(favorite.key));
selected.clear();
commitPickerStickerChange();
showToast(`${keys.length} ${plural(keys.length, 'emote removed.', 'emotes removed.')}`, false, [{
label: 'Undo',
onClick: () => {
state.stickerPreferences.hidden = previous.hidden;
state.stickerPreferences.favorites = previous.favorites;
state.stickerPreferences.assignments = previous.assignments;
commitPickerStickerChange();
showToast('Removed emotes restored.');
},
}]);
}
function handleStickerAction(event) {
const target = event.target.closest?.([
'[data-kf-sticker-action]', '[data-kf-sticker-view]', '[data-kf-sticker-show-hidden]',
'[data-kf-sticker-reset]', '[data-kf-sticker-manage]', '[data-kf-sticker-organize]',
'[data-kf-sticker-group-create]', '[data-kf-sticker-group-rename]', '[data-kf-sticker-group-delete]',
'[data-kf-sticker-group-save]', '[data-kf-sticker-group-cancel]', '[data-kf-sticker-select-shown]',
'[data-kf-sticker-clear-selection]', '[data-kf-sticker-batch-move]', '[data-kf-sticker-batch-remove]',
'[data-kf-sticker-batch-reorder]',
].join(', '));
if (!target || !target.closest?.('[data-kf-sticker-organizer]')) return;
event.preventDefault();
event.stopPropagation();
const organizer = target.closest('[data-kf-sticker-organizer]');
const key = target.dataset.kfStickerKey;
const action = target.dataset.kfStickerAction;
if (target.dataset.kfStickerManage) {
openSettings('emotes');
return;
}
if (target.hasAttribute('data-kf-sticker-organize')) {
state.runtime.stickerPickerOrganizing = !state.runtime.stickerPickerOrganizing;
if (!state.runtime.stickerPickerOrganizing) state.runtime.stickerPickerSelection.clear();
else {
if (state.stickerPreferences.view === 'native') state.stickerPreferences.view = 'all';
if (!stickerGroupById(state.runtime.stickerPickerBulkGroup)) {
state.runtime.stickerPickerBulkGroup = state.stickerPreferences.activeGroup || '';
}
}
commitPickerStickerChange();
return;
}
if (target.hasAttribute('data-kf-sticker-group-create')) {
state.stickerPreferences.view = 'group';
state.runtime.stickerPickerGroupEditor = 'new';
renderStickerOrganizer();
focusPickerStickerGroupInput(organizer);
return;
}
if (target.hasAttribute('data-kf-sticker-group-rename')) {
state.runtime.stickerPickerGroupEditor = target.dataset.kfStickerGroupRename;
renderStickerOrganizer();
focusPickerStickerGroupInput(organizer);
return;
}
if (target.hasAttribute('data-kf-sticker-group-delete')) {
deletePickerStickerGroup(target.dataset.kfStickerGroupDelete);
return;
}
if (target.hasAttribute('data-kf-sticker-group-save')) {
savePickerStickerGroup(organizer, target.dataset.kfStickerGroupSave);
return;
}
if (target.hasAttribute('data-kf-sticker-group-cancel')) {
state.runtime.stickerPickerGroupEditor = '';
renderStickerOrganizer();
return;
}
if (target.hasAttribute('data-kf-sticker-select-shown')) {
editPickerStickerSelection('shown');
return;
}
if (target.hasAttribute('data-kf-sticker-clear-selection')) {
editPickerStickerSelection('clear');
return;
}
if (target.hasAttribute('data-kf-sticker-batch-move')) {
editPickerStickerSelection('move');
return;
}
if (target.hasAttribute('data-kf-sticker-batch-remove')) {
editPickerStickerSelection('remove');
return;
}
if (target.hasAttribute('data-kf-sticker-batch-reorder')) {
editPickerStickerSelection(target.dataset.kfStickerBatchReorder === 'up' ? 'earlier' : 'later');
return;
}
if (action === 'send') {
if (state.runtime.stickerPickerOrganizing) {
if (state.runtime.stickerPickerSelection.has(key)) state.runtime.stickerPickerSelection.delete(key);
else state.runtime.stickerPickerSelection.add(key);
patchStickerSelection(organizer);
return;
}
const original = state.stickerCatalog.get(key)?.originals?.find((button) => button.isConnected);
original?.click?.();
return;
}
if ((action === 'pin' || action === 'hide') && key) rememberStickerGridScroll(target);
if (action === 'pin' && key) {
const previous = [...state.stickerPreferences.favorites];
const scope = isFavorited(key) ? favoriteScopeOf(key) : newFavoriteChannel();
state.stickerPreferences.favorites = toggleStickerFavorite(state.stickerPreferences.favorites, key, scope);
if (isFavorited(key)) state.stickerPreferences.hidden.delete(key);
const name = state.stickerCatalog.get(key)?.name || 'Emote';
const message = isFavorited(key) ? trf('Favorited {name}.', { name }) : trf('Removed {name} from favorites.', { name });
commitPickerStickerChange();
showToast(message, false, [{
label: 'Undo',
onClick: () => {
state.stickerPreferences.favorites = previous;
commitPickerStickerChange();
showToast('Favorite change restored.');
},
}]);
return;
} else if (action === 'move-favorite' && key) {
const earlier = target.dataset.kfStickerMove === 'up';
state.stickerPreferences.favorites = moveStickerFavorite(
state.stickerPreferences.favorites,
key,
favoriteScopeOf(key),
earlier ? -1 : 1,
);
commitPickerStickerChange();
showToast(earlier ? 'Emote moved earlier.' : 'Emote moved later.');
return;
} else if (action === 'hide' && key) {
const hidden = state.stickerPreferences.hidden.has(key);
const name = state.stickerCatalog.get(key)?.name || 'Emote';
if (hidden) {
state.stickerPreferences.hidden.delete(key);
commitPickerStickerChange();
showToast(trf('Restored {name}.', { name }));
} else {
const previous = {
favorites: [...state.stickerPreferences.favorites],
assignment: state.stickerPreferences.assignments.get(key) || '',
};
state.stickerPreferences.hidden.add(key);
state.stickerPreferences.favorites = state.stickerPreferences.favorites.filter((entry) => entry.key !== key);
state.stickerPreferences.assignments.delete(key);
commitPickerStickerChange();
showToast(trf('Removed {name}.', { name }), false, [{
label: 'Undo',
onClick: () => {
state.stickerPreferences.hidden.delete(key);
state.stickerPreferences.favorites = previous.favorites;
if (previous.assignment) state.stickerPreferences.assignments.set(key, previous.assignment);
commitPickerStickerChange();
showToast(trf('Restored {name}.', { name }));
},
}]);
}
return;
} else if (target.dataset.kfStickerView) {
state.stickerPreferences.view = target.dataset.kfStickerView;
state.stickerPreferences.activeGroup = target.dataset.kfStickerGroup || state.stickerPreferences.activeGroup;
state.runtime.stickerGridAnchor = 0;
state.runtime.stickerGridScrollTop = 0;
if (state.stickerPreferences.view === 'group' && !state.stickerPreferences.groups.length) {
state.runtime.stickerPickerGroupEditor = 'new';
applySettingsAttributes();
renderStickerOrganizer();
focusPickerStickerGroupInput(organizer);
return;
} else if (state.stickerPreferences.view !== 'group') {
state.runtime.stickerPickerGroupEditor = '';
}
if (state.stickerPreferences.view === 'native') {
state.runtime.stickerPickerOrganizing = false;
state.runtime.stickerPickerSelection.clear();
}
commitPickerStickerChange();
if (state.runtime.stickerPickerGroupEditor) focusPickerStickerGroupInput(organizer);
return;
} else if (target.dataset.kfStickerShowHidden) {
state.stickerPreferences.showHidden = !state.stickerPreferences.showHidden;
commitPickerStickerChange();
return;
} else if (target.dataset.kfStickerReset) {
resetStickerPreferences({ keepLibrary: true });
commitPickerStickerChange();
showToast('Emote changes reset.');
return;
} else {
return;
}
}
function updateFollowedEmoteState(slug, followed) {
const source = String(slug || '').toLowerCase();
for (const emote of state.live.catalog?.emotes || []) {
if (String(emote.sourceSlug || '').toLowerCase() === source) emote.followed = followed;
}
for (const [key, entry] of state.stickerPreferences.library) {
if (String(entry.sourceSlug || '').toLowerCase() !== source || !entry.requiresFollow) continue;
state.stickerPreferences.library.set(key, {
...entry,
followed,
access: followed ? (entry.subscribersOnly ? 'locked' : 'channel') : 'locked',
});
}
persistStickerPreferences();
}
async function undoChatStickerSave({ key, scope, removeFavorite, unfollowSlug }) {
if (removeFavorite) {
state.stickerPreferences.favorites = state.stickerPreferences.favorites
.filter((entry) => !(entry.key === key && entry.channel === scope));
persistStickerPreferences();
scheduleApply(0);
}
if (unfollowSlug) {
const result = await mutateKickChannelFollow(unfollowSlug, 'DELETE');
if (result.ok) updateFollowedEmoteState(unfollowSlug, false);
else {
showToast(trf('The emote was removed, but Kick could not unfollow {channel}.', { channel: unfollowSlug }), true);
return;
}
}
showToast(unfollowSlug ? `Removed the emote and unfollowed ${unfollowSlug}.` : 'Emote removed from favorites.');
}
async function saveChatSticker(image) {
if (!state.settings.content.clickChatEmotes || pendingChatStickerSaves.has(image)) return;
const sticker = chatStickerInfo(image);
if (!sticker) return;
pendingChatStickerSaves.add(image);
image.setAttribute('aria-busy', 'true');
try {
const scope = newFavoriteChannel();
const alreadyFavorite = isFavorited(sticker.key);
state.stickerPreferences.hidden.delete(sticker.key);
mergeStickerLibrary([sticker]);
if (!alreadyFavorite) {
state.stickerPreferences.favorites = toggleStickerFavorite(
state.stickerPreferences.favorites,
sticker.key,
scope,
);
}
persistStickerPreferences();
announce(alreadyFavorite ? 'Emote already saved' : 'Emote saved');
const follow = emoteFollowRequirement(sticker, sticker.sourceSlug);
let followedNow = false;
if (follow.required && !follow.followed) {
if (!follow.slug) {
showToast(trf('Saved {name}, but Kick did not identify the follow-gated source channel.', { name: sticker.name }), true);
return;
}
showToast(trf('Saved {name}. Following {channel}…', { name: sticker.name, channel: follow.slug }));
const result = await mutateKickChannelFollow(follow.slug, 'POST');
if (!result.ok) {
showToast(trf('Saved {name} locally, but Kick could not follow {channel} ({status}). Sign in or reload the channel and try again.', { name: sticker.name, channel: follow.slug, status: result.status }), true);
return;
}
followedNow = true;
updateFollowedEmoteState(follow.slug, true);
}
const stored = state.stickerPreferences.library.get(sticker.key) || sticker;
const locked = emoteLockState(stored, stored.sourceSlug);
const message = followedNow
? `Saved ${sticker.name} and followed ${follow.slug}.`
: alreadyFavorite
? `${sticker.name} is already in your favorites.`
: locked.locked && stored.subscribersOnly
? `Saved ${sticker.name}. A subscription is still required to use it.`
: `Saved ${sticker.name} to your ${scope ? 'channel' : 'global'} favorites.`;
const canUndo = !alreadyFavorite || followedNow;
showToast(message, false, canUndo ? [{
label: followedNow && alreadyFavorite ? 'Undo follow' : 'Undo',
onClick: () => undoChatStickerSave({
key: sticker.key,
scope,
removeFavorite: !alreadyFavorite,
unfollowSlug: followedNow ? follow.slug : '',
}),
}] : []);
scheduleApply(0);
} finally {
pendingChatStickerSaves.delete(image);
image.removeAttribute('aria-busy');
}
}
function handleChatStickerSave(event) {
if (!state.settings.content.clickChatEmotes) return;
const image = event.target.closest?.('[data-kf-chat-emote-save]');
if (!image || !image.closest?.('[data-testid="chatroom-messages"], #chatroom-messages')) return;
if (event.type === 'keydown' && event.key !== 'Enter' && event.key !== ' ') return;
event.preventDefault();
event.stopPropagation();
saveChatSticker(image).catch((error) => {
logAppError('save chat emote', error);
showToast('The emote could not be saved. The error log on the Content & Ads page says why.', true);
});
}
function chatKeywordsForChannel() {
const value = state.chatKeywords[channelPath()];
return Array.isArray(value) ? value.filter((item) => typeof item === 'string').slice(0, 20) : [];
}
const KEYWORD_HIGHLIGHT_NAME = 'kick-focus-keyword';
const KEYWORD_RANGE_LIMIT = 400;
function highlightRegistry() {
try {
return typeof Highlight === 'function' && typeof CSS !== 'undefined' && CSS.highlights ? CSS.highlights : null;
} catch {
return null;
}
}
function clearKeywordHighlight() {
try { highlightRegistry()?.delete(KEYWORD_HIGHLIGHT_NAME); } catch { }
}
function applyChatHighlights() {
const messages = document.querySelector('[data-testid="chatroom-messages"], #chatroom-messages');
const registry = highlightRegistry();
if (!messages) {
clearKeywordHighlight();
return;
}
const keywords = state.settings.content.chatHighlights ? chatKeywordsForChannel() : [];
const ranges = [];
const comfort = {
settings: state.settings.content,
people: state.settings.content.chatPriorityPeople || [],
own: liveLocalUsername(),
channel: currentChannelSlug(),
now: Date.now(),
};
syncChatComfortShell(messages);
for (const node of messages.querySelectorAll?.('[data-index], [data-message-id], .group') || []) {
const text = node.textContent || '';
const hit = keywords.length > 0 && findKeywordSpans(text, keywords, 1).length > 0;
node.dataset.kfHighlighted = String(hit);
if (hit && registry && ranges.length < KEYWORD_RANGE_LIMIT) collectKeywordRanges(node, keywords, ranges);
applyChatComfortToMessage(node, { ...comfort, keywordHit: hit });
}
if (!registry) return;
try {
if (ranges.length) registry.set(KEYWORD_HIGHLIGHT_NAME, new Highlight(...ranges));
else registry.delete(KEYWORD_HIGHLIGHT_NAME);
} catch (error) {
logAppError('keyword highlight', error);
}
}
const CHAT_TIMESTAMP_VAR = '--chatroom-timestamps-display';
const CHAT_AUTHOR_PROBES = ['button[data-prevent-expand="true"]', 'button.font-bold', 'button'];
function chatMessageAuthor(node) {
for (const selector of CHAT_AUTHOR_PROBES) {
const found = node.querySelector?.(selector);
const name = String(found?.textContent || '').trim();
if (name && name.length < 40) return name;
}
return '';
}
function chatMessageText(node, author) {
const raw = String(node.textContent || '').replace(/\s+/g, ' ').trim();
if (!author) return raw;
const at = raw.indexOf(author);
return at === -1 ? raw : raw.slice(at + author.length).replace(/^\s*:\s*/, '').trim();
}
function chatMessageId(node) {
return String(node.dataset?.messageId || node.dataset?.chatEntry || node.dataset?.index || '').trim();
}
function playMentionTone() {
try {
const Ctor = window.AudioContext || window.webkitAudioContext;
if (!Ctor) return;
const context = new Ctor();
const oscillator = context.createOscillator();
const gain = context.createGain();
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(660, context.currentTime);
gain.gain.setValueAtTime(0.0001, context.currentTime);
gain.gain.exponentialRampToValueAtTime(0.08, context.currentTime + 0.02);
gain.gain.exponentialRampToValueAtTime(0.0001, context.currentTime + 0.18);
oscillator.connect(gain).connect(context.destination);
oscillator.start();
oscillator.stop(context.currentTime + 0.2);
oscillator.onended = () => { context.close?.(); };
} catch {
}
}
function mentionNames() {
const people = state.settings.content.chatPriorityPeople || [];
const own = liveLocalUsername();
return own ? [...people, own] : [...people];
}
function applyChatComfortToMessage(node, { keywordHit, settings, people, own, channel, now }) {
const id = chatMessageId(node);
const author = (people.length > 0 || settings.chatHistory || settings.chatMentionSound)
? chatMessageAuthor(node)
: '';
const priority = people.length > 0 && isPriorityPerson(people, author);
if (people.length > 0) {
if (node.dataset.kfChatPriority !== String(priority)) node.dataset.kfChatPriority = String(priority);
} else if (node.dataset.kfChatPriority) {
delete node.dataset.kfChatPriority;
}
if (settings.chatHideMessages) {
const hidden = id !== '' && state.chatComfort.hidden.has(id);
if (node.dataset.kfChatHidden !== String(hidden)) {
node.dataset.kfChatHidden = String(hidden);
if (hidden) node.dataset.kfHiddenNote = tr('Hidden by you');
else delete node.dataset.kfHiddenNote;
}
if (id && !node.querySelector('[data-kf-chat-hide]')) {
const button = document.createElement('button');
button.type = 'button';
button.dataset.kfChatHide = id;
button.setAttribute('aria-label', tr('Hide this message for me'));
button.title = tr('Hide this message for me');
button.textContent = '×';
node.append(button);
}
} else if (node.dataset.kfChatHidden) {
delete node.dataset.kfChatHidden;
node.querySelector('[data-kf-chat-hide]')?.remove();
}
const isOwn = Boolean(own) && author.toLowerCase() === own;
if (!id) return;
const text = settings.chatHistory || settings.chatMentionSound ? chatMessageText(node, author) : '';
if (settings.chatHistory && !state.chatComfort.seen.has(id)) {
state.chatComfort.seen.add(id);
state.chatComfort.rows = appendChatEntry(
state.chatComfort.rows,
{ id, author, text, at: now, channel },
CHAT_HISTORY_LIMITS,
now,
);
}
if (!settings.chatMentionSound || state.chatComfort.sounded.has(id)) return;
state.chatComfort.sounded.add(id);
const mentioned = keywordHit || priority
|| mentionNames().some((name) => text.toLowerCase().includes(String(name).toLowerCase()));
if (!shouldPlayMentionSound({
enabled: true,
matched: mentioned,
own: isOwn,
hidden: document.visibilityState === 'hidden',
now,
lastPlayedAt: state.chatComfort.lastSoundAt,
})) return;
state.chatComfort.lastSoundAt = now;
playMentionTone();
}
function syncChatComfortShell(messages) {
const settings = state.settings.content;
if (messages) {
if (settings.chatTimestamps) messages.style.setProperty(CHAT_TIMESTAMP_VAR, 'inline');
else messages.style.removeProperty(CHAT_TIMESTAMP_VAR);
}
if (state.chatComfort.seen.size > 4000) state.chatComfort.seen.clear();
if (state.chatComfort.sounded.size > 4000) state.chatComfort.sounded.clear();
if (state.chatComfort.rows.length) {
state.chatComfort.rows = pruneChatHistory(state.chatComfort.rows, CHAT_HISTORY_LIMITS, Date.now());
}
}
function hideChatMessage(id) {
if (!id) return;
state.chatComfort.hidden.add(id);
document.querySelector(chatMessageSelector(id))?.setAttribute('data-kf-chat-hidden', 'true');
showToast('Message hidden for you. It is still there for everyone else.', false, [{
label: 'Undo',
onClick: () => {
state.chatComfort.hidden.delete(id);
const node = document.querySelector(chatMessageSelector(id));
if (node) node.dataset.kfChatHidden = 'false';
},
}]);
}
function forgetChatMessage(id) {
if (!state.chatComfort.rows.length) return;
state.chatComfort.rows = dropChatMessage(state.chatComfort.rows, id);
renderChatHistoryResults();
}
function renderChatHistoryResults() {
const host = state.shadow?.querySelector('[data-kf-chat-history-results]');
if (!host) return;
const rows = searchChatHistory(state.chatComfort.rows, state.chatComfort.query, 40);
if (!rows.length) {
setMarkup(host, `
${escapeHtml(state.chatComfort.rows.length
? tr('Nothing in this session matches that.')
: tr('Nothing recorded yet. Open a chat with the switch on.'))}
`).join(''));
localizeInterface();
}
function exportChatHistory() {
const rows = state.chatComfort.rows;
if (!rows.length) {
showToast('Nothing recorded yet. Open a chat with the switch on.', true);
return;
}
const text = buildChatHistoryExport(rows, currentChannelSlug());
const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `kick-focus-chat-${currentChannelSlug() || 'session'}.txt`;
link.click();
setTimeout(() => URL.revokeObjectURL(url), 4000);
showToast(trf('Saved {n} messages from this session.', { n: rows.length }), false);
}
function clearChatHistory() {
state.chatComfort.rows = [];
state.chatComfort.seen.clear();
renderChatHistoryResults();
showToast('Session chat log cleared.', false);
}
function collectKeywordRanges(root, keywords, ranges) {
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
for (let textNode = walker.nextNode(); textNode; textNode = walker.nextNode()) {
const spans = findKeywordSpans(textNode.nodeValue || '', keywords, KEYWORD_RANGE_LIMIT - ranges.length);
for (const span of spans) {
const range = document.createRange();
range.setStart(textNode, span.start);
range.setEnd(textNode, span.end);
ranges.push(range);
if (ranges.length >= KEYWORD_RANGE_LIMIT) return;
}
}
}
function streamStartedAt() {
const channel = state.live.channel;
if (channel?.isLive && channel.startedAt) return channel.startedAt;
if (channel && !channel.isLive) return 0;
if (location.pathname.split('/').filter(Boolean).length !== 1) return 0;
return streamStartFromLinkedData(
[...document.querySelectorAll('script[type="application/ld+json"]')].map((node) => node.textContent),
);
}
function videoIsVisible(video) {
if (!video?.isConnected) return false;
try {
const box = video.getBoundingClientRect();
const style = getComputedStyle(video);
return box.width > 0 && box.height > 0
&& style.display !== 'none' && style.visibility !== 'hidden';
} catch {
return false;
}
}
function primaryVideo() {
return [...document.querySelectorAll('video')].find(videoIsVisible) || null;
}
function applyStreamUptime() {
const existing = document.querySelector('[data-kf-uptime]');
const startedAt = streamStartedAt();
const enabled = state.settings.content.showUptime
&& state.route === 'channel'
&& formatUptime(startedAt);
if (!enabled) {
existing?.remove();
clearInterval(state.uptimeTimer);
state.uptimeTimer = 0;
return;
}
const video = primaryVideo();
const owner = playerOverlayHost(video);
if (!owner) return;
let chip = owner.querySelector?.('[data-kf-uptime]');
if (!chip) {
chip = document.createElement('div');
chip.dataset.kfUptime = 'true';
chip.setAttribute('role', 'status');
chip.setAttribute('aria-live', 'off');
owner.append(chip);
}
const update = () => {
const text = formatUptime(streamStartedAt());
if (!text) {
chip.remove();
clearInterval(state.uptimeTimer);
state.uptimeTimer = 0;
return;
}
chip.textContent = text;
chip.setAttribute('aria-label', trf('Live for {duration}', { duration: text }));
chip.title = trf('Live for {duration}', { duration: text });
};
update();
if (!state.uptimeTimer) state.uptimeTimer = window.setInterval(update, 1000);
}
function applyVodExpiry() {
const existing = document.querySelector('[data-kf-vod-expiry]');
const vod = state.live.vod;
const channel = state.live.channel;
const expiry = state.settings.content.showVodExpiry && state.route === 'channel' && vod && channel
? vodExpiry(vod.startedAt, channel.verified)
: null;
const text = expiry ? formatVodRetention(expiry.remaining) : '';
if (!text) {
existing?.remove();
return;
}
const video = primaryVideo();
const owner = playerOverlayHost(video);
if (!owner) return;
let chip = owner.querySelector?.('[data-kf-vod-expiry]');
if (!chip) {
chip = document.createElement('div');
chip.dataset.kfVodExpiry = 'true';
chip.setAttribute('role', 'status');
chip.setAttribute('aria-live', 'off');
owner.append(chip);
}
chip.textContent = text;
const label = trf('{time} before Kick deletes this recording', { time: text });
chip.setAttribute('aria-label', label);
chip.title = label;
}
function applyPlaybackDiagnostics() {
const existing = document.querySelector('[data-kf-playback-diagnostics]');
if (!state.settings.content.playbackDiagnostics || state.route !== 'channel') {
existing?.remove();
clearInterval(state.playbackDiagnosticsTimer);
state.playbackDiagnosticsTimer = 0;
return;
}
const video = primaryVideo();
if (!video) return;
const owner = playerOverlayHost(video);
if (!owner) return;
let panel = owner.querySelector?.('[data-kf-playback-diagnostics]');
if (!panel) {
panel = document.createElement('div');
panel.dataset.kfPlaybackDiagnostics = 'true';
owner.append(panel);
}
const update = () => {
const buffered = video.buffered?.length ? Math.max(0, video.buffered.end(video.buffered.length - 1) - video.currentTime) : 0;
const quality = video.getVideoPlaybackQuality?.();
panel.textContent = `ready ${video.readyState}/4 · buffer ${buffered.toFixed(1)}s${quality ? ` · dropped ${quality.droppedVideoFrames}` : ''}`;
};
update();
if (!state.playbackDiagnosticsTimer) state.playbackDiagnosticsTimer = window.setInterval(update, 1000);
}
function localChannelBlocked(path) {
const normalized = observedChannelPath(path);
if (!normalized) return false;
return state.settings.content.hiddenChannels.includes(normalized);
}
function remoteBlocklistMatches(path, labels, text) {
const remote = state.remoteBlocklist;
if (remote.status !== 'ready') return false;
const normalized = String(text || '').toLowerCase();
const channel = observedChannelPath(path);
return (channel && remote.channels.has(channel))
|| labels.categories?.some?.((category) => remote.categories.has(category.toLowerCase()))
|| [...remote.keywords].some((keyword) => normalized.includes(keyword));
}
function remoteBlocklistSummary() {
const remote = state.remoteBlocklist;
if (!state.settings.content.blocklistSubscription) return 'Optional remote blocklist is off. No remote data is fetched.';
if (!state.settings.content.blocklistUrl) return 'Add an HTTPS URL to enable the data-only subscription.';
if (remote.status === 'loading') return 'Fetching and validating the blocklist…';
if (remote.status === 'ready') {
const via = remote.method === 'companion' ? ' via companion' : remote.method === 'userscript' ? ' via manager' : '';
return `Active${via}: ${remote.channels.size} channels, ${remote.categories.size} categories, and ${remote.keywords.size} keywords. Last checked ${new Date(remote.fetchedAt).toLocaleString()}.`;
}
if (remote.status === 'stale') return 'The last valid blocklist is stale; the subscription will retry on its next interval.';
if (remote.status === 'error') return 'The last blocklist refresh failed. Existing valid data was kept if it came from the same URL.';
return 'No valid blocklist has been loaded yet.';
}
function updateRemoteBlocklistInPlace() {
const notice = state.shadow?.querySelector('[data-kf-remote-blocklist]');
if (!notice) return;
notice.textContent = remoteBlocklistSummary();
notice.dataset.status = state.remoteBlocklist.status;
}
function clearRemoteBlocklist() {
gmDelete(REMOTE_BLOCKLIST_KEY);
state.remoteBlocklist = { source: '', fetchedAt: 0, attemptedAt: 0, channels: new Set(), categories: new Set(), keywords: new Set(), status: 'off', method: '' };
updateRemoteBlocklistInPlace();
scheduleApply(0);
}
function fetchBlocklistText(href) {
if (companionInfo().active) {
return new Promise((resolve, reject) => {
const timer = window.setTimeout(() => reject(new Error('companion timeout')), 10000);
const handler = (event) => {
window.clearTimeout(timer);
document.removeEventListener('kick-focus:blocklist-result', handler);
try {
const result = typeof event.detail === 'string' ? JSON.parse(event.detail) : event.detail;
if (!result?.ok) throw new Error(result?.error || 'companion fetch failed');
resolve({ text: result.text, method: 'companion' });
} catch (error) {
reject(error);
}
};
document.addEventListener('kick-focus:blocklist-result', handler);
document.dispatchEvent(new CustomEvent('kick-focus:fetch-blocklist', { detail: { url: href } }));
});
}
if (typeof GM_xmlhttpRequest === 'function') {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: href,
anonymous: true,
timeout: 8000,
onload(response) {
if (response.status >= 200 && response.status < 300) resolve({ text: response.responseText, method: 'userscript' });
else reject(new Error(`HTTP ${response.status}`));
},
onerror() { reject(new Error('GM_xmlhttpRequest network error')); },
ontimeout() { reject(new Error('GM_xmlhttpRequest timeout')); },
});
});
}
const controller = new AbortController();
const timeout = window.setTimeout(() => controller.abort(), 8000);
return fetch(href, { credentials: 'omit', cache: 'no-store', signal: controller.signal })
.then((response) => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.text();
})
.then((text) => ({ text, method: 'page' }))
.finally(() => window.clearTimeout(timeout));
}
function scheduleRemoteBlocklistSync(force = false) {
const settings = state.settings.content;
if (!settings.blocklistSubscription) {
if (state.remoteBlocklist.status !== 'off') clearRemoteBlocklist();
return;
}
let url;
try {
url = new URL(settings.blocklistUrl);
if (url.protocol !== 'https:') throw new Error('HTTPS required');
} catch {
state.remoteBlocklist.status = 'error';
updateRemoteBlocklistInPlace();
return;
}
const now = Date.now();
const interval = settings.blocklistRefreshHours * 60 * 60 * 1000;
const sameSource = state.remoteBlocklist.source === url.href;
if (!force && state.remoteSyncInFlight) return;
if (!force && sameSource && state.remoteBlocklist.fetchedAt && now - state.remoteBlocklist.fetchedAt < interval) return;
if (!force && state.remoteBlocklist.attemptedAt && now - state.remoteBlocklist.attemptedAt < 60 * 1000) return;
state.remoteSyncInFlight = true;
state.remoteBlocklist.attemptedAt = now;
state.remoteBlocklist.status = 'loading';
updateRemoteBlocklistInPlace();
fetchBlocklistText(url.href)
.then(({ text, method }) => {
if (text.length > 512 * 1024) throw new Error('blocklist too large');
const result = validateRemoteBlocklist(JSON.parse(text));
if (!result.ok) throw new Error(result.error);
const payload = result.value;
state.remoteBlocklist = {
source: url.href,
fetchedAt: Date.now(),
attemptedAt: Date.now(),
channels: new Set(payload.channels),
categories: new Set(payload.categories),
keywords: new Set(payload.keywords),
status: 'ready',
method,
};
gmSet(REMOTE_BLOCKLIST_KEY, { source: url.href, fetchedAt: state.remoteBlocklist.fetchedAt, method, payload });
recordProtection('Blocklist', { category: 'local', label: `validated ${payload.channels.length + payload.categories.length + payload.keywords.length} entries via ${method}` });
scheduleApply(0);
})
.catch(() => {
state.remoteBlocklist.status = sameSource && state.remoteBlocklist.fetchedAt ? 'stale' : 'error';
updateRemoteBlocklistInPlace();
})
.finally(() => {
state.remoteSyncInFlight = false;
updateRemoteBlocklistInPlace();
});
}
function installRemoteBlocklistTimer() {
if (state.remoteSyncTimer) return;
state.remoteSyncTimer = window.setInterval(() => scheduleRemoteBlocklistSync(false), 60 * 1000);
scheduleRemoteBlocklistSync(false);
}
function applyContentFilters() {
const settings = state.settings.content;
const mainCards = mainCardCandidates();
for (const node of mainCards) applyCardActions(node);
applyDiscoveryCardUptimes(mainCards);
const scored = [];
for (const node of cardCandidates()) {
delete node.dataset.kfFiltered;
delete node.dataset.kfMature;
delete node.dataset.kfDismissed;
const context = cardContext(node);
const labels = detectContentLabels(node.textContent, context);
const link = node.matches?.('a[href]') ? node : node.querySelector?.('a[href]');
let path = '';
try { path = observedChannelPath(link ? new URL(link.href, location.origin).pathname : ''); } catch { }
if (labels.casino && path) state.casinoPaths.add(path);
if (path && state.casinoPaths.has(path)) labels.casino = true;
node.dataset.kfWatched = String(Boolean(path && state.watched.has(path)));
node.dataset.kfLiveCard = String(/(^|\s)live(?:\s|$)/i.test(node.textContent || ''));
node.dataset.kfDismissed = String(Boolean(path && state.dismissed.has(path)));
const remoteBlocked = remoteBlocklistMatches(path, context, node.textContent);
const channelBlocked = localChannelBlocked(path);
const hide = remoteBlocked || channelBlocked
|| (settings.hideCasino && labels.casino)
|| (settings.suppressPromoted && labels.promoted)
|| (settings.hideDropsPromotions && labels.drops);
scored.push({ node, labels, hide });
}
const decision = filterDecision(scored.length, scored.filter((entry) => entry.hide).length, { route: state.route });
for (const entry of scored) {
if (decision.apply && entry.hide) entry.node.dataset.kfFiltered = 'true';
if (entry.labels.mature) entry.node.dataset.kfMature = 'true';
}
recordFilterDecision(decision);
if (settings.pauseHomeAutoplay && state.route === 'home') quietHomeAutoplay();
}
function recordFilterDecision(decision) {
const previous = state.filter.suspended;
state.filter = {
suspended: !decision.apply,
hidden: decision.apply ? decision.hidden : 0,
wouldHide: decision.hidden,
total: decision.total,
};
document.documentElement.dataset.kfFilterSuspended = String(!decision.apply);
if (!decision.apply && !previous) {
const percent = Math.round(decision.ratio * 100);
announce(trf('Content filtering suspended: it would have hidden {percent}% of this page.', { percent }));
recordProtection('Filter', {
category: 'filter',
label: `suspended (${decision.hidden}/${decision.total} cards)`,
});
}
updateFilterNoticeInPlace();
}
function updateFilterNoticeInPlace() {
const notice = state.shadow?.querySelector('[data-kf-filter-notice]');
if (!notice) return;
notice.hidden = !state.filter.suspended;
notice.textContent = state.filter.suspended
? `Filtering is suspended on this page. It would have hidden ${state.filter.wouldHide} of ${state.filter.total} cards, which usually means Kick changed its labels rather than that the page is really that promotional. Everything is shown.`
: '';
}
function updateCompatibilityInPlace() {
const status = state.shadow?.querySelector('[data-kf-compatibility]');
const detail = state.shadow?.querySelector('[data-kf-compatibility-detail]');
if (!status || !detail || !state.compatibility) return;
status.textContent = state.compatibility.healthy ? 'Healthy' : 'Needs attention';
status.dataset.error = String(!state.compatibility.healthy);
detail.textContent = `${compatibilitySummary(state.compatibility)} Probes: ${Object.entries(state.compatibility.probes).filter(([, probe]) => probe).map(([name, probe]) => `${name}=${probe}`).join(', ') || 'none'}.`;
}
function removeAdShells() {
if (!state.settings.content.removeAdContainers) return;
for (const node of document.querySelectorAll(AD_SHELL_SELECTORS.join(','))) {
if (node.dataset.kfAdShell === 'true') continue;
node.dataset.kfAdShell = 'true';
const value = node.getAttribute('src') || node.id || node.getAttribute('data-testid') || node.getAttribute('aria-label') || node.className;
const classification = classifyRequest(value, { reduceTelemetry: false });
const safe = classification.label === '[unparseable URL]' ? 'ad shell' : classification.label;
state.diagnostics.shells += 1;
recordProtection('DOM', { category: 'advertising', label: safe });
node.remove();
}
}
function scheduleApply(delay = 50) {
if (state.runtime.suspended) return;
const now = Date.now();
if (!state.applyPendingSince) state.applyPendingSince = now;
const effective = nextApplyDelay(delay, now - state.applyPendingSince);
clearTimeout(state.applyTimer);
state.applyTimer = window.setTimeout(runApplyCycle, effective);
}
function yieldToInput() {
try {
return typeof scheduler !== 'undefined' && typeof scheduler.yield === 'function' ? scheduler.yield() : null;
} catch {
return null;
}
}
async function runApplyCycle() {
if (state.runtime.suspended || state.runtime.applyRunning) return;
state.runtime.applyRunning = true;
let elapsed = 0;
let started = performance.now();
try {
state.applyPendingSince = 0;
const currentPath = observedChannelPath(location.pathname) || location.pathname.replace(/\/$/, '') || '/';
state.route = routeKind(location.href);
if (state.lastPath !== currentPath) {
state.lastPath = currentPath;
const restored = state.route === 'channel' && restoreChannelLayout(currentPath);
if (!restored) {
state.runtime.focus = state.route === 'channel' && state.settings.layout.streamStart === 'focus';
state.runtime.theater = state.route === 'channel' && state.settings.layout.streamStart === 'theater';
state.runtime.chatHidden = false;
state.runtime.sidebarHidden = false;
state.runtime.chatPaused = false;
state.runtime.chatScrollLastTop = 0;
state.observers.chat?.disconnect?.();
state.observers.chat = null;
}
state.runtime.matureVisible = false;
announce(trf('Kick Focus applied to {route}', { route: state.route }));
}
ensureSiteStyle();
applySettingsAttributes();
tagChatPanel();
tagMonetizationSurfaces();
tagHideableElements();
tagSignedInRouteChrome();
ensureProfileStatsControl();
removeAdShells();
applyContentFilters();
syncNativeSidebar();
tagFollowingPreviewRows();
applyRailVisibility();
applySearchEnhancements();
applyDropsEnhancements();
applyMediaMemory();
syncSessionWatchTime();
applyPlayerResilience();
applyChatPause();
observeChatStickerDiscovery();
refreshLiveChannel().catch(() => {});
const resume = yieldToInput();
if (resume) {
elapsed += performance.now() - started;
await resume;
if (state.runtime.suspended) return;
started = performance.now();
}
replayPendingDeletions();
replayPendingBadges();
runRewardClaim();
renderStickerOrganizer();
applyChatHighlights();
applyPlaybackDiagnostics();
applyStreamUptime();
applyVodExpiry();
state.compatibility = compatibilitySnapshot(document, { expectedChat: state.route === 'channel', derive: compatibilityDerivers() });
publishCompatibility();
updateCompatibilityInPlace();
applyRouteLayout();
syncQuickButton();
} catch (error) {
logAppError('apply cycle', error);
} finally {
state.runtime.applyRunning = false;
state.diagnostics.apply = recordApplyCost(state.diagnostics.apply, elapsed + (performance.now() - started));
updateApplyCostInPlace();
if (state.currentPage === 'viewer' && state.modal && !state.modal.hidden) renderViewerHubInPlace();
}
}
function updateApplyCostInPlace() {
const node = state.shadow?.querySelector('[data-kf-apply-cost]');
if (node) node.textContent = tr(applyCostSummary(state.diagnostics.apply));
}
function installSpaHooks() {
if (pageWindow.__kickFocusSpaHooksV1) return;
pageWindow.__kickFocusSpaHooksV1 = true;
const routeChanged = () => pageWindow.dispatchEvent(new pageWindow.Event(ROUTE_EVENT));
const navigation = pageWindow.navigation;
if (navigation && typeof navigation.addEventListener === 'function') {
navigation.addEventListener('currententrychange', routeChanged);
state.runtime.routeSource = 'navigation-api';
} else {
for (const method of ['pushState', 'replaceState']) {
try {
const original = pageWindow.history[method];
pageWindow.history[method] = function kickFocusHistory(...args) {
const result = original.apply(this, args);
routeChanged();
return result;
};
} catch {
}
}
pageWindow.addEventListener('popstate', routeChanged);
state.runtime.routeSource = 'history-patch';
}
pageWindow.addEventListener(ROUTE_EVENT, () => scheduleApply(20));
}
function installDocumentObserver() {
if (state.observers.document) return;
state.observers.document = new MutationObserver(() => scheduleApply(80));
state.observers.document.observe(document.documentElement, { childList: true, subtree: true });
}
function installRuntimeInteractions() {
if (pageWindow.__kickFocusRuntimeInteractionsV1) return;
pageWindow.__kickFocusRuntimeInteractionsV1 = true;
document.addEventListener('click', handleCardAction, true);
document.addEventListener('click', handleSearchAction, true);
document.addEventListener('click', handleStickerAction, true);
document.addEventListener('click', handleChatStickerSave, true);
document.addEventListener('keydown', handleChatStickerSave, true);
document.addEventListener('click', (event) => {
const button = event.target.closest?.('[data-kf-chat-pause]');
if (!button) return;
event.preventDefault();
event.stopPropagation();
state.runtime.chatPaused = !state.runtime.chatPaused;
applySettingsAttributes();
applyChatPause();
if (!state.runtime.chatPaused) {
const messages = document.querySelector('[data-testid="chatroom-messages"], #chatroom-messages');
if (messages) messages.scrollTop = messages.scrollHeight;
}
announce(state.runtime.chatPaused ? 'Chat updates paused' : 'Chat updates resumed');
}, true);
const refreshPlayer = () => {
if (state.settings.layout.playerResizeRecovery) scheduleApply(0);
};
window.addEventListener('resize', refreshPlayer, { passive: true });
window.addEventListener('orientationchange', refreshPlayer, { passive: true });
window.visualViewport?.addEventListener('resize', refreshPlayer, { passive: true });
}
function rememberWatchedCard(event) {
const actualTarget = event.composedPath?.()[0] || event.target;
const link = actualTarget.closest?.('a[href]');
const main = findProbe(document, 'main').element;
const sidebar = findProbe(document, 'sidebar').element;
if (!link || (main && !main.contains(link) && sidebar && !sidebar.contains(link))) return;
if (!link) return;
try {
const path = observedChannelPath(new URL(link.href, location.origin).pathname);
if (!path) return;
state.watched.add(path);
const values = [...state.watched].slice(-200);
sessionStorage.setItem(WATCHED_KEY, JSON.stringify(values));
} catch {
}
}
function channelLayoutMap() {
return normalizeChannelLayouts(gmGet(CHANNEL_LAYOUT_KEY, {}));
}
function restoreChannelLayout(path) {
if (!state.settings.layout.rememberPerChannel) return false;
const canonical = observedChannelPath(path);
const saved = channelLayoutMap()[canonical];
if (!saved || typeof saved !== 'object') return false;
state.runtime.focus = Boolean(saved.focus);
state.runtime.theater = Boolean(saved.theater);
state.runtime.chatHidden = Boolean(saved.chatHidden);
state.runtime.sidebarHidden = Boolean(saved.sidebarHidden);
return true;
}
function saveChannelLayout() {
if (state.route !== 'channel' || !state.settings.layout.rememberPerChannel) return;
const path = channelPath();
if (!path) return;
const map = channelLayoutMap();
map[path] = {
focus: state.runtime.focus,
theater: state.runtime.theater,
chatHidden: state.runtime.chatHidden,
sidebarHidden: state.runtime.sidebarHidden,
};
const trimmed = Object.fromEntries(Object.entries(map).slice(-50));
gmSet(CHANNEL_LAYOUT_KEY, trimmed);
}
function announce(message) {
if (!state.settings.accessibility.announceChanges) return;
const live = state.shadow?.querySelector('[data-kf-live]');
if (!live) return;
const spoken = tr(message);
live.textContent = '';
requestAnimationFrame(() => { live.textContent = spoken; });
}
function updateSetting(path, value, message = 'Autosaved') {
const [section, key] = path.split('.');
if (!state.settings[section] || !(key in state.settings[section])) return;
state.settings = normalizeSettings({
...state.settings,
[section]: { ...state.settings[section], [key]: value },
});
if (path === 'content.rememberVolume' && !state.settings.content.rememberVolume) clearMediaPreferenceKind('volume');
if (path === 'content.rememberQuality' && !state.settings.content.rememberQuality) clearMediaPreferenceKind('quality');
if (path === 'content.preferBestQuality' && !state.settings.content.preferBestQuality) clearMediaPreferenceKind('ladder');
if (path === 'content.rememberVodPosition' && !state.settings.content.rememberVodPosition) clearMediaPreferenceKind('position');
if (path === 'content.stickyChatPause' && !state.settings.content.stickyChatPause) {
state.runtime.chatPaused = false;
state.observers.chat?.disconnect?.();
state.observers.chat = null;
}
if (path === 'content.chatComposerRecall' && !state.settings.content.chatComposerRecall) {
state.chatComfort.composerRecall = [];
state.chatComfort.composerRecallIndex = -1;
}
if (path === 'content.blocklistSubscription' && !state.settings.content.blocklistSubscription) clearRemoteBlocklist();
if (path === 'content.blocklistUrl' && state.remoteBlocklist.source !== state.settings.content.blocklistUrl) clearRemoteBlocklist();
if (path.startsWith('content.blocklist')) window.setTimeout(() => scheduleRemoteBlocklistSync(true), 0);
saveSettings(message);
scheduleApply(0);
renderSettingsPage();
renderCommands();
}
const TRUSTED_HTML_POLICY = (() => {
try {
const api = typeof window !== 'undefined' ? window.trustedTypes : undefined;
return typeof api?.createPolicy === 'function'
? api.createPolicy('kick-focus', { createHTML: (value) => value })
: null;
} catch {
return null;
}
})();
function trustedHTML(value) {
return TRUSTED_HTML_POLICY ? TRUSTED_HTML_POLICY.createHTML(String(value)) : value;
}
function setMarkup(node, value) {
if (!node) return;
node.innerHTML = trustedHTML(String(value));
}
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
}
const UI_CSS = `
:host {
color-scheme: dark;
--accent: var(--kf-accent, #7cff2b);
--accent-rgb: var(--kf-accent-rgb, 124, 255, 43);
--surface-0: var(--kf-canvas, #070908);
--surface-1: var(--kf-panel, #101311);
--surface-2: var(--kf-panel-raised, #151917);
--surface-3: var(--kf-panel-high, #1c211e);
--surface-inset: var(--kf-surface-inset, #0b0e0c);
--surface-hover: var(--kf-surface-hover, #202621);
--surface-selected: var(--kf-surface-selected, #182019);
--surface-danger: #2a1416;
--border: var(--kf-border, #353b37);
--border-subtle: color-mix(in srgb, var(--border) 68%, transparent);
--border-control: color-mix(in srgb, var(--border-strong) 78%, var(--border));
--border-strong: var(--kf-border-strong, #59645c);
--text: var(--kf-text, #f4f7f5);
--text-secondary: var(--kf-text-secondary, #c7cec9);
--muted: var(--kf-text-muted, #a5aea8);
--subtle: color-mix(in srgb, var(--muted) 72%, var(--surface-0));
--on-accent: var(--kf-on-accent, #071004);
--danger: var(--kf-danger, #ff6258);
--danger-text: #ffaaa4;
--warning: var(--kf-warning, #f6b943);
--success: var(--accent);
--radius-sm: calc(var(--kf-radius, 7px) - 3px);
--radius-md: calc(var(--kf-radius, 7px) - 1px);
--radius-lg: calc(var(--kf-radius, 7px) + 3px);
--radius: var(--kf-radius, 7px);
--focus-ring: var(--kf-focus-ring, 3px solid var(--accent));
--focus-offset: var(--kf-focus-offset, 2px);
--shadow-dialog: 0 38px 110px rgba(0,0,0,.72), 0 0 0 1px rgba(255,255,255,.015);
--shadow-control: 0 10px 28px rgba(0,0,0,.24);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 14px;
line-height: 1.45;
}
*, *::before, *::after { box-sizing: border-box; }
[hidden] { display: none !important; }
button, input, select, textarea { font: inherit; }
button { color: inherit; }
:is(button, input, select, textarea) { transition: border-color 150ms ease, background-color 150ms ease, color 150ms ease, box-shadow 150ms ease, transform 150ms ease; }
::selection { background: rgba(var(--accent-rgb), .28); color: var(--text); }
.kf-quick {
position: fixed;
left: 18px;
bottom: 18px;
z-index: 2147483000;
min-width: 76px;
height: 38px;
padding: 0 16px;
border: 1px solid var(--border-control);
border-radius: var(--radius-md);
background: var(--surface-2);
color: var(--text);
box-shadow: 0 14px 38px rgba(0,0,0,.5);
cursor: pointer;
font-size: 12px;
font-weight: 800;
letter-spacing: .06em;
text-transform: uppercase;
}
.kf-quick:hover { border-color: var(--accent); background: var(--surface-hover); color: var(--accent); transform: translateY(-1px); }
.kf-quick:active { transform: translateY(0); }
.kf-backdrop {
position: fixed;
inset: 0;
z-index: 2147483200;
display: grid;
place-items: center;
padding: 22px;
background: radial-gradient(circle at 50% 14%, rgba(var(--accent-rgb), .055), transparent 38%), rgba(2, 3, 3, .9);
}
.kf-settings {
position: relative;
zoom: var(--kf-interface-scale, 1);
width: min(1140px, calc((100vw - 44px) / var(--kf-interface-scale, 1)));
height: min(940px, calc((100vh - 44px) / var(--kf-interface-scale, 1)));
min-width: calc(860px / var(--kf-interface-scale, 1));
min-height: calc(640px / var(--kf-interface-scale, 1));
display: grid;
grid-template-rows: 76px minmax(0, 1fr) 68px;
overflow: hidden;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--surface-1);
box-shadow: var(--shadow-dialog);
color: var(--text);
font-size: 14px;
}
.kf-header {
display: grid;
grid-template-columns: 1fr auto auto;
align-items: center;
gap: 18px;
padding: 0 20px;
border-bottom: 1px solid var(--border-subtle);
background: var(--surface-2);
}
.kf-brand { display: flex; align-items: center; gap: 9px; min-width: 0; font-size: 16px; font-weight: 820; letter-spacing: -.02em; }
.kf-brand-mark { width: 28px; height: 28px; display: block; object-fit: contain; }
.kf-badge { padding: 2px 6px; border: 1px solid rgba(var(--accent-rgb), .68); border-radius: 3px; color: var(--accent); font-size: 9px; font-weight: 900; letter-spacing: .06em; text-transform: uppercase; }
.kf-save { display: flex; align-items: center; color: var(--text-secondary); font-size: 12px; font-weight: 650; }
.kf-save::before { content: ''; display: inline-block; width: 8px; height: 8px; margin-right: 8px; border: 1px solid var(--accent); border-radius: 2px; background: var(--accent); }
.kf-save[data-error="true"] { color: var(--danger); }
.kf-save[data-error="true"]::before { border-color: var(--danger); background: var(--danger); }
.kf-icon-button {
width: 38px;
height: 38px;
display: grid;
place-items: center;
border: 1px solid transparent;
border-radius: 5px;
background: transparent;
color: var(--text);
cursor: pointer;
line-height: 1;
}
.kf-icon-button:hover { border-color: var(--border-control); background: var(--surface-hover); }
.kf-icon-button:active { background: var(--surface-selected); transform: translateY(1px); }
.kf-icon { width: 18px; height: 18px; display: block; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.kf-body { min-height: 0; display: grid; grid-template-columns: 232px minmax(0, 1fr); }
.kf-nav { padding: 14px 10px; border-right: 1px solid var(--border); background: var(--surface-0); }
.kf-nav button {
position: relative;
width: 100%;
display: grid;
grid-template-columns: 24px minmax(0, 1fr);
align-items: center;
gap: 13px;
min-height: 56px;
padding: 0 14px;
border: 1px solid transparent;
border-radius: var(--radius-md);
background: transparent;
text-align: left;
cursor: pointer;
}
.kf-nav button::before { content: ''; position: absolute; inset: 12px auto 12px 0; width: 3px; border-radius: 2px; background: transparent; }
.kf-nav button:hover { border-color: var(--border-subtle); background: var(--surface-hover); }
.kf-nav button:active { transform: translateY(1px); }
.kf-nav button[aria-current="page"]::before { background: var(--accent); box-shadow: 0 0 14px rgba(var(--accent-rgb), .35); }
.kf-nav button[aria-current="page"] { border-color: rgba(var(--accent-rgb), .22); background: var(--surface-selected); color: var(--text); }
.kf-nav .kf-icon { width: 20px; height: 20px; color: var(--text-secondary); }
.kf-nav button[aria-current="page"] .kf-icon { color: var(--accent); }
.kf-nav-copy { display: grid; gap: 2px; min-width: 0; }
.kf-nav strong { font-size: 13px; font-weight: 760; }
.kf-nav span { overflow: hidden; color: var(--muted); font-size: 11px; line-height: 1.3; text-overflow: ellipsis; white-space: nowrap; }
.kf-page { min-width: 0; overflow-x: hidden; overflow-y: auto; padding: 24px 34px 40px 36px; scrollbar-color: var(--border-control) transparent; scrollbar-width: thin; }
.kf-page:focus { outline: 0; }
.kf-nav-search { padding: 0 10px 10px; }
.kf-nav-search input {
width: 100%;
min-height: 32px;
padding: 6px 10px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--surface-inset);
color: var(--text);
font: inherit;
font-size: 12px;
}
.kf-nav-search input:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.kf-search-results { display: flex; flex-direction: column; gap: 2px; padding: 6px; }
.kf-search-result {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
width: 100%;
min-height: 44px;
padding: 8px 10px;
border: 1px solid transparent;
border-radius: var(--radius-sm);
background: none;
color: var(--text);
font: inherit;
text-align: left;
cursor: pointer;
}
.kf-search-result:hover, .kf-search-result:focus-visible { border-color: var(--accent); background: var(--surface-inset); }
.kf-search-result-copy { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.kf-search-result-copy strong { font-size: 13px; }
.kf-search-result-copy small { color: var(--muted); font-size: 11px; line-height: 1.35; }
.kf-search-result-page { flex: none; color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .06em; }
.kf-search-empty { padding: 18px; color: var(--muted); }
.kf-search-empty p { margin: 0 0 6px; }
.kf-page-header { min-height: 86px; display: flex; align-items: center; justify-content: space-between; gap: 28px; padding-bottom: 20px; border-bottom: 1px solid var(--border); }
.kf-page-header h2 { margin: 2px 0 5px; font-size: 29px; line-height: 1.06; letter-spacing: -.04em; }
.kf-page-header p { max-width: 560px; margin: 0; color: var(--muted); font-size: 13px; line-height: 1.45; }
.kf-eyebrow { display: block; color: var(--accent); font-size: 10px; font-weight: 850; letter-spacing: .12em; text-transform: uppercase; }
.kf-page-meta { display: grid; gap: 3px; min-width: 140px; text-align: right; }
.kf-page-meta span { color: var(--subtle); font-size: 10px; font-weight: 850; letter-spacing: .1em; text-transform: uppercase; }
.kf-page-meta strong { color: var(--text-secondary); font-size: 12px; font-weight: 740; }
.kf-page-meta-control { min-width: 118px; justify-items: end; }
.kf-panel { border: 0; border-radius: 0; background: transparent; overflow: visible; }
.kf-row {
min-height: 74px;
display: grid;
grid-template-columns: minmax(230px, 1fr) minmax(300px, auto);
align-items: center;
gap: 26px;
padding: 12px 0;
border-bottom: 1px solid var(--border-subtle);
}
.kf-row h3 { margin: 0 0 4px; color: var(--text); font-size: 13px; font-weight: 780; letter-spacing: .01em; }
.kf-row p { max-width: 420px; margin: 0; color: var(--muted); font-size: 12px; line-height: 1.5; }
.kf-row-wide { grid-template-columns: minmax(220px, .82fr) minmax(360px, 1.18fr); }
.kf-control { min-width: 300px; display: flex; justify-content: flex-end; }
.kf-segmented { display: inline-flex; border: 1px solid var(--border-control); border-radius: var(--radius-md); overflow: hidden; background: var(--surface-inset); box-shadow: inset 0 1px rgba(255,255,255,.02); }
.kf-segmented button {
min-width: 78px;
height: 40px;
padding: 0 13px;
border: 0;
border-left: 1px solid var(--border-control);
background: transparent;
color: var(--text-secondary);
cursor: pointer;
font-size: 12px;
font-weight: 680;
}
.kf-segmented button:first-child { border-left: 0; }
.kf-segmented button:hover { background: var(--surface-hover); color: var(--text); }
.kf-segmented button:active { background: var(--surface-selected); }
.kf-segmented button[aria-pressed="true"] { background: rgba(var(--accent-rgb), .1); color: var(--text); box-shadow: inset 0 0 0 1px var(--accent); }
.kf-hide-grid { display: grid; gap: 14px; width: 100%; }
.kf-hide-heading { display: block; margin-bottom: 7px; color: var(--muted); font-size: 10px; font-weight: 850; letter-spacing: .1em; text-transform: uppercase; }
.kf-hide-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.kf-hide-chip {
min-height: 32px;
padding: 0 11px;
border: 1px solid var(--border-control);
border-radius: var(--radius-md);
background: var(--surface-inset);
color: var(--text-secondary);
cursor: pointer;
font-size: 12px;
font-weight: 680;
}
.kf-hide-chip:hover { background: var(--surface-hover); color: var(--text); }
.kf-hide-chip:active { background: var(--surface-selected); }
.kf-hide-chip[aria-pressed="true"] {
background: rgba(var(--accent-rgb), .1);
color: var(--text);
box-shadow: inset 0 0 0 1px var(--accent);
text-decoration: line-through;
text-decoration-color: var(--accent);
}
.kf-switch {
width: 58px;
height: 32px;
position: relative;
border: 1px solid var(--border-control);
border-radius: var(--radius-md);
background: var(--surface-inset);
color: var(--text-secondary);
cursor: pointer;
font-size: 10px;
font-weight: 850;
letter-spacing: .06em;
text-transform: uppercase;
}
.kf-switch:hover { border-color: var(--border-strong); background: var(--surface-hover); }
.kf-switch[aria-checked="true"] { border-color: var(--accent); background: var(--accent); color: var(--on-accent); box-shadow: 0 0 0 1px rgba(var(--accent-rgb), .12); }
.kf-switch:disabled { opacity: .72; cursor: not-allowed; }
.kf-text::placeholder, .kf-textarea::placeholder { color: var(--muted); opacity: 1; }
.kf-lock { display: inline-block; margin-left: 7px; padding: 2px 6px; border: 1px solid rgba(var(--accent-rgb), .5); border-radius: 3px; color: var(--accent); font-size: 9px; font-weight: 850; text-transform: uppercase; }
.kf-range { display: grid; grid-template-columns: 48px minmax(220px, 1fr) 48px; align-items: end; gap: 10px; width: 100%; }
.kf-range span { color: var(--muted); font-size: 11px; }
.kf-range span:last-child { text-align: right; }
.kf-range-wrap { position: relative; display: grid; gap: 4px; }
.kf-range output { justify-self: center; min-width: 52px; padding: 2px 7px; color: var(--text); text-align: center; font-size: 10px; font-weight: 750; }
.kf-range input { width: 100%; height: 16px; accent-color: var(--accent); }
.kf-text, .kf-textarea {
width: 100%;
min-height: 40px;
padding: 9px 11px;
border: 1px solid var(--border-control);
border-radius: var(--radius-md);
background: var(--surface-inset);
color: var(--text);
}
.kf-textarea { min-height: 86px; resize: vertical; }
.kf-text:focus, .kf-textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(var(--accent-rgb), .15); }
.kf-select { min-width: 118px; height: 36px; padding: 0 28px 0 10px; border: 1px solid var(--border-control); border-radius: var(--radius-md); background: var(--surface-inset); color: var(--text); font-size: 12px; }
.kf-select:hover { border-color: var(--border-strong); }
.kf-select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(var(--accent-rgb), .15); }
.kf-theme-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
.kf-swatch-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 7px; }
.kf-preset-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 7px; }
.kf-preset-card { position: relative; display: grid; align-content: start; gap: 4px; min-height: 82px; padding: 11px; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface-inset); color: var(--text); text-align: left; cursor: pointer; }
.kf-preset-card::before { content: ''; position: absolute; inset: 0 auto 0 0; width: 2px; background: rgba(var(--accent-rgb), .5); opacity: .55; }
.kf-preset-card:hover { border-color: var(--border-strong); background-color: var(--surface-hover); transform: translateY(-1px); box-shadow: var(--shadow-control); }
.kf-preset-card:active { transform: translateY(0); box-shadow: none; }
.kf-preset-card span { color: var(--accent); font-size: 9px; font-weight: 850; letter-spacing: .09em; text-transform: uppercase; }
.kf-preset-card strong { font-size: 12px; }
.kf-preset-card small { color: var(--muted); font-size: 9px; line-height: 1.4; }
.kf-theme-board {
position: relative;
min-height: 126px;
display: grid;
align-content: start;
gap: 10px;
padding: 12px;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--surface-inset);
color: var(--text);
cursor: pointer;
text-align: left;
}
.kf-theme-board:hover { border-color: var(--border-strong); transform: translateY(-2px); box-shadow: var(--shadow-control); }
.kf-theme-board:active { transform: translateY(0); box-shadow: none; }
.kf-theme-board[aria-pressed="true"] { border-color: var(--accent); background: var(--surface-selected); box-shadow: inset 0 0 0 1px rgba(var(--accent-rgb), .15); }
.kf-theme-board-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.kf-theme-board-top > span:first-child { color: var(--subtle); font-size: 8px; font-weight: 850; letter-spacing: .1em; text-transform: uppercase; }
.kf-theme-selected { min-height: 15px; color: var(--accent); font-size: 8px; font-weight: 850; letter-spacing: .08em; text-transform: uppercase; }
.kf-theme-tones { height: 34px; display: grid; grid-template-columns: 1.4fr 1fr .8fr; gap: 3px; padding: 4px; border: 1px solid var(--theme-border); border-radius: 4px; background: var(--theme-canvas); }
.kf-theme-tones i { display: block; border-radius: 2px; background: var(--theme-panel); }
.kf-theme-tones i:nth-child(2) { background: var(--theme-raised); }
.kf-theme-tones i:nth-child(3) { background: var(--theme-high); }
.kf-theme-board[data-value="studio"] { --theme-canvas: #080b09; --theme-panel: #0e130f; --theme-raised: #141a16; --theme-high: #1a221c; --theme-border: #46564b; }
.kf-theme-board[data-value="oled"] { --theme-canvas: #000; --theme-panel: #030404; --theme-raised: #080a09; --theme-high: #0e1110; --theme-border: #414b45; }
.kf-theme-board[data-value="slate"] { --theme-canvas: #0b0f14; --theme-panel: #111820; --theme-raised: #18222c; --theme-high: #202d39; --theme-border: #5a7084; }
.kf-theme-copy { display: grid; gap: 2px; }
.kf-theme-copy strong { font-size: 12px; }
.kf-theme-copy small { color: var(--muted); font-size: 9px; line-height: 1.35; }
.kf-accent-chip { min-width: 0; min-height: 52px; display: grid; grid-template-columns: 18px minmax(0, 1fr); align-items: center; gap: 7px; padding: 8px; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface-inset); color: var(--text-secondary); cursor: pointer; text-align: left; }
.kf-accent-chip:hover { border-color: var(--border-strong); background: var(--surface-hover); }
.kf-accent-chip[aria-pressed="true"] { border-color: var(--accent); background: var(--surface-selected); color: var(--text); box-shadow: inset 0 0 0 1px rgba(var(--accent-rgb), .12); }
.kf-accent-chip strong { min-width: 0; font-size: 9px; line-height: 1.15; }
.kf-swatch { width: 18px; height: 30px; border-radius: 3px; border: 1px solid rgba(255,255,255,.24); }
.kf-swatch[data-color="kick"] { background: #7cff2b; }
.kf-swatch[data-color="cyan"] { background: #38d7d0; }
.kf-swatch[data-color="violet"] { background: #9667ff; }
.kf-swatch[data-color="gold"] { background: #ffbe2e; }
.kf-swatch[data-color="custom"] { background: var(--kf-custom-accent, #ff5ca8); }
.kf-custom-color { display: flex; align-items: center; gap: 10px; width: 100%; padding: 9px 10px; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface-inset); }
.kf-custom-color input { width: 42px; height: 34px; padding: 2px; border: 0; border-radius: 4px; background: transparent; cursor: pointer; }
.kf-custom-color strong { color: var(--text); font-size: 11px; }
.kf-custom-color small { display: block; margin-top: 2px; color: var(--muted); font-size: 9px; }
.kf-custom-accent-row[data-visible="false"] { display: none; }
.kf-appearance-layout { display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(300px, .7fr); gap: 24px; }
.kf-appearance-controls { min-width: 0; }
.kf-appearance-controls .kf-row, .kf-appearance-controls .kf-row-wide { min-height: 0; grid-template-columns: 1fr; gap: 8px; padding: 11px 0; }
.kf-appearance-controls .kf-row:has(.kf-control) { min-height: 64px; grid-template-columns: minmax(160px, 1fr) minmax(190px, auto); align-items: center; gap: 12px; }
.kf-appearance-controls .kf-row:has(.kf-control) p { max-width: 200px; }
.kf-appearance-controls .kf-control { width: 190px; min-width: 0; justify-content: flex-end; }
.kf-appearance-controls .kf-segmented { width: 100%; }
.kf-appearance-controls .kf-segmented button { min-width: 0; flex: 1; padding-inline: 8px; }
.kf-appearance-controls .kf-range { grid-template-columns: 38px minmax(90px, 1fr) 34px; gap: 6px; }
.kf-preview { position: sticky; top: 0; align-self: start; min-width: 0; padding-left: 20px; border-left: 1px solid var(--border); }
.kf-preview-kicker { color: var(--accent); font-size: 10px; font-weight: 850; letter-spacing: .1em; text-transform: uppercase; }
.kf-preview-intro { margin: 3px 0 14px; color: var(--muted); font-size: 11px; }
.kf-preview-surface { overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface-inset); box-shadow: var(--shadow-control); }
.kf-preview-surface header { display: flex; align-items: center; gap: 12px; min-height: 48px; padding: 0 12px; border-bottom: 1px solid var(--border-subtle); font-size: 9px; }
.kf-preview-surface header strong { margin-right: auto; color: var(--accent); font-size: 12px; }
.kf-preview-surface header span { color: var(--muted); }
.kf-preview-image { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; border-bottom: 1px solid var(--border-subtle); filter: saturate(var(--kf-thumb-saturation, 1.03)); }
.kf-preview-feature { padding: 18px 14px; border-bottom: 1px solid var(--border-subtle); }
.kf-preview-feature h3 { margin: 7px 0 3px; font-size: 15px; line-height: 1.2; }
.kf-preview-feature p { margin: 0; color: var(--muted); font-size: 10px; }
.kf-preview-live { display: inline-flex; align-items: center; gap: 7px; color: #dce3de; font-size: 10px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; }
.kf-preview-live::before { content: ''; width: 7px; height: 7px; border-radius: 2px; background: var(--accent); }
.kf-preview-action { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 18px; color: var(--muted); font-size: 9px; }
.kf-preview-action b { padding: 7px 10px; border: 1px solid var(--accent); border-radius: 3px; color: var(--accent); font-size: 9px; }
.kf-preview-list { display: flex; justify-content: space-between; gap: 10px; padding: 12px 14px; border-bottom: 1px solid var(--border-subtle); font-size: 9px; }
.kf-preview-list:last-child { border-bottom: 0; }
.kf-preview-list span { color: var(--muted); }
.kf-preview-list strong { text-align: right; }
.kf-status-card { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 18px; padding: 18px 0; border-bottom: 1px solid var(--border-subtle); }
.kf-status-card h3 { margin: 0 0 3px; font-size: 15px; }
.kf-status-card p { max-width: 560px; margin: 0; color: var(--muted); font-size: 12px; line-height: 1.5; }
.kf-active { color: var(--accent); font-weight: 800; }
.kf-stats { min-width: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); border-bottom: 1px solid var(--border-subtle); }
.kf-stat { min-width: 0; padding: 15px 12px; border-left: 1px solid var(--border-subtle); text-align: left; }
.kf-stat:first-child { border-left: 0; }
.kf-stat span { display: block; color: var(--muted); font-size: 9px; letter-spacing: .06em; text-transform: uppercase; }
.kf-stat strong { display: block; overflow: hidden; margin-top: 4px; color: var(--accent); font-size: 15px; text-overflow: ellipsis; white-space: nowrap; }
.kf-defense-overview { min-width: 0; display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(280px, .65fr); border-bottom: 1px solid var(--border); }
.kf-defense-overview .kf-status-card, .kf-defense-overview .kf-stats { border-bottom: 0; }
.kf-defense-overview .kf-status-card { padding-right: 18px; }
.kf-defense-overview .kf-status-card > .kf-active { display: none; }
.kf-defense-overview .kf-stats { border-left: 1px solid var(--border-subtle); }
.kf-content-section { margin-top: 18px; }
.kf-content-section {
overflow: hidden;
padding: 0 16px;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--surface-inset);
box-shadow: 0 1px 0 rgba(255,255,255,.015) inset;
}
.kf-content-section .kf-subsection-header { margin: 0 -16px; padding: 14px 16px 12px; border-bottom-color: var(--border); background: var(--surface-2); }
[data-kf-current-page="content"] .kf-row { min-height: 62px; padding: 10px 0; }
[data-kf-current-page="content"] .kf-row h3 { margin-bottom: 2px; font-size: 12px; }
[data-kf-current-page="content"] .kf-row p { font-size: 11px; }
[data-kf-current-page="content"] .kf-subsection { margin-top: 20px; }
[data-kf-current-page="content"] .kf-status-note { font-size: 10px; }
.kf-tool-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-top: 20px; }
.kf-tool-card { min-height: 96px; display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 14px; padding: 14px; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface-inset); }
.kf-tool-card:hover { border-color: var(--border-control); background: var(--surface-hover); }
.kf-tool-card h3 { margin: 0 0 3px; font-size: 11px; }
.kf-tool-card p { margin: 0; color: var(--muted); font-size: 11px; line-height: 1.45; }
.kf-channel-input-row { display: grid; grid-template-columns: minmax(220px, 1fr) auto; gap: 9px; align-items: center; }
.kf-channel-list { display: grid; gap: 6px; margin-top: 10px; max-height: 280px; overflow: auto; scrollbar-gutter: stable; }
.kf-channel-entry { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 6px 10px; border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); background: var(--surface-inset); font-size: 13px; }
.kf-channel-entry span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.kf-emote-manager { margin-top: 18px; }
.kf-sticker-library-workspace { min-width: 0; display: grid; grid-template-columns: 210px minmax(0, 1fr); gap: 12px; }
.kf-sticker-group-panel { min-width: 0; padding: 12px; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface-inset); }
.kf-sticker-group-heading { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.kf-sticker-group-heading h4 { margin: 0; font-size: 13px; }
.kf-sticker-group-heading span { color: var(--muted); font-size: 11px; }
.kf-sticker-group-panel > p { margin: 4px 0 10px; color: var(--muted); font-size: 11px; line-height: 1.45; }
.kf-sticker-library-main { min-width: 0; }
.kf-sticker-library-controls { display: grid; grid-template-columns: minmax(220px, 1fr) 180px; gap: 9px; }
.kf-sticker-library-controls .kf-select { width: 100%; height: 40px; }
.kf-sticker-group-builder { display: grid; gap: 7px; }
.kf-sticker-group-list { display: grid; gap: 7px; margin-top: 10px; }
.kf-sticker-group-row { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 5px; align-items: center; }
.kf-sticker-group-row .kf-text { min-height: 34px; padding-block: 6px; }
.kf-sticker-group-row > span, .kf-sticker-group-empty { color: var(--muted); font-size: 10px; }
.kf-sticker-library-bulk { display: grid; grid-template-columns: auto auto auto minmax(120px, 1fr) auto auto; gap: 6px; align-items: center; margin-top: 9px; padding: 8px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface-2); }
.kf-sticker-library-bulk > strong { color: var(--text-secondary); font-size: 11px; white-space: nowrap; }
.kf-sticker-library-bulk .kf-select { min-width: 0; width: 100%; }
.kf-sticker-library-meta { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 13px 0 8px; color: var(--muted); font-size: 10px; }
.kf-sticker-library-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); gap: 8px; max-height: 520px; overflow: auto; padding-right: 4px; scrollbar-gutter: stable; }
.kf-my-emote-group { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border); }
.kf-my-emote-group:first-of-type { margin-top: 8px; }
.kf-my-emote-group > header { display: flex; align-items: end; justify-content: space-between; gap: 16px; margin-bottom: 8px; }
.kf-my-emote-group > header span { color: var(--accent); font-size: 8px; font-weight: 850; letter-spacing: .09em; text-transform: uppercase; }
.kf-my-emote-group > header h4 { margin: 3px 0 0; color: var(--text); font-size: 13px; }
.kf-my-emote-group > header > strong { color: var(--muted); font-size: 10px; }
.kf-my-emote-group .kf-sticker-library-grid { max-height: none; overflow: visible; padding-right: 0; }
.kf-sticker-library-item { min-width: 0; display: grid; grid-template-columns: 52px minmax(0, 1fr); gap: 10px; padding: 9px; border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); background: var(--surface-inset); content-visibility: auto; contain-intrinsic-size: auto 86px; }
.kf-sticker-library-item[data-removed="true"] { opacity: .58; }
.kf-sticker-library-item[data-selected="true"] { border-color: var(--accent); box-shadow: 0 0 0 1px rgba(var(--accent-rgb),.18); }
.kf-sticker-library-image { width: 52px; height: 52px; display: grid; place-items: center; padding: 5px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface-2); }
.kf-sticker-library-image img { width: 100%; height: 100%; object-fit: contain; }
.kf-sticker-library-copy { min-width: 0; }
.kf-sticker-library-copy strong { display: block; overflow: hidden; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
.kf-sticker-library-copy small { display: block; overflow: hidden; margin-top: 2px; color: var(--muted); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
.kf-sticker-access { display: inline-flex; margin-top: 5px; padding: 2px 5px; border: 1px solid #4b534e; border-radius: 3px; color: #b8c0bb; font-size: 9px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }
.kf-sticker-access[data-access="available"] { border-color: rgba(139,232,92,.55); color: #8be85c; }
.kf-sticker-access[data-access="channel"] { border-color: rgba(255,190,46,.58); color: #ffcf61; }
.kf-sticker-access[data-access="observed"] { border-color: rgba(56,215,208,.58); color: #70e9e3; }
.kf-emote-catalog-browser { display: grid; grid-template-columns: minmax(170px,.7fr) minmax(260px,1.3fr); gap: 12px; align-items: center; margin-bottom: 12px; padding: 12px; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface-2); }
.kf-emote-catalog-browser h4 { margin: 0; color: var(--text); font-size: 13px; }
.kf-emote-catalog-browser p { margin: 2px 0 0; color: var(--muted); font-size: 11px; line-height: 1.45; }
.kf-emote-catalog-status { grid-column: 1 / -1; }
.kf-emote-catalog-form { display: grid; grid-template-columns: minmax(0,1fr) auto; gap: 8px; }
.kf-emote-catalog-status[data-error="true"] { color: var(--danger-text); }
.kf-sticker-changed { display: inline-flex; margin: 5px 0 0 5px; padding: 2px 5px; border: 1px solid rgba(217,139,58,.62); border-radius: 3px; color: #e0a367; font-size: 8px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }
.kf-sticker-library-item[data-changed="true"] { border-color: rgba(217,139,58,.42); }
.kf-sticker-lock { display: block; margin-top: 5px; color: var(--muted); font-size: 9px; line-height: 1.5; white-space: normal; }
.kf-sticker-lock a { color: var(--accent); }
.kf-sticker-library-actions { grid-column: 1 / -1; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }
.kf-sticker-library-actions .kf-select { grid-column: 1 / -1; min-width: 0; width: 100%; }
.kf-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.kf-table th, .kf-table td { padding: 11px 9px; border-bottom: 1px solid var(--border-subtle); text-align: left; vertical-align: middle; }
.kf-table th { color: var(--text-secondary); background: transparent; font-size: 10px; letter-spacing: .07em; text-transform: uppercase; }
.kf-table .kf-muted { color: var(--muted); }
.kf-table tr:last-child td { border-bottom: 0; }
.kf-table .kf-table-actions { text-align: right; }
.kf-shortcut { display: inline-flex; min-width: 62px; justify-content: center; padding: 4px 8px; border: 1px solid #434a45; border-radius: 3px; background: #171b18; font-weight: 700; }
.kf-conflict td { background: rgba(255,98,88,.055); border-top: 1px solid var(--danger); border-bottom: 1px solid var(--danger); }
.kf-conflict-message { color: var(--danger); font-size: 11px; }
.kf-status-note { margin-top: 12px; padding: 10px 12px; border-left: 2px solid #4b534e; background: rgba(255,255,255,.018); color: var(--muted); font-size: 11px; }
.kf-status-note[data-drifted="true"] { border-color: #7b5d20; background: rgba(246,185,67,.065); color: #e7c77e; }
.kf-notice { margin-top: 12px; padding: 11px 13px; border-left: 2px solid #997326; background: rgba(246,185,67,.055); color: #e7c77e; font-size: 11px; }
.kf-subsection { margin-top: 26px; }
.kf-subsection-header { display: flex; align-items: end; justify-content: space-between; gap: 16px; margin-bottom: 10px; padding-bottom: 11px; border-bottom: 1px solid var(--border); }
.kf-subsection h3 { margin: 0; font-size: 13px; letter-spacing: .02em; }
.kf-subsection p { margin: 2px 0 0; color: var(--muted); font-size: 11px; }
.kf-about-status { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; padding: 18px 0; border-bottom: 1px solid var(--border-subtle); }
.kf-mini-card { padding: 14px; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface-inset); }
.kf-mini-card span { display: block; color: var(--muted); font-size: 9px; letter-spacing: .07em; text-transform: uppercase; }
.kf-mini-card strong { display: block; margin-top: 4px; color: var(--accent); }
.kf-nav-earned:empty { display: none; }
.kf-nav-earned { display: block; margin-top: 3px; color: var(--accent); font-size: 11px; }
.kf-nav-earned::before { content: '● '; }
[data-kf-earned="reward-ready"] { position: relative; }
[data-kf-earned="reward-ready"]::after {
content: ''; position: absolute; top: 4px; right: 4px; width: 7px; height: 7px;
border: 1px solid var(--surface-2); border-radius: 50%; background: var(--accent);
animation: kf-earned-pulse 2.4s ease-in-out infinite;
}
@keyframes kf-earned-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }
@media (prefers-reduced-motion: reduce) {
[data-kf-earned="reward-ready"]::after { animation: none; }
}
.kf-layout-save { display: grid; gap: 8px; justify-items: stretch; min-width: 240px; }
.kf-chip-row { display: flex; flex-wrap: wrap; gap: 6px; }
.kf-chip { padding: 5px 10px; color: var(--muted); border: 1px solid var(--border); border-radius: 999px; background: transparent; font-size: 11px; cursor: pointer; }
.kf-chip[aria-pressed="true"] { color: var(--accent); border-color: var(--accent); }
.kf-layout-list { display: grid; gap: 6px; padding: 10px 0 2px; }
.kf-layout-entry { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 12px; padding: 10px 12px; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface-inset); }
.kf-layout-entry[data-active="true"] { border-color: var(--accent); }
.kf-layout-entry span { display: block; margin-top: 2px; color: var(--muted); font-size: 11px; }
.kf-chat-log { display: grid; gap: 8px; min-width: 0; }
.kf-chat-log-row { display: grid; grid-template-columns: auto auto 1fr; gap: 8px; padding: 4px 0; border-bottom: 1px solid var(--border-subtle); font-size: 12px; }
.kf-chat-log-row span:first-child { color: var(--muted); font-variant-numeric: tabular-nums; }
.kf-chat-log-row strong { color: var(--accent); }
.kf-chat-log-row span:last-child { overflow-wrap: anywhere; }
.kf-hub-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; padding: 18px 0; }
.kf-hub-card { position: relative; overflow: hidden; min-height: 118px; padding: 15px; border: 1px solid var(--border); border-radius: var(--radius-md); background: linear-gradient(145deg, var(--surface-inset), var(--surface-1)); }
.kf-hub-card::before { content: ''; position: absolute; inset: 0 0 auto; height: 2px; background: var(--accent); opacity: .32; }
.kf-hub-card > span { display: block; color: var(--subtle); font-size: 9px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
.kf-hub-card > strong { display: block; margin-top: 7px; color: var(--accent); font-size: 19px; }
.kf-hub-card em { display: block; margin-top: 6px; color: var(--muted); font-size: 11px; font-style: normal; line-height: 1.45; }
.kf-hub-card[data-state="unavailable"] strong, .kf-hub-card[data-state="loading"] strong { color: var(--muted); }
.kf-hub-card[data-state="error"] strong { color: var(--danger-text); }
.kf-action-row { min-height: 78px; display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 22px; padding: 14px 0; border-bottom: 1px solid var(--border-subtle); }
.kf-action-row h3 { margin: 0 0 4px; font-size: 13px; }
.kf-action-row p { max-width: 560px; margin: 0; color: var(--muted); font-size: 12px; line-height: 1.5; }
.kf-danger { border-color: rgba(255,98,88,.65) !important; color: var(--danger-text) !important; }
[data-kf-current-page="accessibility"] { padding-top: 14px; }
[data-kf-current-page="accessibility"] .kf-page-header { min-height: 70px; padding-bottom: 12px; }
[data-kf-current-page="accessibility"] .kf-row { min-height: 60px; padding: 9px 0; }
[data-kf-current-page="accessibility"] .kf-subsection { margin-top: 14px; }
[data-kf-current-page="accessibility"] .kf-subsection-header { margin-bottom: 0; padding-bottom: 8px; }
[data-kf-current-page="accessibility"] .kf-table th, [data-kf-current-page="accessibility"] .kf-table td { padding-block: 7px; }
[data-kf-current-page="accessibility"] .kf-button-small { min-height: 32px; }
[data-kf-current-page="layout"] > .kf-panel,
[data-kf-current-page="accessibility"] > .kf-panel,
[data-kf-current-page="about"] > .kf-panel {
margin-top: 16px;
padding: 0 16px;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--surface-inset);
}
[data-kf-current-page="about"] .kf-action-row { min-height: 78px; padding-block: 13px; }
[data-kf-current-page="about"] .kf-subsection { margin-top: 18px; }
[data-kf-current-page="about"] .kf-subsection > .kf-panel { overflow: hidden; border: 1px solid var(--border); border-radius: 4px; }
.kf-button {
min-height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 0 14px;
border: 1px solid var(--border-control);
border-radius: var(--radius-md);
background: var(--surface-3);
color: var(--text);
cursor: pointer;
font-size: 12px;
font-weight: 700;
}
.kf-button:hover { border-color: var(--border-strong); background: var(--surface-hover); transform: translateY(-1px); box-shadow: var(--shadow-control); }
.kf-button:active { transform: translateY(0); box-shadow: none; }
.kf-button-primary { border-color: var(--accent); background: var(--accent); color: var(--on-accent); }
.kf-button-primary:hover { border-color: var(--accent); background: var(--accent); filter: brightness(1.08); }
.kf-button:disabled { opacity: .48; cursor: not-allowed; transform: none; box-shadow: none; }
.kf-button-primary:disabled { opacity: 1; border-color: var(--border); background: var(--surface-hover); color: var(--muted); }
.kf-button-small { min-height: 32px; padding-inline: 10px; font-size: 11px; }
.kf-button .kf-icon { width: 16px; height: 16px; }
.kf-button-group { display: flex; gap: 8px; justify-content: flex-end; flex-wrap: wrap; }
.kf-footer { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 0 22px; border-top: 1px solid var(--border); background: var(--surface-2); }
.kf-footer-left, .kf-footer-right { display: flex; align-items: center; gap: 10px; }
.kf-confirm {
position: absolute;
inset: 0;
z-index: 3;
display: grid;
place-items: center;
background: rgba(2,3,4,.76);
}
.kf-confirm-card { width: min(430px, calc(100vw - 32px)); padding: 24px; border: 1px solid var(--border-strong); border-radius: var(--radius-lg); background: var(--surface-2); box-shadow: var(--shadow-dialog); }
.kf-confirm-card h2 { margin: 0 0 8px; font-size: 19px; }
.kf-confirm-card p { margin: 0 0 18px; color: var(--muted); }
.kf-toast {
position: fixed;
right: 24px;
bottom: 24px;
z-index: 2147483500;
max-width: 430px;
padding: 11px 14px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--surface-3);
box-shadow: 0 18px 48px rgba(0,0,0,.5);
color: var(--text);
}
.kf-toast[data-error="true"] { border-color: var(--danger); background: var(--surface-danger); }
.kf-toast:has(.kf-toast-action) { display: flex; align-items: center; gap: 10px; }
.kf-toast-text { flex: 1 1 auto; }
.kf-toast-action {
flex: 0 0 auto;
padding: 5px 10px;
border: 1px solid var(--accent);
border-radius: 4px;
background: transparent;
color: var(--accent);
font: inherit;
font-weight: 650;
cursor: pointer;
}
.kf-toast-action:hover { background: var(--accent); color: var(--on-accent); }
.kf-toast-action:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.kf-storage-alert {
position: fixed;
z-index: 2147483001;
inset-inline: 16px;
bottom: 16px;
margin-inline: auto;
max-width: 640px;
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
border: 1px solid var(--danger);
border-radius: var(--radius-lg);
background: var(--surface-danger);
color: var(--text);
box-shadow: 0 18px 44px rgba(0,0,0,.55);
font-size: 12px;
}
.kf-storage-alert[hidden] { display: none; }
.kf-storage-alert-body { display: grid; gap: 2px; flex: 1; min-width: 0; }
.kf-storage-alert-body strong { font-size: 12px; }
.kf-storage-alert-body span { opacity: .85; }
.kf-command-shell {
width: min(560px, calc(100vw - 48px));
max-height: min(620px, calc(100vh - 80px));
overflow: hidden;
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background: var(--surface-1);
box-shadow: var(--shadow-dialog);
color: var(--text);
}
.kf-command-head { display: grid; grid-template-columns: 1fr auto; align-items: end; gap: 7px 12px; padding: 16px; border-bottom: 1px solid var(--border); }
.kf-command-head label { color: var(--text-secondary); font-size: 11px; font-weight: 780; letter-spacing: .05em; text-transform: uppercase; }
.kf-command-head span { color: var(--muted); font-size: 10px; }
.kf-command-head input {
grid-column: 1 / -1;
width: 100%;
height: 44px;
padding: 0 13px;
border: 1px solid var(--border-control);
border-radius: var(--radius-md);
background: var(--surface-inset);
color: var(--text);
outline: 0;
}
.kf-command-head input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(var(--accent-rgb), .15); }
@media (forced-colors: active) {
.kf-text:focus,
.kf-textarea:focus,
.kf-command-head input:focus,
.kf-select:focus,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
button:focus-visible,
[tabindex]:focus-visible {
outline: 3px solid Highlight;
outline-offset: 2px;
}
.kf-panel, .kf-settings, .kf-command-shell { border: 1px solid CanvasText; }
.kf-storage-alert { border: 2px solid CanvasText; }
.kf-switch { border: 1px solid CanvasText; }
.kf-switch[aria-checked="true"] { background: Highlight; }
[aria-checked="true"], [aria-selected="true"], [aria-pressed="true"], [aria-current="page"] {
outline: 2px solid Highlight;
outline-offset: 1px;
}
}
:host :is(button, a[href], input, select, textarea, [role="button"], [role="switch"], [role="option"]) {
min-width: 24px;
min-height: 24px;
}
:host p a[href], :host small a[href], :host li a[href] { min-width: 0; min-height: 0; }
:host [data-kf-page] :is(button, a[href], input, select, textarea, [role="switch"]) {
scroll-margin-block: 72px;
}
:host([data-kf-large-targets="true"]) :is(button, a[href], input, select, textarea) { min-height: 40px; }
:host([data-kf-large-targets="true"]) .kf-switch { min-width: 74px; }
:host([data-kf-large-targets="true"]) .kf-icon-button { min-width: 40px; }
:host([data-kf-large-targets="true"]) .kf-ms-bar :is(button, .kf-ms-link) { min-height: 32px; padding: 6px 10px; }
:host([data-kf-large-targets="true"]) .kf-ms-bar { opacity: 1; }
:host([data-kf-reduce-motion="true"]) *,
:host([data-kf-reduce-motion="true"]) *::before,
:host([data-kf-reduce-motion="true"]) *::after {
animation-duration: .001ms !important;
animation-iteration-count: 1 !important;
transition-duration: .001ms !important;
scroll-behavior: auto !important;
}
.kf-fact-list { margin: 0; padding: 0; display: grid; gap: 10px; }
.kf-fact { margin: 0; padding: 10px 12px; border-left: 3px solid var(--border-subtle); background: rgba(255,255,255,.02); border-radius: 0 4px 4px 0; }
.kf-fact dt { margin: 0 0 3px; font-size: 12px; font-weight: 700; }
.kf-fact dd { margin: 0; color: var(--muted); font-size: 11px; line-height: 1.55; }
.kf-rarity {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 1px 6px;
border-radius: var(--radius-sm);
border: 1px solid currentColor;
font-size: 10px;
font-weight: 700;
text-transform: capitalize;
}
.kf-rarity[data-rarity="common"] { color: #9ba59f; }
.kf-rarity[data-rarity="uncommon"] { color: #6fd47a; }
.kf-rarity[data-rarity="rare"] { color: #57b6ff; }
.kf-rarity[data-rarity="epic"] { color: #b184ff; }
.kf-rarity[data-rarity="legendary"] { color: #ffb648; }
.kf-rarity[data-rarity="mythic"] { color: #ff6b8b; }
.kf-ms-backdrop { padding: 0; }
.kf-ms-shell {
display: grid;
grid-template-rows: auto auto 1fr auto;
width: 100vw;
height: 100vh;
background: radial-gradient(circle at 50% -12%, rgba(var(--accent-rgb), .06), transparent 34%), var(--surface-0);
color: var(--text);
}
.kf-ms-head, .kf-ms-foot {
display: grid;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-bottom: 1px solid var(--border);
background: var(--surface-2);
}
.kf-ms-head { grid-row: 1; }
.kf-ms-error { grid-row: 2; }
.kf-ms-body { grid-row: 3; }
.kf-ms-foot { grid-row: 4; }
.kf-ms-head { grid-template-columns: minmax(150px, auto) minmax(300px, 1fr) auto; }
.kf-ms-foot { grid-template-columns: auto minmax(0, 1fr); border-bottom: 0; border-top: 1px solid var(--border); }
.kf-ms-brand { display: grid; gap: 2px; }
.kf-ms-brand strong { font-size: 15px; letter-spacing: -.015em; }
.kf-ms-add { display: flex; align-items: center; justify-content: center; gap: 7px; min-width: 0; }
.kf-ms-add input { width: min(420px, 100%); }
.kf-ms-controls { display: flex; align-items: center; justify-content: flex-end; gap: 6px; flex-wrap: wrap; }
.kf-ms-save-layout { display: flex; align-items: center; gap: 7px; }
.kf-ms-points-note { grid-column: 1 / -1; display: flex; align-items: center; gap: 7px; margin: 0; color: var(--muted); font-size: 10px; line-height: 1.45; }
.kf-ms-points-note svg { width: 14px; height: 14px; flex: 0 0 14px; color: var(--warning); }
.kf-ms-count { color: var(--muted); font-size: 11px; }
.kf-ms-head input, .kf-ms-foot input {
min-width: 220px;
min-height: 32px;
padding: 0 10px;
border: 1px solid var(--border-control);
border-radius: var(--radius-md);
background: var(--surface-inset);
color: var(--text);
font-size: 12px;
}
.kf-ms-head input:focus, .kf-ms-foot input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(var(--accent-rgb), .15); }
.kf-ms-select { min-height: 32px; font-size: 12px; }
.kf-ms-error { padding: 9px 14px; border-bottom: 1px solid var(--danger); background: var(--surface-danger); color: var(--danger-text); font-size: 12px; }
.kf-ms-error[hidden] { display: none; }
.kf-ms-body { display: grid; grid-template-columns: 1fr 0; min-height: 0; }
.kf-ms-backdrop[data-kf-multistream-show-chat="true"] .kf-ms-body { grid-template-columns: 1fr 340px; }
.kf-ms-grid {
display: grid;
grid-template-columns: repeat(var(--kf-multistream-columns, 1), 1fr);
gap: 8px;
padding: 10px;
min-height: 0;
align-content: stretch;
}
.kf-ms-empty-state {
grid-column: 1 / -1;
align-self: center;
justify-self: center;
width: min(520px, calc(100% - 36px));
display: grid;
justify-items: center;
gap: 10px;
padding: 34px;
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background: linear-gradient(145deg, var(--surface-1), var(--surface-inset));
box-shadow: var(--shadow-control);
text-align: center;
}
.kf-ms-empty-state img { width: 48px; height: 48px; object-fit: contain; }
.kf-ms-empty-state span { color: var(--accent); font-size: 9px; font-weight: 850; letter-spacing: .12em; text-transform: uppercase; }
.kf-ms-empty-state h2 { margin: 0; font-size: 23px; letter-spacing: -.035em; }
.kf-ms-empty-state p { max-width: 420px; margin: 0 0 4px; color: var(--muted); font-size: 12px; line-height: 1.55; }
.kf-ms-tile {
position: relative;
min-height: 0;
border: 1px solid var(--border);
border-radius: var(--radius-md);
overflow: hidden;
background: #000;
}
.kf-ms-tile[data-kf-multistream-focused="true"] { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }
.kf-ms-backdrop[data-kf-multistream-paused="true"] .kf-ms-tile::after {
content: attr(data-kf-multistream-tile) " (paused)";
position: absolute;
inset: 0;
display: grid;
place-items: center;
background: var(--surface-inset);
color: var(--muted);
font-size: 12px;
font-weight: 700;
}
.kf-ms-backdrop[data-kf-multistream-paused="true"] .kf-ms-bar { z-index: 1; }
.kf-ms-tile iframe { width: 100%; height: 100%; border: 0; display: block; }
.kf-ms-bar {
position: absolute;
inset-inline: 0;
top: 0;
display: flex;
align-items: center;
gap: 6px;
padding: 4px 6px;
background: linear-gradient(180deg, rgba(0,0,0,.78), transparent);
opacity: .88;
transition: opacity .12s ease;
font-size: 11px;
}
.kf-ms-bar .kf-ms-spacer { flex: 1; }
.kf-ms-tile:hover .kf-ms-bar, .kf-ms-tile:focus-within .kf-ms-bar { opacity: 1; }
.kf-ms-bar button, .kf-ms-bar .kf-ms-link {
border: 1px solid rgba(255,255,255,.25);
border-radius: 4px;
background: rgba(0,0,0,.55);
color: var(--text);
padding: 2px 7px;
font-size: 11px;
font-weight: 700;
cursor: pointer;
text-decoration: none;
}
.kf-ms-bar button:hover, .kf-ms-bar .kf-ms-link:hover { border-color: var(--accent); color: var(--accent); }
.kf-ms-tile[data-kf-multistream-focused="true"] .kf-ms-name { border-color: var(--accent); color: var(--accent); }
.kf-ms-merged { min-width: 0; border-left: 1px solid var(--border); display: grid; grid-template-rows: auto 1fr; }
.kf-ms-merged-list { margin: 0; padding: 6px 8px; overflow-y: auto; list-style: none; font-size: 12px; line-height: 1.45; }
.kf-ms-merged-row { padding: 2px 0; overflow-wrap: anywhere; }
.kf-ms-merged-source { display: inline-block; margin-right: 6px; padding: 0 5px; border-radius: 4px;
background: var(--kf-panel-high, #202626); color: var(--kf-accent, #53fc18); font-size: 11px; font-weight: 700; }
.kf-ms-merged-who { margin-right: 4px; font-weight: 700; }
.kf-ms-merged-who::after { content: ':'; }
.kf-ms-backdrop[data-kf-multistream-merged-on="true"] .kf-ms-chat { display: none; }
.kf-ms-chat { min-width: 0; border-left: 1px solid var(--border); display: grid; grid-template-rows: auto 1fr; }
.kf-ms-backdrop[data-kf-multistream-chat-popped-out="true"] .kf-ms-chat { display: none; }
.kf-ms-chat iframe { width: 100%; height: 100%; border: 0; display: block; }
.kf-ms-chat-notice {
margin: 0;
padding: 6px 10px;
border-bottom: 1px solid var(--border);
background: var(--surface-2);
color: var(--muted);
font-size: 11px;
}
.kf-ms-layouts { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.kf-ms-layout { display: inline-flex; }
.kf-ms-layout button {
border: 1px solid var(--border);
background: var(--surface-inset);
color: var(--text);
padding: 3px 8px;
font-size: 11px;
cursor: pointer;
}
.kf-ms-layout button:first-child { border-radius: 6px 0 0 6px; }
.kf-ms-layout button:last-child { border-radius: 0 6px 6px 0; border-left: 0; }
.kf-ms-layout button:hover { border-color: var(--accent); color: var(--accent); }
.kf-ms-layout small { opacity: .6; }
.kf-ms-layout small.kf-ms-live { opacity: 1; color: var(--muted); }
.kf-ms-layout small.kf-ms-live[data-live="true"] { color: var(--accent); font-weight: 700; }
.kf-ms-empty { color: var(--muted); font-size: 11px; }
@media (max-width: 1180px) {
.kf-ms-head { grid-template-columns: auto minmax(280px, 1fr); }
.kf-ms-controls { grid-column: 1 / -1; justify-content: flex-start; }
}
@media (max-width: 760px) {
.kf-ms-head, .kf-ms-foot { grid-template-columns: 1fr; }
.kf-ms-add, .kf-ms-save-layout { justify-content: stretch; }
.kf-ms-add input, .kf-ms-save-layout input { width: 100%; min-width: 0; }
.kf-ms-layouts, .kf-ms-points-note { grid-column: 1; }
.kf-ms-empty-state { padding: 24px 18px; }
}
.kf-shadow-warning { display: grid; gap: 6px; }
.kf-shadow-warning code { font-size: 11px; color: var(--accent); }
.kf-command-list { max-height: 490px; overflow: auto; padding: 8px; }
.kf-command-item { width: 100%; display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 14px; padding: 12px; border: 1px solid transparent; border-left: 3px solid transparent; border-radius: var(--radius-sm); background: transparent; text-align: left; cursor: pointer; }
.kf-command-item:hover, .kf-command-item[data-active="true"] { border-color: var(--border-subtle); border-left-color: var(--accent); background: var(--surface-hover); }
.kf-command-item strong { display: block; margin-bottom: 2px; }
.kf-command-item span { color: var(--muted); font-size: 12px; }
.kf-command-empty { display: grid; gap: 4px; padding: 38px 28px; color: var(--muted); text-align: center; }
.kf-command-empty strong { color: var(--text); font-size: 13px; }
.kf-command-empty span { font-size: 11px; }
.kf-sr-only { position: absolute !important; width: 1px !important; height: 1px !important; padding: 0 !important; margin: -1px !important; overflow: hidden !important; clip: rect(0,0,0,0) !important; white-space: nowrap !important; border: 0 !important; }
:is(button, input, select, textarea):focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
@media (max-width: 920px) {
.kf-settings { width: calc((100vw - 28px) / var(--kf-interface-scale, 1)); height: calc((100vh - 28px) / var(--kf-interface-scale, 1)); min-width: 0; min-height: calc(620px / var(--kf-interface-scale, 1)); }
.kf-header { grid-template-columns: 1fr auto auto; gap: 14px; padding-inline: 18px; }
.kf-body { grid-template-columns: 204px minmax(0, 1fr); }
.kf-nav button { grid-template-columns: 21px minmax(0, 1fr); gap: 10px; padding-inline: 18px; }
.kf-nav .kf-nav-copy { overflow: visible; white-space: normal; }
.kf-nav-copy > strong { line-height: 1.15; white-space: normal; }
.kf-nav-copy > span { display: none; }
.kf-page { padding-inline: 24px 20px; }
.kf-row, .kf-row-wide { grid-template-columns: 1fr; gap: 10px; }
.kf-control { min-width: 0; justify-content: flex-start; }
.kf-range { max-width: 420px; }
.kf-defense-overview { grid-template-columns: 1fr; }
.kf-defense-overview .kf-stats { border-left: 0; border-top: 1px solid var(--border-subtle); }
.kf-tool-grid { grid-template-columns: 1fr; }
.kf-sticker-library-workspace, .kf-sticker-library-controls, .kf-sticker-library-grid { grid-template-columns: 1fr; }
.kf-sticker-group-builder { grid-template-columns: minmax(180px, 1fr) auto; }
.kf-sticker-library-bulk { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.kf-sticker-library-bulk > strong, .kf-sticker-library-bulk .kf-select { grid-column: span 2; }
.kf-sticker-library-actions { grid-template-columns: repeat(2, auto); }
.kf-sticker-library-actions .kf-select { grid-column: 1 / -1; }
}
@media (max-width: 700px) {
.kf-backdrop { padding: 0; }
.kf-settings { width: calc(100vw / var(--kf-interface-scale, 1)); height: calc(100vh / var(--kf-interface-scale, 1)); min-height: 0; grid-template-rows: 66px minmax(0, 1fr) 68px; border: 0; border-radius: 0; }
.kf-header { grid-template-columns: 1fr auto auto; padding-inline: 14px; }
.kf-body { grid-template-columns: 1fr; grid-template-rows: auto minmax(0, 1fr); }
.kf-nav { display: flex; overflow-x: auto; padding: 0; border-right: 0; border-bottom: 1px solid var(--border); scrollbar-width: none; overscroll-behavior-inline: contain; }
.kf-nav::-webkit-scrollbar { display: none; }
.kf-nav-search { display: none; }
.kf-nav button { flex: 0 0 auto; width: auto; min-width: 0; min-height: 54px; padding-inline: 16px; }
.kf-nav-copy, .kf-nav-copy > strong { white-space: nowrap; }
.kf-nav button::before { inset: auto 14px 0; width: auto; height: 3px; }
.kf-page { padding: 18px 18px 32px; }
.kf-page-header { min-height: 72px; }
.kf-page-header h2 { font-size: 23px; }
.kf-page-meta { display: none; }
.kf-control { width: 100%; }
.kf-segmented { width: 100%; }
.kf-segmented button { min-width: 0; flex: 1 1 0; padding-inline: 7px; }
.kf-range { grid-template-columns: 42px minmax(120px, 1fr) 42px; }
.kf-channel-input-row, .kf-emote-catalog-browser, .kf-emote-catalog-form { grid-template-columns: 1fr; }
.kf-sticker-library-bulk { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.kf-sticker-library-bulk > strong, .kf-sticker-library-bulk .kf-select { grid-column: 1 / -1; }
.kf-theme-grid, .kf-swatch-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.kf-preset-grid { grid-template-columns: 1fr; }
.kf-appearance-layout { grid-template-columns: 1fr; }
.kf-preview { position: static; padding: 18px 0 0; border-top: 1px solid var(--border); border-left: 0; }
.kf-about-status, .kf-stats, .kf-hub-grid { grid-template-columns: 1fr; }
.kf-mini-card, .kf-stat { border-left: 0; border-top: 1px solid var(--border-subtle); }
.kf-action-row { grid-template-columns: 1fr; }
.kf-button-group { justify-content: flex-start; }
.kf-footer { padding-inline: 14px; }
.kf-footer [data-action="export"] { display: none; }
.kf-storage-alert { align-items: stretch; flex-wrap: wrap; }
.kf-storage-alert-body { flex-basis: 100%; }
}
@media (max-width: 430px) {
.kf-brand { font-size: 14px; }
.kf-badge { display: none; }
.kf-save { font-size: 11px; }
.kf-page-header p { font-size: 12px; }
.kf-row { gap: 12px; padding-block: 13px; }
.kf-row p { font-size: 11px; }
.kf-footer-left .kf-button { padding-inline: 10px; }
.kf-command-shell { width: calc(100vw - 24px); }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}
`;
const TRANSLATIONS = {
es: {
'Home': 'Inicio',
'Browse': 'Explorar',
'Following': 'Siguiendo',
'Saved views': 'Vistas guardadas',
'Keep the density, thumbnail size, rails, and content filters you like as a named view, and have it applied when you open the pages you chose. It is your own settings, applied to what Kick already sent. It changes nothing about what Kick recommends or the order anything appears in.': 'Guarda la densidad, el tamaño de miniatura, los raíles y los filtros de contenido que te gustan como una vista con nombre, y aplícala al abrir las páginas que elijas. Son tus propios ajustes aplicados a lo que Kick ya envió. No cambia nada de lo que Kick recomienda ni el orden en que aparece.',
'Save this page as a view': 'Guardar esta página como vista',
'Pick the pages it should apply to, or none to keep it manual.': 'Elige las páginas donde debe aplicarse, o ninguna para dejarla manual.',
'Save this view': 'Guardar esta vista',
'No saved views yet. Set the page up the way you want it, name it, and save.': 'Todavía no hay vistas guardadas. Deja la página como la quieres, ponle nombre y guárdala.',
'Applied only when you press it': 'Se aplica solo cuando la pulsas',
'Currently applied': 'Aplicada ahora',
'Applied {name} for this page.': 'Se aplicó {name} en esta página.',
'{n} saved views is the limit. Delete one first.': 'El límite es {n} vistas guardadas. Borra una primero.',
'Saved {name}.': 'Se guardó {name}.',
'Applied {name}. {n} settings changed.': 'Se aplicó {name}. Cambiaron {n} ajustes.',
'{name} is already what you are looking at.': '{name} es justo lo que estás viendo.',
'Deleted {name}.': 'Se borró {name}.',
'Name the view before saving it.': 'Ponle nombre a la vista antes de guardarla.',
'Delete this saved view': 'Borrar esta vista guardada',
'Name this view': 'Nombre de la vista',
'Category pages': 'Páginas de categoría',
'Search results': 'Resultados de búsqueda',
'Show message times': 'Mostrar la hora de los mensajes',
'Reveals the timestamp Kick already renders on every message and keeps hidden. It is Kick’s own value, so scrolling back shows when a message was sent rather than when this build first saw it.': 'Muestra la marca de hora que Kick ya dibuja en cada mensaje y mantiene oculta. Es el valor de Kick, así que al subir por el chat verás cuándo se envió el mensaje y no cuándo lo vio esta extensión.',
'People worth noticing': 'Personas que quieres notar',
'Names you want to catch in a fast chat. Their messages get a marker of their own, separate from keyword highlights. Comma separated, and stored only in your settings.': 'Nombres que quieres captar en un chat rápido. Sus mensajes reciben una marca propia, distinta de los resaltados por palabra clave. Separados por comas y guardados solo en tus ajustes.',
'Sound on a mention': 'Sonido al mencionarte',
'A short tone when a message matches your highlights, comes from someone you listed, or says your name. Synthesised in the browser, so nothing is downloaded. Silent while the tab is in the background, silent for your own messages, and never more than once every few seconds.': 'Un tono corto cuando un mensaje coincide con tus resaltados, viene de alguien de tu lista o dice tu nombre. Se genera en el navegador, así que no se descarga nada. Callado con la pestaña en segundo plano, callado con tus propios mensajes y nunca más de una vez cada pocos segundos.',
'Hide a message for yourself': 'Ocultar un mensaje solo para ti',
'Adds a small dismiss control to each message. It hides that message in your own browser for this session only, changes nothing for anyone else, and offers an undo.': 'Añade un pequeño control de descarte a cada mensaje. Oculta ese mensaje en tu navegador solo durante esta sesión, no cambia nada para los demás y ofrece deshacer.',
'Recall my sent messages': 'Recordar mis mensajes enviados',
'Keep the last five messages sent from this tab in memory. Shift+Up cycles them. Whispers are skipped, reload clears them, and ordinary Arrow Up stays with Kick.': 'Guarda en memoria los últimos cinco mensajes enviados desde esta pestaña. Mayús+Arriba los recorre. Los susurros se omiten, recargar los borra y Flecha arriba sigue siendo de Kick.',
'Search this session’s chat': 'Buscar en el chat de esta sesión',
'Keeps what this tab has seen so you can find it again. It stays in memory, never reaches storage, and is gone on reload. Whispers are never recorded, and a message a moderator removes leaves the log the moment the deletion arrives.': 'Guarda lo que esta pestaña ha visto para que puedas encontrarlo otra vez. Queda en memoria, nunca llega al almacenamiento y desaparece al recargar. Los susurros no se guardan nunca, y un mensaje que un moderador elimine sale del registro en cuanto llega el borrado.',
'Session chat log': 'Registro del chat de esta sesión',
'Hidden by you': 'Ocultado por ti',
'Hide this message for me': 'Ocultar este mensaje para mí',
'Nothing in this session matches that.': 'Nada de esta sesión coincide con eso.',
'Nothing recorded yet. Open a chat with the switch on.': 'Todavía no hay nada guardado. Abre un chat con la opción activada.',
'Saved {n} messages from this session.': 'Se guardaron {n} mensajes de esta sesión.',
'Message hidden for you. It is still there for everyone else.': 'Mensaje oculto para ti. Sigue ahí para todos los demás.',
'Session chat log cleared.': 'Registro del chat de la sesión borrado.',
'message held. Capped at 400 messages, 200 KB, and one hour.': 'mensaje guardado. Con un tope de 400 mensajes, 200 KB y una hora.',
'messages held. Capped at 400 messages, 200 KB, and one hour.': 'mensajes guardados. Con un tope de 400 mensajes, 200 KB y una hora.',
'name, name': 'nombre, nombre',
'Search what you have seen': 'Busca lo que has visto',
'Save as a file': 'Guardar como archivo',
'Clear the log': 'Borrar el registro',
'Daily reward ready': 'Recompensa diaria lista',
'Viewer': 'Cuenta',
'Account readings and this browser session, in one place. Nothing here is claimed or sent anywhere.': 'Lecturas de la cuenta y esta sesión del navegador, reunidas en un sitio. Aquí no se reclama ni se envía nada.',
'Reading': 'Lecturas',
'Reading…': 'Leyendo…',
'From Kick’s API': 'Desde la API de Kick',
'Read from the page': 'Leído de la página',
'This browser session only': 'Solo esta sesión del navegador',
'{n} min ago': 'hace {n} min',
'Daily reward': 'Recompensa diaria',
'Channel points': 'Puntos del canal',
'Collectibles': 'Coleccionables',
'Drops': 'Drops',
'Level': 'Nivel',
'Streak': 'Racha',
'Session watch time': 'Tiempo visto en esta sesión',
'Not read yet on this page.': 'Todavía no se ha leído en esta página.',
'Kick shows this to a signed-in account only.': 'Kick solo muestra esto a una cuenta con sesión iniciada.',
'Open a channel to see its points.': 'Abre un canal para ver sus puntos.',
'Open Drops to count the campaigns waiting.': 'Abre Drops para contar las campañas pendientes.',
'Kick shows this inside the daily reward dialog only.': 'Kick solo muestra esto dentro del diálogo de la recompensa diaria.',
'The reward dialog did not show a figure this time.': 'Esta vez el diálogo de la recompensa no mostró ninguna cifra.',
'Kick did not answer that read. Nothing was changed.': 'Kick no respondió a esa lectura. No se cambió nada.',
'This card could not be built. The rest of the hub is unaffected.': 'Esta tarjeta no se pudo construir. El resto de la página no se ve afectado.',
'Claimed today': 'Reclamada hoy',
'Not ready yet': 'Todavía no está lista',
'Ready to claim': 'Lista para reclamar',
'Where these come from': 'De dónde salen estos datos',
'Nothing has been read yet. Each card above says why.': 'Todavía no se ha leído nada. Cada tarjeta de arriba explica por qué.',
'Read again': 'Leer otra vez',
'Nothing is claimed for you here': 'Aquí no se reclama nada por ti',
'This page reads. The daily reward is still claimed by Kick’s own dialog, and only when you have turned that on under Content & Ads. A card with no reading says so rather than showing a zero, because an empty balance and an unreadable one are not the same thing.': 'Esta página lee. La recompensa diaria la sigue reclamando el propio diálogo de Kick, y solo si lo has activado en Contenido y anuncios. Una tarjeta sin lectura lo dice en lugar de mostrar un cero, porque un saldo vacío y uno que no se puede leer no son lo mismo.',
'Settings': 'Configuración',
'Autosaved': 'Guardado automático',
'Could not save': 'No se pudo guardar',
'Imported': 'Importado',
'All settings reset': 'Se restablecieron todos los ajustes',
'Page reset': 'Se restableció la página',
'Shortcuts restored': 'Se restauraron los atajos',
'Shortcut saved': 'Atajo guardado',
'Added {name}. Now {count} of {max}.': 'Se añadió {name}. Ahora {count} de {max}.',
'Removed {name}. Now {count} of {max}.': 'Se quitó {name}. Ahora {count} de {max}.',
'Removed {name} from the grid.': 'Se quitó {name} de la cuadrícula.',
'{name} added to the multi-stream grid': '{name} se añadió a la cuadrícula multi-stream',
'The grid is full at {max} of {max}.': 'La cuadrícula está llena con {max} de {max}.',
'The grid is full at {max} channels.': 'La cuadrícula está llena con {max} canales.',
'{name} now has the audio': '{name} tiene ahora el audio',
'Loaded board {name}': 'Se cargó el tablero {name}',
'Copied a link to {name}.': 'Se copió un enlace a {name}.',
'Opened a shared board with {count} {word}.': 'Se abrió un tablero compartido con {count} {word}.',
'The shared board replaced {count} {word} you had collected.': 'El tablero compartido reemplazó {count} {word} que habías reunido.',
'Loaded {count} emotes from {channel}.': 'Se cargaron {count} emotes de {channel}.',
'Kick Focus applied to {route}': 'Kick Focus aplicado a {route}',
'Content filtering suspended: it would have hidden {percent}% of this page.': 'Filtrado de contenido suspendido: habría ocultado el {percent}% de esta página.',
'Exported settings, {emotes} emotes, {counts} usage counts, {boards} boards, and your channels, notes, and filters.': 'Se exportó la configuración, {emotes} emotes, {counts} recuentos de uso, {boards} tableros y tus canales, notas y filtros.',
'The emote was removed, but Kick could not unfollow {channel}.': 'Se eliminó el emote, pero Kick no pudo dejar de seguir a {channel}.',
'Saved {name}, but Kick did not identify the follow-gated source channel.': 'Se guardó {name}, pero Kick no identificó el canal de origen que exige seguirlo.',
'Saved {name}. Following {channel}…': 'Se guardó {name}. Siguiendo a {channel}…',
'Saved {name} locally, but Kick could not follow {channel} ({status}). Sign in or reload the channel and try again.': 'Se guardó {name} en este dispositivo, pero Kick no pudo seguir a {channel} ({status}). Inicia sesión o recarga el canal e inténtalo de nuevo.',
'Open multi-stream': 'Abrir multi-stream',
'Close multi-stream': 'Cerrar multi-stream',
'emote id in card URL': 'el id del emote en la URL de la carta',
'emote name in card URL': 'el nombre del emote en la URL de la carta',
'Use comfortable density': 'Usar densidad cómoda',
'Use compact density': 'Usar densidad compacta',
'Recent': 'Recientes',
'New group': 'Nuevo grupo',
'Save': 'Guardar',
'Rename': 'Cambiar nombre',
'Delete': 'Eliminar',
'Ungrouped': 'Sin grupo',
'Select shown': 'Seleccionar visibles',
'Clear': 'Borrar',
'Earlier': 'Antes',
'Later': 'Después',
'Move': 'Mover',
'Groups': 'Grupos',
'Your emotes': 'Tus emotes',
'Library': 'Biblioteca',
'Favorites': 'Favoritos',
'All': 'Todos',
'Kick': 'Kick',
'Kick groups': 'Grupos de Kick',
'Kick’s original emote groups are shown below.': 'Los grupos de emotes originales de Kick aparecen abajo.',
'No emotes found': 'No se encontraron emotes',
'Try a different search or clear the search field.': 'Prueba otra búsqueda o borra el campo de búsqueda.',
'No favorites yet': 'Aún no hay favoritos',
'Open All and use the star on any emote to keep it here.': 'Abre Todos y usa la estrella de cualquier emote para guardarlo aquí.',
'No recent emotes yet': 'Aún no hay emotes recientes',
'Emotes you send in this channel will appear here.': 'Los emotes que envíes en este canal aparecerán aquí.',
'Create your first group': 'Crea tu primer grupo',
'Use New group above, then select emotes and move them into it.': 'Usa Nuevo grupo arriba, luego selecciona emotes y muévelos allí.',
'This group is empty': 'Este grupo está vacío',
'Choose Organize, select emotes, then move them into this group.': 'Elige Organizar, selecciona emotes y muévelos a este grupo.',
'Try a different search or return to Kick groups.': 'Prueba otra búsqueda o vuelve a los grupos de Kick.',
'{count} selected emote': '{count} emote seleccionado',
'{count} selected emotes': '{count} emotes seleccionados',
'Organize': 'Organizar',
'Remove this-channel favorite {name}': 'Quitar {name} de los favoritos de este canal',
'Emote suggestions': 'Sugerencias de emotes',
'Removed {name} from the grid ({count} of {max})': 'Se quitó {name} de la cuadrícula ({count} de {max})',
'Added {name} to the grid ({count} of {max})': 'Se añadió {name} a la cuadrícula ({count} de {max})',
'Add to Multi': 'Añadir a Multi',
'In Multi': 'En Multi',
'Add {name} to the multi-stream grid': 'Añadir {name} a la cuadrícula multi-stream',
'Remove {name} from the multi-stream grid': 'Quitar {name} de la cuadrícula multi-stream',
'Favorite {name}': 'Marcar {name} como favorito',
'Remove favorite {name}': 'Quitar {name} de favoritos',
'Not interested in {name}': 'No me interesa {name}',
'Restore {name}': 'Restaurar {name}',
'Remove {name}': 'Quitar {name}',
'Restore': 'Restaurar',
'Remove favorite': 'Quitar de favoritos',
'Remove favorite (this channel)': 'Quitar de favoritos (este canal)',
'Use emote {name}': 'Usar el emote {name}',
'Select emote {name}': 'Seleccionar el emote {name}',
'Use {name}': 'Usar {name}',
'{count} in {name}': '{count} en {name}',
'{shown} of {total} available': '{shown} de {total} disponibles',
'{count} available': '{count} disponibles',
', {count} locked by Kick': ', {count} bloqueados por Kick',
'Created {name}. Select emotes, then move them into it.': 'Se creó {name}. Selecciona emotes y luego muévelos ahí.',
'Deleted emote group {name}.': 'Se eliminó el grupo de emotes {name}.',
'Favorited {name}.': 'Se añadió {name} a favoritos.',
'Removed {name} from favorites.': 'Se quitó {name} de favoritos.',
'Restored {name}.': 'Se restauró {name}.',
'Removed {name}.': 'Se quitó {name}.',
'Emote group renamed.': 'Se cambió el nombre del grupo de emotes.',
'Group name restored.': 'Se restauró el nombre del grupo.',
'Emote group restored.': 'Se restauró el grupo de emotes.',
'Favorite order restored.': 'Se restauró el orden de favoritos.',
'Emote group changes restored.': 'Se restauraron los cambios de grupos de emotes.',
'Removed emotes restored.': 'Se restauraron los emotes eliminados.',
'Favorite change restored.': 'Se restauró el cambio de favorito.',
'Emote changes reset.': 'Se restablecieron los cambios de emotes.',
'Favorite moved earlier.': 'El favorito se movió antes.',
'Favorite moved later.': 'El favorito se movió después.',
'Emote moved earlier.': 'El emote se movió antes.',
'Emote moved later.': 'El emote se movió después.',
'emote moved.': 'emote movido.',
'emotes moved.': 'emotes movidos.',
'emote removed.': 'emote eliminado.',
'emotes removed.': 'emotes eliminados.',
'Move selected emotes to group': 'Mover los emotes seleccionados al grupo',
'Emote views': 'Vistas de emotes',
'Kick rarity, matched by {basis}': 'Rareza de Kick, encontrada por {basis}',
'Insert {name}': 'Insertar {name}',
'Open {name} artwork': 'Abrir la imagen de {name}',
'Copy the name {name}': 'Copiar el nombre {name}',
'Type the name {name} into chat': 'Escribir el nombre {name} en el chat',
'Custom group for {name}': 'Grupo personalizado para {name}',
'Rename {name}': 'Cambiar el nombre de {name}',
'Show {name} again': 'Mostrar {name} de nuevo',
'Open {name} on Kick': 'Abrir {name} en Kick',
'Remove {name} from the grid': 'Quitar {name} de la cuadrícula',
'Copy a link to board {name}': 'Copiar un enlace al tablero {name}',
'Delete board {name}': 'Eliminar el tablero {name}',
'Press keys, or Escape to cancel': 'Pulsa las teclas, o Escape para cancelar',
'{preset} preset applied': 'Preajuste {preset} aplicado',
'Hidden {channel}': '{channel} oculto',
'Showing {channel} again': '{channel} vuelve a mostrarse',
'That file is not valid JSON.': 'Ese archivo no contiene JSON válido.',
'Settings must be a JSON object.': 'Los ajustes deben ser un objeto JSON.',
'Settings schema {schema} is newer than this build supports.': 'El esquema de ajustes {schema} es más reciente que el compatible con esta versión.',
'The emote library must be a JSON object.': 'La biblioteca de emotes debe ser un objeto JSON.',
'The emote usage counts must be a JSON object.': 'Los recuentos de uso de emotes deben ser un objeto JSON.',
'The multi-stream boards must be a JSON object.': 'Los diseños de multitransmisión deben ser un objeto JSON.',
'Emote schema {schema} is newer than this build supports.': 'El esquema de emotes {schema} es más reciente que el compatible con esta versión.',
'That file does not contain Kick Focus settings.': 'Ese archivo no contiene ajustes de Kick Focus.',
'settings': 'ajustes',
'emote library': 'biblioteca de emotes',
'emote usage counts': 'recuentos de uso de emotes',
'multi-stream boards': 'diseños de multitransmisión',
'per-channel layout': 'diseño por canal',
'favorite channels': 'canales favoritos',
'not-interested channels': 'canales marcados como no interesantes',
'chat keyword filters': 'filtros de palabras clave del chat',
'channel notes': 'notas de canales',
'volume and quality memory': 'memoria de volumen y calidad',
'blocklist cache': 'caché de la lista de bloqueo',
'watched this session': 'vistos en esta sesión',
'Kick Focus could not save your {list}. Browser storage is full or blocked, so those changes exist only until you reload.': 'Kick Focus no pudo guardar {list}. El almacenamiento del navegador está lleno o bloqueado, por lo que esos cambios solo existirán hasta que recargues.',
'The browser reported': 'El navegador informó',
'Exporting now is the only way to keep these changes.': 'Exportar ahora es la única forma de conservar estos cambios.',
'{items} read from the page': '{items}, leídos de la página',
'{items} from Kick’s API': '{items}, desde la API de Kick',
'{items} kept in this browser session': '{items}, guardado en esta sesión del navegador',
'{n} showing an older reading.': '{n} con una lectura anterior.',
'{n} could not be built.': 'No se pudieron crear: {n}.',
'{shortcut} is already used by {action}.': '{shortcut} ya lo usa {action}.',
'Added': 'Añadido',
'Removed': 'Eliminado',
'Error log copied.': 'Registro de errores copiado.',
'Could not copy the error log.': 'No se pudo copiar el registro de errores.',
'Diagnostic summary copied.': 'Resumen de diagnóstico copiado.',
'Clipboard access was unavailable.': 'El acceso al portapapeles no estaba disponible.',
'Close settings': 'Cerrar configuración',
'Reset page': 'Restablecer página',
'Export settings': 'Exportar configuración',
'Done': 'Listo',
'Reset settings?': '¿Restablecer configuración?',
'Reset all Kick Focus settings?': '¿Restablecer toda la configuración de Kick Focus?',
'This restores the defaults for this page.': 'Esto restaura los valores predeterminados de esta página.',
'Every preference, shortcut, note, filter, and channel list returns to its factory default. Your recorded emote library is kept.': 'Todas las preferencias, atajos, notas, filtros y listas de canales volverán a sus valores de fábrica. Tu biblioteca de emotes registrada se conserva.',
'Only the settings on this page will return to their defaults.': 'Solo la configuración de esta página volverá a sus valores predeterminados.',
'Cancel': 'Cancelar',
'Reset': 'Restablecer',
'On': 'Activado',
'Off': 'Desactivado',
'Core protection always stays on': 'La protección principal permanece siempre activada',
'This page has its own reset control': 'Esta página tiene su propio control de restablecimiento',
'Restore page defaults': 'Restaurar los valores predeterminados de la página',
'Find a command': 'Buscar un comando',
'Type an action or setting…': 'Escribe una acción o ajuste…',
'Available commands': 'Comandos disponibles',
'No matching commands': 'No hay comandos coincidentes',
'Try “chat”, “layout”, “casino”, or “settings”.': 'Prueba “chat”, “diseño”, “casino” o “configuración”.',
'command available': 'comando disponible',
'commands available': 'comandos disponibles',
'Focus': 'Enfoque',
'Resume': 'Reanudar',
'Resume Kick Focus': 'Reanudar Kick Focus',
'Kick Focus settings': 'Configuración de Kick Focus',
'Layout': 'Diseño',
'Appearance': 'Apariencia',
'Content & Ads': 'Contenido y anuncios',
'Emotes': 'Emotes',
'Find, favorite, remove, and group every emote you have recorded.': 'Busca, marca como favorito, elimina y agrupa todos los emotes registrados.',
'Accessibility & Shortcuts': 'Accesibilidad y atajos',
'About': 'Acerca de',
'Shell, player, and chat': 'Estructura, reproductor y chat',
'Theme, color, and scale': 'Tema, color y escala',
'Privacy, filters, and playback': 'Privacidad, filtros y reproducción',
'Comfort and shortcuts': 'Comodidad y atajos',
'Read-only account signals': 'Datos de cuenta de solo lectura',
'Status, privacy, and diagnostics': 'Estado, privacidad y diagnósticos',
'Control how Kick is arranged across your desktop.': 'Controla cómo se organiza Kick en tu escritorio.',
'Keep the page calm, private, and focused on streams.': 'Mantén la página tranquila, privada y centrada en los streams.',
'Improve comfort and keep core actions within reach.': 'Mejora la comodidad y mantén las acciones principales al alcance.',
'A desktop-first layout and control layer for Kick.': 'Una capa de diseño y control para Kick pensada para escritorio.',
'Language': 'Idioma',
'Auto': 'Automático',
'Sidebar mode': 'Modo de barra lateral',
'Left': 'Izquierda',
'Chat layout': 'Diseño del chat',
'Chat width': 'Ancho del chat',
'Chat width saved.': 'Ancho del chat guardado.',
'Content density': 'Densidad del contenido',
'Stream start behavior': 'Comportamiento al abrir streams',
'Remember per-channel layout': 'Recordar diseño por canal',
'Widen browse grids': 'Ampliar cuadrículas de exploración',
'Show Following rail': 'Mostrar barra de seguidos',
'Show Recommended rail': 'Mostrar barra recomendada',
'Hide Kick’s own controls': 'Ocultar los controles propios de Kick',
'Switch off the player buttons and sidebar entries you never use. Each one is hidden with styling only. Nothing is clicked or removed, and turning it back on restores it immediately.': 'Desactiva los botones del reproductor y las entradas de la barra lateral que nunca usas. Cada uno se oculta solo con estilos: no se pulsa ni se elimina nada, y al reactivarlo vuelve de inmediato.',
'Player controls': 'Controles del reproductor',
'Sidebar': 'Barra lateral',
'Miniplayer': 'Minirreproductor',
'Clip': 'Clip',
'Theater mode': 'Modo cine',
'Fullscreen': 'Pantalla completa',
'Quality menu': 'Menú de calidad',
'Volume': 'Volumen',
'Share': 'Compartir',
'Report': 'Reportar',
'Home link': 'Enlace de Inicio',
'Browse link': 'Enlace de Explorar',
'Following link': 'Enlace de Siguiendo',
'Drops link': 'Enlace de Drops',
'Followed channel list': 'Lista de canales seguidos',
'Recommended channel list': 'Lista de canales recomendados',
'Sticky compact top bar': 'Barra superior compacta fija',
'Show quick command button': 'Mostrar botón de comandos',
'Move mini-player clear of controls': 'Mover el minirreproductor lejos de los controles',
'Recover player after resize': 'Recuperar el reproductor tras cambiar el tamaño',
'Keep ultrawide video uncropped': 'Mantener el video panorámico sin recortar',
'Theme': 'Tema',
'Choose a clear visual direction, then tune only what matters to you.': 'Elige una dirección visual clara y ajusta solo lo que te importe.',
'Quick directions': 'Direcciones rápidas',
'Apply a viewing setup without changing filters or account choices.': 'Aplica una configuración de visualización sin cambiar filtros ni opciones de cuenta.',
'Direction': 'Dirección',
'Each theme changes the full surface hierarchy, not just the page background.': 'Cada tema cambia toda la jerarquía de superficies, no solo el fondo de la página.',
'Layered graphite': 'Grafito en capas',
'Selected': 'Seleccionado',
'Balanced depth with a quiet green undertone.': 'Profundidad equilibrada con un matiz verde discreto.',
'True black': 'Negro real',
'Minimal lift and maximum contrast for dark rooms.': 'Relieve mínimo y contraste máximo para habitaciones oscuras.',
'Cool graphite': 'Grafito frío',
'Blue-toned surfaces with stronger separation.': 'Superficies azuladas con una separación más marcada.',
'Use one accent for focus, selection, and live state.': 'Usa un solo acento para el foco, la selección y el estado en vivo.',
'Low-contrast choices fall back to a safe rose.': 'Las opciones de bajo contraste vuelven a un rosa seguro.',
'Your current theme, accent, scale, and card treatment.': 'Tu tema, acento, escala y tratamiento de tarjetas actuales.',
'Accent color': 'Color de acento',
'Calm': 'Calma',
'Cinema': 'Cine',
'Chat First': 'Chat primero',
'Discovery': 'Descubrimiento',
'Roomier cards, quieter live color, and a compact rail.': 'Tarjetas más amplias, color en vivo más discreto y una barra compacta.',
'OLED surfaces with the player first and chrome tucked away.': 'Superficies OLED con el reproductor primero y los controles apartados.',
'A wider docked chat, compact density, and a clearer accent.': 'Un chat acoplado más ancho, densidad compacta y un acento más claro.',
'More stream cards, vivid thumbnails, and both discovery rails.': 'Más tarjetas de streams, miniaturas intensas y ambas barras de descubrimiento.',
'Custom': 'Personalizado',
'Custom accent': 'Acento personalizado',
'Custom accent color': 'Color de acento personalizado',
'Contrast protected': 'Contraste protegido',
'{preset} preset applied. Content filters were not changed.': 'Se aplicó el preajuste {preset}. Los filtros de contenido no cambiaron.',
'Corner radius': 'Radio de esquinas',
'Thumbnail treatment': 'Tratamiento de miniaturas',
'Interface scale': 'Escala de la interfaz',
'Dim watched cards': 'Atenuar tarjetas vistas',
'Strengthen text contrast': 'Aumentar contraste del texto',
'Colorize live indicators': 'Colorear indicadores en vivo',
'Ad defense active': 'Defensa contra anuncios activa',
'Network + page': 'Red + página',
'Page only': 'Solo página',
'Local channel tools': 'Herramientas locales del canal',
'Clear favorites': 'Borrar favoritos',
'Clear not-interested': 'Borrar no me interesa',
'Protection log': 'Registro de protección',
'Reduce motion': 'Reducir movimiento',
'High-contrast controls': 'Controles de alto contraste',
'Always show keyboard focus': 'Mostrar siempre el foco del teclado',
'Larger pointer targets': 'Objetivos táctiles más grandes',
'Announce layout changes': 'Anunciar cambios de diseño',
'Text size': 'Tamaño del texto',
'Caption background opacity': 'Opacidad del fondo de subtítulos',
'Search': 'Buscar',
'Every page, searched at once.': 'Todas las páginas, buscadas a la vez.',
'Try a shorter word, or the name of the Kick control you are looking for.': 'Prueba con una palabra más corta o con el nombre del control de Kick que buscas.',
'Nothing matches “{query}”.': 'Nada coincide con «{query}».',
'Search settings': 'Buscar ajustes',
'Kick Focus updated to {version}.': 'Kick Focus se actualizó a {version}.',
'Changed defaults: {list}.': 'Valores predeterminados que cambiaron: {list}.',
'What changed': 'Qué cambió',
'Keyboard shortcuts': 'Atajos de teclado',
'Restore defaults': 'Restaurar valores predeterminados',
'Action': 'Acción',
'Current shortcut': 'Atajo actual',
'Status': 'Estado',
'Change': 'Cambiar',
'Script health': 'Estado del script',
'Site compatibility': 'Compatibilidad del sitio',
'Protection layer': 'Capa de protección',
'Data & privacy': 'Datos y privacidad',
'Diagnostics': 'Diagnósticos',
'Copy diagnostic summary': 'Copiar resumen de diagnóstico',
'Run self-check': 'Ejecutar autocomprobación',
'Compatibility self-test': 'Autocomprobación de compatibilidad',
'Settings portability': 'Portabilidad de configuración',
'Import settings': 'Importar configuración',
'Reset all settings': 'Restablecer toda la configuración',
'Panic switch': 'Interruptor de emergencia',
'Pause Kick Focus': 'Pausar Kick Focus',
'Restore Kick Focus': 'Restaurar Kick Focus',
'Temporarily remove enhanced layout and request hooks': 'Quita temporalmente el diseño mejorado y los interceptores',
'Enter focus mode': 'Entrar en modo enfoque',
'Exit focus mode': 'Salir del modo enfoque',
'Maximize the stream and hide side panels': 'Maximiza el stream y oculta los paneles laterales',
'Enter theater mode': 'Entrar en modo teatro',
'Exit theater mode': 'Salir del modo teatro',
'Hide discovery while keeping chat': 'Oculta el descubrimiento y conserva el chat',
'Show chat': 'Mostrar chat',
'Hide chat': 'Ocultar chat',
'Toggle the chat panel for this session': 'Alterna el panel de chat durante esta sesión',
'Show sidebar': 'Mostrar barra lateral',
'Hide sidebar': 'Ocultar barra lateral',
'Toggle the discovery rail for this session': 'Alterna la barra de descubrimiento durante esta sesión',
'Blur mature thumbnails': 'Desenfocar miniaturas maduras',
'Reveal mature thumbnails': 'Mostrar miniaturas maduras',
'Temporarily override mature-card blur': 'Anula temporalmente el desenfoque de tarjetas maduras',
'Change discovery spacing and save it': 'Cambia y guarda el espaciado del descubrimiento',
'New favorites apply to': 'Los nuevos favoritos se aplican a',
'Global favorites follow you everywhere. Per-channel favorites appear only on the channel you saved them from, above your global ones. Existing favorites are global and are not moved.': 'Los favoritos globales te acompañan en todas partes. Los favoritos por canal solo aparecen en el canal donde los guardaste, encima de los globales. Los favoritos existentes son globales y no se mueven.',
'Everywhere': 'En todas partes',
'This channel': 'Este canal',
'Show badges Kick leaves out': 'Mostrar insignias que Kick omite',
'Kick’s chat payload carries collectible and global badges its own markup omits, leaving a gap where other clients show a badge. A badge image that fails to load is replaced by its name.': 'La respuesta del chat de Kick incluye insignias globales y de coleccionables que su propio marcado omite, dejando un hueco donde otros clientes muestran una insignia. Si la imagen no carga, se sustituye por su nombre.',
'Hidden channels': 'Canales ocultos',
'Hide specific channels from Home, Browse, Following, and Search.': 'Oculta canales específicos de Inicio, Explorar, Siguiendo y Búsqueda.',
'No channels hidden. Use the input above or the ✕ action on a card.': 'No hay canales ocultos. Usa el campo de arriba o la acción ✕ en una tarjeta.',
'Show casino content': 'Mostrar contenido de casino',
'Hide casino content': 'Ocultar contenido de casino',
'Filter clearly labeled casino streams': 'Filtra streams marcados claramente como casino',
'Open Kick Focus settings': 'Abrir configuración de Kick Focus',
'Customize layout, appearance, content, and access': 'Personaliza el diseño, la apariencia, el contenido y el acceso',
'Choose how the left discovery rail behaves. Dropdown collapses it to a tab that expands on hover, giving the grid full width. Desktop widths only.': 'Elige cómo se comporta la barra de descubrimiento izquierda. Desplegable la reduce a una pestaña que se expande al pasar el cursor, dando a la cuadrícula todo el ancho. Solo en anchos de escritorio.',
'Place chat on either side, float it as a dock, or hide it.': 'Coloca el chat a cualquier lado, como panel flotante, u ocúltalo.',
'Set the width of the live chat column.': 'Define el ancho de la columna del chat en vivo.',
'Adjust spacing and padding across discovery pages.': 'Ajusta el espaciado y el relleno en las páginas de descubrimiento.',
'Choose how each channel opens.': 'Elige cómo se abre cada canal.',
'Keep the last runtime layout for each channel.': 'Conserva el último diseño usado en cada canal.',
'Use reclaimed sidebar space for larger, calmer stream cards.': 'Usa el espacio recuperado de la barra lateral para tarjetas de directo más grandes y tranquilas.',
'Keep the Following discovery rail visible when Kick provides it.': 'Mantén visible la barra de canales seguidos cuando Kick la ofrezca.',
'Keep recommended stream rows visible in the main content.': 'Mantén visibles las filas de directos recomendados en el contenido principal.',
'Keep search and account controls available while browsing.': 'Mantén disponibles la búsqueda y los controles de cuenta mientras navegas.',
'Keep the Focus control beside Get KICKs in Kick’s top header.': 'Mantén el control de Focus junto a Get KICKs en la cabecera superior de Kick.',
'Raise Kick’s embedded mini-player only when the Focus control has to use its floating fallback.': 'Eleva el mini-reproductor integrado de Kick solo cuando el control de Focus deba usar su alternativa flotante.',
'Re-apply player geometry after a window or monitor change.': 'Vuelve a aplicar la geometría del reproductor tras cambiar de ventana o monitor.',
'Prefer contained video geometry on wide or moved displays.': 'Prefiere una geometría de vídeo contenida en pantallas anchas o desplazadas.',
'Adjust the roundness of enhanced UI.': 'Ajusta el redondeo de la interfaz mejorada.',
'Adjust stream-card color intensity.': 'Ajusta la intensidad de color de las tarjetas de directo.',
'Set the size of Kick Focus controls.': 'Define el tamaño de los controles de Kick Focus.',
'Reduce emphasis on streams you have already opened.': 'Reduce el énfasis en los directos que ya has abierto.',
'Increase legibility on muted surfaces.': 'Aumenta la legibilidad en superficies atenuadas.',
'Use the selected accent for live-state emphasis.': 'Usa el color de acento elegido para destacar el estado en directo.',
'Enable subscription': 'Activar suscripción',
'Off by default. When enabled, refreshes only over HTTPS with credentials omitted.': 'Desactivado por defecto. Si se activa, solo se actualiza por HTTPS y sin enviar credenciales.',
'Refresh interval': 'Intervalo de actualización',
'Keep the last valid payload if a later request fails.': 'Conserva el último contenido válido si una petición posterior falla.',
'Load the emote catalog from Kick': 'Cargar el catálogo de emotes desde Kick',
'Read the full channel, global, and emoji sets without treating public artwork as account access. Falls back to the picker if the response changes shape.': 'Lee los conjuntos completos del canal, globales y de emojis sin tratar las imágenes públicas como acceso de la cuenta. Vuelve al selector si la respuesta cambia de forma.',
'Follow live chat events': 'Seguir los eventos del chat en vivo',
'Subscribe to the same realtime chat feed Kick’s own client uses. The provider is read from Kick rather than hardcoded.': 'Suscríbete al mismo flujo de chat en tiempo real que usa el cliente de Kick. El proveedor se lee desde Kick en lugar de estar fijado en el código.',
'Explain removed messages': 'Explicar los mensajes eliminados',
'Kick’s automatic moderation removes messages without saying why. The realtime event carries the reason; the page does not.': 'La moderación automática de Kick elimina mensajes sin decir por qué. El evento en tiempo real lleva el motivo; la página no.',
'Count emote usage': 'Contar el uso de emotes',
'Kick’s own “Frequently Used” never counts anything, so no real ranking exists. This one is yours, stored locally and exported with your library.': 'La sección “Usados con frecuencia” de Kick nunca cuenta nada, así que no existe una clasificación real. Esta es tuya, guardada localmente y exportada con tu biblioteca.',
'Show collectible rarity': 'Mostrar la rareza de los coleccionables',
'Kick publishes rarity on card art and identity in the picker, with no key joining them. Rarity is shown only where the match is confident.': 'Kick publica la rareza en la ilustración de la carta y la identidad en el selector, sin ninguna clave que las una. La rareza solo se muestra cuando la coincidencia es fiable.',
'Warn about shadowed emote names': 'Avisar de nombres de emote duplicados',
'Subscriber emotes work in every chat and Kick resolves typed names through one map, so two channels sharing a name means one silently sends the other’s.': 'Los emotes de suscriptor funcionan en todos los chats y Kick resuelve los nombres escritos con un único mapa, así que si dos canales comparten un nombre, uno envía en silencio el del otro.',
'Render animated emotes and collectibles as a single static frame, in chat and in the picker. Applied automatically when your system asks for reduced motion.': 'Muestra los emotes animados y los coleccionables como un único fotograma fijo, en el chat y en el selector. Se aplica automáticamente si tu sistema pide movimiento reducido.',
'Block separable ad requests': 'Bloquear las peticiones de anuncios separables',
'Intercept known ad hosts at the earliest userscript-supported page layer.': 'Intercepta los servidores de anuncios conocidos en la capa más temprana que permite un userscript.',
'Remove ad containers': 'Eliminar los contenedores de anuncios',
'Remove empty ad containers and reinjected ad frames.': 'Elimina los contenedores de anuncios vacíos y los marcos reinsertados.',
'Suppress sponsored and promoted cards': 'Ocultar las tarjetas patrocinadas y promocionadas',
'Hide clearly labeled promotional cards and modules.': 'Oculta las tarjetas y módulos claramente marcados como promocionales.',
'Pause home-page autoplay': 'Pausar la reproducción automática de la página de inicio',
'Keep background Home previews silent and paused; deliberate playback remains available.': 'Mantiene en silencio y en pausa las vistas previas de fondo del inicio; la reproducción deliberada sigue disponible.',
'Hide Slots & Casino content': 'Ocultar contenido de Slots y Casino',
'Hide cards and sidebar entries clearly labeled as casino content.': 'Oculta las tarjetas y entradas de la barra lateral marcadas claramente como contenido de casino.',
'Blur marked mature cards until hover or keyboard focus.': 'Difumina las tarjetas marcadas para adultos hasta pasar el cursor o enfocarlas con el teclado.',
'Hide Drops and gambling promotions': 'Ocultar promociones de Drops y apuestas',
'Hide clearly labeled Drops and gambling promotion modules.': 'Oculta los módulos claramente marcados como promociones de Drops y apuestas.',
'Poor mode': 'Modo sin gastos',
'Show how long the stream has been live': 'Mostrar cuánto tiempo lleva en directo',
'Kick sends the start time with every channel and shows it nowhere. This reads that field and counts from it in the player corner, with no extra request and no polling.': 'Kick envía la hora de inicio con cada canal y no la muestra en ninguna parte. Esto lee ese campo y cuenta desde él en la esquina del reproductor, sin peticiones extra ni sondeos.',
'Show stream uptime': 'Mostrar tiempo en directo',
'Pop out chat': 'Chat en ventana flotante',
'Channel points: Kick says Picture-in-Picture and mirrored viewing do not accrue points. Keep a normal Kick player open when progress matters.': 'Puntos del canal: Kick indica que la imagen en imagen y la visualización reflejada no acumulan puntos. Mantén abierto un reproductor normal de Kick cuando el progreso importe.',
'Merge all chats': 'Unir todos los chats',
'Merged chat from every channel in the grid': 'Chat unificado de todos los canales de la cuadrícula',
'One chat per tile': 'Un chat por canal',
'Read-only. Every channel in the grid, in the order messages arrived.': 'Solo lectura. Todos los canales de la cuadrícula, en el orden en que llegaron los mensajes.',
'Showing one merged chat for every channel in the grid': 'Mostrando un chat unificado de todos los canales de la cuadrícula',
'Showing the focused channel chat': 'Mostrando el chat del canal enfocado',
'Return chat': 'Devolver el chat',
'Kick Focus could not open the pop-out chat window.': 'Kick Focus no ha podido abrir la ventana flotante del chat.',
'Chat for {channel} opened in a floating window': 'El chat de {channel} se ha abierto en una ventana flotante',
'Show VOD expiry': 'Mostrar caducidad del vídeo',
'Show how long Kick keeps this recording': 'Mostrar cuánto tiempo conserva Kick esta grabación',
'Kick deletes recordings after 7 days, or 30 for a verified channel, and shows that deadline nowhere. On a VOD page this reads the recording date from Kick’s own video list and counts down to it. It says nothing at all when the recording is older than the list Kick returns, or when the tier cannot be established. A guess between 7 and 30 days would be a confident wrong date.': 'Kick borra las grabaciones a los 7 días, o a los 30 si el canal está verificado, y no muestra ese plazo en ninguna parte. En la página de un vídeo, esto lee la fecha de grabación de la propia lista de vídeos de Kick y cuenta atrás hasta ella. No dice nada cuando la grabación es más antigua que la lista que devuelve Kick, o cuando no se puede establecer el nivel: adivinar entre 7 y 30 días sería dar una fecha equivocada con total seguridad.',
'{time} before Kick deletes this recording': '{time} antes de que Kick borre esta grabación',
'Live for {duration}': 'En directo desde hace {duration}',
'{count} emotes usable in any chat': '{count} emotes utilizables en cualquier chat',
'subscribed channel': 'canal suscrito',
'subscribed channels': 'canales suscritos',
'your global sets': 'tus conjuntos globales',
'Kick reports no emotes this account can send anywhere.': 'Kick no indica ningún emote que esta cuenta pueda enviar en cualquier chat.',
'My emotes': 'Mis emotes',
'My emotes ({count})': 'Mis emotes ({count})',
'Subscribed channel': 'Canal suscrito',
'Global collection': 'Colección global',
'Kick reports no emotes this account can use in every chat.': 'Kick no indica ningún emote que esta cuenta pueda usar en todos los chats.',
'Sign in to Kick and open any channel once to load your owned emotes. Nothing is sent or changed.': 'Inicia sesión en Kick y abre cualquier canal una vez para cargar tus emotes. No se envía ni cambia nada.',
'Works in every chat': 'Funciona en todos los chats',
'Only works in its own channel': 'Solo funciona en su propio canal',
'Only works in {channel}’s chat': 'Solo funciona en el chat de {channel}',
'Hide Subscribe, Gift Subs/Dubs, Get KICKs, gift-shop controls, and spend-based leaderboards. Follow, chat, and free daily rewards stay available.': 'Oculta Suscribirse, Regalar subs/dubs, Obtener KICKs, la tienda de regalos y las clasificaciones de gasto. Seguir, el chat y las recompensas diarias gratuitas siguen disponibles.',
'Enable Poor mode': 'Activar modo sin gastos',
'Disable Poor mode': 'Desactivar modo sin gastos',
'Remove spending prompts without changing your Kick account': 'Oculta las invitaciones de gasto sin cambiar tu cuenta de Kick',
'Channel emote catalog': 'Catálogo de emotes del canal',
'Loading…': 'Cargando…',
'Load emotes': 'Cargar emotes',
'Channel-only': 'Solo en el canal',
'Subscriber-only': 'Solo para suscriptores',
'Reduce tracking telemetry': 'Reducir la telemetría de seguimiento',
'Block observed third-party video and error telemetry hosts.': 'Bloquea los servidores de telemetría de vídeo y errores de terceros detectados.',
'Remember volume locally': 'Recordar el volumen localmente',
'Restore each channel’s volume and mute state from local storage.': 'Restaura el volumen y el estado de silencio de cada canal desde el almacenamiento local.',
'Remember quality locally': 'Recordar la calidad localmente',
'Restore a matching quality control when Kick exposes one.': 'Restaura el control de calidad correspondiente cuando Kick lo ofrece.',
'Always start at the highest quality': 'Empezar siempre en la calidad más alta',
'Open every stream at the best rung Kick offers, taking precedence over remembered quality. The rungs are learned from Kick’s own quality menu, so this does nothing until that menu has been opened once. It will not open it for you.': 'Abre cada directo en la mejor opción que ofrezca Kick, con prioridad sobre la calidad recordada. Las opciones se aprenden del propio menú de calidad de Kick, así que no hace nada hasta que ese menú se haya abierto una vez: no lo abrirá por ti.',
'Remember VOD position locally': 'Recordar la posición del VOD localmente',
'Resume finite VODs from the last local playback position.': 'Reanuda los VOD finitos desde la última posición de reproducción local.',
'Pause chat updates': 'Pausar las actualizaciones del chat',
'Scrolling the transcript up freezes it, as does the button. Resume is always one control away.': 'Desplazarte hacia arriba en el chat lo congela, igual que el botón. Reanudar siempre está a un control de distancia.',
'Chat updates paused': 'Actualizaciones del chat en pausa',
'Userscript size': 'Tamaño del userscript',
'Injection ceiling': 'Límite de inyección',
'Help': 'Ayuda',
'Open help and recovery': 'Abrir ayuda y recuperación',
'Chat updates resumed': 'Actualizaciones del chat reanudadas',
'Organize chat emotes': 'Organizar los emotes del chat',
'Continuously record emotes from live chat and Kick’s picker, then add favorites, removals, search, and custom groups.': 'Registra continuamente los emotes del chat en vivo y del selector de Kick, y añade favoritos, eliminaciones, búsqueda y grupos personalizados.',
'Click chat emotes to save': 'Haz clic en los emotes del chat para guardarlos',
'Click any emote in chat to add it to your favorites. If Kick explicitly marks it as follow-gated, the same click follows its source channel; subscriber access is never bypassed.': 'Haz clic en cualquier emote del chat para añadirlo a tus favoritos. Si Kick lo marca explícitamente como restringido a seguidores, el mismo clic sigue su canal de origen; el acceso de suscriptor nunca se omite.',
'Highlight chat keywords': 'Resaltar palabras clave del chat',
'Use the per-channel keyword list below without sending it anywhere.': 'Usa la lista de palabras clave por canal de abajo sin enviarla a ningún sitio.',
'Show playback diagnostics': 'Mostrar diagnósticos de reproducción',
'Show ready state, buffered seconds, and dropped-frame counts on a channel.': 'Muestra el estado de preparación, los segundos en búfer y los fotogramas perdidos en un canal.',
'Start playback without waiting for blocked ad scripts': 'Iniciar la reproducción sin esperar a los scripts de anuncios bloqueados',
'Kick waits on Google PAL, Datazoom, and OM before requesting playback. Blocking them, which this build does, leaves the dead script in the page and the player waits out the full timeout. Removing it lets playback start immediately.': 'Kick espera a Google PAL, Datazoom y OM antes de pedir la reproducción. Bloquearlos, lo que hace esta versión, deja el script muerto en la página y el reproductor agota todo el tiempo de espera. Quitarlo permite que la reproducción empiece de inmediato.',
'Minimize non-essential animations and transitions.': 'Minimiza las animaciones y transiciones no esenciales.',
'Increase separation for controls, borders, and surfaces.': 'Aumenta la separación de controles, bordes y superficies.',
'Keep a strong outline for keyboard navigation.': 'Mantiene un contorno marcado para la navegación con teclado.',
'Increase the minimum height of interactive controls.': 'Aumenta la altura mínima de los controles interactivos.',
'Report view changes to assistive technology.': 'Informa de los cambios de vista a las tecnologías de asistencia.',
'Scale text in the main Kick content area.': 'Escala el texto en el área de contenido principal de Kick.',
'Set the preferred caption background strength.': 'Define la intensidad preferida del fondo de los subtítulos.',
'Multi-stream opened': 'Multitransmisión abierta',
'Your own multi-stream grid is back.': 'Tu propia cuadrícula de multitransmisión ha vuelto.',
'Watch several Kick channels in one grid': 'Mira varios canales de Kick en una sola cuadrícula',
'Freeze animated emotes': 'Congelar los emotes animados',
'Read-only here. Kick blocks sending from an embedded chat; open the channel to talk.': 'Solo lectura aquí. Kick impide enviar desde un chat incrustado; abre el canal para hablar.',
'Emote favorites, removals, and custom groups reset.': 'Se restablecieron los favoritos, las eliminaciones y los grupos personalizados de emotes.',
'The emote could not be saved. The error log on the Content & Ads page says why.': 'No se pudo guardar el emote. El registro de errores de la página Contenido y anuncios dice por qué.',
'Open a channel page first.': 'Abre primero la página de un canal.',
'Local channel tools saved.': 'Herramientas locales del canal guardadas.',
'Local channel tools cleared.': 'Herramientas locales del canal borradas.',
'Enter a custom emote group name.': 'Escribe un nombre para el grupo personalizado de emotes.',
'The emote group limit is 40.': 'El límite de grupos de emotes es 40.',
'That emote group already exists.': 'Ese grupo de emotes ya existe.',
'Enter a valid emote group name.': 'Escribe un nombre de grupo de emotes válido.',
'Board saved.': 'Tablero guardado.',
'That board has no usable channels.': 'Ese tablero no tiene canales utilizables.',
'Channel name or kick.com link': 'Nombre del canal o enlace de kick.com',
'Multi-stream controls': 'Controles de transmisión múltiple',
'Name this board': 'Nombra este tablero',
'Board name': 'Nombre del tablero',
'Ready for your first channel': 'Listo para tu primer canal',
'Multi-stream workspace': 'Espacio de transmisión múltiple',
'Build your viewing board': 'Crea tu tablero de visualización',
'Add a channel above to start. Focus decides which stream owns audio and chat, and your saved boards stay on this device.': 'Añade un canal arriba para empezar. El enfoque decide qué stream controla el audio y el chat, y tus tableros guardados permanecen en este dispositivo.',
'Add your first channel': 'Añade tu primer canal',
'Add channel': 'Añadir canal',
'Save board': 'Guardar tablero',
'Saved boards will appear here.': 'Los tableros guardados aparecerán aquí.',
'Could not reach the clipboard. Check the clipboard permission for kick.com.': 'No se pudo acceder al portapapeles. Revisa el permiso del portapapeles para kick.com.',
'Cached blocklist removed.': 'Se eliminó la lista de bloqueo almacenada en caché.',
'Enter a channel name or URL.': 'Escribe un nombre de canal o una URL.',
'That does not look like a Kick channel.': 'Eso no parece un canal de Kick.',
'That channel is already hidden.': 'Ese canal ya está oculto.',
'Hidden channel list is full (200). Remove a channel before hiding another.': 'La lista de canales ocultos está llena (200). Quita un canal antes de ocultar otro.',
'Favorites cleared.': 'Favoritos borrados.',
'Not-interested channels restored.': 'Se restauraron los canales marcados como no interesantes.',
'Could not export settings. Check that your browser allows downloads from kick.com.': 'No se pudo exportar la configuración. Comprueba que tu navegador permita descargas desde kick.com.',
'Could not read that settings file. Pick a JSON file exported by Kick Focus.': 'No se pudo leer ese archivo de configuración. Elige un archivo JSON exportado por Kick Focus.',
'That backup is too large for this browser’s storage. Nothing was changed.': 'Esa copia de seguridad es demasiado grande para el almacenamiento de este navegador. No se cambió nada.',
'The import could not be saved. Your previous settings are unchanged.': 'No se pudo guardar la importación. Tu configuración anterior no ha cambiado.',
'Settings imported.': 'Configuración importada.',
'Previous settings backed up. Use Undo import to restore them.': 'Se hizo una copia de la configuración anterior. Usa Deshacer importación para restaurarla.',
' and {count} more': ' y {count} más',
'{count} more': '{count} más',
'Ignored unknown section "{key}".': 'Se ignoró la sección desconocida "{key}".',
'Ignored unknown setting "{path}".': 'Se ignoró la configuración desconocida "{path}".',
'Adjusted "{path}" to a supported value.': 'Se ajustó "{path}" a un valor compatible.',
'Upgraded from an unversioned file to schema {schema}.': 'Se actualizó un archivo sin versión al esquema {schema}.',
'Upgraded from schema {from} to schema {to}.': 'Se actualizó del esquema {from} al esquema {to}.',
'{count} emote could not be kept: {sample}{more}.': 'No se pudo conservar {count} emote: {sample}{more}.',
'{count} emotes could not be kept: {sample}{more}.': 'No se pudieron conservar {count} emotes: {sample}{more}.',
'Adjusted emote {field} to supported entries.': 'Se ajustó emote {field} a entradas compatibles.',
'Upgraded emotes to schema {schema}.': 'Se actualizaron los emotes al esquema {schema}.',
'Adjusted emote usage counts to {count} supported entries.': 'Se ajustaron los recuentos de uso de emotes a {count} entradas compatibles.',
'Adjusted the multi-stream grid to {count} supported channels.': 'Se ajustó la cuadrícula multitransmisión a {count} canales compatibles.',
'Adjusted saved boards to {count} supported entries.': 'Se ajustaron los diseños guardados a {count} entradas compatibles.',
'Density saved': 'Densidad guardada',
'Content filter saved': 'Filtro de contenido guardado',
'Poor mode saved': 'Modo sin gastos guardado',
'No import to undo.': 'No hay ninguna importación que deshacer.',
'The backup could not be restored. Your current settings are unchanged.': 'No se pudo restaurar la copia de seguridad. Tu configuración actual no ha cambiado.',
'Import undone. Your previous settings are back.': 'Importación deshecha: tu configuración anterior está de vuelta.',
'Kick Focus restored.': 'Kick Focus restaurado.',
'Kick Focus paused. Use the Resume button or Ctrl+Shift+F to restore.': 'Kick Focus en pausa. Usa el botón Reanudar o Ctrl+Mayús+F para restaurarlo.',
'Settings reset': 'Configuración restablecida',
'Kick Focus restored': 'Kick Focus restaurado',
'Give this stream the audio and chat': 'Dar a esta transmisión el audio y el chat',
'Remove': 'Quitar',
'Clear search': 'Borrar la búsqueda',
'Campaign status': 'Estado de la campaña',
'No open campaigns': 'No hay campañas abiertas',
'Campaigns appear here when a reward is available.': 'Las campañas aparecen aquí cuando hay una recompensa disponible.',
'Browse eligible streams': 'Explorar transmisiones elegibles',
'View coming soon': 'Ver próximas campañas',
'Reward activity': 'Actividad de recompensas',
'Active': 'Activas',
'Claimed': 'Reclamadas',
'Expired': 'Caducadas',
'How drops work': 'Cómo funcionan los drops',
'Watch eligible streams': 'Ver transmisiones elegibles',
'Pick an active campaign.': 'Elige una campaña activa.',
'Track progress': 'Seguir el progreso',
'Keep watching to advance.': 'Sigue mirando para avanzar.',
'Claim your reward': 'Reclamar la recompensa',
'Claim it before time runs out.': 'Reclámala antes de que se acabe el tiempo.',
'Kick Focus command menu': 'Menú de comandos de Kick Focus',
'Kick Focus multi-stream': 'Multitransmisión de Kick Focus',
'Which chat to show': 'Qué chat mostrar',
'Live style preview': 'Vista previa del estilo en vivo',
'release, giveaway, raid': 'estreno, sorteo, raid',
'Chat keywords for this channel': 'Palabras clave del chat para este canal',
'Why I follow this channel…': 'Por qué sigo este canal…',
'Private channel note': 'Nota privada del canal',
'Optional blocklist URL': 'URL de lista de bloqueo opcional',
'Search emotes or Kick groups': 'Buscar emotes o grupos de Kick',
'Search recorded emotes': 'Buscar emotes registrados',
'Filter recorded emotes': 'Filtrar emotes registrados',
'Group name': 'Nombre del grupo',
'Custom emote groups': 'Grupos personalizados de emotes',
'Group for selected emotes': 'Grupo para los emotes seleccionados',
'Select {name}': 'Seleccionar {name}',
'Deselect {name}': 'Deseleccionar {name}',
'Delete group {name}': 'Eliminar grupo {name}',
'New emote group name': 'Nombre del nuevo grupo de emotes',
'Channel name or kick.com URL': 'Nombre del canal o URL de kick.com',
'Channel to hide': 'Canal que ocultar',
'Open Kick Focus multi-stream': 'Abrir la multitransmisión de Kick Focus',
'Multi-stream': 'Multitransmisión',
'Stats': 'Estadísticas',
'Open {channel} stats in StreamerStats': 'Abrir las estadísticas de {channel} en StreamerStats',
'Opened {channel} in StreamerStats.': 'Se abrió {channel} en StreamerStats.',
'The browser blocked the stats popup.': 'El navegador bloqueó la ventana emergente de estadísticas.',
'Open tab': 'Abrir pestaña',
'Add this channel to Kick Focus multi-stream': 'Añadir este canal a la multitransmisión de Kick Focus',
'Add to multi-stream': 'Añadir a la multitransmisión',
'Undo': 'Deshacer',
'View': 'Ver',
'Enable optional blocklist subscription': 'Activar la suscripción opcional a la lista de bloqueo',
'Core ad protection is on': 'La protección principal contra anuncios está activada',
'Suppress promoted cards': 'Ocultar las tarjetas promocionadas',
'Hide Slots and Casino content': 'Ocultar el contenido de Slots y Casino',
'Your inventory holds {copies} {copiesWord} across {distinct} distinct {distinctWord}. That is {duplicates} {duplicatesWord}, or {rate}% of what you have pulled.': 'Tu inventario tiene {copies} {copiesWord} repartidos en {distinct} {distinctWord} distintos: {duplicates} {duplicatesWord}, es decir, el {rate}% de lo que has conseguido.',
'Your inventory holds {distinct} distinct {distinctWord}. Kick’s response carries no per-item quantity, so a duplicate rate cannot be measured from it. That number is unavailable rather than zero.': 'Tu inventario tiene {distinct} {distinctWord} distintos. La respuesta de Kick no incluye la cantidad por artículo, así que no se puede medir una tasa de duplicados a partir de ella: ese número no está disponible, no es cero.',
'emote name shadowed.': 'nombre de emote duplicado.',
'emote names shadowed.': 'nombres de emote duplicados.',
'result loaded': 'resultado cargado',
'results loaded': 'resultados cargados',
'emote': 'emote',
'emotes': 'emotes',
'collectible': 'coleccionable',
'collectibles': 'coleccionables',
'item': 'artículo',
'items': 'artículos',
'duplicate': 'duplicado',
'duplicates': 'duplicados',
'recorded emote has been changed by Kick since first capture. See the Changed by Kick filter in the library below.': 'emote registrado ha sido modificado por Kick desde su primera captura: consulta el filtro Modificados por Kick en la biblioteca de abajo.',
'recorded emotes have been changed by Kick since first capture. See the Changed by Kick filter in the library below.': 'emotes registrados han sido modificados por Kick desde su primera captura: consulta el filtro Modificados por Kick en la biblioteca de abajo.',
'channel hidden. These count toward the fail-open ceiling.': 'canal oculto. Cuenta para el límite de seguridad.',
'channels hidden. These count toward the fail-open ceiling.': 'canales ocultos. Cuentan para el límite de seguridad.',
'channel': 'canal',
'channels': 'canales',
'minute': 'minuto',
'minutes': 'minutos',
'time': 'vez',
'times': 'veces',
'Claim the daily reward automatically': 'Reclamar la recompensa diaria automáticamente',
'Opens Kick’s own reward dialog when one is waiting and clicks its claim button for you. It clicks nothing else: a reward Kick has not unlocked yet shows a disabled button, and this leaves it alone rather than trying. It waits until you are not typing, checks at most every ten minutes, and stops for the day once it claims. Signed-in only, because the reward button does not exist otherwise.': 'Abre el propio diálogo de recompensa de Kick cuando hay una esperando y pulsa su botón de reclamar por ti. No pulsa nada más: una recompensa que Kick aún no ha desbloqueado muestra un botón desactivado, y esto lo deja en paz en lugar de intentarlo. Espera a que no estés escribiendo, comprueba como mucho cada diez minutos y se detiene por hoy en cuanto reclama. Solo con sesión iniciada: el botón de recompensa no existe de otro modo.',
'Daily reward claimed. It is in your collectibles.': 'Recompensa diaria reclamada. Está en tus coleccionables.',
'Daily reward claimed.': 'Recompensa diaria reclamada.',
'Add open tabs ({count})': 'Añadir pestañas abiertas ({count})',
'Added {count} from your other tabs ({total} of {max})': 'Se añadieron {count} de tus otras pestañas: {total} de {max}',
'Added {count} channels from your other tabs.': 'Se añadieron {count} canales de tus otras pestañas.',
'Apply cycle cost': 'Coste del ciclo de aplicación',
'No apply cycle has run yet.': 'Aún no se ha ejecutado ningún ciclo de aplicación.',
'Type an emote name into chat': 'Escribir el nombre de un emote en el chat',
'Suggest emotes as you type': 'Sugerir emotes mientras escribes',
'Typing a colon and two or more letters in chat offers matching emotes from your library, ranked by what you actually send here. Click one to put its plain name at your cursor. Suggestions are clicked, never accepted with a key, so nothing you type is ever captured, and it never sends the message.': 'Al escribir dos puntos y dos o más letras en el chat se ofrecen emotes de tu biblioteca, ordenados según lo que realmente envías aquí. Haz clic en uno para poner su nombre simple en el cursor. Las sugerencias se eligen con el ratón, nunca con una tecla, así que nada de lo que escribes queda capturado, y nunca envía el mensaje.',
'Adds a Type in chat action beside Copy name in the emote library. It types the plain name at your cursor and stops. Never the wire token, never an id, and it never sends the message.': 'Añade una acción Escribir en el chat junto a Copiar nombre en la biblioteca de emotes. Escribe solo el nombre en la posición del cursor y se detiene ahí: nunca el código interno, nunca un id, y nunca envía el mensaje.',
'That emote has no plain name to copy.': 'Ese emote no tiene un nombre simple que copiar.',
'That emote has no plain name to type.': 'Ese emote no tiene un nombre simple que escribir.',
'Open a channel chat first.': 'Abre primero el chat de un canal.',
'Kick’s chat box did not accept the text. The name is on your clipboard instead.': 'El cuadro de chat de Kick no aceptó el texto. El nombre está en tu portapapeles.',
'Seen available': 'Visto como disponible',
'Seen in chat': 'Visto en el chat',
'Click to save': 'Haz clic para guardar',
'Saved. Click to open in the library': 'Guardado: haz clic para abrirlo en la biblioteca',
'Name shadowed by another set': 'Nombre eclipsado por otro conjunto',
'{count} of {max} streams': '{count} de {max} transmisiones',
},
pt: {
'Home': 'Início',
'Browse': 'Explorar',
'Following': 'A seguir',
'Saved views': 'Vistas guardadas',
'Keep the density, thumbnail size, rails, and content filters you like as a named view, and have it applied when you open the pages you chose. It is your own settings, applied to what Kick already sent. It changes nothing about what Kick recommends or the order anything appears in.': 'Guarda a densidade, o tamanho das miniaturas, as barras e os filtros de conteúdo que preferes como uma vista com nome, e aplica-a ao abrir as páginas que escolheres. São as tuas próprias definições aplicadas ao que a Kick já enviou. Não muda nada do que a Kick recomenda nem a ordem em que aparece.',
'Save this page as a view': 'Guardar esta página como vista',
'Pick the pages it should apply to, or none to keep it manual.': 'Escolhe as páginas onde deve aplicar-se, ou nenhuma para a deixar manual.',
'Save this view': 'Guardar esta vista',
'No saved views yet. Set the page up the way you want it, name it, and save.': 'Ainda não há vistas guardadas. Deixa a página como a queres, dá-lhe um nome e guarda.',
'Applied only when you press it': 'Aplica-se apenas quando a carregas',
'Currently applied': 'Aplicada agora',
'Applied {name} for this page.': 'Foi aplicada {name} nesta página.',
'{n} saved views is the limit. Delete one first.': 'O limite é {n} vistas guardadas. Apaga uma primeiro.',
'Saved {name}.': 'Foi guardada {name}.',
'Applied {name}. {n} settings changed.': 'Foi aplicada {name}. Mudaram {n} definições.',
'{name} is already what you are looking at.': '{name} é exatamente o que estás a ver.',
'Deleted {name}.': 'Foi apagada {name}.',
'Name the view before saving it.': 'Dá um nome à vista antes de a guardares.',
'Delete this saved view': 'Apagar esta vista guardada',
'Name this view': 'Nome da vista',
'Category pages': 'Páginas de categoria',
'Search results': 'Resultados da procura',
'Show message times': 'Mostrar a hora das mensagens',
'Reveals the timestamp Kick already renders on every message and keeps hidden. It is Kick’s own value, so scrolling back shows when a message was sent rather than when this build first saw it.': 'Mostra a marca de hora que a Kick já desenha em cada mensagem e mantém escondida. É o valor da própria Kick, por isso ao subir no chat vês quando a mensagem foi enviada e não quando esta extensão a viu.',
'People worth noticing': 'Pessoas que queres notar',
'Names you want to catch in a fast chat. Their messages get a marker of their own, separate from keyword highlights. Comma separated, and stored only in your settings.': 'Nomes que queres apanhar num chat rápido. As mensagens deles recebem uma marca própria, diferente dos destaques por palavra-chave. Separados por vírgulas e guardados apenas nas tuas definições.',
'Sound on a mention': 'Som quando te mencionam',
'A short tone when a message matches your highlights, comes from someone you listed, or says your name. Synthesised in the browser, so nothing is downloaded. Silent while the tab is in the background, silent for your own messages, and never more than once every few seconds.': 'Um tom curto quando uma mensagem corresponde aos teus destaques, vem de alguém da tua lista ou diz o teu nome. É gerado no navegador, por isso nada é transferido. Silencioso com o separador em segundo plano, silencioso nas tuas próprias mensagens e nunca mais do que uma vez a cada poucos segundos.',
'Hide a message for yourself': 'Esconder uma mensagem só para ti',
'Adds a small dismiss control to each message. It hides that message in your own browser for this session only, changes nothing for anyone else, and offers an undo.': 'Adiciona um pequeno controlo de dispensa a cada mensagem. Esconde essa mensagem no teu navegador apenas nesta sessão, não muda nada para os outros e oferece anular.',
'Recall my sent messages': 'Recordar as minhas mensagens enviadas',
'Keep the last five messages sent from this tab in memory. Shift+Up cycles them. Whispers are skipped, reload clears them, and ordinary Arrow Up stays with Kick.': 'Guarda na memória as últimas cinco mensagens enviadas neste separador. Shift+Cima percorre-as. Os sussurros são ignorados, recarregar apaga-as e a Seta para cima continua com o Kick.',
'Search this session’s chat': 'Procurar no chat desta sessão',
'Keeps what this tab has seen so you can find it again. It stays in memory, never reaches storage, and is gone on reload. Whispers are never recorded, and a message a moderator removes leaves the log the moment the deletion arrives.': 'Guarda o que este separador viu para que o possas encontrar outra vez. Fica em memória, nunca chega ao armazenamento e desaparece ao recarregar. Os sussurros nunca são guardados, e uma mensagem que um moderador apague sai do registo assim que a eliminação chega.',
'Session chat log': 'Registo do chat desta sessão',
'Hidden by you': 'Escondido por ti',
'Hide this message for me': 'Esconder esta mensagem para mim',
'Nothing in this session matches that.': 'Nada nesta sessão corresponde a isso.',
'Nothing recorded yet. Open a chat with the switch on.': 'Ainda não há nada guardado. Abre um chat com a opção ligada.',
'Saved {n} messages from this session.': 'Foram guardadas {n} mensagens desta sessão.',
'Message hidden for you. It is still there for everyone else.': 'Mensagem escondida para ti. Continua lá para toda a gente.',
'Session chat log cleared.': 'Registo do chat da sessão apagado.',
'message held. Capped at 400 messages, 200 KB, and one hour.': 'mensagem guardada. Com um limite de 400 mensagens, 200 KB e uma hora.',
'messages held. Capped at 400 messages, 200 KB, and one hour.': 'mensagens guardadas. Com um limite de 400 mensagens, 200 KB e uma hora.',
'name, name': 'nome, nome',
'Search what you have seen': 'Procura o que já viste',
'Save as a file': 'Guardar como ficheiro',
'Clear the log': 'Apagar o registo',
'Daily reward ready': 'Recompensa diária pronta',
'Viewer': 'Conta',
'Account readings and this browser session, in one place. Nothing here is claimed or sent anywhere.': 'Leituras da conta e esta sessão do navegador, reunidas num só lugar. Aqui nada é resgatado ou enviado.',
'Reading': 'Leituras',
'Reading…': 'A ler…',
'From Kick’s API': 'Da API da Kick',
'Read from the page': 'Lido da página',
'This browser session only': 'Apenas esta sessão do navegador',
'{n} min ago': 'há {n} min',
'Daily reward': 'Recompensa diária',
'Channel points': 'Pontos do canal',
'Collectibles': 'Colecionáveis',
'Drops': 'Drops',
'Level': 'Nível',
'Streak': 'Sequência',
'Session watch time': 'Tempo assistido nesta sessão',
'Not read yet on this page.': 'Ainda não foi lido nesta página.',
'Kick shows this to a signed-in account only.': 'A Kick só mostra isto a uma conta com sessão iniciada.',
'Open a channel to see its points.': 'Abre um canal para ver os seus pontos.',
'Open Drops to count the campaigns waiting.': 'Abre Drops para contar as campanhas pendentes.',
'Kick shows this inside the daily reward dialog only.': 'A Kick só mostra isto dentro da janela da recompensa diária.',
'The reward dialog did not show a figure this time.': 'Desta vez a janela da recompensa não mostrou nenhum número.',
'Kick did not answer that read. Nothing was changed.': 'A Kick não respondeu a essa leitura. Nada foi alterado.',
'This card could not be built. The rest of the hub is unaffected.': 'Este cartão não pôde ser construído. O resto da página não é afetado.',
'Claimed today': 'Resgatada hoje',
'Not ready yet': 'Ainda não está pronta',
'Ready to claim': 'Pronta para resgatar',
'Where these come from': 'De onde vêm estes dados',
'Nothing has been read yet. Each card above says why.': 'Ainda não foi lido nada. Cada cartão acima explica porquê.',
'Read again': 'Ler outra vez',
'Nothing is claimed for you here': 'Aqui nada é resgatado por ti',
'This page reads. The daily reward is still claimed by Kick’s own dialog, and only when you have turned that on under Content & Ads. A card with no reading says so rather than showing a zero, because an empty balance and an unreadable one are not the same thing.': 'Esta página lê. A recompensa diária continua a ser resgatada pela própria janela da Kick, e só se tiveres ativado isso em Conteúdo e anúncios. Um cartão sem leitura di-lo em vez de mostrar um zero, porque um saldo vazio e um saldo ilegível não são a mesma coisa.',
'Settings': 'Configurações',
'Autosaved': 'Salvo automaticamente',
'Could not save': 'Não foi possível salvar',
'Imported': 'Importado',
'All settings reset': 'Todas as configurações foram redefinidas',
'Page reset': 'A página foi redefinida',
'Shortcuts restored': 'Atalhos restaurados',
'Shortcut saved': 'Atalho salvo',
'Added {name}. Now {count} of {max}.': '{name} foi adicionado. Agora {count} de {max}.',
'Removed {name}. Now {count} of {max}.': '{name} foi removido. Agora {count} de {max}.',
'Removed {name} from the grid.': '{name} foi removido da grade.',
'{name} added to the multi-stream grid': '{name} foi adicionado à grade multi-stream',
'The grid is full at {max} of {max}.': 'A grade está cheia com {max} de {max}.',
'The grid is full at {max} channels.': 'A grade está cheia com {max} canais.',
'{name} now has the audio': '{name} tem agora o áudio',
'Loaded board {name}': 'Painel {name} carregado',
'Copied a link to {name}.': 'Link para {name} copiado.',
'Opened a shared board with {count} {word}.': 'Foi aberto um painel compartilhado com {count} {word}.',
'The shared board replaced {count} {word} you had collected.': 'O painel compartilhado substituiu {count} {word} que você tinha reunido.',
'Loaded {count} emotes from {channel}.': 'Foram carregados {count} emotes de {channel}.',
'Kick Focus applied to {route}': 'Kick Focus aplicado a {route}',
'Content filtering suspended: it would have hidden {percent}% of this page.': 'Filtragem de conteúdo suspensa: teria ocultado {percent}% desta página.',
'Exported settings, {emotes} emotes, {counts} usage counts, {boards} boards, and your channels, notes, and filters.': 'Foram exportadas as configurações, {emotes} emotes, {counts} contagens de uso, {boards} painéis e seus canais, notas e filtros.',
'The emote was removed, but Kick could not unfollow {channel}.': 'O emote foi removido, mas a Kick não conseguiu deixar de seguir {channel}.',
'Saved {name}, but Kick did not identify the follow-gated source channel.': '{name} foi salvo, mas a Kick não identificou o canal de origem que exige seguir.',
'Saved {name}. Following {channel}…': '{name} foi salvo. Seguindo {channel}…',
'Saved {name} locally, but Kick could not follow {channel} ({status}). Sign in or reload the channel and try again.': '{name} foi salvo neste dispositivo, mas a Kick não conseguiu seguir {channel} ({status}). Entre na sua conta ou recarregue o canal e tente de novo.',
'Open multi-stream': 'Abrir multi-stream',
'Close multi-stream': 'Fechar multi-stream',
'emote id in card URL': 'o id do emote na URL da carta',
'emote name in card URL': 'o nome do emote na URL da carta',
'Use comfortable density': 'Usar densidade confortável',
'Use compact density': 'Usar densidade compacta',
'Recent': 'Recentes',
'New group': 'Novo grupo',
'Save': 'Salvar',
'Rename': 'Renomear',
'Delete': 'Excluir',
'Ungrouped': 'Sem grupo',
'Select shown': 'Selecionar visíveis',
'Clear': 'Limpar',
'Earlier': 'Antes',
'Later': 'Depois',
'Move': 'Mover',
'Groups': 'Grupos',
'Your emotes': 'Seus emotes',
'Library': 'Biblioteca',
'Favorites': 'Favoritos',
'All': 'Todos',
'Kick': 'Kick',
'Kick groups': 'Grupos da Kick',
'Kick’s original emote groups are shown below.': 'Os grupos de emotes originais da Kick aparecem abaixo.',
'No emotes found': 'Nenhum emote encontrado',
'Try a different search or clear the search field.': 'Tente outra busca ou limpe o campo de busca.',
'No favorites yet': 'Ainda não há favoritos',
'Open All and use the star on any emote to keep it here.': 'Abra Todos e use a estrela em qualquer emote para mantê-lo aqui.',
'No recent emotes yet': 'Ainda não há emotes recentes',
'Emotes you send in this channel will appear here.': 'Os emotes que você enviar neste canal aparecerão aqui.',
'Create your first group': 'Crie seu primeiro grupo',
'Use New group above, then select emotes and move them into it.': 'Use Novo grupo acima, depois selecione emotes e mova-os para ele.',
'This group is empty': 'Este grupo está vazio',
'Choose Organize, select emotes, then move them into this group.': 'Escolha Organizar, selecione emotes e mova-os para este grupo.',
'Try a different search or return to Kick groups.': 'Tente outra busca ou volte aos grupos da Kick.',
'{count} selected emote': '{count} emote selecionado',
'{count} selected emotes': '{count} emotes selecionados',
'Organize': 'Organizar',
'Remove this-channel favorite {name}': 'Remover {name} dos favoritos deste canal',
'Emote suggestions': 'Sugestões de emotes',
'Removed {name} from the grid ({count} of {max})': '{name} foi removido da grade ({count} de {max})',
'Added {name} to the grid ({count} of {max})': '{name} foi adicionado à grade ({count} de {max})',
'Add to Multi': 'Adicionar ao Multi',
'In Multi': 'No Multi',
'Add {name} to the multi-stream grid': 'Adicionar {name} à grade multi-stream',
'Remove {name} from the multi-stream grid': 'Remover {name} da grade multi-stream',
'Favorite {name}': 'Marcar {name} como favorito',
'Remove favorite {name}': 'Remover {name} dos favoritos',
'Not interested in {name}': 'Não tenho interesse em {name}',
'Restore {name}': 'Restaurar {name}',
'Remove {name}': 'Remover {name}',
'Restore': 'Restaurar',
'Remove favorite': 'Remover dos favoritos',
'Remove favorite (this channel)': 'Remover dos favoritos (este canal)',
'Use emote {name}': 'Usar o emote {name}',
'Select emote {name}': 'Selecionar o emote {name}',
'Use {name}': 'Usar {name}',
'{count} in {name}': '{count} em {name}',
'{shown} of {total} available': '{shown} de {total} disponíveis',
'{count} available': '{count} disponíveis',
', {count} locked by Kick': ', {count} bloqueados pela Kick',
'Created {name}. Select emotes, then move them into it.': '{name} foi criado. Selecione emotes e depois mova-os para ele.',
'Deleted emote group {name}.': 'O grupo de emotes {name} foi excluído.',
'Favorited {name}.': '{name} foi adicionado aos favoritos.',
'Removed {name} from favorites.': '{name} foi removido dos favoritos.',
'Restored {name}.': '{name} foi restaurado.',
'Removed {name}.': '{name} foi removido.',
'Emote group renamed.': 'O grupo de emotes foi renomeado.',
'Group name restored.': 'O nome do grupo foi restaurado.',
'Emote group restored.': 'O grupo de emotes foi restaurado.',
'Favorite order restored.': 'A ordem dos favoritos foi restaurada.',
'Emote group changes restored.': 'As alterações dos grupos de emotes foram restauradas.',
'Removed emotes restored.': 'Os emotes removidos foram restaurados.',
'Favorite change restored.': 'A alteração de favorito foi restaurada.',
'Emote changes reset.': 'As alterações de emotes foram redefinidas.',
'Favorite moved earlier.': 'O favorito foi movido para antes.',
'Favorite moved later.': 'O favorito foi movido para depois.',
'Emote moved earlier.': 'O emote foi movido para antes.',
'Emote moved later.': 'O emote foi movido para depois.',
'emote moved.': 'emote movido.',
'emotes moved.': 'emotes movidos.',
'emote removed.': 'emote removido.',
'emotes removed.': 'emotes removidos.',
'Move selected emotes to group': 'Mover os emotes selecionados para o grupo',
'Emote views': 'Visualizações de emotes',
'Kick rarity, matched by {basis}': 'Raridade da Kick, encontrada por {basis}',
'Insert {name}': 'Inserir {name}',
'Open {name} artwork': 'Abrir a imagem de {name}',
'Copy the name {name}': 'Copiar o nome {name}',
'Type the name {name} into chat': 'Digitar o nome {name} no chat',
'Custom group for {name}': 'Grupo personalizado para {name}',
'Rename {name}': 'Renomear {name}',
'Show {name} again': 'Mostrar {name} novamente',
'Open {name} on Kick': 'Abrir {name} na Kick',
'Remove {name} from the grid': 'Remover {name} da grade',
'Copy a link to board {name}': 'Copiar um link para o painel {name}',
'Delete board {name}': 'Excluir o painel {name}',
'Press keys, or Escape to cancel': 'Pressione as teclas, ou Escape para cancelar',
'{preset} preset applied': 'Predefinição {preset} aplicada',
'Hidden {channel}': '{channel} oculto',
'Showing {channel} again': '{channel} voltou a ser exibido',
'That file is not valid JSON.': 'Esse arquivo não contém JSON válido.',
'Settings must be a JSON object.': 'As configurações devem ser um objeto JSON.',
'Settings schema {schema} is newer than this build supports.': 'O esquema de configurações {schema} é mais recente do que esta versão aceita.',
'The emote library must be a JSON object.': 'A biblioteca de emotes deve ser um objeto JSON.',
'The emote usage counts must be a JSON object.': 'As contagens de uso de emotes devem ser um objeto JSON.',
'The multi-stream boards must be a JSON object.': 'Os layouts de multistream devem ser um objeto JSON.',
'Emote schema {schema} is newer than this build supports.': 'O esquema de emotes {schema} é mais recente do que esta versão aceita.',
'That file does not contain Kick Focus settings.': 'Esse arquivo não contém configurações do Kick Focus.',
'settings': 'configurações',
'emote library': 'biblioteca de emotes',
'emote usage counts': 'contagens de uso de emotes',
'multi-stream boards': 'layouts de multistream',
'per-channel layout': 'layout por canal',
'favorite channels': 'canais favoritos',
'not-interested channels': 'canais sem interesse',
'chat keyword filters': 'filtros de palavras-chave do chat',
'channel notes': 'notas de canais',
'volume and quality memory': 'memória de volume e qualidade',
'blocklist cache': 'cache da lista de bloqueio',
'watched this session': 'assistidos nesta sessão',
'Kick Focus could not save your {list}. Browser storage is full or blocked, so those changes exist only until you reload.': 'O Kick Focus não conseguiu salvar {list}. O armazenamento do navegador está cheio ou bloqueado, então essas alterações existirão apenas até você recarregar.',
'The browser reported': 'O navegador informou',
'Exporting now is the only way to keep these changes.': 'Exportar agora é a única forma de manter essas alterações.',
'{items} read from the page': '{items}, lidos da página',
'{items} from Kick’s API': '{items}, da API da Kick',
'{items} kept in this browser session': '{items}, guardado nesta sessão do navegador',
'{n} showing an older reading.': '{n} com uma leitura anterior.',
'{n} could not be built.': 'Não foi possível criar: {n}.',
'{shortcut} is already used by {action}.': '{shortcut} já é usado por {action}.',
'Added': 'Adicionado',
'Removed': 'Removido',
'Error log copied.': 'Log de erros copiado.',
'Could not copy the error log.': 'Não foi possível copiar o log de erros.',
'Diagnostic summary copied.': 'Resumo de diagnóstico copiado.',
'Clipboard access was unavailable.': 'O acesso à área de transferência não estava disponível.',
'Close settings': 'Fechar configurações',
'Reset page': 'Redefinir página',
'Export settings': 'Exportar configurações',
'Done': 'Concluído',
'Reset settings?': 'Redefinir configurações?',
'Reset all Kick Focus settings?': 'Redefinir todas as configurações do Kick Focus?',
'This restores the defaults for this page.': 'Isso restaura os padrões desta página.',
'Every preference, shortcut, note, filter, and channel list returns to its factory default. Your recorded emote library is kept.': 'Todas as preferências, atalhos, notas, filtros e listas de canais voltam ao padrão de fábrica. Sua biblioteca de emotes registrada é mantida.',
'Only the settings on this page will return to their defaults.': 'Somente as configurações desta página voltarão aos padrões.',
'Cancel': 'Cancelar',
'Reset': 'Redefinir',
'On': 'Ativado',
'Off': 'Desativado',
'Core protection always stays on': 'A proteção principal permanece sempre ativada',
'This page has its own reset control': 'Esta página tem seu próprio controle de redefinição',
'Restore page defaults': 'Restaurar os padrões da página',
'Find a command': 'Buscar um comando',
'Type an action or setting…': 'Digite uma ação ou configuração…',
'Available commands': 'Comandos disponíveis',
'No matching commands': 'Nenhum comando correspondente',
'Try “chat”, “layout”, “casino”, or “settings”.': 'Tente “chat”, “layout”, “cassino” ou “configurações”.',
'command available': 'comando disponível',
'commands available': 'comandos disponíveis',
'Focus': 'Foco',
'Resume': 'Retomar',
'Resume Kick Focus': 'Retomar Kick Focus',
'Kick Focus settings': 'Configurações do Kick Focus',
'Layout': 'Layout',
'Appearance': 'Aparência',
'Content & Ads': 'Conteúdo e anúncios',
'Emotes': 'Emotes',
'Find, favorite, remove, and group every emote you have recorded.': 'Encontre, favorite, remova e agrupe todos os emotes registrados.',
'Accessibility & Shortcuts': 'Acessibilidade e atalhos',
'About': 'Sobre',
'Shell, player, and chat': 'Estrutura, player e chat',
'Theme, color, and scale': 'Tema, cor e escala',
'Privacy, filters, and playback': 'Privacidade, filtros e reprodução',
'Comfort and shortcuts': 'Conforto e atalhos',
'Read-only account signals': 'Sinais da conta somente para leitura',
'Status, privacy, and diagnostics': 'Status, privacidade e diagnósticos',
'Control how Kick is arranged across your desktop.': 'Controle como o Kick é organizado na sua área de trabalho.',
'Keep the page calm, private, and focused on streams.': 'Mantenha a página calma, privada e focada nas transmissões.',
'Improve comfort and keep core actions within reach.': 'Melhore o conforto e mantenha as ações principais ao alcance.',
'A desktop-first layout and control layer for Kick.': 'Uma camada de layout e controle para Kick pensada para desktop.',
'Language': 'Idioma',
'Auto': 'Automático',
'Sidebar mode': 'Modo da barra lateral',
'Left': 'Esquerda',
'Chat layout': 'Layout do chat',
'Chat width': 'Largura do chat',
'Chat width saved.': 'Largura do chat salva.',
'Content density': 'Densidade do conteúdo',
'Stream start behavior': 'Comportamento ao abrir transmissões',
'Remember per-channel layout': 'Lembrar layout por canal',
'Widen browse grids': 'Ampliar grades de descoberta',
'Show Following rail': 'Mostrar barra de Seguindo',
'Show Recommended rail': 'Mostrar barra de Recomendados',
'Hide Kick’s own controls': 'Ocultar os controles do próprio Kick',
'Switch off the player buttons and sidebar entries you never use. Each one is hidden with styling only. Nothing is clicked or removed, and turning it back on restores it immediately.': 'Desative os botões do player e os itens da barra lateral que você nunca usa. Cada um é ocultado apenas por estilo: nada é clicado ou removido, e ao reativar ele volta imediatamente.',
'Player controls': 'Controles do player',
'Sidebar': 'Barra lateral',
'Miniplayer': 'Minirreprodutor',
'Clip': 'Clipe',
'Theater mode': 'Modo cinema',
'Fullscreen': 'Tela cheia',
'Quality menu': 'Menu de qualidade',
'Volume': 'Volume',
'Share': 'Compartilhar',
'Report': 'Denunciar',
'Home link': 'Link de Início',
'Browse link': 'Link de Explorar',
'Following link': 'Link de Seguindo',
'Drops link': 'Link de Drops',
'Followed channel list': 'Lista de canais seguidos',
'Recommended channel list': 'Lista de canais recomendados',
'Sticky compact top bar': 'Barra superior compacta fixa',
'Show quick command button': 'Mostrar botão de comandos',
'Move mini-player clear of controls': 'Mover miniplayer para longe dos controles',
'Recover player after resize': 'Recuperar player após redimensionar',
'Keep ultrawide video uncropped': 'Manter vídeo ultrawide sem corte',
'Theme': 'Tema',
'Choose a clear visual direction, then tune only what matters to you.': 'Escolha uma direção visual clara e ajuste apenas o que importa para você.',
'Quick directions': 'Direções rápidas',
'Apply a viewing setup without changing filters or account choices.': 'Aplique uma configuração de visualização sem mudar filtros ou opções da conta.',
'Direction': 'Direção',
'Each theme changes the full surface hierarchy, not just the page background.': 'Cada tema muda toda a hierarquia de superfícies, não apenas o fundo da página.',
'Layered graphite': 'Grafite em camadas',
'Selected': 'Selecionado',
'Balanced depth with a quiet green undertone.': 'Profundidade equilibrada com um tom verde discreto.',
'True black': 'Preto real',
'Minimal lift and maximum contrast for dark rooms.': 'Elevação mínima e contraste máximo para ambientes escuros.',
'Cool graphite': 'Grafite frio',
'Blue-toned surfaces with stronger separation.': 'Superfícies azuladas com separação mais forte.',
'Use one accent for focus, selection, and live state.': 'Use um só destaque para foco, seleção e estado ao vivo.',
'Low-contrast choices fall back to a safe rose.': 'Opções de baixo contraste voltam para um rosa seguro.',
'Your current theme, accent, scale, and card treatment.': 'Seu tema, destaque, escala e tratamento de cartões atuais.',
'Accent color': 'Cor de destaque',
'Calm': 'Calmo',
'Cinema': 'Cinema',
'Chat First': 'Chat primeiro',
'Discovery': 'Descoberta',
'Roomier cards, quieter live color, and a compact rail.': 'Cartões mais espaçosos, cor ao vivo mais discreta e uma barra compacta.',
'OLED surfaces with the player first and chrome tucked away.': 'Superfícies OLED com o player em primeiro plano e os controles recolhidos.',
'A wider docked chat, compact density, and a clearer accent.': 'Um chat acoplado mais largo, densidade compacta e um destaque mais claro.',
'More stream cards, vivid thumbnails, and both discovery rails.': 'Mais cartões de transmissão, miniaturas vivas e as duas barras de descoberta.',
'Custom': 'Personalizado',
'Custom accent': 'Destaque personalizado',
'Custom accent color': 'Cor de destaque personalizada',
'Contrast protected': 'Contraste protegido',
'{preset} preset applied. Content filters were not changed.': 'A predefinição {preset} foi aplicada. Os filtros de conteúdo não mudaram.',
'Corner radius': 'Raio dos cantos',
'Thumbnail treatment': 'Tratamento das miniaturas',
'Interface scale': 'Escala da interface',
'Dim watched cards': 'Atenuar cartões assistidos',
'Strengthen text contrast': 'Aumentar contraste do texto',
'Colorize live indicators': 'Colorir indicadores ao vivo',
'Ad defense active': 'Defesa contra anúncios ativa',
'Network + page': 'Rede + página',
'Page only': 'Somente página',
'Local channel tools': 'Ferramentas locais do canal',
'Clear favorites': 'Limpar favoritos',
'Clear not-interested': 'Limpar não tenho interesse',
'Protection log': 'Registro de proteção',
'Reduce motion': 'Reduzir movimento',
'High-contrast controls': 'Controles de alto contraste',
'Always show keyboard focus': 'Sempre mostrar foco do teclado',
'Larger pointer targets': 'Alvos de ponteiro maiores',
'Announce layout changes': 'Anunciar mudanças de layout',
'Text size': 'Tamanho do texto',
'Caption background opacity': 'Opacidade do fundo das legendas',
'Search': 'Pesquisar',
'Every page, searched at once.': 'Todas as páginas, pesquisadas de uma vez.',
'Try a shorter word, or the name of the Kick control you are looking for.': 'Tente uma palavra mais curta ou o nome do controle da Kick que procura.',
'Nothing matches “{query}”.': 'Nada corresponde a “{query}”.',
'Search settings': 'Pesquisar configurações',
'Kick Focus updated to {version}.': 'O Kick Focus foi atualizado para {version}.',
'Changed defaults: {list}.': 'Padrões que mudaram: {list}.',
'What changed': 'O que mudou',
'Keyboard shortcuts': 'Atalhos de teclado',
'Restore defaults': 'Restaurar padrões',
'Action': 'Ação',
'Current shortcut': 'Atalho atual',
'Status': 'Status',
'Change': 'Alterar',
'Script health': 'Saúde do script',
'Site compatibility': 'Compatibilidade do site',
'Protection layer': 'Camada de proteção',
'Data & privacy': 'Dados e privacidade',
'Diagnostics': 'Diagnósticos',
'Copy diagnostic summary': 'Copiar resumo de diagnóstico',
'Run self-check': 'Executar autoteste',
'Compatibility self-test': 'Autoteste de compatibilidade',
'Settings portability': 'Portabilidade das configurações',
'Import settings': 'Importar configurações',
'Reset all settings': 'Redefinir todas as configurações',
'Panic switch': 'Interruptor de emergência',
'Pause Kick Focus': 'Pausar Kick Focus',
'Restore Kick Focus': 'Restaurar Kick Focus',
'Temporarily remove enhanced layout and request hooks': 'Remove temporariamente o layout aprimorado e os interceptadores',
'Enter focus mode': 'Entrar no modo foco',
'Exit focus mode': 'Sair do modo foco',
'Maximize the stream and hide side panels': 'Maximiza a transmissão e oculta os painéis laterais',
'Enter theater mode': 'Entrar no modo teatro',
'Exit theater mode': 'Sair do modo teatro',
'Hide discovery while keeping chat': 'Oculta a descoberta mantendo o chat',
'Show chat': 'Mostrar chat',
'Hide chat': 'Ocultar chat',
'Toggle the chat panel for this session': 'Alterna o painel de chat nesta sessão',
'Show sidebar': 'Mostrar barra lateral',
'Hide sidebar': 'Ocultar barra lateral',
'Toggle the discovery rail for this session': 'Alterna a barra de descoberta nesta sessão',
'Blur mature thumbnails': 'Desfocar miniaturas maduras',
'Reveal mature thumbnails': 'Revelar miniaturas maduras',
'Temporarily override mature-card blur': 'Substitui temporariamente o desfoque de cartões maduros',
'Change discovery spacing and save it': 'Altera e salva o espaçamento da descoberta',
'New favorites apply to': 'Novos favoritos se aplicam a',
'Global favorites follow you everywhere. Per-channel favorites appear only on the channel you saved them from, above your global ones. Existing favorites are global and are not moved.': 'Os favoritos globais acompanham você em todos os lugares. Os favoritos por canal aparecem apenas no canal onde foram salvos, acima dos globais. Os favoritos existentes são globais e não são movidos.',
'Everywhere': 'Em todos os lugares',
'This channel': 'Este canal',
'Show badges Kick leaves out': 'Mostrar selos que o Kick omite',
'Kick’s chat payload carries collectible and global badges its own markup omits, leaving a gap where other clients show a badge. A badge image that fails to load is replaced by its name.': 'A resposta do chat do Kick traz selos globais e de colecionáveis que a própria marcação omite, deixando uma lacuna onde outros clientes mostram um selo. Se a imagem não carregar, ela é substituída pelo nome.',
'Hidden channels': 'Canais ocultos',
'Hide specific channels from Home, Browse, Following, and Search.': 'Oculte canais específicos de Início, Explorar, Seguindo e Busca.',
'No channels hidden. Use the input above or the ✕ action on a card.': 'Nenhum canal oculto. Use o campo acima ou a ação ✕ em um card.',
'Show casino content': 'Mostrar conteúdo de cassino',
'Hide casino content': 'Ocultar conteúdo de cassino',
'Filter clearly labeled casino streams': 'Filtra transmissões claramente marcadas como cassino',
'Open Kick Focus settings': 'Abrir configurações do Kick Focus',
'Customize layout, appearance, content, and access': 'Personalize layout, aparência, conteúdo e acesso',
'Choose how the left discovery rail behaves. Dropdown collapses it to a tab that expands on hover, giving the grid full width. Desktop widths only.': 'Escolha como a barra lateral de descoberta se comporta. Suspensa reduz a barra a uma aba que se expande ao passar o cursor, dando à grade toda a largura. Apenas em larguras de desktop.',
'Place chat on either side, float it as a dock, or hide it.': 'Coloque o chat em qualquer lado, como painel flutuante, ou oculte-o.',
'Set the width of the live chat column.': 'Defina a largura da coluna do chat ao vivo.',
'Adjust spacing and padding across discovery pages.': 'Ajuste o espaçamento e o preenchimento nas páginas de descoberta.',
'Choose how each channel opens.': 'Escolha como cada canal abre.',
'Keep the last runtime layout for each channel.': 'Mantenha o último layout usado em cada canal.',
'Use reclaimed sidebar space for larger, calmer stream cards.': 'Use o espaço recuperado da barra lateral para cartões de transmissão maiores e mais calmos.',
'Keep the Following discovery rail visible when Kick provides it.': 'Mantenha a barra de canais seguidos visível quando o Kick a fornecer.',
'Keep recommended stream rows visible in the main content.': 'Mantenha as linhas de transmissões recomendadas visíveis no conteúdo principal.',
'Keep search and account controls available while browsing.': 'Mantenha a busca e os controles de conta disponíveis enquanto navega.',
'Keep the Focus control beside Get KICKs in Kick’s top header.': 'Mantenha o controle do Focus ao lado de Get KICKs no cabeçalho superior do Kick.',
'Raise Kick’s embedded mini-player only when the Focus control has to use its floating fallback.': 'Eleve o mini-player integrado do Kick apenas quando o controle do Focus precisar usar sua alternativa flutuante.',
'Re-apply player geometry after a window or monitor change.': 'Reaplique a geometria do player após uma mudança de janela ou monitor.',
'Prefer contained video geometry on wide or moved displays.': 'Prefira uma geometria de vídeo contida em telas largas ou deslocadas.',
'Adjust the roundness of enhanced UI.': 'Ajuste o arredondamento da interface aprimorada.',
'Adjust stream-card color intensity.': 'Ajuste a intensidade de cor dos cartões de transmissão.',
'Set the size of Kick Focus controls.': 'Defina o tamanho dos controles do Kick Focus.',
'Reduce emphasis on streams you have already opened.': 'Reduza o destaque das transmissões que você já abriu.',
'Increase legibility on muted surfaces.': 'Aumente a legibilidade em superfícies atenuadas.',
'Use the selected accent for live-state emphasis.': 'Use a cor de destaque escolhida para enfatizar o estado ao vivo.',
'Enable subscription': 'Ativar assinatura',
'Off by default. When enabled, refreshes only over HTTPS with credentials omitted.': 'Desativado por padrão. Quando ativado, atualiza apenas por HTTPS e sem enviar credenciais.',
'Refresh interval': 'Intervalo de atualização',
'Keep the last valid payload if a later request fails.': 'Mantenha o último conteúdo válido se uma requisição posterior falhar.',
'Load the emote catalog from Kick': 'Carregar o catálogo de emotes do Kick',
'Read the full channel, global, and emoji sets without treating public artwork as account access. Falls back to the picker if the response changes shape.': 'Lê os conjuntos completos do canal, globais e de emojis sem tratar imagens públicas como acesso da conta. Volta ao seletor se a resposta mudar de formato.',
'Follow live chat events': 'Acompanhar os eventos do chat ao vivo',
'Subscribe to the same realtime chat feed Kick’s own client uses. The provider is read from Kick rather than hardcoded.': 'Assine o mesmo fluxo de chat em tempo real que o cliente do Kick usa. O provedor é lido do Kick em vez de estar fixo no código.',
'Explain removed messages': 'Explicar as mensagens removidas',
'Kick’s automatic moderation removes messages without saying why. The realtime event carries the reason; the page does not.': 'A moderação automática do Kick remove mensagens sem dizer por quê. O evento em tempo real carrega o motivo; a página não.',
'Count emote usage': 'Contar o uso de emotes',
'Kick’s own “Frequently Used” never counts anything, so no real ranking exists. This one is yours, stored locally and exported with your library.': 'A seção “Usados com frequência” do Kick nunca conta nada, então não existe uma classificação real. Esta é sua, guardada localmente e exportada com sua biblioteca.',
'Show collectible rarity': 'Mostrar a raridade dos colecionáveis',
'Kick publishes rarity on card art and identity in the picker, with no key joining them. Rarity is shown only where the match is confident.': 'O Kick publica a raridade na arte da carta e a identidade no seletor, sem nenhuma chave que as una. A raridade só aparece quando a correspondência é confiável.',
'Warn about shadowed emote names': 'Avisar sobre nomes de emote duplicados',
'Subscriber emotes work in every chat and Kick resolves typed names through one map, so two channels sharing a name means one silently sends the other’s.': 'Os emotes de assinante funcionam em todos os chats e o Kick resolve os nomes digitados por um único mapa, então se dois canais compartilham um nome, um envia silenciosamente o do outro.',
'Freeze animated emotes': 'Congelar os emotes animados',
'Render animated emotes and collectibles as a single static frame, in chat and in the picker. Applied automatically when your system asks for reduced motion.': 'Exibe os emotes animados e os colecionáveis como um único quadro estático, no chat e no seletor. Aplicado automaticamente quando seu sistema pede movimento reduzido.',
'Block separable ad requests': 'Bloquear as requisições de anúncios separáveis',
'Intercept known ad hosts at the earliest userscript-supported page layer.': 'Intercepta os servidores de anúncios conhecidos na camada mais inicial que um userscript permite.',
'Remove ad containers': 'Remover os contêineres de anúncios',
'Remove empty ad containers and reinjected ad frames.': 'Remove os contêineres de anúncios vazios e os quadros reinseridos.',
'Suppress sponsored and promoted cards': 'Ocultar os cartões patrocinados e promovidos',
'Hide clearly labeled promotional cards and modules.': 'Oculta os cartões e módulos claramente marcados como promocionais.',
'Pause home-page autoplay': 'Pausar a reprodução automática da página inicial',
'Keep background Home previews silent and paused; deliberate playback remains available.': 'Mantém as prévias de fundo da página inicial silenciadas e pausadas; a reprodução deliberada continua disponível.',
'Hide Slots & Casino content': 'Ocultar conteúdo de Slots e Cassino',
'Hide cards and sidebar entries clearly labeled as casino content.': 'Oculta os cartões e as entradas da barra lateral marcados claramente como conteúdo de cassino.',
'Blur marked mature cards until hover or keyboard focus.': 'Desfoca os cartões marcados como adultos até passar o cursor ou focar pelo teclado.',
'Hide Drops and gambling promotions': 'Ocultar promoções de Drops e apostas',
'Hide clearly labeled Drops and gambling promotion modules.': 'Oculta os módulos claramente marcados como promoções de Drops e apostas.',
'Poor mode': 'Modo sem gastos',
'Show how long the stream has been live': 'Mostrar há quanto tempo a transmissão está ao vivo',
'Kick sends the start time with every channel and shows it nowhere. This reads that field and counts from it in the player corner, with no extra request and no polling.': 'O Kick envia o horário de início com cada canal e não o mostra em lugar nenhum. Isto lê esse campo e conta a partir dele no canto do player, sem requisições extras e sem sondagem.',
'Show stream uptime': 'Mostrar tempo ao vivo',
'Pop out chat': 'Chat em janela flutuante',
'Channel points: Kick says Picture-in-Picture and mirrored viewing do not accrue points. Keep a normal Kick player open when progress matters.': 'Pontos do canal: o Kick informa que Picture-in-Picture e visualização espelhada não acumulam pontos. Mantenha um player normal do Kick aberto quando o progresso for importante.',
'Merge all chats': 'Juntar todos os chats',
'Merged chat from every channel in the grid': 'Chat unificado de todos os canais da grelha',
'One chat per tile': 'Um chat por canal',
'Read-only. Every channel in the grid, in the order messages arrived.': 'Apenas leitura. Todos os canais da grelha, na ordem em que as mensagens chegaram.',
'Showing one merged chat for every channel in the grid': 'A mostrar um chat unificado de todos os canais da grelha',
'Showing the focused channel chat': 'A mostrar o chat do canal em foco',
'Return chat': 'Devolver o chat',
'Kick Focus could not open the pop-out chat window.': 'A Kick Focus não conseguiu abrir a janela flutuante do chat.',
'Chat for {channel} opened in a floating window': 'O chat de {channel} abriu numa janela flutuante',
'Show VOD expiry': 'Mostrar validade do vídeo',
'Show how long Kick keeps this recording': 'Mostrar por quanto tempo a Kick guarda esta gravação',
'Kick deletes recordings after 7 days, or 30 for a verified channel, and shows that deadline nowhere. On a VOD page this reads the recording date from Kick’s own video list and counts down to it. It says nothing at all when the recording is older than the list Kick returns, or when the tier cannot be established. A guess between 7 and 30 days would be a confident wrong date.': 'A Kick apaga as gravações ao fim de 7 dias, ou 30 num canal verificado, e não mostra esse prazo em lado nenhum. Na página de um vídeo, isto lê a data da gravação da própria lista de vídeos da Kick e faz a contagem decrescente até lá. Não diz nada quando a gravação é mais antiga do que a lista que a Kick devolve, ou quando o nível não pode ser estabelecido. Adivinhar entre 7 e 30 dias seria dar uma data errada com toda a confiança.',
'{time} before Kick deletes this recording': '{time} antes de a Kick apagar esta gravação',
'Live for {duration}': 'Ao vivo há {duration}',
'{count} emotes usable in any chat': '{count} emotes utilizáveis em qualquer chat',
'subscribed channel': 'canal assinado',
'subscribed channels': 'canais assinados',
'your global sets': 'seus conjuntos globais',
'Kick reports no emotes this account can send anywhere.': 'O Kick não indica nenhum emote que esta conta possa enviar em qualquer chat.',
'My emotes': 'Meus emotes',
'My emotes ({count})': 'Meus emotes ({count})',
'Subscribed channel': 'Canal assinado',
'Global collection': 'Coleção global',
'Kick reports no emotes this account can use in every chat.': 'O Kick não indica nenhum emote que esta conta possa usar em todos os chats.',
'Sign in to Kick and open any channel once to load your owned emotes. Nothing is sent or changed.': 'Entre no Kick e abra qualquer canal uma vez para carregar seus emotes. Nada é enviado ou alterado.',
'Works in every chat': 'Funciona em todos os chats',
'Only works in its own channel': 'Só funciona no próprio canal',
'Only works in {channel}’s chat': 'Só funciona no chat de {channel}',
'Hide Subscribe, Gift Subs/Dubs, Get KICKs, gift-shop controls, and spend-based leaderboards. Follow, chat, and free daily rewards stay available.': 'Oculta Inscrever-se, Presentear subs/dubs, Obter KICKs, a loja de presentes e os placares de gastos. Seguir, o chat e as recompensas diárias gratuitas continuam disponíveis.',
'Enable Poor mode': 'Ativar modo sem gastos',
'Disable Poor mode': 'Desativar modo sem gastos',
'Remove spending prompts without changing your Kick account': 'Oculta os convites de gasto sem alterar sua conta do Kick',
'Channel emote catalog': 'Catálogo de emotes do canal',
'Loading…': 'Carregando…',
'Load emotes': 'Carregar emotes',
'Channel-only': 'Somente no canal',
'Subscriber-only': 'Somente para assinantes',
'Reduce tracking telemetry': 'Reduzir a telemetria de rastreamento',
'Block observed third-party video and error telemetry hosts.': 'Bloqueia os servidores de telemetria de vídeo e de erros de terceiros detectados.',
'Remember volume locally': 'Lembrar o volume localmente',
'Restore each channel’s volume and mute state from local storage.': 'Restaura o volume e o estado de mudo de cada canal a partir do armazenamento local.',
'Remember quality locally': 'Lembrar a qualidade localmente',
'Restore a matching quality control when Kick exposes one.': 'Restaura o controle de qualidade correspondente quando o Kick o oferece.',
'Always start at the highest quality': 'Sempre começar na qualidade mais alta',
'Open every stream at the best rung Kick offers, taking precedence over remembered quality. The rungs are learned from Kick’s own quality menu, so this does nothing until that menu has been opened once. It will not open it for you.': 'Abre cada transmissão na melhor opção que o Kick oferecer, com prioridade sobre a qualidade lembrada. As opções são aprendidas do próprio menu de qualidade do Kick, então isso não faz nada até que esse menu seja aberto uma vez: ele não será aberto para você.',
'Remember VOD position locally': 'Lembrar a posição do VOD localmente',
'Resume finite VODs from the last local playback position.': 'Retoma os VODs finitos a partir da última posição de reprodução local.',
'Pause chat updates': 'Pausar as atualizações do chat',
'Scrolling the transcript up freezes it, as does the button. Resume is always one control away.': 'Rolar a transcrição para cima congela o chat, assim como o botão. Retomar está sempre a um controle de distância.',
'Chat updates paused': 'Atualizações do chat pausadas',
'Userscript size': 'Tamanho do userscript',
'Injection ceiling': 'Limite de injeção',
'Help': 'Ajuda',
'Open help and recovery': 'Abrir ajuda e recuperação',
'Chat updates resumed': 'Atualizações do chat retomadas',
'Organize chat emotes': 'Organizar os emotes do chat',
'Continuously record emotes from live chat and Kick’s picker, then add favorites, removals, search, and custom groups.': 'Registra continuamente os emotes do chat ao vivo e do seletor do Kick, e adiciona favoritos, remoções, busca e grupos personalizados.',
'Click chat emotes to save': 'Clique nos emotes do chat para salvar',
'Click any emote in chat to add it to your favorites. If Kick explicitly marks it as follow-gated, the same click follows its source channel; subscriber access is never bypassed.': 'Clique em qualquer emote do chat para adicioná-lo aos favoritos. Se o Kick o marcar explicitamente como restrito a seguidores, o mesmo clique segue o canal de origem; o acesso de assinante nunca é contornado.',
'Highlight chat keywords': 'Destacar palavras-chave do chat',
'Use the per-channel keyword list below without sending it anywhere.': 'Usa a lista de palavras-chave por canal abaixo sem enviá-la a lugar nenhum.',
'Show playback diagnostics': 'Mostrar diagnósticos de reprodução',
'Show ready state, buffered seconds, and dropped-frame counts on a channel.': 'Mostra o estado de prontidão, os segundos em buffer e os quadros perdidos em um canal.',
'Start playback without waiting for blocked ad scripts': 'Iniciar a reprodução sem esperar pelos scripts de anúncios bloqueados',
'Kick waits on Google PAL, Datazoom, and OM before requesting playback. Blocking them, which this build does, leaves the dead script in the page and the player waits out the full timeout. Removing it lets playback start immediately.': 'O Kick espera por Google PAL, Datazoom e OM antes de solicitar a reprodução. Bloqueá-los, o que esta versão faz, deixa o script morto na página e o player aguarda todo o tempo limite. Removê-lo faz a reprodução começar imediatamente.',
'Minimize non-essential animations and transitions.': 'Minimiza as animações e transições não essenciais.',
'Increase separation for controls, borders, and surfaces.': 'Aumenta a separação de controles, bordas e superfícies.',
'Keep a strong outline for keyboard navigation.': 'Mantém um contorno forte para a navegação por teclado.',
'Increase the minimum height of interactive controls.': 'Aumenta a altura mínima dos controles interativos.',
'Report view changes to assistive technology.': 'Informa as mudanças de exibição às tecnologias assistivas.',
'Scale text in the main Kick content area.': 'Dimensiona o texto na área de conteúdo principal do Kick.',
'Set the preferred caption background strength.': 'Define a intensidade preferida do fundo das legendas.',
'Multi-stream opened': 'Multitransmissão aberta',
'Your own multi-stream grid is back.': 'A sua própria grade de multitransmissão voltou.',
'Watch several Kick channels in one grid': 'Assista a vários canais do Kick em uma única grade',
'Read-only here. Kick blocks sending from an embedded chat; open the channel to talk.': 'Somente leitura aqui. O Kick impede o envio a partir de um chat incorporado; abra o canal para falar.',
'Emote favorites, removals, and custom groups reset.': 'Favoritos, remoções e grupos personalizados de emotes redefinidos.',
'The emote could not be saved. The error log on the Content & Ads page says why.': 'Não foi possível salvar o emote. O registro de erros da página Conteúdo e anúncios diz por quê.',
'Open a channel page first.': 'Abra primeiro a página de um canal.',
'Local channel tools saved.': 'Ferramentas locais do canal salvas.',
'Local channel tools cleared.': 'Ferramentas locais do canal limpas.',
'Enter a custom emote group name.': 'Digite um nome para o grupo personalizado de emotes.',
'The emote group limit is 40.': 'O limite de grupos de emotes é 40.',
'That emote group already exists.': 'Esse grupo de emotes já existe.',
'Enter a valid emote group name.': 'Digite um nome de grupo de emotes válido.',
'Board saved.': 'Painel salvo.',
'That board has no usable channels.': 'Esse painel não tem canais utilizáveis.',
'Channel name or kick.com link': 'Nome do canal ou link do kick.com',
'Multi-stream controls': 'Controles de multistream',
'Name this board': 'Dê um nome a este painel',
'Board name': 'Nome do painel',
'Ready for your first channel': 'Pronto para o primeiro canal',
'Multi-stream workspace': 'Área de trabalho multistream',
'Build your viewing board': 'Monte seu painel de visualização',
'Add a channel above to start. Focus decides which stream owns audio and chat, and your saved boards stay on this device.': 'Adicione um canal acima para começar. O foco decide qual transmissão controla o áudio e o chat, e seus painéis salvos ficam neste dispositivo.',
'Add your first channel': 'Adicione seu primeiro canal',
'Add channel': 'Adicionar canal',
'Save board': 'Salvar painel',
'Saved boards will appear here.': 'Os painéis salvos aparecerão aqui.',
'Could not reach the clipboard. Check the clipboard permission for kick.com.': 'Não foi possível acessar a área de transferência. Verifique a permissão da área de transferência para kick.com.',
'Cached blocklist removed.': 'Lista de bloqueio em cache removida.',
'Enter a channel name or URL.': 'Digite um nome de canal ou uma URL.',
'That does not look like a Kick channel.': 'Isso não parece um canal do Kick.',
'That channel is already hidden.': 'Esse canal já está oculto.',
'Hidden channel list is full (200). Remove a channel before hiding another.': 'A lista de canais ocultos está cheia (200). Remova um canal antes de ocultar outro.',
'Favorites cleared.': 'Favoritos limpos.',
'Not-interested channels restored.': 'Canais marcados como sem interesse restaurados.',
'Could not export settings. Check that your browser allows downloads from kick.com.': 'Não foi possível exportar as configurações. Verifique se o navegador permite downloads de kick.com.',
'Could not read that settings file. Pick a JSON file exported by Kick Focus.': 'Não foi possível ler esse arquivo de configurações. Escolha um arquivo JSON exportado pelo Kick Focus.',
'That backup is too large for this browser’s storage. Nothing was changed.': 'Esse backup é grande demais para o armazenamento deste navegador. Nada foi alterado.',
'The import could not be saved. Your previous settings are unchanged.': 'Não foi possível salvar a importação. Suas configurações anteriores não foram alteradas.',
'Settings imported.': 'Configurações importadas.',
'Previous settings backed up. Use Undo import to restore them.': 'Foi feito backup das configurações anteriores. Use Desfazer importação para restaurá-las.',
' and {count} more': ' e mais {count}',
'{count} more': 'mais {count}',
'Ignored unknown section "{key}".': 'A seção desconhecida "{key}" foi ignorada.',
'Ignored unknown setting "{path}".': 'A configuração desconhecida "{path}" foi ignorada.',
'Adjusted "{path}" to a supported value.': '"{path}" foi ajustada para um valor compatível.',
'Upgraded from an unversioned file to schema {schema}.': 'Um arquivo sem versão foi atualizado para o esquema {schema}.',
'Upgraded from schema {from} to schema {to}.': 'O esquema {from} foi atualizado para o esquema {to}.',
'{count} emote could not be kept: {sample}{more}.': 'Não foi possível manter {count} emote: {sample}{more}.',
'{count} emotes could not be kept: {sample}{more}.': 'Não foi possível manter {count} emotes: {sample}{more}.',
'Adjusted emote {field} to supported entries.': 'O campo de emotes {field} foi ajustado para entradas compatíveis.',
'Upgraded emotes to schema {schema}.': 'Os emotes foram atualizados para o esquema {schema}.',
'Adjusted emote usage counts to {count} supported entries.': 'As contagens de uso de emotes foram ajustadas para {count} entradas compatíveis.',
'Adjusted the multi-stream grid to {count} supported channels.': 'A grade multistream foi ajustada para {count} canais compatíveis.',
'Adjusted saved boards to {count} supported entries.': 'Os layouts salvos foram ajustados para {count} entradas compatíveis.',
'Density saved': 'Densidade salva',
'Content filter saved': 'Filtro de conteúdo salvo',
'Poor mode saved': 'Modo sem gastos salvo',
'No import to undo.': 'Não há importação para desfazer.',
'The backup could not be restored. Your current settings are unchanged.': 'Não foi possível restaurar o backup. Suas configurações atuais não foram alteradas.',
'Import undone. Your previous settings are back.': 'Importação desfeita: suas configurações anteriores voltaram.',
'Kick Focus restored.': 'Kick Focus restaurado.',
'Kick Focus paused. Use the Resume button or Ctrl+Shift+F to restore.': 'Kick Focus pausado. Use o botão Retomar ou Ctrl+Shift+F para restaurar.',
'Settings reset': 'Configurações redefinidas',
'Kick Focus restored': 'Kick Focus restaurado',
'Give this stream the audio and chat': 'Dar a esta transmissão o áudio e o chat',
'Remove': 'Remover',
'Clear search': 'Limpar a busca',
'Campaign status': 'Status da campanha',
'No open campaigns': 'Nenhuma campanha aberta',
'Campaigns appear here when a reward is available.': 'As campanhas aparecem aqui quando há uma recompensa disponível.',
'Browse eligible streams': 'Explorar transmissões elegíveis',
'View coming soon': 'Ver próximas campanhas',
'Reward activity': 'Atividade de recompensas',
'Active': 'Ativas',
'Claimed': 'Resgatadas',
'Expired': 'Expiradas',
'How drops work': 'Como os drops funcionam',
'Watch eligible streams': 'Assistir a transmissões elegíveis',
'Pick an active campaign.': 'Escolha uma campanha ativa.',
'Track progress': 'Acompanhar o progresso',
'Keep watching to advance.': 'Continue assistindo para avançar.',
'Claim your reward': 'Resgatar sua recompensa',
'Claim it before time runs out.': 'Resgate antes que o tempo acabe.',
'Kick Focus command menu': 'Menu de comandos do Kick Focus',
'Kick Focus multi-stream': 'Multitransmissão do Kick Focus',
'Which chat to show': 'Qual chat mostrar',
'Live style preview': 'Prévia do estilo ao vivo',
'release, giveaway, raid': 'lançamento, sorteio, raid',
'Chat keywords for this channel': 'Palavras-chave do chat para este canal',
'Why I follow this channel…': 'Por que eu sigo este canal…',
'Private channel note': 'Nota privada do canal',
'Optional blocklist URL': 'URL de lista de bloqueio opcional',
'Search emotes or Kick groups': 'Buscar emotes ou grupos do Kick',
'Search recorded emotes': 'Buscar emotes registrados',
'Filter recorded emotes': 'Filtrar emotes registrados',
'Group name': 'Nome do grupo',
'Custom emote groups': 'Grupos personalizados de emotes',
'Group for selected emotes': 'Grupo para os emotes selecionados',
'Select {name}': 'Selecionar {name}',
'Deselect {name}': 'Desmarcar {name}',
'Delete group {name}': 'Excluir grupo {name}',
'New emote group name': 'Nome do novo grupo de emotes',
'Channel name or kick.com URL': 'Nome do canal ou URL do kick.com',
'Channel to hide': 'Canal a ocultar',
'Open Kick Focus multi-stream': 'Abrir a multitransmissão do Kick Focus',
'Multi-stream': 'Multitransmissão',
'Stats': 'Estatísticas',
'Open {channel} stats in StreamerStats': 'Abrir as estatísticas de {channel} no StreamerStats',
'Opened {channel} in StreamerStats.': '{channel} foi aberto no StreamerStats.',
'The browser blocked the stats popup.': 'O navegador bloqueou a janela de estatísticas.',
'Open tab': 'Abrir aba',
'Add this channel to Kick Focus multi-stream': 'Adicionar este canal à multitransmissão do Kick Focus',
'Add to multi-stream': 'Adicionar à multitransmissão',
'Undo': 'Desfazer',
'View': 'Ver',
'Enable optional blocklist subscription': 'Ativar a assinatura opcional da lista de bloqueio',
'Core ad protection is on': 'A proteção principal contra anúncios está ativada',
'Suppress promoted cards': 'Ocultar os cards promovidos',
'Hide Slots and Casino content': 'Ocultar o conteúdo de Slots e Cassino',
'Your inventory holds {copies} {copiesWord} across {distinct} distinct {distinctWord}. That is {duplicates} {duplicatesWord}, or {rate}% of what you have pulled.': 'Seu inventário tem {copies} {copiesWord} distribuídos em {distinct} {distinctWord} distintos: {duplicates} {duplicatesWord}, ou seja, {rate}% do que você já obteve.',
'Your inventory holds {distinct} distinct {distinctWord}. Kick’s response carries no per-item quantity, so a duplicate rate cannot be measured from it. That number is unavailable rather than zero.': 'Seu inventário tem {distinct} {distinctWord} distintos. A resposta do Kick não traz a quantidade por item, então não é possível medir uma taxa de duplicatas a partir dela: esse número está indisponível, não é zero.',
'emote name shadowed.': 'nome de emote duplicado.',
'emote names shadowed.': 'nomes de emote duplicados.',
'result loaded': 'resultado carregado',
'results loaded': 'resultados carregados',
'emote': 'emote',
'emotes': 'emotes',
'collectible': 'colecionável',
'collectibles': 'colecionáveis',
'item': 'item',
'items': 'itens',
'duplicate': 'duplicata',
'duplicates': 'duplicatas',
'recorded emote has been changed by Kick since first capture. See the Changed by Kick filter in the library below.': 'emote registrado foi alterado pelo Kick desde a primeira captura: veja o filtro Alterados pelo Kick na biblioteca abaixo.',
'recorded emotes have been changed by Kick since first capture. See the Changed by Kick filter in the library below.': 'emotes registrados foram alterados pelo Kick desde a primeira captura: veja o filtro Alterados pelo Kick na biblioteca abaixo.',
'channel hidden. These count toward the fail-open ceiling.': 'canal oculto. Ele conta para o limite de segurança.',
'channels hidden. These count toward the fail-open ceiling.': 'canais ocultos. Eles contam para o limite de segurança.',
'channel': 'canal',
'channels': 'canais',
'minute': 'minuto',
'minutes': 'minutos',
'time': 'vez',
'times': 'vezes',
'Claim the daily reward automatically': 'Reivindicar a recompensa diária automaticamente',
'Opens Kick’s own reward dialog when one is waiting and clicks its claim button for you. It clicks nothing else: a reward Kick has not unlocked yet shows a disabled button, and this leaves it alone rather than trying. It waits until you are not typing, checks at most every ten minutes, and stops for the day once it claims. Signed-in only, because the reward button does not exist otherwise.': 'Abre o próprio diálogo de recompensa da Kick quando há uma à espera e clica no botão de reivindicar por você. Não clica em mais nada: uma recompensa que a Kick ainda não liberou mostra um botão desativado, e isto deixa-o em paz em vez de tentar. Espera até você não estar digitando, verifica no máximo a cada dez minutos e para por hoje assim que reivindica. Apenas com sessão iniciada, porque o botão de recompensa não existe de outra forma.',
'Daily reward claimed. It is in your collectibles.': 'Recompensa diária reivindicada. Está nos seus colecionáveis.',
'Daily reward claimed.': 'Recompensa diária reivindicada.',
'Add open tabs ({count})': 'Adicionar abas abertas ({count})',
'Added {count} from your other tabs ({total} of {max})': 'Foram adicionados {count} das suas outras abas: {total} de {max}',
'Added {count} channels from your other tabs.': 'Foram adicionados {count} canais das suas outras abas.',
'Apply cycle cost': 'Custo do ciclo de aplicação',
'No apply cycle has run yet.': 'Nenhum ciclo de aplicação foi executado ainda.',
'Type an emote name into chat': 'Digitar o nome de um emote no chat',
'Suggest emotes as you type': 'Sugerir emotes enquanto você digita',
'Typing a colon and two or more letters in chat offers matching emotes from your library, ranked by what you actually send here. Click one to put its plain name at your cursor. Suggestions are clicked, never accepted with a key, so nothing you type is ever captured, and it never sends the message.': 'Digitar dois-pontos e duas ou mais letras no chat oferece emotes da sua biblioteca, ordenados pelo que você realmente envia aqui. Clique em um para colocar o nome simples no seu cursor. As sugestões são escolhidas com o mouse, nunca aceitas com uma tecla, então nada do que você digita é capturado, e nunca envia a mensagem.',
'Adds a Type in chat action beside Copy name in the emote library. It types the plain name at your cursor and stops. Never the wire token, never an id, and it never sends the message.': 'Adiciona uma ação Digitar no chat ao lado de Copiar nome na biblioteca de emotes. Digita apenas o nome na posição do cursor e para por aí: nunca o código interno, nunca um id, e nunca envia a mensagem.',
'That emote has no plain name to copy.': 'Esse emote não tem um nome simples para copiar.',
'That emote has no plain name to type.': 'Esse emote não tem um nome simples para digitar.',
'Open a channel chat first.': 'Abra primeiro o chat de um canal.',
'Kick’s chat box did not accept the text. The name is on your clipboard instead.': 'A caixa de chat do Kick não aceitou o texto. O nome está na sua área de transferência.',
'Seen available': 'Visto como disponível',
'Seen in chat': 'Visto no chat',
'Click to save': 'Clique para salvar',
'Saved. Click to open in the library': 'Salvo: clique para abrir na biblioteca',
'Name shadowed by another set': 'Nome ofuscado por outro conjunto',
'{count} of {max} streams': '{count} de {max} transmissões',
},
};
function activeLocale() {
const preference = state.settings.appearance.language;
if (preference !== 'auto') return preference;
const language = typeof navigator === 'undefined' ? '' : String(navigator.language || '').toLowerCase();
if (language.startsWith('es')) return 'es';
if (language.startsWith('pt')) return 'pt';
return 'en';
}
const TEXT_SOURCE = new WeakMap();
const ATTRIBUTE_SOURCE = new WeakMap();
function tr(value) {
const source = String(value);
return TRANSLATIONS[activeLocale()]?.[source] || source;
}
function plural(count, one, other) {
return tr(pluralForm(count, { one, other }, activeLocale()));
}
function trf(template, values) {
return tr(template).replace(/\{(\w+)\}/g, (whole, key) => (
Object.prototype.hasOwnProperty.call(values, key) ? String(values[key]) : whole));
}
function applyInterfaceLanguage() {
const locale = activeLocale();
for (const id of ['kick-focus-root', 'kick-focus-emote-complete', 'kick-focus-emote-tooltip', 'kick-focus-header-control', 'kick-focus-streamer-stats', 'kick-focus-following-preview']) {
const host = document.getElementById(id);
if (host && host.lang !== locale) host.lang = locale;
}
return locale;
}
function localizeInterface(root = state.shadow) {
applyInterfaceLanguage();
if (!root) return;
const walk = (node) => {
if (node.nodeType === 3) {
const recorded = TEXT_SOURCE.get(node);
const text = recorded === undefined ? node.nodeValue : recorded;
const trimmed = text.trim();
if (!trimmed || node.parentElement?.matches?.('input, textarea')) return;
if (recorded === undefined) TEXT_SOURCE.set(node, text);
const start = text.indexOf(trimmed);
node.nodeValue = `${text.slice(0, start)}${tr(trimmed)}${text.slice(start + trimmed.length)}`;
return;
}
if (node.nodeType !== 1 && node.nodeType !== 11) return;
if (node.nodeType === 1) {
let sources = ATTRIBUTE_SOURCE.get(node);
for (const attribute of ['aria-label', 'placeholder', 'title']) {
if (!node.hasAttribute(attribute)) continue;
const recorded = sources?.[attribute];
const value = recorded === undefined ? node.getAttribute(attribute) : recorded;
if (recorded === undefined) {
sources = sources || {};
sources[attribute] = value;
ATTRIBUTE_SOURCE.set(node, sources);
}
node.setAttribute(attribute, tr(value));
}
if (node.hasAttribute('data-kf-no-translate')) return;
}
for (const child of node.childNodes || []) walk(child);
};
walk(root);
}
function buildInterface() {
if (document.getElementById('kick-focus-root')) return;
const root = document.createElement('div');
root.id = 'kick-focus-root';
root.lang = activeLocale();
const shadow = root.attachShadow({ mode: 'open' });
setMarkup(shadow, `