From 55660a5dbe6f61bec639a5c075b80b0e602c1589 Mon Sep 17 00:00:00 2001 From: Jared Ondricek Date: Fri, 7 Aug 2026 11:57:33 -0500 Subject: [PATCH 01/16] fix: standardize STIX file path handling --- modules/stixtests/stixtests.py | 10 ++-------- modules/util/stixhelpers.py | 16 +++++++++------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/modules/stixtests/stixtests.py b/modules/stixtests/stixtests.py index a619414ae61..824148a4a7d 100644 --- a/modules/stixtests/stixtests.py +++ b/modules/stixtests/stixtests.py @@ -36,16 +36,10 @@ def run_tests(): logger.debug(f"Skipping validation for {domain['name']} because it is deprecated") continue - # TODO: refactor this to use a function rather than copy/paste from modules/util/stixhelpers.py - # this can be used because it was called previously in modules/util/stixhelpers.py to download the file - if domain["location"].startswith("http"): - download_dir = Path(f"{site_config.web_directory}/stix") - stix_filename = f"{download_dir}/{domain['name']}.json" - else: - stix_filename = domain["location"] + stix_filename = util.stixhelpers.get_stix_file_path(domain["name"]) logger.info(f"Validating STIX for domain: {domain['name']}") - results = stix2validator.validate_file(fn=stix_filename, options=options) + results = stix2validator.validate_file(fn=str(stix_filename), options=options) if results.is_valid: logger.info(f"File {stix_filename} is valid") else: diff --git a/modules/util/stixhelpers.py b/modules/util/stixhelpers.py index b8635f31249..befaf8a234a 100644 --- a/modules/util/stixhelpers.py +++ b/modules/util/stixhelpers.py @@ -482,23 +482,20 @@ def get_stix_memory_stores(): ms = {} srcs = [] - stix_output_dir = Path(f"{site_config.web_directory}/stix") - stix_output_dir.mkdir(parents=True, exist_ok=True) - for domain in site_config.domains: - stix_filename = None logger.info(f"Loading {domain['name']} domain STIX from: {domain['location']}") # Download json from http or https - stix_filename = f"{stix_output_dir}/{domain['name']}.json" + stix_filename = get_stix_file_path(domain_name=domain["name"]) + stix_filename.parent.mkdir(parents=True, exist_ok=True) if domain["location"].startswith("http"): - download_stix_file(url=domain["location"], filepath=stix_filename) + download_stix_file(url=domain["location"], filepath=str(stix_filename)) else: shutil.copy(domain["location"], str(stix_filename)) if os.path.exists(stix_filename): ms[domain["name"]] = stix2.MemoryStore() - ms[domain["name"]].load_from_file(stix_filename) + ms[domain["name"]].load_from_file(str(stix_filename)) else: logger.error(f"\n{stix_filename} file does not exist.") exit() @@ -509,6 +506,11 @@ def get_stix_memory_stores(): return ms, srcs +def get_stix_file_path(domain_name: str) -> Path: + """Return the path where a domain's STIX bundle is materialized.""" + return Path(site_config.web_directory) / "stix" / f"{domain_name}.json" + + def get_contributors(ms): """Get all contributors in the STIX content.""" # contributors not in STIX are stored here: From 509e547a84c29156298040229126bf9e2f4f26c7 Mon Sep 17 00:00:00 2001 From: ppcvote Date: Sun, 23 Aug 2026 19:35:42 +0800 Subject: [PATCH 02/16] fix(search): settle bulkPut when an IndexedDB write fails TableWrapper.bulkPut ran its work inside an async Promise executor with no reject, so a rejected chunk write settled the executor's own promise rather than the returned one. The promise never settled at all. SearchService's cold-cache path waits on it behind `while (!searchServiceIsLoaded)`, so the parsing spinner stays up for good, and the .catch already written for that path in index.js could not run because nothing ever rejected. Signed-off-by: ppcvote --- .../__tests__/indexed-db-wrapper.test.js | 25 ++++++++++++ attack-search/src/indexed-db-wrapper.js | 39 +++++++++++-------- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/attack-search/__tests__/indexed-db-wrapper.test.js b/attack-search/__tests__/indexed-db-wrapper.test.js index 51e4727b3fc..2b379212209 100644 --- a/attack-search/__tests__/indexed-db-wrapper.test.js +++ b/attack-search/__tests__/indexed-db-wrapper.test.js @@ -57,4 +57,29 @@ describe('IndexedDBWrapper', () => { const count = await contentDb.count(); expect(count).toEqual(data.length); }); + + // A failed write must settle the promise. Racing against a sentinel tells a + // rejection apart from a promise that never settles at all, which a plain + // rejects assertion cannot do: it would time out and look like a slow test. + const settle = (promise) => Promise.race([ + promise.then(() => 'resolved', (error) => `rejected:${error.message}`), + new Promise((resolve) => setTimeout(() => resolve('HUNG'), 1000)), + ]); + + test('Bulk put rejects when the underlying write fails', async () => { + jest.spyOn(contentDb.indexeddb[contentDb.tableName], 'bulkPut') + .mockRejectedValue(new Error('QuotaExceededError')); + + await expect(settle(contentDb.bulkPut(data))).resolves.toBe('rejected:QuotaExceededError'); + }); + + test('Bulk put rejects when a later chunk fails', async () => { + let calls = 0; + jest.spyOn(contentDb.indexeddb[contentDb.tableName], 'bulkPut') + .mockImplementation(() => (++calls === 2 + ? Promise.reject(new Error('DatabaseClosedError')) + : Promise.resolve())); + + await expect(settle(contentDb.bulkPut(data, 1))).resolves.toBe('rejected:DatabaseClosedError'); + }); }); diff --git a/attack-search/src/indexed-db-wrapper.js b/attack-search/src/indexed-db-wrapper.js index df24f3ff08b..9ac1400b02a 100644 --- a/attack-search/src/indexed-db-wrapper.js +++ b/attack-search/src/indexed-db-wrapper.js @@ -23,7 +23,7 @@ class TableWrapper { */ async bulkPut(data, chunkSize = 100) { - return new Promise(async (resolve) => { + return new Promise((resolve, reject) => { /** * Schedules work using requestIdleCallback if supported, or setTimeout as a fallback. * @param {Function} callback - The function to be executed when the browser is idle or after the specified delay. @@ -44,23 +44,28 @@ class TableWrapper { * @param {number} start - The index of the first item in the data array to be included in the current chunk. */ const putChunk = async (start) => { - // If all data has been processed, resolve the promise - if (start >= data.length) { - resolve(); - return; + try { + // If all data has been processed, resolve the promise + if (start >= data.length) { + resolve(); + return; + } + + // Determine the end index for the current chunk + const end = Math.min(start + chunkSize, data.length); + + // Extract the chunk from the data array + const chunk = data.slice(start, end); + + // Insert the chunk into the IndexedDB table + await this.indexeddb[this.tableName].bulkPut(chunk); + + // Schedule the next chunk to be processed + scheduleWork(() => putChunk(end)); + } catch (error) { + // Nothing else settles this promise, so callers would wait forever. + reject(error); } - - // Determine the end index for the current chunk - const end = Math.min(start + chunkSize, data.length); - - // Extract the chunk from the data array - const chunk = data.slice(start, end); - - // Insert the chunk into the IndexedDB table - await this.indexeddb[this.tableName].bulkPut(chunk); - - // Schedule the next chunk to be processed - scheduleWork(() => putChunk(end)); }; // Start processing the data array by inserting the first chunk From 7742c5cc78fdd7410c2cdd7a52d63fda96b5cb38 Mon Sep 17 00:00:00 2001 From: ppcvote Date: Mon, 24 Aug 2026 02:24:50 +0800 Subject: [PATCH 03/16] docs(changelog): note the bulkPut settle fix Signed-off-by: ppcvote --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2039c128b43..d34f790ff57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Website Changelog +## Unreleased + +### Bug Fixes + +* Settle the search index write when an IndexedDB write fails, instead of leaving the promise pending and the search spinner up. + ## v5.0.0 (2026-08-06) * Release ATT&CK content version 19.2. From 05ee4b2fc6588fe4685b8286ec861e0a4d81f2ae Mon Sep 17 00:00:00 2001 From: Jared Ondricek Date: Wed, 26 Aug 2026 13:46:37 -0500 Subject: [PATCH 04/16] docs: update pull request template --- .github/pull_request_template.md | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 2b494f00aa6..000a7329776 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,19 +1,7 @@ - - - - - +## Description -## Description of what has changed - - - +## Problem Solved -## Issues addressed by pull request - - - - - - - +## Alternatives Considered + +## Related issue(s) \ No newline at end of file From d0b48f28f6ec6910e00be6af6956655320844e5b Mon Sep 17 00:00:00 2001 From: Jared Ondricek Date: Wed, 2 Sep 2026 15:00:26 -0500 Subject: [PATCH 05/16] fix: fix loading sidebar on https redirects --- attack-theme/static/scripts/sidebar-load-all.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/attack-theme/static/scripts/sidebar-load-all.js b/attack-theme/static/scripts/sidebar-load-all.js index 9c3e5672a3f..e6220a56c2f 100644 --- a/attack-theme/static/scripts/sidebar-load-all.js +++ b/attack-theme/static/scripts/sidebar-load-all.js @@ -9,7 +9,7 @@ else{ if (mod_name.includes('contact')){ mod_entry = "/" + "resources/sidebar-resources" } -$("#sidebars").load(mod_entry, function() { +$("#sidebars").load(`${mod_entry}/`, function() { let old_winlocation = window.location.href; if (mod_name.includes('versions')){ let v_number = mod_name[2]; @@ -63,4 +63,4 @@ $("#sidebars").load(mod_entry, function() { }); mediaQuery.addEventListener('change', mobileSidenav) -}); \ No newline at end of file +}); From 062f47721098e23a43cc345528a66996ad6e17f6 Mon Sep 17 00:00:00 2001 From: ppcvote Date: Sat, 5 Sep 2026 04:33:38 +0800 Subject: [PATCH 06/16] fix(search): surface a failed index build instead of spinning forever A failed cold start left searchServiceIsLoaded false, and `search` waits on that flag in a loop, so every query after the failure re-showed the parsing icon every 100ms for as long as the page stayed open. The warm restore path had the opposite problem: its `finally` set the flag to true unconditionally, overriding the false its own `catch` had just set, so a failed restore reported success and then queried a SearchService that never initialized. Both paths now put the controls into the same unavailable state the unsupported browser branch already used, which that branch now shares, and `search` stops waiting once the index is known not to be coming. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 1 + attack-search/__tests__/search-events.test.js | 45 +++++++++++++++++++ attack-search/src/index.js | 38 +++++++++++----- 3 files changed, 74 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d34f790ff57..aa3d3086648 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Bug Fixes * Settle the search index write when an IndexedDB write fails, instead of leaving the promise pending and the search spinner up. +* Disable the search controls and explain why when the search index cannot be built, instead of leaving the spinner running for as long as the page is open. ## v5.0.0 (2026-08-06) diff --git a/attack-search/__tests__/search-events.test.js b/attack-search/__tests__/search-events.test.js index 86af5fc0e19..745e06df3b6 100644 --- a/attack-search/__tests__/search-events.test.js +++ b/attack-search/__tests__/search-events.test.js @@ -112,8 +112,53 @@ describe('search event bindings', () => { expect(mockJqueryApis['[data-search-filter-dropdown="core"]'].attr) .toHaveBeenCalledWith('aria-hidden', 'false'); }); + + test('a failed index build stops search from waiting for an index that never arrives', async () => { + await loadIndexWithAFailingColdStart(); + + const parsingIcon = mockJqueryApis['#search-parsing-icon']; + parsingIcon.show.mockClear(); + parsingIcon.hide.mockClear(); + + handlerForSelector('#search-input')({ target: { value: 'mimikatz' } }); + + // Before the fix `search` looped on the loaded flag, so it showed the parsing icon on + // its first pass and kept doing so every 100ms for as long as the page stayed open. + expect(parsingIcon.show).not.toHaveBeenCalled(); + expect(parsingIcon.hide).toHaveBeenCalled(); + }); + + test('a failed index build puts the search controls into their unavailable state', async () => { + await loadIndexWithAFailingColdStart(); + + expect(mockJqueryApis['#search-input'].prop).toHaveBeenCalledWith('disabled', true); + expect(mockJqueryApis['#search-button'].prop).toHaveBeenCalledWith('disabled', true); + expect(mockJqueryApis['#search-icon'].removeClass).toHaveBeenCalledWith('search-icon'); + expect(mockJqueryApis['#search-icon'].addClass).toHaveBeenCalledWith('error-icon'); + expect(mockJqueryApis['#search-button'].prop) + .toHaveBeenCalledWith('title', expect.stringContaining('search index could not be built')); + }); }); +// Load the module on the cold-start path with the document fetch failing, and run the +// debouncer straight through so the input handler reaches `search` without a timer. +async function loadIndexWithAFailingColdStart() { + global.window = { indexedDB: {} }; + global.localStorage.getItem.mockReturnValue(null); + + jest.doMock('../src/search-loader.js', () => ({ + loadSearchDocuments: () => Promise.reject(new Error('documents unavailable')), + })); + jest.doMock('../src/debouncer.js', () => class { + debounce(callback) { + callback(); + } + }); + + require('../src/index'); + await new Promise(resolve => setImmediate(resolve)); +} + function eventsForSelector(selector) { return mockJqueryCalls .filter(call => call.selector === selector || call.delegatedSelector === selector) diff --git a/attack-search/src/index.js b/attack-search/src/index.js index d3aa49f3b30..5ee5c82bbf0 100644 --- a/attack-search/src/index.js +++ b/attack-search/src/index.js @@ -89,6 +89,23 @@ const closeSearch = function () { // Variable to check if search service is loaded let searchServiceIsLoaded = false; +// Set once the index cannot be built at all. Without it `search` waits for a flag that is +// never going to flip and the parsing spinner runs for as long as the page is open. +let searchServiceUnavailable = false; + +// Put the search controls into their unavailable state and explain why on hover. +function markSearchUnavailable(reason) { + searchServiceUnavailable = true; + searchServiceIsLoaded = false; + searchInput.prop('disabled', true); + searchButton.prop('disabled', true); + searchIcon.removeClass('search-icon'); + searchIcon.addClass('error-icon'); + searchButton.prop('title', reason); +} + +const SEARCH_INDEX_FAILED_MESSAGE = 'The search index could not be built. Reload the page to try again.'; + // Initialize the search service async function initializeSearchService() { console.debug('Initializing search service...'); @@ -111,12 +128,12 @@ async function initializeSearchService() { await searchService.initializeAsync(null); // Passing null will instruct the search service to attempt // restoring itself from the IndexedDB console.debug('SearchService is initialized.'); + searchServiceIsLoaded = true; } catch (error) { console.error('Failed to initialize SearchService:', error); - searchServiceIsLoaded = false; + markSearchUnavailable(SEARCH_INDEX_FAILED_MESSAGE); } finally { searchParsingIcon.hide(); - searchServiceIsLoaded = true; } } else { @@ -139,18 +156,14 @@ async function initializeSearchService() { .catch(error => { console.error('Failed to initialize SearchService:', error); searchParsingIcon.hide(); - searchServiceIsLoaded = false; + markSearchUnavailable(SEARCH_INDEX_FAILED_MESSAGE); }); } } else { // Disable the search button and display an error icon with a hover effect that displays a message/explanation console.error('Search is only available in browsers that support IndexedDB. Please try using Firefox, Chrome, Safari, or another browser that supports IndexedDB.'); - searchInput.prop('disabled', true); - searchButton.prop('disabled', true); - searchIcon.removeClass('search-icon'); - searchIcon.addClass('error-icon'); - searchButton.prop('title', 'To use the search feature, please make sure your browser supports IndexedDB. If not, consider upgrading your browser or switching to a supported browser such as Firefox, Chrome, or Safari.') + markSearchUnavailable('To use the search feature, please make sure your browser supports IndexedDB. If not, consider upgrading your browser or switching to a supported browser such as Firefox, Chrome, or Safari.'); } } @@ -158,13 +171,18 @@ async function initializeSearchService() { const search = async function (query) { console.debug(`search -> Received search query: ${query}`); - // Wait until the search service is loaded - while (!searchServiceIsLoaded) { + // Wait until the search service is loaded, or until we know it never will be. + while (!searchServiceIsLoaded && !searchServiceUnavailable) { console.debug('search -> search index is not loaded...'); searchParsingIcon.show(); await new Promise(resolve => setTimeout(resolve, 100)); } + if (searchServiceUnavailable) { + searchParsingIcon.hide(); + return; + } + console.debug(`Executing search: ${query}`); await searchService.query(query); searchParsingIcon.hide(); From 0d58ced80e37c78c0caf6dbd18e4d55906b58c9a Mon Sep 17 00:00:00 2001 From: ppcvote Date: Sat, 5 Sep 2026 04:46:16 +0800 Subject: [PATCH 07/16] test(search): cover the warm restore path as well as the cold start The two tests added with the fix both drive the cold-start branch. This adds one for the cached branch, where the `finally` used to override the `catch`, so both halves of the change have a test that fails without it. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 2 +- attack-search/__tests__/search-events.test.js | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa3d3086648..57b7b2c46aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Bug Fixes * Settle the search index write when an IndexedDB write fails, instead of leaving the promise pending and the search spinner up. -* Disable the search controls and explain why when the search index cannot be built, instead of leaving the spinner running for as long as the page is open. +* Disable the search controls and explain why when the search index cannot be built, instead of leaving the spinner running for as long as the page is open. A failed restore from the cached index is no longer reported as a successful load. ## v5.0.0 (2026-08-06) diff --git a/attack-search/__tests__/search-events.test.js b/attack-search/__tests__/search-events.test.js index 745e06df3b6..5c27afdf8a5 100644 --- a/attack-search/__tests__/search-events.test.js +++ b/attack-search/__tests__/search-events.test.js @@ -128,6 +128,15 @@ describe('search event bindings', () => { expect(parsingIcon.hide).toHaveBeenCalled(); }); + test('a failed restore from the cache is not reported as a successful load', async () => { + await loadIndexWithAFailingWarmRestore(); + + // The catch used to set the loaded flag false and the finally set it straight back to + // true, so `search` went on to query an index that was never populated. + expect(mockJqueryApis['#search-input'].prop).toHaveBeenCalledWith('disabled', true); + expect(mockJqueryApis['#search-icon'].addClass).toHaveBeenCalledWith('error-icon'); + }); + test('a failed index build puts the search controls into their unavailable state', async () => { await loadIndexWithAFailingColdStart(); @@ -159,6 +168,24 @@ async function loadIndexWithAFailingColdStart() { await new Promise(resolve => setImmediate(resolve)); } +// Load the module on the cached path, with restoring the index from IndexedDB failing. +async function loadIndexWithAFailingWarmRestore() { + const { searchCacheCompatibilityVersion, searchCacheSchemaVersion } = require('../src/settings'); + const version = `${searchCacheSchemaVersion}-${searchCacheCompatibilityVersion}`; + + global.window = { indexedDB: {} }; + global.localStorage.getItem.mockReturnValue(`${global.build_uuid}-search-${version}`); + + jest.doMock('../src/search-service.js', () => class { + initializeAsync() { + return Promise.reject(new Error('cached index is unreadable')); + } + }); + + require('../src/index'); + await new Promise(resolve => setImmediate(resolve)); +} + function eventsForSelector(selector) { return mockJqueryCalls .filter(call => call.selector === selector || call.delegatedSelector === selector) From 156341fda2e62085579cac604435399eefb9ff22 Mon Sep 17 00:00:00 2001 From: adpare Date: Thu, 10 Sep 2026 15:32:12 -0400 Subject: [PATCH 08/16] fix: update search filtering to give more accurate results --- .../__tests__/search-service.test.js | 95 +++++++++++++++++++ attack-search/src/search-service.js | 76 ++++++++++++++- modules/search/search.py | 15 +++ 3 files changed, 185 insertions(+), 1 deletion(-) diff --git a/attack-search/__tests__/search-service.test.js b/attack-search/__tests__/search-service.test.js index 87beddcec06..77088b09357 100644 --- a/attack-search/__tests__/search-service.test.js +++ b/attack-search/__tests__/search-service.test.js @@ -22,11 +22,17 @@ describe('SearchService', () => { }); beforeEach(() => { + global.base_url = '/'; searchService = new SearchService('search-service', null); + searchService.render_container = { + append: jest.fn(), + html: jest.fn(), + }; }); afterEach(async () => { searchService = null; + delete global.base_url; }); it('Access data from mock-index.json', () => { @@ -111,4 +117,93 @@ describe('SearchService', () => { }); }); + + test('Keeps only exact ATT&CK ID matches and references, with the object first', async () => { + const documents = { + 1: { + id: 1, + title: 'TA577, Group G1037', + path: '/groups/G1037/index.html', + content: 'A group with no reference to the queried technique.', + attackId: 'G1037', + }, + 2: { + id: 2, + title: 'Ingress Tool Transfer, Technique T1105 - Enterprise', + path: '/techniques/T1105/index.html', + content: 'The T1105 technique.', + attackId: 'T1105', + }, + 3: { + id: 3, + title: 'A valid reference', + path: '/resources/reference/index.html', + content: 'This page references T1105.', + }, + }; + searchService.attackIndex.search = jest.fn().mockResolvedValue([{ field: 'title', result: [1, 2, 3] }]); + searchService.resolveSearchResults = jest.fn(async positions => positions.map(position => documents[position])); + + await searchService.query('t1105'); + + expect(searchService.allSearchResults.map(result => result.id)).toEqual([2, 3]); + }); + + test('Treats a four-digit query as an exact ATT&CK ID suffix search', async () => { + const documents = { + 1: { + id: 1, + title: 'TA577, Group G1037', + path: '/groups/G1037/index.html', + content: 'A group with no reference to the queried technique.', + attackId: 'G1037', + }, + 2: { + id: 2, + title: 'Data from Local System, Technique T1005 - Enterprise', + path: '/techniques/T1005/index.html', + content: 'The T1005 technique.', + attackId: 'T1005', + }, + 3: { + id: 3, + title: 'Matching software, Software S1005', + path: '/software/S1005/index.html', + content: 'The S1005 software.', + attackId: 'S1005', + }, + 4: { + id: 4, + title: 'A valid reference', + path: '/resources/reference/index.html', + content: 'This page references T1005.', + }, + 5: { + id: 5, + title: 'Data from Local System: Archive Collected Data, Sub-technique T1005.001', + path: '/techniques/T1005/001/index.html', + content: 'The T1005.001 sub-technique.', + attackId: 'T1005.001', + }, + }; + searchService.attackIndex.search = jest.fn().mockResolvedValue([{ field: 'title', result: [1, 3, 4, 2, 5] }]); + searchService.resolveSearchResults = jest.fn(async positions => positions.map(position => documents[position])); + + await searchService.query('1005'); + + expect(searchService.allSearchResults.map(result => result.id)).toEqual([2, 5, 3, 4]); + }); + + test('Preserves result ordering for non-ID queries', async () => { + const documents = { + 1: { id: 1, title: 'First result', path: '/resources/faq/index.html', content: 'Resources' }, + 2: { id: 2, title: 'Second result', path: '/resources/attackcon/index.html', content: 'Resources' }, + }; + searchService.attackIndex.search = jest.fn().mockResolvedValue([{ field: 'title', result: [1, 2] }]); + searchService.resolveSearchResults = jest.fn(async positions => positions.map(position => documents[position])); + + await searchService.query('Resources'); + + expect(searchService.allSearchResults.map(result => result.id)).toEqual([1, 2]); + }); }); diff --git a/attack-search/src/search-service.js b/attack-search/src/search-service.js index 5a38f6ecfe5..bd03b763296 100644 --- a/attack-search/src/search-service.js +++ b/attack-search/src/search-service.js @@ -313,10 +313,84 @@ module.exports = class SearchService { * ] */ - this.allSearchResults = await this.#setSearchResults(results); + this.allSearchResults = this.#filterAndPromoteExactAttackIdMatches(await this.#setSearchResults(results)); this.#renderFilteredSearchResults(); } + /** + * Limits ATT&CK ID searches to matching objects, their sub-techniques, and genuine references. + * Non-ID and multi-token queries retain FlexSearch's existing ordering. + * + * @private + * @param {Array} documents - Search results in their existing relevance order. + * @returns {Array} Exact ATT&CK ID results, with the matching object detail page first when applicable. + */ + #filterAndPromoteExactAttackIdMatches(documents) { + const query = this.currentQuery.clean; + const isExactAttackId = /^[A-Z]+\d+(?:\.\d+)?$/i.test(query); + const isNumericIdSuffix = /^\d{4}$/.test(query); + // If user queries for normal text and not attack ids, normal search takes place + if (!isExactAttackId && !isNumericIdSuffix) return documents; + + const normalizedQuery = query.toUpperCase(); + + // Collect the IDs stored on object-detail search records. Resource and reference pages have no attackId. + const candidateAttackIds = documents + .map(document => document.attackId?.toUpperCase()) + .filter(Boolean); + + let directAttackIds; + if (isExactAttackId) { + // A complete query such as T1005 refers directly to that one ID. + directAttackIds = [normalizedQuery]; + } else { + // A numeric query such as 1005 may match T1005, S1005, or another complete ATT&CK ID. + const numericSuffixPattern = new RegExp(`^[A-Z]+${normalizedQuery}$`); + directAttackIds = [...new Set(candidateAttackIds.filter(attackId => numericSuffixPattern.test(attackId)))]; + } + + // Include sub-techniques of a matching parent technique, such as T1005.001 for a T1005 query. + const subTechniqueIds = candidateAttackIds.filter((attackId) => directAttackIds.some((directAttackId) => ( + directAttackId.startsWith('T') + && !directAttackId.includes('.') + && attackId.startsWith(`${directAttackId}.`) + ))); + const matchingAttackIds = [...new Set([...directAttackIds, ...subTechniqueIds])]; + if (matchingAttackIds.length === 0) return []; + + // Put parent techniques first, then their sub-techniques, followed by other matching ATT&CK object types. + const exactMatches = documents.filter(document => matchingAttackIds.includes(document.attackId?.toUpperCase())); + exactMatches.sort((first, second) => { + const firstIsTechnique = first.attackId.startsWith('T'); + const secondIsTechnique = second.attackId.startsWith('T'); + if (firstIsTechnique !== secondIsTechnique) return firstIsTechnique ? -1 : 1; + + const firstIsSubTechnique = first.attackId.includes('.'); + const secondIsSubTechnique = second.attackId.includes('.'); + if (firstIsSubTechnique !== secondIsSubTechnique) return firstIsSubTechnique ? 1 : -1; + + return 0; + }); + + // Escape dots in sub-technique IDs before making one expression that matches only whole IDs. + const escapedIds = matchingAttackIds.map(attackId => attackId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); + // A trailing period is valid sentence punctuation, unless it begins a sub-technique suffix such as .001. + const exactIdInText = new RegExp( + `(^|[^A-Z0-9.])(?:${escapedIds.join('|')})(?=$|[^A-Z0-9.]|\\.(?!\\d))`, + 'i', + ); + + // Add pages that reference a matching ID, but do not add an object-detail page twice. + const referencedDocuments = documents.filter((document) => { + const title = document.title ?? ''; + const content = document.content ?? ''; + const referencesMatchingId = exactIdInText.test(title) || exactIdInText.test(content); + return referencesMatchingId && !exactMatches.includes(document); + }); + + return exactMatches.concat(referencedDocuments); + } + /** * Renders the search results on the web page based on the given search result page. * If the search query is empty, it will show the "Load More Results" button. diff --git a/modules/search/search.py b/modules/search/search.py index 92bd2b746de..6faac32e8e0 100644 --- a/modules/search/search.py +++ b/modules/search/search.py @@ -29,6 +29,7 @@ "x-mitre-data-component": "datacomponents", "x-mitre-detection-strategy": "detectionstrategies", } +searchable_object_path_prefixes = set(object_path_prefixes.values()) | {"techniques"} def generate_index(): @@ -64,6 +65,7 @@ def generate_index(): "title": title, "path": path, "content": cleancontent, + "attackId": get_search_attack_id(path), "pageType": file_type, "domains": get_domains(title, path, domain_lookup), } @@ -136,6 +138,19 @@ def should_skip_search_path(path): return bool(re.search(r"/sidebar-[^/]+/index\.html$", path)) +def get_search_attack_id(path): + """Return the ATT&CK ID represented by a canonical object detail-page path.""" + subtechnique_match = re.fullmatch(r"/techniques/(T\d+)/(\d{3})/index\.html", path) + if subtechnique_match: + return f"{subtechnique_match.group(1)}.{subtechnique_match.group(2)}" + + object_match = re.fullmatch(r"/([^/]+)/([A-Z]+\d+(?:\.\d+)?)/index\.html", path) + if object_match and object_match.group(1) in searchable_object_path_prefixes: + return object_match.group(2) + + return None + + def get_domains(title, path=None, domain_lookup=None): """Get ATT&CK domains for a search result, preferring source metadata before title inference.""" if path and domain_lookup and path in domain_lookup: From 9412110d0ecea35ea05c66c5e2d51ff6933f5a50 Mon Sep 17 00:00:00 2001 From: Jared Ondricek Date: Mon, 14 Sep 2026 12:23:21 -0500 Subject: [PATCH 09/16] feat: add dark mode --- attack-search/__tests__/search-style.test.js | 2 +- attack-search/__tests__/theme.test.js | 280 ++++++ attack-style/README.md | 5 +- attack-style/abstracts/README.md | 21 +- attack-style/abstracts/_color-functions.scss | 70 +- attack-style/abstracts/_variables.scss | 13 +- attack-style/components/_matrix.scss | 8 +- attack-style/components/_search.scss | 3 +- attack-style/layout/_footer.scss | 4 +- attack-style/layout/_layout.scss | 140 ++- attack-style/layout/_nav.scss | 91 +- attack-style/package.json | 4 +- attack-style/style-archive.scss | 3 + attack-style/style.scss | 5 +- attack-style/themes/README.md | 21 +- attack-style/themes/_archive.scss | 337 +++++++ attack-style/themes/_colors.scss | 130 +++ attack-style/themes/_palette.scss | 92 ++ attack-theme/static/scripts/theme.js | 164 ++++ attack-theme/static/style-archive.css | 629 ++++++++++++ attack-theme/static/style-attack.css | 904 +++++++++++++----- attack-theme/static/style-user.css | 904 +++++++++++++----- .../templates/general/attack-index.html | 6 +- .../templates/general/base-template.html | 2 + .../templates/macros/navigation_menu.html | 11 + .../templates/datasources-index.html | 4 +- .../attack-advisory-council-members.html | 7 +- .../templates/attack-data-and-tools.html | 16 +- .../templates/attackcon-overview.html | 7 +- modules/versions/versions.py | 41 +- 30 files changed, 3271 insertions(+), 653 deletions(-) create mode 100644 attack-search/__tests__/theme.test.js create mode 100644 attack-style/style-archive.scss create mode 100644 attack-style/themes/_archive.scss create mode 100644 attack-style/themes/_palette.scss create mode 100644 attack-theme/static/scripts/theme.js create mode 100644 attack-theme/static/style-archive.css diff --git a/attack-search/__tests__/search-style.test.js b/attack-search/__tests__/search-style.test.js index 73a43d2718e..f5713431710 100644 --- a/attack-search/__tests__/search-style.test.js +++ b/attack-search/__tests__/search-style.test.js @@ -20,7 +20,7 @@ describe('search styles', () => { const badgeStyle = styles.match(/\.search-result-badge\s*\{(?[^}]+)\}/)?.groups?.body ?? ''; - expect(badgeStyle).toContain('color: white;'); + expect(badgeStyle).toContain('color: color-functions.on-color(active);'); expect(badgeStyle).toContain('font-size: 0.8rem;'); expect(styles).toContain('.search-result-badge-page-type'); diff --git a/attack-search/__tests__/theme.test.js b/attack-search/__tests__/theme.test.js new file mode 100644 index 00000000000..55eb51efd55 --- /dev/null +++ b/attack-search/__tests__/theme.test.js @@ -0,0 +1,280 @@ +const fs = require('fs'); +const path = require('path'); + +const themeModulePath = '../../attack-theme/static/scripts/theme.js'; + +describe('site theme', () => { + let theme; + + beforeEach(() => { + jest.resetModules(); + theme = require(themeModulePath); + }); + + test('uses System when no saved override exists', () => { + const storage = createStorage(); + const root = createRoot(); + + expect(theme.readStoredPreference(storage)).toBe('system'); + + theme.applyPreference(root, 'system'); + + expect(root.removeAttribute).toHaveBeenCalledWith('data-theme'); + expect(root.style.setProperty).not.toHaveBeenCalled(); + }); + + test.each(['light', 'dark'])('applies a saved %s override before controls initialize', preference => { + const storage = createStorage(preference); + const root = createRoot(); + + const storedPreference = theme.readStoredPreference(storage); + theme.applyPreference(root, storedPreference); + + expect(storedPreference).toBe(preference); + expect(root.setAttribute).toHaveBeenCalledWith('data-theme', preference); + // CSS owns color-scheme so the print stylesheet can force light controls. + expect(root.style.setProperty).not.toHaveBeenCalled(); + }); + + test('discards an invalid saved preference', () => { + const storage = createStorage('sepia'); + + expect(theme.readStoredPreference(storage)).toBe('system'); + expect(storage.removeItem).toHaveBeenCalledWith(theme.STORAGE_KEY); + }); + + test('adds a working archive switch to the existing banner without replacing its content', () => { + const fixture = createControllerFixture({ storedPreference: 'dark' }); + const banner = { appendChild: jest.fn(), textContent: 'Currently viewing ATT&CK v3.0' }; + fixture.document.querySelector = jest.fn(selector => ( + selector === '.version-banner' ? banner : null + )); + fixture.document.createElement = jest.fn(() => fixture.toggle); + fixture.document.readyState = 'complete'; + + theme.bootstrap({ ...fixture, archived: true }); + + expect(fixture.document.documentElement.setAttribute).toHaveBeenCalledWith('data-archive-theme', ''); + expect(banner.appendChild).toHaveBeenCalledWith(fixture.toggle); + expect(banner.textContent).toBe('Currently viewing ATT&CK v3.0'); + expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-label', 'Dark mode'); + expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-checked', 'true'); + fixture.toggle.click(); + expect(fixture.storage.setItem).toHaveBeenCalledWith(theme.STORAGE_KEY, 'light'); + expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-checked', 'false'); + }); + + test('toggles from the system light theme to a saved dark override', () => { + const fixture = createControllerFixture({ systemDark: false }); + const controller = theme.createThemeController(fixture); + + controller.init(); + fixture.toggle.click(); + + expect(fixture.document.documentElement.setAttribute).toHaveBeenLastCalledWith('data-theme', 'dark'); + expect(fixture.storage.setItem).toHaveBeenCalledWith(theme.STORAGE_KEY, 'dark'); + expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-label', 'Switch to light mode'); + expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-checked', 'true'); + }); + + test('toggles from the system dark theme to a saved light override', () => { + const fixture = createControllerFixture({ systemDark: true }); + const controller = theme.createThemeController(fixture); + + controller.init(); + fixture.toggle.click(); + + expect(fixture.document.documentElement.setAttribute).toHaveBeenLastCalledWith('data-theme', 'light'); + expect(fixture.storage.setItem).toHaveBeenCalledWith(theme.STORAGE_KEY, 'light'); + expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-label', 'Switch to dark mode'); + expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-checked', 'false'); + }); + + test.each([ + ['light', 'dark'], + ['dark', 'light'], + ])('toggles a saved %s override to %s', (storedPreference, expectedPreference) => { + const fixture = createControllerFixture({ storedPreference }); + const controller = theme.createThemeController(fixture); + + controller.init(); + fixture.toggle.click(); + + expect(fixture.document.documentElement.setAttribute).toHaveBeenLastCalledWith( + 'data-theme', + expectedPreference, + ); + expect(fixture.storage.setItem).toHaveBeenCalledWith(theme.STORAGE_KEY, expectedPreference); + }); + + test('continues applying a choice when storage access fails', () => { + const fixture = createControllerFixture(); + fixture.storage.setItem.mockImplementation(() => { + throw new Error('Storage disabled'); + }); + const controller = theme.createThemeController(fixture); + + expect(() => { + controller.init(); + fixture.toggle.click(); + }).not.toThrow(); + expect(fixture.document.documentElement.setAttribute).toHaveBeenLastCalledWith('data-theme', 'dark'); + }); + + test('updates the toggle when the system preference changes before an override', () => { + const fixture = createControllerFixture({ systemDark: false }); + const controller = theme.createThemeController(fixture); + + controller.init(); + expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-label', 'Switch to dark mode'); + + fixture.mediaQuery.matches = true; + fixture.mediaQuery.dispatchChange(); + + expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-label', 'Switch to light mode'); + }); + + test('ignores OS preference changes while an explicit override is active', () => { + const fixture = createControllerFixture({ storedPreference: 'light', systemDark: false }); + const controller = theme.createThemeController(fixture); + + controller.init(); + fixture.mediaQuery.matches = true; + fixture.mediaQuery.dispatchChange(); + + expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-label', 'Switch to dark mode'); + }); + + test('loads the early theme script before styles and renders one toggle before search', () => { + const template = fs.readFileSync( + path.join(__dirname, '../../attack-theme/templates/general/base-template.html'), + 'utf8', + ); + const navigation = fs.readFileSync( + path.join(__dirname, '../../attack-theme/templates/macros/navigation_menu.html'), + 'utf8', + ); + + expect(template).toContain(''); + expect(template.indexOf('/theme/scripts/theme.js')).toBeLessThan(template.indexOf('bootstrap.min.css')); + expect(navigation.indexOf('data-theme-toggle')).toBeLessThan(navigation.indexOf('id="search-button"')); + expect(navigation).toContain('role="switch"'); + expect(navigation).toContain('class="theme-toggle-track"'); + expect(navigation).toContain('class="theme-toggle-thumb"'); + expect(navigation).toContain('aria-checked="false"'); + expect(navigation).not.toContain('aria-pressed'); + expect(navigation).not.toContain('data-theme-option'); + expect(navigation).not.toContain('theme-menu'); + expect(navigation).not.toContain('dropdown-toggle" type="button" data-theme-toggle'); + }); + + test('uses theme-aware surfaces for the affected resource pages', () => { + const council = fs.readFileSync( + path.join(__dirname, '../../modules/resources/templates/attack-advisory-council-members.html'), + 'utf8', + ); + const dataTools = fs.readFileSync( + path.join(__dirname, '../../modules/resources/templates/attack-data-and-tools.html'), + 'utf8', + ); + const attackcon = fs.readFileSync( + path.join(__dirname, '../../modules/resources/templates/attackcon-overview.html'), + 'utf8', + ); + + expect(council).toContain('background: var(--attack-color-body-alternate);'); + expect(council).toContain('color: var(--attack-on-color-body);'); + expect(dataTools).toContain('class="tab-content card card-body p-3 attack-excel-files"'); + expect(dataTools).not.toContain('style="background: #f8f9fa;"'); + expect(attackcon).toContain('"ATT&CKcon 4.0", "ATT&CKcon 5.0", "ATT&CKcon 6.0", "ATT&CKcon 7.0"'); + expect(attackcon).toContain('attackcon-banner-image{% if con.title in light_banner_titles %} on-light{% endif %}'); + }); + + test('uses theme-aware home controls and announcement banner colors', () => { + const home = fs.readFileSync( + path.join(__dirname, '../../attack-theme/templates/general/attack-index.html'), + 'utf8', + ); + const colors = fs.readFileSync( + path.join(__dirname, '../../attack-style/themes/_palette.scss'), + 'utf8', + ); + + expect(home).toContain('fa-up-right-from-square external-link-icon'); + expect(home).toContain('dropdown-toggle-split random-page-toggle'); + expect(home).not.toContain('external-site-dark.jpeg'); + expect(home).not.toContain('style="color: #4f7cac; background-color: white;'); + expect(colors).toContain('--attack-color-banner: #e7f0f6;'); + expect(colors).toContain('--attack-color-banner: #263a49;'); + }); + + test('uses a warm metadata label color only in dark mode', () => { + const colors = fs.readFileSync( + path.join(__dirname, '../../attack-style/themes/_palette.scss'), + 'utf8', + ); + const layout = fs.readFileSync( + path.join(__dirname, '../../attack-style/layout/_layout.scss'), + 'utf8', + ); + + expect(colors).toContain('--attack-color-property-label: #1d2226;'); + expect(colors).toContain('--attack-color-property-label: #f2d2a4;'); + expect(layout).toMatch(/\.card-data \.card-title\s*\{\s*color: color-functions\.color\(property-label\);/); + }); +}); + +function createStorage(value = null) { + return { + getItem: jest.fn(() => value), + removeItem: jest.fn(), + setItem: jest.fn(), + }; +} + +function createRoot() { + return { + removeAttribute: jest.fn(), + setAttribute: jest.fn(), + style: { + removeProperty: jest.fn(), + setProperty: jest.fn(), + }, + }; +} + +function createElement() { + const listeners = {}; + return { + classList: { toggle: jest.fn() }, + setAttribute: jest.fn(), + addEventListener: jest.fn((eventName, listener) => { + listeners[eventName] = listener; + }), + click: () => listeners.click({ preventDefault: jest.fn() }), + }; +} + +function createControllerFixture({ storedPreference = null, systemDark = false } = {}) { + const root = createRoot(); + const toggle = createElement(); + let changeListener; + const mediaQuery = { + matches: systemDark, + addEventListener: jest.fn((eventName, listener) => { + if (eventName === 'change') changeListener = listener; + }), + dispatchChange: () => changeListener({ matches: mediaQuery.matches }), + }; + const document = { + documentElement: root, + querySelector: jest.fn(selector => (selector === '[data-theme-toggle]' ? toggle : null)), + }; + + return { + document, + mediaQuery, + storage: createStorage(storedPreference), + toggle, + }; +} diff --git a/attack-style/README.md b/attack-style/README.md index 201a7c83507..edc7014b010 100644 --- a/attack-style/README.md +++ b/attack-style/README.md @@ -1,10 +1,11 @@ # ATT&CK Style ATT&CK Style is a JavaScript package that builds the CSS styles for the ATT&CK website. -The outputs are simply 2 CSS files: +The outputs are 3 CSS files: * `dist/style-attack.css` * `dist/style-user.css` +* `dist/style-archive.css` (preserved-site appearance compatibility) These files are then copied into `/attack-theme/static/`. Currently this is done manually - no automation. @@ -47,7 +48,7 @@ To set up the ATT&CK Style package, follow these steps: 2. **Copy CSS Files**: - Copy both `dist/style-attack.css` and `dist/style-user.css` to `/attack-theme/static/`. + Copy `dist/style-attack.css`, `dist/style-user.css`, and `dist/style-archive.css` to `/attack-theme/static/`. ```bash npm run copy diff --git a/attack-style/abstracts/README.md b/attack-style/abstracts/README.md index c6327328313..c354b0af464 100644 --- a/attack-style/abstracts/README.md +++ b/attack-style/abstracts/README.md @@ -8,14 +8,14 @@ Files in this folder should not emit large blocks of CSS on their own unless the | File | Purpose | | --- | --- | | `_variables.scss` | Defines brand and user color maps plus the semantic `$colors` map used across the site. | -| `_color-functions.scss` | Provides accessors and derived color helpers for entries in `$colors`. | +| `_color-functions.scss` | Provides accessors for runtime semantic color tokens. | | `_utilities.scss` | Provides small reusable mixins and unit helpers. | | `_font-faces.scss` | Defines shared font-face declarations. | ## Color Model -`_variables.scss` keeps raw brand values separate from semantic color names. -Most styles should use semantic keys from `$colors`, such as `primary`, `secondary`, `footer`, `active`, `body`, `link`, `matrix-header`, `search-highlight`, and `deemphasis`. +`_variables.scss` keeps raw brand values separate from semantic color names. The theme palette in `themes/_palette.scss` turns those values into CSS custom properties so the appearance can change without loading another stylesheet. +Most styles should use semantic names such as `primary`, `secondary`, `footer`, `active`, `body`, `link`, `matrix-header`, `search-highlight`, and `deemphasis`. Each color entry may contain: @@ -28,18 +28,17 @@ Some entries omit `on-color` when they are not meant to contain inner text. ## Helper Functions -Use the functions in `_color-functions.scss` instead of reading `$colors` directly from component or layout files: +Use the functions in `_color-functions.scss` instead of reading `$colors` directly from component or layout files. Each helper returns the appropriate runtime CSS custom property: | Function | Use | | --- | --- | | `color($name)` | Reads the base color for a semantic color name. | -| `on-color($name)` | Reads the readable text color for a semantic color name. | -| `color-alternate($name, $contrast: 1)` | Computes a nearby alternate shade for patterning or subtle contrast. | -| `on-color-emphasis($name)` | Computes a stronger foreground color against a semantic background. | -| `on-color-deemphasis($name)` | Computes a quieter foreground color against a semantic background. | -| `border-color($name)` | Computes a border color for a semantic background. | -| `background-color($name)` | Computes a subtle derived background shade. | -| `escape-color($color)` | Escapes a concrete color for use inside inline SVG data URLs. | +| `on-color($name)` | Reads the readable foreground for a semantic color name. | +| `color-alternate($name, $contrast: 1)` | Reads an explicit alternate surface token. Supported contrast levels are `0.8`, `1`, `1.5`, `2`, and `3`. | +| `on-color-emphasis($name)` | Reads a stronger foreground token. | +| `on-color-deemphasis($name)` | Reads a quieter foreground token. | +| `border-color($name)` | Reads a border token. | +| `background-color($name)` | Reads a related background token. | ## Utility Mixins And Functions diff --git a/attack-style/abstracts/_color-functions.scss b/attack-style/abstracts/_color-functions.scss index 4407f1f3d8b..69b0b1dc9c7 100644 --- a/attack-style/abstracts/_color-functions.scss +++ b/attack-style/abstracts/_color-functions.scss @@ -1,55 +1,55 @@ -@use "sass:color"; -@use "sass:map"; -@use "sass:string"; -@use "variables"; - -// accessor helper for $colors. Gets the color of the named pair +// Accessor helper for semantic runtime colors. @function color($name) { - @return map.get(map.get(variables.$colors, $name), "color"); + @return var(--attack-color-#{$name}); } -// given a color name, get an alternate version of the color, for patterning -// if the base color is dark, the alternate will be slightly lighter. -// if the base color is light, the alternate will be slightly darker. -// contrast, an optional argument, multiplies to create a more distint or similar color. >1 is more distant, <1 is more similar. +// Get an explicit alternate surface token. Supported contrast values match the +// existing call sites and avoid requiring runtime color-mix support. @function color-alternate($name, $contrast: 1) { - @return color.mix(color.invert(color($name)), color($name), $weight: $contrast * 5%); + @if $contrast == 0.8 { + @return var(--attack-color-#{$name}-alternate-subtle); + } + + @if $contrast == 1 { + @return var(--attack-color-#{$name}-alternate); + } + + @if $contrast == 1.5 { + @return var(--attack-color-#{$name}-alternate-medium); + } + + @if $contrast == 2 { + @return var(--attack-color-#{$name}-alternate-strong); + } + + @if $contrast == 3 { + @return var(--attack-color-#{$name}-alternate-strongest); + } + + @error "Unsupported alternate color contrast: #{$contrast}"; } -/// accessor helper for $colors. Gets the on-color of the named pair +/// Accessor helper for readable text on a semantic color. @function on-color($name) { - @return map.get(map.get(variables.$colors, $name), "on-color"); + @return var(--attack-on-color-#{$name}); } -/// given a color-name, get an emphasized version of the on-color. -/// The emphasized on-color is less like the background color. +/// Get an emphasized foreground token for a semantic color. @function on-color-emphasis($name) { - @return color.mix(color.invert(color($name)), on-color($name)); + @return var(--attack-on-color-#{$name}-emphasis); } -// given a color-name, get an deemphasized version of the on-color. -// The deemphasized on-color is more like the background color. +// Get a deemphasized foreground token for a semantic color. @function on-color-deemphasis($name) { - @return color.mix(color($name), on-color($name), 25%); + @return var(--attack-on-color-#{$name}-deemphasis); } -// given a color name, compute a border color for the color +// Get an explicit border token for a semantic color. @function border-color($name) { - @return color.mix(color.invert(color($name)), color($name), 12.5%); - - // @return rgba(invert(color($name)), 0.125); + @return var(--attack-border-color-#{$name}); } -// given a color name, compute a border color for the color +// Get an explicit hover/background token for a semantic color. @function background-color($name) { - @return color.mix(color.invert(color($name)), color($name), 12.5%); -} - -// escape the color. Note param is a color and not a color name: this is not an accessor to the color map above. -// replaces # with %23 in hex colors -// see https://codepen.io/gunnarbittersmann/pen/BoovjR for explanation of why we have to escape # for the background image -@function escape-color($color) { - $hex: color.ie-hex-str($color); - - @return "%23" + string.slice($string: #{$hex}, $start-at: 4); // skip #AA in #AARRGGBB + @return var(--attack-background-color-#{$name}); } diff --git a/attack-style/abstracts/_variables.scss b/attack-style/abstracts/_variables.scss index bdbc472efa0..9d387c8261b 100644 --- a/attack-style/abstracts/_variables.scss +++ b/attack-style/abstracts/_variables.scss @@ -27,25 +27,25 @@ $user-colors: ( /// $colors: ( primary: ( - color: if(config.$use-attack-theme, map.get($attack-colors, attack-orange), map.get($user-colors, user-gray)), + color: if(sass(config.$use-attack-theme): map.get($attack-colors, attack-orange); else: map.get($user-colors, user-gray)), on-color: white ), // used for header and some nav elements secondary: ( - color: if(config.$use-attack-theme, map.get($attack-colors, attack-blue), map.get($user-colors, user-gray)), + color: if(sass(config.$use-attack-theme): map.get($attack-colors, attack-blue); else: map.get($user-colors, user-gray)), on-color: white ), // used for some buttons footer: ( - color: if(config.$use-attack-theme, map.get($attack-colors, attack-footer), map.get($user-colors, user-gray)), + color: if(sass(config.$use-attack-theme): map.get($attack-colors, attack-footer); else: map.get($user-colors, user-gray)), on-color: #87deff ), // used for footer and some buttons active: ( - color: if(config.$use-attack-theme, map.get($attack-colors, attack-active), map.get($user-colors, user-gray)), + color: if(sass(config.$use-attack-theme): map.get($attack-colors, attack-active); else: map.get($user-colors, user-gray)), on-color: #eaeaea ), // used for active buttons and sidebar links @@ -59,7 +59,7 @@ $colors: ( // body: (color: rgb(50, 50, 50), on-color: #cdcdcd), link: ( - color: #4f7cac + color: #3f709e ), // hyperlinks matrix-header: @@ -75,6 +75,7 @@ $colors: ( on-color: black ), deemphasis: ( - color: #303435 + color: #686f75, + on-color: white ) ); diff --git a/attack-style/components/_matrix.scss b/attack-style/components/_matrix.scss index 0804423c173..a49c57de959 100644 --- a/attack-style/components/_matrix.scss +++ b/attack-style/components/_matrix.scss @@ -123,7 +123,7 @@ $sizeunit: 14px; &.count { font-size: $sizeunit - 1px; - border-bottom: 1px solid black; + border-bottom: 1px solid color-functions.border-color(body); padding-bottom: 5px; margin-bottom: 5px; } @@ -247,7 +247,7 @@ $sizeunit: 14px; &.count { font-size: $sizeunit - 1px; - border-bottom: 1px solid black; + border-bottom: 1px solid color-functions.border-color(body); padding-bottom: 5px; margin-bottom: 5px; } @@ -365,9 +365,9 @@ $sizeunit: 14px; // the menu when the user is clicking. Instead use the // bootstrap hover style. &:active { - color: #16181b; + color: color-functions.on-color(body); text-decoration: none; - background-color: #f8f9fa; + background-color: color-functions.color-alternate(body, 0.8); } } } diff --git a/attack-style/components/_search.scss b/attack-style/components/_search.scss index fbcdaba0216..24b6c9a869c 100644 --- a/attack-style/components/_search.scss +++ b/attack-style/components/_search.scss @@ -232,7 +232,7 @@ padding: 2px 8px; border: 1px solid; border-radius: 4px; - color: white; + color: color-functions.on-color(active); font-size: 0.8rem; font-weight: 700; } @@ -245,6 +245,7 @@ .search-result-badge-domain { border-color: color-functions.color(deemphasis); background: color-functions.color(deemphasis); + color: color-functions.on-color(deemphasis); } .search-no-results .preview { diff --git a/attack-style/layout/_footer.scss b/attack-style/layout/_footer.scss index cd7f7f1cbb4..095dfd31644 100644 --- a/attack-style/layout/_footer.scss +++ b/attack-style/layout/_footer.scss @@ -1,4 +1,3 @@ -@use "sass:color"; @use "../abstracts/color-functions"; @use "../abstracts/utilities"; @@ -53,8 +52,7 @@ color: color-functions.on-color(footer); &:hover { - // add some link color to this so that it resembles a link, but is still visible if normal link color is not visible on footer color - color: color.mix(color-functions.on-color(footer), color-functions.color(link)); + color: color-functions.color(footer-link-hover); } } } diff --git a/attack-style/layout/_layout.scss b/attack-style/layout/_layout.scss index 87fbda5516b..d7bbc97be32 100644 --- a/attack-style/layout/_layout.scss +++ b/attack-style/layout/_layout.scss @@ -1,4 +1,3 @@ -@use "sass:color"; @use "sass:math"; @use "../abstracts/color-functions"; @use "../abstracts/utilities"; @@ -65,6 +64,10 @@ strong { a { color: color-functions.color(link); + &:hover { + color: color-functions.color(link-hover); + } + .anchor::before { content: ""; display: block; @@ -133,6 +136,27 @@ a { tr + tr { border-top: 1px solid color-functions.border-color(body); } + + .external-link-icon { + margin-left: utilities.to-rem(2); + font-size: utilities.to-rem(13); + } + + .random-page-toggle { + margin-left: utilities.to-rem(6); + padding: 0 utilities.to-rem(6); + border: 1px solid color-functions.color(active); + color: color-functions.color(link); + background: color-functions.color(body); + + &:hover, + &:focus, + &[aria-expanded="true"] { + border-color: color-functions.color(secondary); + color: color-functions.on-color(secondary); + background: color-functions.color(secondary); + } + } } .row-main-page { @@ -151,7 +175,7 @@ a { // p for home page .p-line { p { - border-top: 0.0625rem solid #1c2226; + border-top: 0.0625rem solid color-functions.border-color(body); } } @@ -187,7 +211,7 @@ a { @extend .website-button; border-color: color-functions.color(active); - color: #fff; + color: color-functions.on-color(active); background: color-functions.color(active); padding: 6px 16px; @@ -200,7 +224,7 @@ a { @extend .website-button; color: color-functions.color(active); - background: #fff; + background: color-functions.color(body); border-color: color-functions.color(active); padding: 6px 16px; @@ -218,15 +242,21 @@ a { padding-left: 8px; } -.slide-button:hover { +.slide-button:hover, +.slide-button:focus, +.slide-button:active, +.slide-button[aria-expanded="true"] { background: color-functions.color(secondary); border-color: color-functions.color(secondary); + color: color-functions.on-color(secondary); } -.slide-button-secondary:hover { - background: color-functions.on-color(active); +.slide-button-secondary:hover, +.slide-button-secondary:focus, +.slide-button-secondary:active { + background: color-functions.color(secondary); border-color: color-functions.color(secondary); - color: color-functions.color(secondary); + color: color-functions.on-color(secondary); } // used for data sources filter dropdown @@ -238,7 +268,7 @@ a { .dropdown-content { display: none; position: absolute; - background-color: color-functions.on-color(secondary); + background-color: color-functions.color(body); min-width: 160px; box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 20%); } @@ -264,7 +294,7 @@ a { // Extending placeholder 'button-style' is necessary to keep on-color white. Else, the on-color will change to the tag on-color when hovering the button. @extend %button-style; - background-color: color.scale(color-functions.color(secondary), $lightness: 5%); + background-color: color-functions.color(secondary-hover); background-image: none; } } @@ -313,7 +343,7 @@ a { } .active { - color: color-functions.color(primary); + color: color-functions.color(active); } } @@ -336,8 +366,8 @@ a { // table for techniques .table-techniques { thead tr { - background: #f2f2f2; - border-bottom: 2px solid #dee2e6; + background: color-functions.color-alternate(body); + border-bottom: 2px solid color-functions.border-color(body); } table { @@ -347,11 +377,11 @@ a { td { vertical-align: top; padding: 10px; - border: 1px solid #dfdfdf; + border: 1px solid color-functions.border-color(body); } tr:last-child { - border-bottom: 1px solid #dfdfdf; + border-bottom: 1px solid color-functions.border-color(body); } .sub.technique { @@ -369,11 +399,11 @@ a { } .sub.technique td:not(:nth-child(4)) { - color: #4f7cac; + color: color-functions.color(link); } .technique:not(.sub) td:not(:nth-child(3)) { - color: #4f7cac; + color: color-functions.color(link); } } @@ -386,11 +416,11 @@ a { td { vertical-align: top; padding: 10px; - border: 1px solid #dfdfdf; + border: 1px solid color-functions.border-color(body); } tr:last-child { - border-bottom: 1px solid #dfdfdf; + border-bottom: 1px solid color-functions.border-color(body); } .sub.technique { @@ -420,8 +450,8 @@ a { .techniques-used.background { thead tr { - background: #f2f2f2; - border-bottom: 2px solid #dee2e6; + background: color-functions.color-alternate(body); + border-bottom: 2px solid color-functions.border-color(body); } } @@ -436,18 +466,18 @@ a { } thead tr { - background: #f2f2f2; - border-bottom: 2px solid #dee2e6; + background: color-functions.color-alternate(body); + border-bottom: 2px solid color-functions.border-color(body); } td { vertical-align: top; padding: 10px; - border: 1px solid #dfdfdf; + border: 1px solid color-functions.border-color(body); } tr:last-child { - border-bottom: 1px solid #dfdfdf; + border-bottom: 1px solid color-functions.border-color(body); } .datacomponent.datasource { @@ -583,9 +613,18 @@ a { /* BANNER */ .banner-message { - padding: utilities.to-rem(5) 0; + padding: utilities.to-rem(7) utilities.to-rem(16); + border-top: 1px solid color-functions.border-color(banner); + border-bottom: 1px solid color-functions.border-color(banner); text-align: center; - background-color: color-functions.color-alternate(body, 2); + color: color-functions.on-color(banner); + background-color: color-functions.color(banner); + + a { + color: inherit; + font-weight: 700; + text-decoration: underline; + } } // basic banner @@ -622,7 +661,7 @@ pre { } code { - color: #c63e1f; + color: color-functions.color(code); } /* **** */ @@ -657,7 +696,7 @@ code { width: 20%; top: 9.3rem; float: right; - background: color-functions.on-color(active); + background: color-functions.color-alternate(body, 2); } @media screen and (width <= 90.62rem) { @@ -680,7 +719,7 @@ code { .card-header { color: color-functions.on-color(body); - background: rgba(color-functions.on-color(body), 0.03); + background: color-functions.color(card-header); border-bottom-color: color-functions.border-color(body); } @@ -738,7 +777,7 @@ a.partial-underline { } &.background { - background: color-functions.on-color(active); + background: color-functions.color-alternate(body, 2); } } } @@ -795,6 +834,10 @@ a.partial-underline { color: color-functions.on-color-emphasis(body); } +.card-data .card-title { + color: color-functions.color(property-label); +} + .contact-card-title { font-size: 1.1rem; font-weight: bold; @@ -814,7 +857,7 @@ a.partial-underline { max-width: 100%; height: utilities.to-rem(480); margin: 0 auto; - border: 3px solid #dfdfdf; + border: 3px solid color-functions.border-color(body); padding: 3px; display: flex; flex-direction: column; @@ -894,7 +937,7 @@ a.partial-underline { .usa-card__header { @include utilities.font("Roboto-Regular"); - color: color-functions.color(body); + color: color-functions.on-color(secondary); background: color-functions.color(secondary); border-bottom-color: color-functions.border-color(body); border-radius: 0.3rem 0.3rem 0 0; @@ -1024,7 +1067,7 @@ img.yt-core-image { display: inline-block; vertical-align: top; background-position: center; - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg fill='#{color-functions.escape-color(color-functions.on-color(body))}' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E"); + background-image: var(--attack-select-arrow); z-index: 1; transition: all 0.2s ease; } @@ -1343,7 +1386,7 @@ img.yt-core-image { } .section-shadow { - border-bottom: 1px solid #dfdfdf !important; + border-bottom: 1px solid color-functions.border-color(body) !important; } table { @@ -1372,6 +1415,19 @@ div#sidebars { .attackcons { border-top-width: 0; + .attackcon-banner-image { + display: inline-block; + width: 100%; + box-sizing: border-box; + + &.on-light { + padding: 1rem; + border: 1px solid color-functions.border-color(body); + border-radius: 0.75rem; + background: color-functions.color(image-background); + } + } + .sponsors { flex: 1; padding-left: 25px; @@ -1382,16 +1438,20 @@ div#sidebars { } .sponsors-block { - background: color-functions.on-color(active); + background: color-functions.color(image-background); text-align: center; display: flex; justify-content: space-evenly; flex-wrap: wrap; flex-direction: column; width: 200%; + padding: utilities.to-rem(10); + border: utilities.to-rem(1) solid color-functions.border-color(body); + border-radius: utilities.to-rem(8); + box-sizing: border-box; .img-container { - margin: 10px; + margin: utilities.to-rem(10); flex: 1 1 20%; box-sizing: border-box; @@ -1434,7 +1494,7 @@ div#sidebars { } img.sponsor-logo { - background-color: color-functions.color(body); + background-color: color-functions.color(image-background); border-radius: 6px; object-fit: contain; object-position: center; @@ -1481,7 +1541,7 @@ div#sidebars { .resource { flex: 1; - background-color: color-functions.on-color(active); + background-color: color-functions.color-alternate(body, 2); padding: 10px; box-sizing: border-box; } @@ -1613,7 +1673,7 @@ div#sidebars { } .tip-box { - background: color-functions.on-color(active); + background: color-functions.color-alternate(body, 2); padding: 1rem; } diff --git a/attack-style/layout/_nav.scss b/attack-style/layout/_nav.scss index 85e321a44c3..d9ff833802b 100644 --- a/attack-style/layout/_nav.scss +++ b/attack-style/layout/_nav.scss @@ -85,12 +85,93 @@ .search-icon { cursor: pointer; - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg fill='#{color-functions.escape-color(color-functions.on-color(primary))}' xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E"); + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%23fff' xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E"); } .error-icon { cursor: default; - background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg fill='#{color-functions.escape-color(color-functions.on-color(primary))}' xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 96 960 960' width='24'%3e%3cpath d='M479.982 776q14.018 0 23.518-9.482 9.5-9.483 9.5-23.5 0-14.018-9.482-23.518-9.483-9.5-23.5-9.5-14.018 0-23.518 9.482-9.5 9.483-9.5 23.5 0 14.018 9.482 23.518 9.483 9.5 23.5 9.5ZM453 623h60V370h-60v253Zm27.266 353q-82.734 0-155.5-31.5t-127.266-86q-54.5-54.5-86-127.341Q80 658.319 80 575.5q0-82.819 31.5-155.659Q143 347 197.5 293t127.341-85.5Q397.681 176 480.5 176q82.819 0 155.659 31.5Q709 239 763 293t85.5 127Q880 493 880 575.734q0 82.734-31.5 155.5T763 858.316q-54 54.316-127 86Q563 976 480.266 976Zm.234-60Q622 916 721 816.5t99-241Q820 434 721.188 335 622.375 236 480 236q-141 0-240.5 98.812Q140 433.625 140 576q0 141 99.5 240.5t241 99.5Zm-.5-340Z'/%3e%3c/svg%3e"); + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg fill='%23fff' xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 96 960 960' width='24'%3e%3cpath d='M479.982 776q14.018 0 23.518-9.482 9.5-9.483 9.5-23.5 0-14.018-9.482-23.518-9.483-9.5-23.5-9.5-14.018 0-23.518 9.482-9.5 9.483-9.5 23.5 0 14.018 9.482 23.518 9.483 9.5 23.5 9.5ZM453 623h60V370h-60v253Zm27.266 353q-82.734 0-155.5-31.5t-127.266-86q-54.5-54.5-86-127.341Q80 658.319 80 575.5q0-82.819 31.5-155.659Q143 347 197.5 293t127.341-85.5Q397.681 176 480.5 176q82.819 0 155.659 31.5Q709 239 763 293t85.5 127Q880 493 880 575.734q0 82.734-31.5 155.5T763 858.316q-54 54.316-127 86Q563 976 480.266 976Zm.234-60Q622 916 721 816.5t99-241Q820 434 721.188 335 622.375 236 480 236q-141 0-240.5 98.812Q140 433.625 140 576q0 141 99.5 240.5t241 99.5Zm-.5-340Z'/%3e%3c/svg%3e"); + } + } + + .theme-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: utilities.to-rem(58); + min-height: utilities.to-rem(38); + margin-right: utilities.to-rem(8); + padding: utilities.to-rem(4); + border: 0; + color: color-functions.on-color(primary); + + .theme-toggle-track { + position: relative; + display: inline-flex; + align-items: center; + justify-content: space-between; + width: utilities.to-rem(50); + height: utilities.to-rem(28); + padding: 0 utilities.to-rem(7); + border: utilities.to-rem(1) solid color-functions.on-color(primary); + border-radius: utilities.to-rem(14); + background: rgb(0 0 0 / 20%); + box-sizing: border-box; + transition: background-color 0.2s ease; + } + + .theme-toggle-icon { + position: relative; + z-index: 2; + visibility: visible; + display: inline-block; + font-size: utilities.to-rem(12); + line-height: 1; + } + + &:hover, + &:focus { + color: color-functions.on-color(primary); + + .theme-toggle-track { + background: rgb(0 0 0 / 35%); + box-shadow: 0 0 0 utilities.to-rem(2) rgb(255 255 255 / 30%); + } + } + + .theme-toggle-icon-light { + color: color-functions.color(primary); + } + + .theme-toggle-thumb { + position: absolute; + top: utilities.to-rem(2); + left: utilities.to-rem(2); + z-index: 1; + width: utilities.to-rem(22); + height: utilities.to-rem(22); + border-radius: 50%; + background: color-functions.on-color(primary); + box-shadow: 0 utilities.to-rem(1) utilities.to-rem(3) rgb(0 0 0 / 35%); + transition: transform 0.2s ease; + } + + &[data-theme-effective="dark"] { + .theme-toggle-track { + background: color-functions.color(secondary); + } + + .theme-toggle-icon-light { + color: color-functions.on-color(primary); + } + + .theme-toggle-icon-dark { + color: color-functions.color(secondary); + } + + .theme-toggle-thumb { + transform: translateX(utilities.to-rem(22)); + } } } } @@ -228,7 +309,7 @@ cursor: col-resize; height: 100%; position: absolute; - background-color: #dfdfdf; + background-color: color-functions.border-color(body); } .data-sources-menu { @@ -286,7 +367,7 @@ .expand-button { // any direct child cursor: pointer; - color: black; + color: color-functions.on-color(body); &:hover { background: color-functions.color-alternate(body); @@ -320,7 +401,7 @@ & > a { color: color-functions.color(active) !important; font-weight: bolder; - background: color-functions.on-color(active); + background: color-functions.color-alternate(body, 2); font-family: Roboto-Bold, sans-serif; } diff --git a/attack-style/package.json b/attack-style/package.json index 490ad27ae26..d84729c00b3 100644 --- a/attack-style/package.json +++ b/attack-style/package.json @@ -8,8 +8,8 @@ "main": "index.js", "scripts": { "clean": "rm -rf dist/", - "build": "sass style-attack.scss dist/style-attack.css && sass style-user.scss dist/style-user.css", - "copy": "cp dist/style-attack.css dist/style-user.css ../attack-theme/static/", + "build": "sass style-attack.scss dist/style-attack.css && sass style-user.scss dist/style-user.css && sass style-archive.scss dist/style-archive.css", + "copy": "cp dist/style-attack.css dist/style-user.css dist/style-archive.css ../attack-theme/static/", "build-copy": "npm run clean && npm run build && npm run copy", "watch": "sass --watch style-attack.scss dist/style-attack.css && sass --watch style-user.scss dist/style-user.css", "lint": "stylelint **/*.scss" diff --git a/attack-style/style-archive.scss b/attack-style/style-archive.scss new file mode 100644 index 00000000000..3cb39748fbf --- /dev/null +++ b/attack-style/style-archive.scss @@ -0,0 +1,3 @@ +// Compatibility colors for preserved ATT&CK sites; keep their original layout. +@use "config" with ($use-attack-theme: true); +@use "themes/archive"; diff --git a/attack-style/style.scss b/attack-style/style.scss index c8c19234ddd..77fe3a8e71d 100644 --- a/attack-style/style.scss +++ b/attack-style/style.scss @@ -21,4 +21,7 @@ @use "components/search"; // Search component styles @use "components/tour"; // Tour component styles @use "components/matrix"; // Matrix component styles -@use "components/versioning"; // Versioning component styles \ No newline at end of file +@use "components/versioning"; // Versioning component styles + +// Emit runtime light/dark palettes and normalize vendor components last. +@use "themes/colors"; diff --git a/attack-style/themes/README.md b/attack-style/themes/README.md index bca278889a1..3f72b0b21db 100644 --- a/attack-style/themes/README.md +++ b/attack-style/themes/README.md @@ -1,21 +1,32 @@ # Themes -This folder is reserved for theme-specific Sass. +This folder contains the runtime color palettes used by the generated stylesheets. ## Files | File | Purpose | | --- | --- | -| `_colors.scss` | Reserved for theme color overrides or extracted theme color definitions. It is currently empty. | +| `_palette.scss` | Shared light/dark token mixins, including the charcoal surfaces. | +| `_archive.scss` | Scoped color compatibility rules and a banner switch for preserved sites. | +| `_colors.scss` | Emits the light and dark semantic color tokens, system preference behavior, explicit theme overrides, and shared Bootstrap surface adjustments. | ## Active Theme Switch -The active color set is currently controlled by `config.scss` and the two top-level entrypoints: +The site has two independent theme layers. The brand layer is selected at build time by `config.scss` and the two top-level entrypoints: | File | Behavior | | --- | --- | | `style-attack.scss` | Sets `$use-attack-theme: true` and imports the shared style graph. | | `style-user.scss` | Sets `$use-attack-theme: false` and imports the shared style graph. | -Most theme-aware styling should continue to use semantic color helpers from `abstracts/_color-functions.scss`. -Add theme-specific Sass here only when the existing semantic color map is not enough. +The light/dark appearance is selected at runtime. With no `data-theme` attribute on the root element, the stylesheet follows `prefers-color-scheme`. The single navigation toggle stores an explicit `data-theme="light"` or `data-theme="dark"` override after the user first switches themes. + +Most styling should use semantic color helpers from `abstracts/_color-functions.scss`. Add a token to `_palette.scss` when a component needs a distinct theme-aware surface or foreground instead of embedding a light-only color in that component. + +## Preserved Sites + +`style-archive.scss` builds `style-archive.css` alongside the two current-site stylesheets. The versions deployment module adds this stylesheet and the shared theme script to extracted HTML pages with an archive banner. It skips sidebar fragments, redirects, and pages with native theme controls. The stored tarballs are unchanged; every extraction source receives the same idempotent enhancement. + +The script adds a keyboard-accessible “Dark mode” switch to the existing `.version-banner`. It shares the current site's stored appearance preference. The archive stylesheet supplies color overrides only in dark screen mode; light mode and print retain the archive's original presentation. The archive's original styles, content, URLs, logos, and external widgets remain in place. + +Theme scripts set `data-theme` only. CSS owns `color-scheme`, allowing print to use light native controls even when a dark preference is saved. diff --git a/attack-style/themes/_archive.scss b/attack-style/themes/_archive.scss new file mode 100644 index 00000000000..e0e07aba3f2 --- /dev/null +++ b/attack-style/themes/_archive.scss @@ -0,0 +1,337 @@ +@use "palette"; + +@mixin dark-surfaces { + @include palette.dark-colors; + + color-scheme: dark; + + body, + .jumbotron, + .card, + .card-filter, + .card-body, + .contact-card .card-header.no-background, + .contact-card .card-footer.no-background, + .sidebar, + .sidebar .heading, + .matrix-container, + .matrix .technique-cell, + .table-matrix td, + .dropdown-content, + .dropdown-menu, + .form-control, + .custom-select, + .bootstrap-select > .dropdown-toggle, + .modal-content, + .popover, + .popover-body, + .search-results, + .overlay.search .overlay-inner, + .overlay.search .overlay-inner .search-header .search-input input { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body); + border-color: var(--attack-border-color-body); + } + + .table, + .table-light, + .table td, + .table th, + .card-data, + .getting-started-color, + .table-techniques td, + .techniques-used td, + .datasources-table td { + color: var(--attack-on-color-body); + border-color: var(--attack-border-color-body); + } + + a:where(:not(.osano-cm-link)), + .dropdown-item, + .matrix-tactics-url, + .matrix-tactics-url:visited, + .matrix-tactics-url:hover, + .matrix-tactics-url:active, + .table-techniques .sub.technique td:not(:nth-child(4)), + .table-techniques .technique:not(.sub) td:not(:nth-child(3)) { + color: var(--attack-color-link); + } + + .matrix-header { + color: var(--attack-on-color-body-emphasis); + } + + .table-light, + .table-matrix .matrix-header, + .table-alternate tbody, + .blog-post table tbody, + .changelog table tbody { + background-color: var(--attack-color-body); + } + + .bg-white { + background-color: var(--attack-color-body) !important; + } + + .bg-alternate, + .bg-gray, + .bg-light, + .bg-accord-light, + .bg-accord-dark, + .table-alternate, + .table-techniques thead tr, + .techniques-used thead tr, + .datasources-table thead tr, + .table-striped tbody tr:nth-of-type(odd), + .card-header, + .contact-card .card-body.background, + .breadcrumb, + .resource, + .tip-box, + .under-development, + .training .exercise, + .example-container, + .section-view .anchor-section { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body-alternate) !important; + border-color: var(--attack-border-color-body); + } + + .table .active, + .search-results .search-header, + .search-results .search-highlight, + .nav-link.side.active { + color: var(--attack-color-active); + } + + .nav .heading-dropdown, + .faq .heading-dropdown, + .faq .nav-link.expand-title, + .nav .nav-link.expand-title, + .expand-icon, + .card-title { + color: var(--attack-on-color-body-emphasis); + } + + .version-banner, + .version-banner a { + color: var(--attack-on-color-banner); + background-color: var(--attack-color-banner); + } + + .version-banner a { + text-decoration: underline; + } + + .footer a, + .footer .footer-link { + color: var(--attack-on-color-footer); + } + + .table-hover tbody tr:hover, + .dropdown-item:hover, + .dropdown-item:focus, + .nav-link.side:hover, + .sidenav .sidenav-head a:hover, + .sidenav .sidenav-head .expand-button:hover { + color: var(--attack-on-color-body-emphasis); + background-color: var(--attack-color-body-alternate-strong); + } + + .sidenav .sidenav-head.active, + .sidenav .sidenav-head.active > a { + color: var(--attack-color-active) !important; + background-color: var(--attack-color-body-alternate-strong); + } + + .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head a, + .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head .expand-button { + color: var(--attack-on-color-body-emphasis); + } + + .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head a:hover, + .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head .expand-button:hover { + background-color: var(--attack-color-body-alternate-strong); + } + + .nav .nav-link.side.active, + .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head.active, + .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head.active > a { + color: var(--attack-color-active) !important; + background-color: var(--attack-color-body-alternate-strong); + } + + .card-data .card-title { + color: var(--attack-color-property-label); + } + + .deemphasis, + .text-label-small, + .text-muted { + color: var(--attack-on-color-body-deemphasis) !important; + } + + .text-danger, + font[color="red"] { + color: var(--attack-color-danger) !important; + } + + .search-word-found, + mark { + color: var(--attack-on-color-search-highlight); + background-color: var(--attack-color-search-highlight); + } + + .custom-select, + .card-block .card-header::after, + .faq .card-header::after, + .heading-dropdown::after, + .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head .expand-button::after, + #usecases .card-header::after { + background-image: var(--attack-select-arrow); + } + + .resizer { + background-color: var(--attack-border-color-body); + } + + code { + color: var(--attack-color-code); + } + + pre, + .jumbotron code { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body-alternate-strongest); + } + + .btn-default, + .btn-outline-secondary, + .matrix-controls button, + .matrix-controls .layout-button:active, + .slide-button-secondary { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body); + border-color: var(--attack-border-color-body); + } + + .btn-primary, + .footer .btn-primary { + color: var(--attack-on-color-active); + background-color: var(--attack-color-active); + border-color: var(--attack-color-active); + } + + .btn-default:hover, + .btn-outline-secondary:not(:disabled, .disabled):hover, + .btn-outline-secondary:not(:disabled, .disabled):focus, + .matrix-controls button:hover, + .slide-button { + color: var(--attack-on-color-active); + background-color: var(--attack-color-active); + border-color: var(--attack-color-active); + } + + .matrix.side .tactic .handle, + .matrix.flat .tactic .supertechnique td.sidebar.technique .handle { + color: var(--attack-color-body); + background-color: var(--attack-on-color-body-deemphasis); + } + + .matrix.side .sidebar.expanded .angle, + .matrix.side .tactic .sidebar.expanded .angle { + background-color: var(--attack-color-body); + } + + .matrix.side .tactic:hover:not(.name, .count), + .matrix.side .tactic:hover:not(.name, .count) .sidebar.expanded .angle { + background-color: var(--attack-background-color-body); + } + + .matrix-container .scroll-indicator-group .scroll-indicator.right.show .cover { + background: linear-gradient(to right, rgb(255 255 255 / 0.1%), var(--attack-color-body)); + } + + .matrix-container .scroll-indicator-group .scroll-indicator.left.show .cover { + background: linear-gradient(to left, rgb(255 255 255 / 0.1%), var(--attack-color-body)); + } + + .matrix .tactic.count, + .matrix .technique-cell, + .resizer, + hr { + border-color: var(--attack-border-color-body); + } + + // Header/footer retain their original branding. Do not recolor images or + // third-party widgets; their own palettes and functionality belong to them. + .navbar-orange .nav-link, + .navbar .nav-tabs .nav-link, + .nav .dropdown-menu .dropdown-item { + color: white; + } + + .nav .dropdown-menu { + background-color: var(--attack-color-primary); + } + +} + +:root[data-archive-theme] { + @include palette.light-colors; + + color-scheme: light; + + .archive-theme-toggle { + display: inline-flex; + align-items: center; + gap: 0.5em; + margin: 0.25em 0.75em; + padding: 0.35em 0.65em; + min-height: 2.75em; + border: 1px solid currentcolor; + border-radius: 0.3em; + color: inherit; + background: transparent; + font: inherit; + cursor: pointer; + + &::after { + content: ""; + width: 2em; + height: 1em; + border: 1px solid currentcolor; + border-radius: 1em; + background: radial-gradient(circle at 0.5em center, currentcolor 0.3em, transparent 0.35em); + } + + &[aria-checked="true"]::after { + background: radial-gradient(circle at 1.5em center, currentcolor 0.3em, transparent 0.35em); + } + + &:focus-visible { + outline: 2px solid currentcolor; + outline-offset: 3px; + } + } +} + +// Restrict all compatibility overrides to the dark screen appearance. Light +// and print retain the archive's original CSS, including native controls. +@media screen { + :root[data-archive-theme][data-theme="dark"] { + @include dark-surfaces; + } +} + +@media screen and (prefers-color-scheme: dark) { + :root[data-archive-theme]:not([data-theme]) { + @include dark-surfaces; + } +} + +@media print { + :root[data-archive-theme] .archive-theme-toggle { + display: none; + } +} diff --git a/attack-style/themes/_colors.scss b/attack-style/themes/_colors.scss index e69de29bb2d..5a96b3ceaea 100644 --- a/attack-style/themes/_colors.scss +++ b/attack-style/themes/_colors.scss @@ -0,0 +1,130 @@ +@use "palette"; + +:root, +:root[data-theme="light"] { + @include palette.light-colors; + + color-scheme: light; +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme]) { + @include palette.dark-colors; + + color-scheme: dark; + } +} + +:root[data-theme="dark"] { + @include palette.dark-colors; + + color-scheme: dark; +} + +// Normalize Bootstrap surfaces that otherwise retain hard-coded light colors. +.form-control, +.custom-select, +.bootstrap-select > .dropdown-toggle, +.dropdown-menu, +.list-group-item, +.modal-content, +.popover, +.popover-body, +.page-link, +.input-group-text { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body); + border-color: var(--attack-border-color-body); +} + +.custom-select { + background-image: var(--attack-select-arrow); +} + +.form-control:focus, +.custom-select:focus { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body); + border-color: var(--attack-color-active); + box-shadow: 0 0 0 0.2rem rgb(76 159 254 / 25%); +} + +.dropdown-item, +.page-link { + color: var(--attack-color-link); +} + +.dropdown-item:hover, +.dropdown-item:focus, +.page-link:hover, +.page-link:focus { + color: var(--attack-on-color-body-emphasis); + background-color: var(--attack-color-body-alternate); +} + +.form-control:disabled, +.form-control[readonly], +.custom-select:disabled, +.page-item.disabled .page-link { + color: var(--attack-on-color-body-deemphasis); + background-color: var(--attack-color-body-alternate); +} + +.dropdown-divider, +hr { + border-color: var(--attack-border-color-body); +} + +.table { + color: var(--attack-on-color-body); +} + +.text-danger { + color: var(--attack-color-danger) !important; +} + +.btn-outline-secondary { + color: var(--attack-on-color-body); + border-color: var(--attack-on-color-body-deemphasis); + + &:disabled, + &.disabled { + color: var(--attack-on-color-body-deemphasis); + background-color: transparent; + } + + &:not(:disabled, .disabled):hover, + &:not(:disabled, .disabled):focus, + &:not(:disabled, .disabled):active { + color: var(--attack-on-color-active); + background-color: var(--attack-color-active); + border-color: var(--attack-color-active); + } +} + +.nav-tabs .nav-link { + color: var(--attack-color-link); + border-color: transparent; +} + +.nav-tabs .nav-link:hover, +.nav-tabs .nav-link:focus { + border-color: var(--attack-border-color-body); +} + +.nav-tabs .nav-link.active, +.nav-tabs .nav-item.show .nav-link { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body); + border-color: var(--attack-border-color-body) var(--attack-border-color-body) var(--attack-color-body); +} + +@media print { + :root, + :root[data-theme], + :root:not([data-theme]) { + @include palette.light-colors; + + color-scheme: light; + } +} diff --git a/attack-style/themes/_palette.scss b/attack-style/themes/_palette.scss new file mode 100644 index 00000000000..d074e1ce87f --- /dev/null +++ b/attack-style/themes/_palette.scss @@ -0,0 +1,92 @@ +@use "sass:color"; +@use "sass:map"; +@use "../abstracts/variables"; +@use "../config" as config; + +$primary: map.get(map.get(variables.$colors, primary), color); +$secondary: map.get(map.get(variables.$colors, secondary), color); +$footer: map.get(map.get(variables.$colors, footer), color); +$active: map.get(map.get(variables.$colors, active), color); +$dark-active: if(sass(config.$use-attack-theme): #60a9ff; else: #9aa3a6); + +@mixin light-colors { + --attack-color-primary: #{$primary}; + --attack-on-color-primary: white; + --attack-color-secondary: #{$secondary}; + --attack-color-secondary-hover: #{color.scale($secondary, $lightness: 5%)}; + --attack-on-color-secondary: white; + --attack-color-footer: #{$footer}; + --attack-on-color-footer: #87deff; + --attack-color-footer-link-hover: #{color.mix(#87deff, #3f709e)}; + --attack-color-active: #{$active}; + --attack-color-active-alternate-medium: #{color.mix(color.invert($active), $active, $weight: 7.5%)}; + --attack-on-color-active: #eaeaea; + --attack-color-body: white; + --attack-on-color-body: #39434c; + --attack-on-color-body-emphasis: #1d2226; + --attack-color-property-label: #1d2226; + --attack-on-color-body-deemphasis: #6b7379; + --attack-color-body-alternate-subtle: #f5f5f5; + --attack-color-body-alternate: #f2f2f2; + --attack-color-body-alternate-strong: #e6e6e6; + --attack-color-body-alternate-strongest: #d9d9d9; + --attack-border-color-body: #dfdfdf; + --attack-background-color-body: #dfdfdf; + --attack-color-link: #3f709e; + --attack-color-link-hover: #0056b3; + --attack-color-matrix-header: gray; + --attack-on-color-matrix-header: white; + --attack-color-search-highlight: yellow; + --attack-on-color-search-highlight: black; + --attack-color-deemphasis: #686f75; + --attack-on-color-deemphasis: white; + --attack-color-card-header: rgb(57 67 76 / 3%); + --attack-color-code: #a52f16; + --attack-color-danger: #bd2130; + --attack-color-image-background: white; + --attack-color-banner: #e7f0f6; + --attack-on-color-banner: #263b4a; + --attack-border-color-banner: #c2d5e2; + --attack-select-arrow: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%2339434c' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z'/%3E%3C/svg%3E"); +} + +@mixin dark-colors { + --attack-color-primary: #{$primary}; + --attack-on-color-primary: white; + --attack-color-secondary: #{$secondary}; + --attack-color-secondary-hover: #{color.scale($secondary, $lightness: 8%)}; + --attack-on-color-secondary: white; + --attack-color-footer: #{$footer}; + --attack-on-color-footer: #87deff; + --attack-color-footer-link-hover: #b7edff; + --attack-color-active: #{$dark-active}; + --attack-color-active-alternate-medium: #{if(sass(config.$use-attack-theme): #3d8cdb; else: #879195)}; + --attack-on-color-active: #0f171c; + --attack-color-body: #222426; + --attack-on-color-body: #e8e6e3; + --attack-on-color-body-emphasis: #fffaf4; + --attack-color-property-label: #f2d2a4; + --attack-on-color-body-deemphasis: #b7b1a8; + --attack-color-body-alternate-subtle: #272a2c; + --attack-color-body-alternate: #2b2e30; + --attack-color-body-alternate-strong: #303437; + --attack-color-body-alternate-strongest: #353a3d; + --attack-border-color-body: #596166; + --attack-background-color-body: #373d40; + --attack-color-link: #7bb8ee; + --attack-color-link-hover: #b7ddff; + --attack-color-matrix-header: #596166; + --attack-on-color-matrix-header: #fffaf4; + --attack-color-search-highlight: #665a00; + --attack-on-color-search-highlight: #fff4b8; + --attack-color-deemphasis: #b7b1a8; + --attack-on-color-deemphasis: #222426; + --attack-color-card-header: #2b2e30; + --attack-color-code: #ff8f70; + --attack-color-danger: #ff8c96; + --attack-color-image-background: white; + --attack-color-banner: #263a49; + --attack-on-color-banner: #f2f7fa; + --attack-border-color-banner: #3f5d72; + --attack-select-arrow: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%23e8e6e3' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z'/%3E%3C/svg%3E"); +} diff --git a/attack-theme/static/scripts/theme.js b/attack-theme/static/scripts/theme.js new file mode 100644 index 00000000000..7f8365a836d --- /dev/null +++ b/attack-theme/static/scripts/theme.js @@ -0,0 +1,164 @@ +(function(globalObject, factory) { + const theme = factory(); + + if (typeof module === 'object' && module.exports) { + module.exports = theme; + } + + if (globalObject && globalObject.document) { + let storage = null; + + try { + storage = globalObject.localStorage; + } catch (error) { + // Accessing localStorage itself can fail in restricted contexts. + } + + theme.bootstrap({ + document: globalObject.document, + archived: !!(globalObject.document.currentScript + && globalObject.document.currentScript.hasAttribute('data-archive-theme')), + mediaQuery: typeof globalObject.matchMedia === 'function' + ? globalObject.matchMedia('(prefers-color-scheme: dark)') + : null, + storage + }); + } +}(typeof window !== 'undefined' ? window : null, function() { + const STORAGE_KEY = 'attack-website-theme'; + const EXPLICIT_PREFERENCES = ['light', 'dark']; + + function isExplicitPreference(preference) { + return EXPLICIT_PREFERENCES.includes(preference); + } + + function readStoredPreference(storage) { + if (!storage) return 'system'; + + try { + const storedPreference = storage.getItem(STORAGE_KEY); + if (isExplicitPreference(storedPreference)) return storedPreference; + if (storedPreference !== null) storage.removeItem(STORAGE_KEY); + } catch (error) { + // Storage may be unavailable in privacy-restricted browser contexts. + } + + return 'system'; + } + + function savePreference(storage, preference) { + if (!storage) return; + + try { + if (isExplicitPreference(preference)) { + storage.setItem(STORAGE_KEY, preference); + } else { + storage.removeItem(STORAGE_KEY); + } + } catch (error) { + // The in-page choice still works when storage is unavailable. + } + } + + function applyPreference(root, preference) { + if (isExplicitPreference(preference)) { + root.setAttribute('data-theme', preference); + } else { + root.removeAttribute('data-theme'); + } + } + + function effectiveTheme(preference, mediaQuery) { + if (isExplicitPreference(preference)) return preference; + return mediaQuery && mediaQuery.matches ? 'dark' : 'light'; + } + + function createThemeController({ document, storage, mediaQuery, archived = false }) { + const systemTheme = mediaQuery || { matches: false }; + let preference = readStoredPreference(storage); + let toggle; + + function updateControl() { + if (!toggle) return; + + const currentTheme = effectiveTheme(preference, systemTheme); + const nextTheme = currentTheme === 'dark' ? 'light' : 'dark'; + const label = `Switch to ${nextTheme} mode`; + toggle.setAttribute('aria-label', archived ? 'Dark mode' : label); + toggle.setAttribute('title', label); + toggle.setAttribute('aria-checked', String(currentTheme === 'dark')); + toggle.setAttribute('data-theme-effective', currentTheme); + } + + function setPreference(nextPreference) { + preference = isExplicitPreference(nextPreference) ? nextPreference : 'system'; + applyPreference(document.documentElement, preference); + savePreference(storage, preference); + updateControl(); + } + + function handleSystemThemeChange() { + if (preference === 'system') updateControl(); + } + + function init() { + applyPreference(document.documentElement, preference); + toggle = document.querySelector('[data-theme-toggle]'); + + if (archived && !toggle) { + const banner = document.querySelector('.version-banner'); + if (banner) { + toggle = document.createElement('button'); + toggle.className = 'archive-theme-toggle'; + toggle.type = 'button'; + toggle.setAttribute('role', 'switch'); + toggle.setAttribute('data-theme-toggle', ''); + toggle.textContent = 'Dark mode'; + banner.appendChild(toggle); + } + } + + if (toggle) { + toggle.addEventListener('click', event => { + event.preventDefault(); + const currentTheme = effectiveTheme(preference, systemTheme); + setPreference(currentTheme === 'dark' ? 'light' : 'dark'); + }); + } + + if (typeof systemTheme.addEventListener === 'function') { + systemTheme.addEventListener('change', handleSystemThemeChange); + } else if (typeof systemTheme.addListener === 'function') { + systemTheme.addListener(handleSystemThemeChange); + } + + updateControl(); + } + + return { init, setPreference }; + } + + function bootstrap({ document, storage, mediaQuery, archived = false }) { + if (archived) document.documentElement.setAttribute('data-archive-theme', ''); + const preference = readStoredPreference(storage); + applyPreference(document.documentElement, preference); + const controller = createThemeController({ document, storage, mediaQuery, archived }); + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', controller.init, { once: true }); + } else { + controller.init(); + } + + return controller; + } + + return { + STORAGE_KEY, + applyPreference, + bootstrap, + createThemeController, + effectiveTheme, + readStoredPreference + }; +})); diff --git a/attack-theme/static/style-archive.css b/attack-theme/static/style-archive.css new file mode 100644 index 00000000000..141e6689a8d --- /dev/null +++ b/attack-theme/static/style-archive.css @@ -0,0 +1,629 @@ +/* COLORS */ +:root[data-archive-theme] { + --attack-color-primary: #c63f1f; + --attack-on-color-primary: white; + --attack-color-secondary: #062f4f; + --attack-color-secondary-hover: rgb(7.5, 58.75, 98.75); + --attack-on-color-secondary: white; + --attack-color-footer: #0b2338; + --attack-on-color-footer: #87deff; + --attack-color-footer-link-hover: rgb(99, 167, 206.5); + --attack-color-active: #0156b3; + --attack-color-active-alternate-medium: rgb(19.975, 92.225, 171.275); + --attack-on-color-active: #eaeaea; + --attack-color-body: white; + --attack-on-color-body: #39434c; + --attack-on-color-body-emphasis: #1d2226; + --attack-color-property-label: #1d2226; + --attack-on-color-body-deemphasis: #6b7379; + --attack-color-body-alternate-subtle: #f5f5f5; + --attack-color-body-alternate: #f2f2f2; + --attack-color-body-alternate-strong: #e6e6e6; + --attack-color-body-alternate-strongest: #d9d9d9; + --attack-border-color-body: #dfdfdf; + --attack-background-color-body: #dfdfdf; + --attack-color-link: #3f709e; + --attack-color-link-hover: #0056b3; + --attack-color-matrix-header: gray; + --attack-on-color-matrix-header: white; + --attack-color-search-highlight: yellow; + --attack-on-color-search-highlight: black; + --attack-color-deemphasis: #686f75; + --attack-on-color-deemphasis: white; + --attack-color-card-header: rgb(57 67 76 / 3%); + --attack-color-code: #a52f16; + --attack-color-danger: #bd2130; + --attack-color-image-background: white; + --attack-color-banner: #e7f0f6; + --attack-on-color-banner: #263b4a; + --attack-border-color-banner: #c2d5e2; + --attack-select-arrow: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%2339434c' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z'/%3E%3C/svg%3E"); + color-scheme: light; +} +:root[data-archive-theme] .archive-theme-toggle { + display: inline-flex; + align-items: center; + gap: 0.5em; + margin: 0.25em 0.75em; + padding: 0.35em 0.65em; + min-height: 2.75em; + border: 1px solid currentcolor; + border-radius: 0.3em; + color: inherit; + background: transparent; + font: inherit; + cursor: pointer; +} +:root[data-archive-theme] .archive-theme-toggle::after { + content: ""; + width: 2em; + height: 1em; + border: 1px solid currentcolor; + border-radius: 1em; + background: radial-gradient(circle at 0.5em center, currentcolor 0.3em, transparent 0.35em); +} +:root[data-archive-theme] .archive-theme-toggle[aria-checked=true]::after { + background: radial-gradient(circle at 1.5em center, currentcolor 0.3em, transparent 0.35em); +} +:root[data-archive-theme] .archive-theme-toggle:focus-visible { + outline: 2px solid currentcolor; + outline-offset: 3px; +} + +@media screen { + :root[data-archive-theme][data-theme=dark] { + --attack-color-primary: #c63f1f; + --attack-on-color-primary: white; + --attack-color-secondary: #062f4f; + --attack-color-secondary-hover: rgb(8.4, 65.8, 110.6); + --attack-on-color-secondary: white; + --attack-color-footer: #0b2338; + --attack-on-color-footer: #87deff; + --attack-color-footer-link-hover: #b7edff; + --attack-color-active: #60a9ff; + --attack-color-active-alternate-medium: #3d8cdb; + --attack-on-color-active: #0f171c; + --attack-color-body: #222426; + --attack-on-color-body: #e8e6e3; + --attack-on-color-body-emphasis: #fffaf4; + --attack-color-property-label: #f2d2a4; + --attack-on-color-body-deemphasis: #b7b1a8; + --attack-color-body-alternate-subtle: #272a2c; + --attack-color-body-alternate: #2b2e30; + --attack-color-body-alternate-strong: #303437; + --attack-color-body-alternate-strongest: #353a3d; + --attack-border-color-body: #596166; + --attack-background-color-body: #373d40; + --attack-color-link: #7bb8ee; + --attack-color-link-hover: #b7ddff; + --attack-color-matrix-header: #596166; + --attack-on-color-matrix-header: #fffaf4; + --attack-color-search-highlight: #665a00; + --attack-on-color-search-highlight: #fff4b8; + --attack-color-deemphasis: #b7b1a8; + --attack-on-color-deemphasis: #222426; + --attack-color-card-header: #2b2e30; + --attack-color-code: #ff8f70; + --attack-color-danger: #ff8c96; + --attack-color-image-background: white; + --attack-color-banner: #263a49; + --attack-on-color-banner: #f2f7fa; + --attack-border-color-banner: #3f5d72; + --attack-select-arrow: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%23e8e6e3' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z'/%3E%3C/svg%3E"); + color-scheme: dark; + } + :root[data-archive-theme][data-theme=dark] body, + :root[data-archive-theme][data-theme=dark] .jumbotron, + :root[data-archive-theme][data-theme=dark] .card, + :root[data-archive-theme][data-theme=dark] .card-filter, + :root[data-archive-theme][data-theme=dark] .card-body, + :root[data-archive-theme][data-theme=dark] .contact-card .card-header.no-background, + :root[data-archive-theme][data-theme=dark] .contact-card .card-footer.no-background, + :root[data-archive-theme][data-theme=dark] .sidebar, + :root[data-archive-theme][data-theme=dark] .sidebar .heading, + :root[data-archive-theme][data-theme=dark] .matrix-container, + :root[data-archive-theme][data-theme=dark] .matrix .technique-cell, + :root[data-archive-theme][data-theme=dark] .table-matrix td, + :root[data-archive-theme][data-theme=dark] .dropdown-content, + :root[data-archive-theme][data-theme=dark] .dropdown-menu, + :root[data-archive-theme][data-theme=dark] .form-control, + :root[data-archive-theme][data-theme=dark] .custom-select, + :root[data-archive-theme][data-theme=dark] .bootstrap-select > .dropdown-toggle, + :root[data-archive-theme][data-theme=dark] .modal-content, + :root[data-archive-theme][data-theme=dark] .popover, + :root[data-archive-theme][data-theme=dark] .popover-body, + :root[data-archive-theme][data-theme=dark] .search-results, + :root[data-archive-theme][data-theme=dark] .overlay.search .overlay-inner, + :root[data-archive-theme][data-theme=dark] .overlay.search .overlay-inner .search-header .search-input input { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body); + border-color: var(--attack-border-color-body); + } + :root[data-archive-theme][data-theme=dark] .table, + :root[data-archive-theme][data-theme=dark] .table-light, + :root[data-archive-theme][data-theme=dark] .table td, + :root[data-archive-theme][data-theme=dark] .table th, + :root[data-archive-theme][data-theme=dark] .card-data, + :root[data-archive-theme][data-theme=dark] .getting-started-color, + :root[data-archive-theme][data-theme=dark] .table-techniques td, + :root[data-archive-theme][data-theme=dark] .techniques-used td, + :root[data-archive-theme][data-theme=dark] .datasources-table td { + color: var(--attack-on-color-body); + border-color: var(--attack-border-color-body); + } + :root[data-archive-theme][data-theme=dark] a:where(:not(.osano-cm-link)), + :root[data-archive-theme][data-theme=dark] .dropdown-item, + :root[data-archive-theme][data-theme=dark] .matrix-tactics-url, + :root[data-archive-theme][data-theme=dark] .matrix-tactics-url:visited, + :root[data-archive-theme][data-theme=dark] .matrix-tactics-url:hover, + :root[data-archive-theme][data-theme=dark] .matrix-tactics-url:active, + :root[data-archive-theme][data-theme=dark] .table-techniques .sub.technique td:not(:nth-child(4)), + :root[data-archive-theme][data-theme=dark] .table-techniques .technique:not(.sub) td:not(:nth-child(3)) { + color: var(--attack-color-link); + } + :root[data-archive-theme][data-theme=dark] .matrix-header { + color: var(--attack-on-color-body-emphasis); + } + :root[data-archive-theme][data-theme=dark] .table-light, + :root[data-archive-theme][data-theme=dark] .table-matrix .matrix-header, + :root[data-archive-theme][data-theme=dark] .table-alternate tbody, + :root[data-archive-theme][data-theme=dark] .blog-post table tbody, + :root[data-archive-theme][data-theme=dark] .changelog table tbody { + background-color: var(--attack-color-body); + } + :root[data-archive-theme][data-theme=dark] .bg-white { + background-color: var(--attack-color-body) !important; + } + :root[data-archive-theme][data-theme=dark] .bg-alternate, + :root[data-archive-theme][data-theme=dark] .bg-gray, + :root[data-archive-theme][data-theme=dark] .bg-light, + :root[data-archive-theme][data-theme=dark] .bg-accord-light, + :root[data-archive-theme][data-theme=dark] .bg-accord-dark, + :root[data-archive-theme][data-theme=dark] .table-alternate, + :root[data-archive-theme][data-theme=dark] .table-techniques thead tr, + :root[data-archive-theme][data-theme=dark] .techniques-used thead tr, + :root[data-archive-theme][data-theme=dark] .datasources-table thead tr, + :root[data-archive-theme][data-theme=dark] .table-striped tbody tr:nth-of-type(odd), + :root[data-archive-theme][data-theme=dark] .card-header, + :root[data-archive-theme][data-theme=dark] .contact-card .card-body.background, + :root[data-archive-theme][data-theme=dark] .breadcrumb, + :root[data-archive-theme][data-theme=dark] .resource, + :root[data-archive-theme][data-theme=dark] .tip-box, + :root[data-archive-theme][data-theme=dark] .under-development, + :root[data-archive-theme][data-theme=dark] .training .exercise, + :root[data-archive-theme][data-theme=dark] .example-container, + :root[data-archive-theme][data-theme=dark] .section-view .anchor-section { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body-alternate) !important; + border-color: var(--attack-border-color-body); + } + :root[data-archive-theme][data-theme=dark] .table .active, + :root[data-archive-theme][data-theme=dark] .search-results .search-header, + :root[data-archive-theme][data-theme=dark] .search-results .search-highlight, + :root[data-archive-theme][data-theme=dark] .nav-link.side.active { + color: var(--attack-color-active); + } + :root[data-archive-theme][data-theme=dark] .nav .heading-dropdown, + :root[data-archive-theme][data-theme=dark] .faq .heading-dropdown, + :root[data-archive-theme][data-theme=dark] .faq .nav-link.expand-title, + :root[data-archive-theme][data-theme=dark] .nav .nav-link.expand-title, + :root[data-archive-theme][data-theme=dark] .expand-icon, + :root[data-archive-theme][data-theme=dark] .card-title { + color: var(--attack-on-color-body-emphasis); + } + :root[data-archive-theme][data-theme=dark] .version-banner, + :root[data-archive-theme][data-theme=dark] .version-banner a { + color: var(--attack-on-color-banner); + background-color: var(--attack-color-banner); + } + :root[data-archive-theme][data-theme=dark] .version-banner a { + text-decoration: underline; + } + :root[data-archive-theme][data-theme=dark] .footer a, + :root[data-archive-theme][data-theme=dark] .footer .footer-link { + color: var(--attack-on-color-footer); + } + :root[data-archive-theme][data-theme=dark] .table-hover tbody tr:hover, + :root[data-archive-theme][data-theme=dark] .dropdown-item:hover, + :root[data-archive-theme][data-theme=dark] .dropdown-item:focus, + :root[data-archive-theme][data-theme=dark] .nav-link.side:hover, + :root[data-archive-theme][data-theme=dark] .sidenav .sidenav-head a:hover, + :root[data-archive-theme][data-theme=dark] .sidenav .sidenav-head .expand-button:hover { + color: var(--attack-on-color-body-emphasis); + background-color: var(--attack-color-body-alternate-strong); + } + :root[data-archive-theme][data-theme=dark] .sidenav .sidenav-head.active, + :root[data-archive-theme][data-theme=dark] .sidenav .sidenav-head.active > a { + color: var(--attack-color-active) !important; + background-color: var(--attack-color-body-alternate-strong); + } + :root[data-archive-theme][data-theme=dark] .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head a, + :root[data-archive-theme][data-theme=dark] .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head .expand-button { + color: var(--attack-on-color-body-emphasis); + } + :root[data-archive-theme][data-theme=dark] .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head a:hover, + :root[data-archive-theme][data-theme=dark] .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head .expand-button:hover { + background-color: var(--attack-color-body-alternate-strong); + } + :root[data-archive-theme][data-theme=dark] .nav .nav-link.side.active, + :root[data-archive-theme][data-theme=dark] .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head.active, + :root[data-archive-theme][data-theme=dark] .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head.active > a { + color: var(--attack-color-active) !important; + background-color: var(--attack-color-body-alternate-strong); + } + :root[data-archive-theme][data-theme=dark] .card-data .card-title { + color: var(--attack-color-property-label); + } + :root[data-archive-theme][data-theme=dark] .deemphasis, + :root[data-archive-theme][data-theme=dark] .text-label-small, + :root[data-archive-theme][data-theme=dark] .text-muted { + color: var(--attack-on-color-body-deemphasis) !important; + } + :root[data-archive-theme][data-theme=dark] .text-danger, + :root[data-archive-theme][data-theme=dark] font[color=red] { + color: var(--attack-color-danger) !important; + } + :root[data-archive-theme][data-theme=dark] .search-word-found, + :root[data-archive-theme][data-theme=dark] mark { + color: var(--attack-on-color-search-highlight); + background-color: var(--attack-color-search-highlight); + } + :root[data-archive-theme][data-theme=dark] .custom-select, + :root[data-archive-theme][data-theme=dark] .card-block .card-header::after, + :root[data-archive-theme][data-theme=dark] .faq .card-header::after, + :root[data-archive-theme][data-theme=dark] .heading-dropdown::after, + :root[data-archive-theme][data-theme=dark] .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head .expand-button::after, + :root[data-archive-theme][data-theme=dark] #usecases .card-header::after { + background-image: var(--attack-select-arrow); + } + :root[data-archive-theme][data-theme=dark] .resizer { + background-color: var(--attack-border-color-body); + } + :root[data-archive-theme][data-theme=dark] code { + color: var(--attack-color-code); + } + :root[data-archive-theme][data-theme=dark] pre, + :root[data-archive-theme][data-theme=dark] .jumbotron code { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body-alternate-strongest); + } + :root[data-archive-theme][data-theme=dark] .btn-default, + :root[data-archive-theme][data-theme=dark] .btn-outline-secondary, + :root[data-archive-theme][data-theme=dark] .matrix-controls button, + :root[data-archive-theme][data-theme=dark] .matrix-controls .layout-button:active, + :root[data-archive-theme][data-theme=dark] .slide-button-secondary { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body); + border-color: var(--attack-border-color-body); + } + :root[data-archive-theme][data-theme=dark] .btn-primary, + :root[data-archive-theme][data-theme=dark] .footer .btn-primary { + color: var(--attack-on-color-active); + background-color: var(--attack-color-active); + border-color: var(--attack-color-active); + } + :root[data-archive-theme][data-theme=dark] .btn-default:hover, + :root[data-archive-theme][data-theme=dark] .btn-outline-secondary:not(:disabled, .disabled):hover, + :root[data-archive-theme][data-theme=dark] .btn-outline-secondary:not(:disabled, .disabled):focus, + :root[data-archive-theme][data-theme=dark] .matrix-controls button:hover, + :root[data-archive-theme][data-theme=dark] .slide-button { + color: var(--attack-on-color-active); + background-color: var(--attack-color-active); + border-color: var(--attack-color-active); + } + :root[data-archive-theme][data-theme=dark] .matrix.side .tactic .handle, + :root[data-archive-theme][data-theme=dark] .matrix.flat .tactic .supertechnique td.sidebar.technique .handle { + color: var(--attack-color-body); + background-color: var(--attack-on-color-body-deemphasis); + } + :root[data-archive-theme][data-theme=dark] .matrix.side .sidebar.expanded .angle, + :root[data-archive-theme][data-theme=dark] .matrix.side .tactic .sidebar.expanded .angle { + background-color: var(--attack-color-body); + } + :root[data-archive-theme][data-theme=dark] .matrix.side .tactic:hover:not(.name, .count), + :root[data-archive-theme][data-theme=dark] .matrix.side .tactic:hover:not(.name, .count) .sidebar.expanded .angle { + background-color: var(--attack-background-color-body); + } + :root[data-archive-theme][data-theme=dark] .matrix-container .scroll-indicator-group .scroll-indicator.right.show .cover { + background: linear-gradient(to right, rgba(255, 255, 255, 0.001), var(--attack-color-body)); + } + :root[data-archive-theme][data-theme=dark] .matrix-container .scroll-indicator-group .scroll-indicator.left.show .cover { + background: linear-gradient(to left, rgba(255, 255, 255, 0.001), var(--attack-color-body)); + } + :root[data-archive-theme][data-theme=dark] .matrix .tactic.count, + :root[data-archive-theme][data-theme=dark] .matrix .technique-cell, + :root[data-archive-theme][data-theme=dark] .resizer, + :root[data-archive-theme][data-theme=dark] hr { + border-color: var(--attack-border-color-body); + } + :root[data-archive-theme][data-theme=dark] .navbar-orange .nav-link, + :root[data-archive-theme][data-theme=dark] .navbar .nav-tabs .nav-link, + :root[data-archive-theme][data-theme=dark] .nav .dropdown-menu .dropdown-item { + color: white; + } + :root[data-archive-theme][data-theme=dark] .nav .dropdown-menu { + background-color: var(--attack-color-primary); + } +} +@media screen and (prefers-color-scheme: dark) { + :root[data-archive-theme]:not([data-theme]) { + --attack-color-primary: #c63f1f; + --attack-on-color-primary: white; + --attack-color-secondary: #062f4f; + --attack-color-secondary-hover: rgb(8.4, 65.8, 110.6); + --attack-on-color-secondary: white; + --attack-color-footer: #0b2338; + --attack-on-color-footer: #87deff; + --attack-color-footer-link-hover: #b7edff; + --attack-color-active: #60a9ff; + --attack-color-active-alternate-medium: #3d8cdb; + --attack-on-color-active: #0f171c; + --attack-color-body: #222426; + --attack-on-color-body: #e8e6e3; + --attack-on-color-body-emphasis: #fffaf4; + --attack-color-property-label: #f2d2a4; + --attack-on-color-body-deemphasis: #b7b1a8; + --attack-color-body-alternate-subtle: #272a2c; + --attack-color-body-alternate: #2b2e30; + --attack-color-body-alternate-strong: #303437; + --attack-color-body-alternate-strongest: #353a3d; + --attack-border-color-body: #596166; + --attack-background-color-body: #373d40; + --attack-color-link: #7bb8ee; + --attack-color-link-hover: #b7ddff; + --attack-color-matrix-header: #596166; + --attack-on-color-matrix-header: #fffaf4; + --attack-color-search-highlight: #665a00; + --attack-on-color-search-highlight: #fff4b8; + --attack-color-deemphasis: #b7b1a8; + --attack-on-color-deemphasis: #222426; + --attack-color-card-header: #2b2e30; + --attack-color-code: #ff8f70; + --attack-color-danger: #ff8c96; + --attack-color-image-background: white; + --attack-color-banner: #263a49; + --attack-on-color-banner: #f2f7fa; + --attack-border-color-banner: #3f5d72; + --attack-select-arrow: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%23e8e6e3' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z'/%3E%3C/svg%3E"); + color-scheme: dark; + } + :root[data-archive-theme]:not([data-theme]) body, + :root[data-archive-theme]:not([data-theme]) .jumbotron, + :root[data-archive-theme]:not([data-theme]) .card, + :root[data-archive-theme]:not([data-theme]) .card-filter, + :root[data-archive-theme]:not([data-theme]) .card-body, + :root[data-archive-theme]:not([data-theme]) .contact-card .card-header.no-background, + :root[data-archive-theme]:not([data-theme]) .contact-card .card-footer.no-background, + :root[data-archive-theme]:not([data-theme]) .sidebar, + :root[data-archive-theme]:not([data-theme]) .sidebar .heading, + :root[data-archive-theme]:not([data-theme]) .matrix-container, + :root[data-archive-theme]:not([data-theme]) .matrix .technique-cell, + :root[data-archive-theme]:not([data-theme]) .table-matrix td, + :root[data-archive-theme]:not([data-theme]) .dropdown-content, + :root[data-archive-theme]:not([data-theme]) .dropdown-menu, + :root[data-archive-theme]:not([data-theme]) .form-control, + :root[data-archive-theme]:not([data-theme]) .custom-select, + :root[data-archive-theme]:not([data-theme]) .bootstrap-select > .dropdown-toggle, + :root[data-archive-theme]:not([data-theme]) .modal-content, + :root[data-archive-theme]:not([data-theme]) .popover, + :root[data-archive-theme]:not([data-theme]) .popover-body, + :root[data-archive-theme]:not([data-theme]) .search-results, + :root[data-archive-theme]:not([data-theme]) .overlay.search .overlay-inner, + :root[data-archive-theme]:not([data-theme]) .overlay.search .overlay-inner .search-header .search-input input { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body); + border-color: var(--attack-border-color-body); + } + :root[data-archive-theme]:not([data-theme]) .table, + :root[data-archive-theme]:not([data-theme]) .table-light, + :root[data-archive-theme]:not([data-theme]) .table td, + :root[data-archive-theme]:not([data-theme]) .table th, + :root[data-archive-theme]:not([data-theme]) .card-data, + :root[data-archive-theme]:not([data-theme]) .getting-started-color, + :root[data-archive-theme]:not([data-theme]) .table-techniques td, + :root[data-archive-theme]:not([data-theme]) .techniques-used td, + :root[data-archive-theme]:not([data-theme]) .datasources-table td { + color: var(--attack-on-color-body); + border-color: var(--attack-border-color-body); + } + :root[data-archive-theme]:not([data-theme]) a:where(:not(.osano-cm-link)), + :root[data-archive-theme]:not([data-theme]) .dropdown-item, + :root[data-archive-theme]:not([data-theme]) .matrix-tactics-url, + :root[data-archive-theme]:not([data-theme]) .matrix-tactics-url:visited, + :root[data-archive-theme]:not([data-theme]) .matrix-tactics-url:hover, + :root[data-archive-theme]:not([data-theme]) .matrix-tactics-url:active, + :root[data-archive-theme]:not([data-theme]) .table-techniques .sub.technique td:not(:nth-child(4)), + :root[data-archive-theme]:not([data-theme]) .table-techniques .technique:not(.sub) td:not(:nth-child(3)) { + color: var(--attack-color-link); + } + :root[data-archive-theme]:not([data-theme]) .matrix-header { + color: var(--attack-on-color-body-emphasis); + } + :root[data-archive-theme]:not([data-theme]) .table-light, + :root[data-archive-theme]:not([data-theme]) .table-matrix .matrix-header, + :root[data-archive-theme]:not([data-theme]) .table-alternate tbody, + :root[data-archive-theme]:not([data-theme]) .blog-post table tbody, + :root[data-archive-theme]:not([data-theme]) .changelog table tbody { + background-color: var(--attack-color-body); + } + :root[data-archive-theme]:not([data-theme]) .bg-white { + background-color: var(--attack-color-body) !important; + } + :root[data-archive-theme]:not([data-theme]) .bg-alternate, + :root[data-archive-theme]:not([data-theme]) .bg-gray, + :root[data-archive-theme]:not([data-theme]) .bg-light, + :root[data-archive-theme]:not([data-theme]) .bg-accord-light, + :root[data-archive-theme]:not([data-theme]) .bg-accord-dark, + :root[data-archive-theme]:not([data-theme]) .table-alternate, + :root[data-archive-theme]:not([data-theme]) .table-techniques thead tr, + :root[data-archive-theme]:not([data-theme]) .techniques-used thead tr, + :root[data-archive-theme]:not([data-theme]) .datasources-table thead tr, + :root[data-archive-theme]:not([data-theme]) .table-striped tbody tr:nth-of-type(odd), + :root[data-archive-theme]:not([data-theme]) .card-header, + :root[data-archive-theme]:not([data-theme]) .contact-card .card-body.background, + :root[data-archive-theme]:not([data-theme]) .breadcrumb, + :root[data-archive-theme]:not([data-theme]) .resource, + :root[data-archive-theme]:not([data-theme]) .tip-box, + :root[data-archive-theme]:not([data-theme]) .under-development, + :root[data-archive-theme]:not([data-theme]) .training .exercise, + :root[data-archive-theme]:not([data-theme]) .example-container, + :root[data-archive-theme]:not([data-theme]) .section-view .anchor-section { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body-alternate) !important; + border-color: var(--attack-border-color-body); + } + :root[data-archive-theme]:not([data-theme]) .table .active, + :root[data-archive-theme]:not([data-theme]) .search-results .search-header, + :root[data-archive-theme]:not([data-theme]) .search-results .search-highlight, + :root[data-archive-theme]:not([data-theme]) .nav-link.side.active { + color: var(--attack-color-active); + } + :root[data-archive-theme]:not([data-theme]) .nav .heading-dropdown, + :root[data-archive-theme]:not([data-theme]) .faq .heading-dropdown, + :root[data-archive-theme]:not([data-theme]) .faq .nav-link.expand-title, + :root[data-archive-theme]:not([data-theme]) .nav .nav-link.expand-title, + :root[data-archive-theme]:not([data-theme]) .expand-icon, + :root[data-archive-theme]:not([data-theme]) .card-title { + color: var(--attack-on-color-body-emphasis); + } + :root[data-archive-theme]:not([data-theme]) .version-banner, + :root[data-archive-theme]:not([data-theme]) .version-banner a { + color: var(--attack-on-color-banner); + background-color: var(--attack-color-banner); + } + :root[data-archive-theme]:not([data-theme]) .version-banner a { + text-decoration: underline; + } + :root[data-archive-theme]:not([data-theme]) .footer a, + :root[data-archive-theme]:not([data-theme]) .footer .footer-link { + color: var(--attack-on-color-footer); + } + :root[data-archive-theme]:not([data-theme]) .table-hover tbody tr:hover, + :root[data-archive-theme]:not([data-theme]) .dropdown-item:hover, + :root[data-archive-theme]:not([data-theme]) .dropdown-item:focus, + :root[data-archive-theme]:not([data-theme]) .nav-link.side:hover, + :root[data-archive-theme]:not([data-theme]) .sidenav .sidenav-head a:hover, + :root[data-archive-theme]:not([data-theme]) .sidenav .sidenav-head .expand-button:hover { + color: var(--attack-on-color-body-emphasis); + background-color: var(--attack-color-body-alternate-strong); + } + :root[data-archive-theme]:not([data-theme]) .sidenav .sidenav-head.active, + :root[data-archive-theme]:not([data-theme]) .sidenav .sidenav-head.active > a { + color: var(--attack-color-active) !important; + background-color: var(--attack-color-body-alternate-strong); + } + :root[data-archive-theme]:not([data-theme]) .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head a, + :root[data-archive-theme]:not([data-theme]) .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head .expand-button { + color: var(--attack-on-color-body-emphasis); + } + :root[data-archive-theme]:not([data-theme]) .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head a:hover, + :root[data-archive-theme]:not([data-theme]) .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head .expand-button:hover { + background-color: var(--attack-color-body-alternate-strong); + } + :root[data-archive-theme]:not([data-theme]) .nav .nav-link.side.active, + :root[data-archive-theme]:not([data-theme]) .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head.active, + :root[data-archive-theme]:not([data-theme]) .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head.active > a { + color: var(--attack-color-active) !important; + background-color: var(--attack-color-body-alternate-strong); + } + :root[data-archive-theme]:not([data-theme]) .card-data .card-title { + color: var(--attack-color-property-label); + } + :root[data-archive-theme]:not([data-theme]) .deemphasis, + :root[data-archive-theme]:not([data-theme]) .text-label-small, + :root[data-archive-theme]:not([data-theme]) .text-muted { + color: var(--attack-on-color-body-deemphasis) !important; + } + :root[data-archive-theme]:not([data-theme]) .text-danger, + :root[data-archive-theme]:not([data-theme]) font[color=red] { + color: var(--attack-color-danger) !important; + } + :root[data-archive-theme]:not([data-theme]) .search-word-found, + :root[data-archive-theme]:not([data-theme]) mark { + color: var(--attack-on-color-search-highlight); + background-color: var(--attack-color-search-highlight); + } + :root[data-archive-theme]:not([data-theme]) .custom-select, + :root[data-archive-theme]:not([data-theme]) .card-block .card-header::after, + :root[data-archive-theme]:not([data-theme]) .faq .card-header::after, + :root[data-archive-theme]:not([data-theme]) .heading-dropdown::after, + :root[data-archive-theme]:not([data-theme]) .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head .expand-button::after, + :root[data-archive-theme]:not([data-theme]) #usecases .card-header::after { + background-image: var(--attack-select-arrow); + } + :root[data-archive-theme]:not([data-theme]) .resizer { + background-color: var(--attack-border-color-body); + } + :root[data-archive-theme]:not([data-theme]) code { + color: var(--attack-color-code); + } + :root[data-archive-theme]:not([data-theme]) pre, + :root[data-archive-theme]:not([data-theme]) .jumbotron code { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body-alternate-strongest); + } + :root[data-archive-theme]:not([data-theme]) .btn-default, + :root[data-archive-theme]:not([data-theme]) .btn-outline-secondary, + :root[data-archive-theme]:not([data-theme]) .matrix-controls button, + :root[data-archive-theme]:not([data-theme]) .matrix-controls .layout-button:active, + :root[data-archive-theme]:not([data-theme]) .slide-button-secondary { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body); + border-color: var(--attack-border-color-body); + } + :root[data-archive-theme]:not([data-theme]) .btn-primary, + :root[data-archive-theme]:not([data-theme]) .footer .btn-primary { + color: var(--attack-on-color-active); + background-color: var(--attack-color-active); + border-color: var(--attack-color-active); + } + :root[data-archive-theme]:not([data-theme]) .btn-default:hover, + :root[data-archive-theme]:not([data-theme]) .btn-outline-secondary:not(:disabled, .disabled):hover, + :root[data-archive-theme]:not([data-theme]) .btn-outline-secondary:not(:disabled, .disabled):focus, + :root[data-archive-theme]:not([data-theme]) .matrix-controls button:hover, + :root[data-archive-theme]:not([data-theme]) .slide-button { + color: var(--attack-on-color-active); + background-color: var(--attack-color-active); + border-color: var(--attack-color-active); + } + :root[data-archive-theme]:not([data-theme]) .matrix.side .tactic .handle, + :root[data-archive-theme]:not([data-theme]) .matrix.flat .tactic .supertechnique td.sidebar.technique .handle { + color: var(--attack-color-body); + background-color: var(--attack-on-color-body-deemphasis); + } + :root[data-archive-theme]:not([data-theme]) .matrix.side .sidebar.expanded .angle, + :root[data-archive-theme]:not([data-theme]) .matrix.side .tactic .sidebar.expanded .angle { + background-color: var(--attack-color-body); + } + :root[data-archive-theme]:not([data-theme]) .matrix.side .tactic:hover:not(.name, .count), + :root[data-archive-theme]:not([data-theme]) .matrix.side .tactic:hover:not(.name, .count) .sidebar.expanded .angle { + background-color: var(--attack-background-color-body); + } + :root[data-archive-theme]:not([data-theme]) .matrix-container .scroll-indicator-group .scroll-indicator.right.show .cover { + background: linear-gradient(to right, rgba(255, 255, 255, 0.001), var(--attack-color-body)); + } + :root[data-archive-theme]:not([data-theme]) .matrix-container .scroll-indicator-group .scroll-indicator.left.show .cover { + background: linear-gradient(to left, rgba(255, 255, 255, 0.001), var(--attack-color-body)); + } + :root[data-archive-theme]:not([data-theme]) .matrix .tactic.count, + :root[data-archive-theme]:not([data-theme]) .matrix .technique-cell, + :root[data-archive-theme]:not([data-theme]) .resizer, + :root[data-archive-theme]:not([data-theme]) hr { + border-color: var(--attack-border-color-body); + } + :root[data-archive-theme]:not([data-theme]) .navbar-orange .nav-link, + :root[data-archive-theme]:not([data-theme]) .navbar .nav-tabs .nav-link, + :root[data-archive-theme]:not([data-theme]) .nav .dropdown-menu .dropdown-item { + color: white; + } + :root[data-archive-theme]:not([data-theme]) .nav .dropdown-menu { + background-color: var(--attack-color-primary); + } +} +@media print { + :root[data-archive-theme] .archive-theme-toggle { + display: none; + } +} + +/*# sourceMappingURL=style-archive.css.map */ diff --git a/attack-theme/static/style-attack.css b/attack-theme/static/style-attack.css index 5e75a1384d2..dbf28785b3d 100644 --- a/attack-theme/static/style-attack.css +++ b/attack-theme/static/style-attack.css @@ -37,18 +37,18 @@ src: url("fonts/Roboto/Roboto-Black.ttf"); } .deemphasis { - color: rgb(106.5, 114, 120.75); + color: var(--attack-on-color-body-deemphasis); } .matrix-header { - background-color: gray; - color: white; + background-color: var(--attack-color-matrix-header); + color: var(--attack-on-color-matrix-header); } .table-alternate, .blog-post table, .changelog table, .bg-alternate { - background-color: rgb(242.25, 242.25, 242.25) !important; + background-color: var(--attack-color-body-alternate) !important; } .text-label { @@ -58,7 +58,7 @@ .text-label-small { font-size: 12px; - color: #303435; + color: var(--attack-color-deemphasis); margin-top: -10px; } @@ -69,8 +69,8 @@ html { body { height: 100%; - background: white; - color: #39434c; + background: var(--attack-color-body); + color: var(--attack-on-color-body); font-family: "Roboto-Regular", sans-serif; display: flex; flex-direction: column; @@ -100,19 +100,22 @@ strong { .jumbotron { padding: 0; - background: white; - color: #39434c; + background: var(--attack-color-body); + color: var(--attack-on-color-body); } .jumbotron code { - color: rgb(28.5, 33.5, 38); - background-color: rgb(229.5, 229.5, 229.5); + color: var(--attack-on-color-body-emphasis); + background-color: var(--attack-color-body-alternate-strong); border-radius: 3px; font-family: courier, monospace; padding: 0 3px; } a { - color: #4f7cac; + color: var(--attack-color-link); +} +a:hover { + color: var(--attack-color-link-hover); } a .anchor::before { content: ""; @@ -175,7 +178,23 @@ a .anchor::before { padding: 0.3125rem 0; } .quick-links tr + tr { - border-top: 1px solid rgb(223.125, 223.125, 223.125); + border-top: 1px solid var(--attack-border-color-body); +} +.quick-links .external-link-icon { + margin-left: 0.125rem; + font-size: 0.8125rem; +} +.quick-links .random-page-toggle { + margin-left: 0.375rem; + padding: 0 0.375rem; + border: 1px solid var(--attack-color-active); + color: var(--attack-color-link); + background: var(--attack-color-body); +} +.quick-links .random-page-toggle:hover, .quick-links .random-page-toggle:focus, .quick-links .random-page-toggle[aria-expanded=true] { + border-color: var(--attack-color-secondary); + color: var(--attack-on-color-secondary); + background: var(--attack-color-secondary); } .row-main-page { @@ -191,16 +210,16 @@ a .anchor::before { } } .p-line p { - border-top: 0.0625rem solid #1c2226; + border-top: 0.0625rem solid var(--attack-border-color-body); } .btn-default { - background: white; - border-color: rgb(106.5, 114, 120.75); - color: #39434c; + background: var(--attack-color-body); + border-color: var(--attack-on-color-body-deemphasis); + color: var(--attack-on-color-body); } .btn-default:hover { - color: rgb(28.5, 33.5, 38); + color: var(--attack-on-color-body-emphasis); } .website-button, .slide-button-secondary, .slide-button { @@ -221,9 +240,9 @@ a .anchor::before { } .slide-button { - border-color: #0156b3; - color: #fff; - background: #0156b3; + border-color: var(--attack-color-active); + color: var(--attack-on-color-active); + background: var(--attack-color-active); padding: 6px 16px; } a .slide-button { @@ -231,9 +250,9 @@ a .slide-button { } .slide-button-secondary { - color: #0156b3; - background: #fff; - border-color: #0156b3; + color: var(--attack-color-active); + background: var(--attack-color-body); + border-color: var(--attack-color-active); padding: 6px 16px; } a .slide-button-secondary { @@ -249,15 +268,21 @@ a .slide-button-secondary { padding-left: 8px; } -.slide-button:hover { - background: #062f4f; - border-color: #062f4f; +.slide-button:hover, +.slide-button:focus, +.slide-button:active, +.slide-button[aria-expanded=true] { + background: var(--attack-color-secondary); + border-color: var(--attack-color-secondary); + color: var(--attack-on-color-secondary); } -.slide-button-secondary:hover { - background: #eaeaea; - border-color: #062f4f; - color: #062f4f; +.slide-button-secondary:hover, +.slide-button-secondary:focus, +.slide-button-secondary:active { + background: var(--attack-color-secondary); + border-color: var(--attack-color-secondary); + color: var(--attack-on-color-secondary); } .dropdown { @@ -268,7 +293,7 @@ a .slide-button-secondary { .dropdown-content { display: none; position: absolute; - background-color: white; + background-color: var(--attack-color-body); min-width: 160px; box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2); } @@ -278,16 +303,16 @@ a .slide-button-secondary { } .btn-navy:hover, .btn-navy { - color: white; - border-color: 0.0625rem solid #062f4f; + color: var(--attack-on-color-secondary); + border-color: 0.0625rem solid var(--attack-color-secondary); } .btn-navy { - background-color: #062f4f; - color: white; + background-color: var(--attack-color-secondary); + color: var(--attack-on-color-secondary); } .btn-navy:hover { - background-color: rgb(7.5, 58.75, 98.75); + background-color: var(--attack-color-secondary-hover); background-image: none; } @@ -312,17 +337,17 @@ a .slide-button-secondary { .changelog table { empty-cells: hide; } +.table td p:last-child, +.blog-post table td p:last-child, +.changelog table td p:last-child { + margin-bottom: 0; +} .table td, .blog-post table td, .changelog table td { padding: 0.75rem; vertical-align: top; } -.table td p:last-child, -.blog-post table td p:last-child, -.changelog table td p:last-child { - margin-bottom: 0; -} .table th, .blog-post table th, .changelog table th { @@ -333,7 +358,7 @@ a .slide-button-secondary { .table .active, .blog-post table .active, .changelog table .active { - color: #c63f1f; + color: var(--attack-color-active); } .blog-post table { @@ -343,13 +368,13 @@ a .slide-button-secondary { .table-alternate tbody, .blog-post table tbody, .changelog table tbody { - background: white; - color: #39434c; + background: var(--attack-color-body); + color: var(--attack-on-color-body); } .table-techniques thead tr { - background: #f2f2f2; - border-bottom: 2px solid #dee2e6; + background: var(--attack-color-body-alternate); + border-bottom: 2px solid var(--attack-border-color-body); } .table-techniques table { border-collapse: collapse; @@ -357,10 +382,10 @@ a .slide-button-secondary { .table-techniques td { vertical-align: top; padding: 10px; - border: 1px solid #dfdfdf; + border: 1px solid var(--attack-border-color-body); } .table-techniques tr:last-child { - border-bottom: 1px solid #dfdfdf; + border-bottom: 1px solid var(--attack-border-color-body); } .table-techniques .sub.technique { border-left: none; @@ -374,10 +399,10 @@ a .slide-button-secondary { border-bottom: none; } .table-techniques .sub.technique td:not(:nth-child(4)) { - color: #4f7cac; + color: var(--attack-color-link); } .table-techniques .technique:not(.sub) td:not(:nth-child(3)) { - color: #4f7cac; + color: var(--attack-color-link); } .techniques-used table { @@ -386,10 +411,10 @@ a .slide-button-secondary { .techniques-used td { vertical-align: top; padding: 10px; - border: 1px solid #dfdfdf; + border: 1px solid var(--attack-border-color-body); } .techniques-used tr:last-child { - border-bottom: 1px solid #dfdfdf; + border-bottom: 1px solid var(--attack-border-color-body); } .techniques-used .sub.technique { border-left: none; @@ -412,24 +437,24 @@ a .slide-button-secondary { } .techniques-used.background thead tr { - background: #f2f2f2; - border-bottom: 2px solid #dee2e6; + background: var(--attack-color-body-alternate); + border-bottom: 2px solid var(--attack-border-color-body); } .datasources-table table { border-collapse: collapse; } .datasources-table thead tr { - background: #f2f2f2; - border-bottom: 2px solid #dee2e6; + background: var(--attack-color-body-alternate); + border-bottom: 2px solid var(--attack-border-color-body); } .datasources-table td { vertical-align: top; padding: 10px; - border: 1px solid #dfdfdf; + border: 1px solid var(--attack-border-color-body); } .datasources-table tr:last-child { - border-bottom: 1px solid #dfdfdf; + border-bottom: 1px solid var(--attack-border-color-body); } .datasources-table .datacomponent.datasource { border-left: none; @@ -465,12 +490,12 @@ a .slide-button-secondary { .changelog table, .changelog table td, .changelog table th { - border: 1px solid rgb(223.125, 223.125, 223.125); + border: 1px solid var(--attack-border-color-body); } .table-bordered th, .blog-post table th, .changelog table th { - border-bottom: 2px solid rgb(223.125, 223.125, 223.125) !important; + border-bottom: 2px solid var(--attack-border-color-body) !important; } .table-matrix { @@ -479,7 +504,7 @@ a .slide-button-secondary { .table-matrix thead th { text-align: center !important; vertical-align: middle !important; - border: 0.0625rem solid rgb(223.125, 223.125, 223.125) !important; + border: 0.0625rem solid var(--attack-border-color-body) !important; } .table-matrix td, .table-matrix th { @@ -491,15 +516,15 @@ a .slide-button-secondary { } .table-matrix td.border, .table-matrix th.border { - border: 0.0625rem solid rgb(223.125, 223.125, 223.125) !important; + border: 0.0625rem solid var(--attack-border-color-body) !important; } .table-matrix td.no-border, .table-matrix th.no-border { border: none !important; } .table-matrix td { - background: white; - color: #39434c; + background: var(--attack-color-body); + color: var(--attack-on-color-body); } .table-mitigations th, @@ -519,18 +544,26 @@ a .slide-button-secondary { } .matrix-tactics-url { - color: white; + color: var(--attack-on-color-primary); } .matrix-tactics-url:visited, .matrix-tactics-url:hover, .matrix-tactics-url:active { - color: white; + color: var(--attack-on-color-primary); } /* **** */ /* BANNER */ .banner-message, .version-banner { - padding: 0.3125rem 0; + padding: 0.4375rem 1rem; + border-top: 1px solid var(--attack-border-color-banner); + border-bottom: 1px solid var(--attack-border-color-banner); text-align: center; - background-color: rgb(229.5, 229.5, 229.5); + color: var(--attack-on-color-banner); + background-color: var(--attack-color-banner); +} +.banner-message a, .version-banner a { + color: inherit; + font-weight: 700; + text-decoration: underline; } /* **** */ @@ -551,22 +584,22 @@ a .slide-button-secondary { /* **** */ /* Pre-block in SIGHTINGS */ pre { - color: #39434c; - background-color: rgb(216.75, 216.75, 216.75); + color: var(--attack-on-color-body); + background-color: var(--attack-color-body-alternate-strongest); border-radius: 5px; padding: 1rem; } code { - color: #c63e1f; + color: var(--attack-color-code); } /* **** */ /* CARDS */ .card { - background: white; - color: #39434c; - border-color: rgb(223.125, 223.125, 223.125); + background: var(--attack-color-body); + color: var(--attack-on-color-body); + border-color: var(--attack-border-color-body); } .button-group { @@ -588,7 +621,7 @@ code { width: 20%; top: 9.3rem; float: right; - background: #eaeaea; + background: var(--attack-color-body-alternate-strong); } @media screen and (width <= 90.62rem) { @@ -609,9 +642,9 @@ code { } .card-header { - color: #39434c; - background: rgba(57, 67, 76, 0.03); - border-bottom-color: rgb(223.125, 223.125, 223.125); + color: var(--attack-on-color-body); + background: var(--attack-color-card-header); + border-bottom-color: var(--attack-border-color-body); } a.partial-underline { @@ -643,21 +676,21 @@ a.partial-underline .hover-line { padding: 1.25rem 1.25rem 0; } .contact-card .card-header.background { - background: #0b2338; + background: var(--attack-color-footer); color: white; padding-bottom: 1.25rem; } .contact-card .card-header.no-background, .contact-card .card-footer.no-background { - background: white; - color: #39434c; + background: var(--attack-color-body); + color: var(--attack-on-color-body); border: unset; } .contact-card .card-body p { margin: 0; } .contact-card .card-body.background { - background: #eaeaea; + background: var(--attack-color-body-alternate-strong); } .card-body > .table { @@ -696,7 +729,11 @@ a.partial-underline .hover-line { .card-title { font-size: 1.1rem; - color: rgb(28.5, 33.5, 38); + color: var(--attack-on-color-body-emphasis); +} + +.card-data .card-title { + color: var(--attack-color-property-label); } .contact-card-title { @@ -706,7 +743,7 @@ a.partial-underline .hover-line { .card-title-icon { float: right; - color: #0156b3; + color: var(--attack-color-active); } /* **** */ @@ -716,13 +753,13 @@ a.partial-underline .hover-line { max-width: 100%; height: 30rem; margin: 0 auto; - border: 3px solid #dfdfdf; + border: 3px solid var(--attack-border-color-body); padding: 3px; display: flex; flex-direction: column; } .attack-box iframe { - border: 1px solid rgb(223.125, 223.125, 223.125) !important; + border: 1px solid var(--attack-border-color-body) !important; border-radius: 0.25rem; } @@ -730,13 +767,13 @@ a.partial-underline .hover-line { /* BREADCRUMBS */ .breadcrumb { font-size: 90%; - background-color: white; + background-color: var(--attack-color-body); max-width: 1140px; padding: 0 15px; } .breadcrumb .breadcrumb-item + .breadcrumb-item::before { content: ">"; - color: rgb(106.5, 114, 120.75); + color: var(--attack-on-color-body-deemphasis); } /* **** */ @@ -753,24 +790,24 @@ a.partial-underline .hover-line { counter-increment: item; } -.danger-card { - border-color: #c63f1f; -} .danger-card .card-header { - background: #c63f1f; - color: white; + background: var(--attack-color-primary); + color: var(--attack-on-color-primary); +} +.danger-card { + border-color: var(--attack-color-primary); } /* **** */ /* ATT&CKCON */ .bg-accord-light { - color: #39434c; - background-color: rgb(242.25, 242.25, 242.25); + color: var(--attack-on-color-body); + background-color: var(--attack-color-body-alternate); } .bg-accord-dark { - color: #39434c; - background-color: rgb(216.75, 216.75, 216.75); + color: var(--attack-on-color-body); + background-color: var(--attack-color-body-alternate-strongest); } /* **** */ @@ -786,9 +823,9 @@ a.partial-underline .hover-line { .usa-card__header { font-family: "Roboto-Regular", sans-serif; - color: white; - background: #062f4f; - border-bottom-color: rgb(223.125, 223.125, 223.125); + color: var(--attack-on-color-secondary); + background: var(--attack-color-secondary); + border-bottom-color: var(--attack-border-color-body); border-radius: 0.3rem 0.3rem 0 0; } @@ -835,7 +872,7 @@ a.partial-underline .hover-line { height: 500px; overflow: auto; padding: 0.9375rem; - background-color: rgb(242.25, 242.25, 242.25); + background-color: var(--attack-color-body-alternate); border-radius: 0.1875rem; } @@ -890,13 +927,13 @@ img.yt-core-image { } /* Ensure the sponsors block is below and not affected by the top image */ /* Card Blocks */ +.card-block .card-header h5 { + font-family: "Roboto-Regular", sans-serif; +} .card-block .card-header { display: flex; flex-direction: row; } -.card-block .card-header h5 { - font-family: "Roboto-Regular", sans-serif; -} .card-block .card-header :first-child { cursor: pointer; display: inline-block; @@ -911,7 +948,7 @@ img.yt-core-image { display: inline-block; vertical-align: top; background-position: center; - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%2339434C' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E"); + background-image: var(--attack-select-arrow); z-index: 1; transition: all 0.2s ease; } @@ -957,7 +994,7 @@ img.yt-core-image { } } .getting-started .getting-started-icon { - color: #c63f1f; + color: var(--attack-color-primary); } .getting-started .video-li { /* don't display the video in the list itself except in mobile layout */ @@ -992,7 +1029,7 @@ img.yt-core-image { padding-left: 0; } .timeline::before { - background: rgb(106.5, 114, 120.75); + background: var(--attack-on-color-body-deemphasis); position: absolute; width: 0.0625rem; height: 100%; @@ -1007,7 +1044,7 @@ img.yt-core-image { height: 300px; } .timeline:not(.show)::after { - background: linear-gradient(to bottom, transparent, white); + background: linear-gradient(to bottom, transparent, var(--attack-color-body)); position: absolute; width: 100%; height: 75px; @@ -1021,7 +1058,7 @@ img.yt-core-image { padding-left: 40px; } .timeline .timeline-card::before { - background: white; + background: var(--attack-color-body); position: absolute; width: 20px; height: 20px; @@ -1029,7 +1066,7 @@ img.yt-core-image { content: ""; display: inline-block; border-radius: 50%; - border: 0.125rem solid rgb(106.5, 114, 120.75); + border: 0.125rem solid var(--attack-on-color-body-deemphasis); left: 10px; } .timeline .timeline-card .timeline-card-header { @@ -1106,7 +1143,7 @@ img.yt-core-image { position: -webkit-sticky; position: sticky; top: 4.125rem; - background: white; + background: var(--attack-color-body); z-index: 100; margin-bottom: 0; } @@ -1129,7 +1166,7 @@ img.yt-core-image { } } .section-view .section-shadow { - border-bottom: 1px solid #dfdfdf !important; + border-bottom: 1px solid var(--attack-border-color-body) !important; } .section-view table { margin-top: -1px; @@ -1143,7 +1180,7 @@ div#sidebars { /* Plus/Minus expand icons */ .expand-icon { font-size: 5%; - color: #39434c; + color: var(--attack-on-color-body); margin-top: 0.4375rem; } @@ -1152,6 +1189,17 @@ div#sidebars { .attackcons { border-top-width: 0; } +.attackcons .attackcon-banner-image { + display: inline-block; + width: 100%; + box-sizing: border-box; +} +.attackcons .attackcon-banner-image.on-light { + padding: 1rem; + border: 1px solid var(--attack-border-color-body); + border-radius: 0.75rem; + background: var(--attack-color-image-background); +} .attackcons .sponsors { flex: 1; padding-left: 25px; @@ -1160,16 +1208,20 @@ div#sidebars { width: 90%; } .attackcons .sponsors-block { - background: #eaeaea; + background: var(--attack-color-image-background); text-align: center; display: flex; justify-content: space-evenly; flex-wrap: wrap; flex-direction: column; width: 200%; + padding: 0.625rem; + border: 0.0625rem solid var(--attack-border-color-body); + border-radius: 0.5rem; + box-sizing: border-box; } .attackcons .sponsors-block .img-container { - margin: 10px; + margin: 0.625rem; flex: 1 1 20%; box-sizing: border-box; } @@ -1186,7 +1238,7 @@ div#sidebars { } .support-box { - background-color: rgb(229.5, 229.5, 229.5); + background-color: var(--attack-color-body-alternate-strong); padding: 1.5em; border-radius: 0.75em; width: fit-content; @@ -1207,7 +1259,7 @@ div#sidebars { display: contents; } .sponsor-square img.sponsor-logo { - background-color: white; + background-color: var(--attack-color-image-background); border-radius: 6px; object-fit: contain; object-position: center; @@ -1216,7 +1268,7 @@ div#sidebars { /* **** */ /* training pages */ .training .exercise { - background: rgb(242.25, 242.25, 242.25); + background: var(--attack-color-body-alternate); } .card-training { @@ -1250,7 +1302,7 @@ div#sidebars { .resource { flex: 1; - background-color: #eaeaea; + background-color: var(--attack-color-body-alternate-strong); padding: 10px; box-sizing: border-box; } @@ -1281,14 +1333,14 @@ div#sidebars { margin-bottom: 16px; } .decorative-panels .decorative-panel .decorative-panel-body.show { - border-top: 1px solid rgb(223.125, 223.125, 223.125); + border-top: 1px solid var(--attack-border-color-body); } .decorative-panels .decorative-panel + .decorative-panel { margin-top: 25px; } .decorative-panels .decorative-panel:nth-of-type(even) { border-radius: 8px; - background: rgb(242.25, 242.25, 242.25); + background: var(--attack-color-body-alternate); } @media screen and (width <= 47.9875rem) { .decorative-panels .row { @@ -1315,16 +1367,16 @@ div#sidebars { object-fit: cover; object-position: left top; border-radius: unset !important; - border: 1px solid rgb(223.125, 223.125, 223.125); + border: 1px solid var(--attack-border-color-body); } .working-with-attack .panel { padding: 0; - border: 1px solid rgb(223.125, 223.125, 223.125); + border: 1px solid var(--attack-border-color-body); border-radius: 0.75em; max-width: 100%; } .working-with-attack img + .panel-body { - border-top: 1px solid rgb(223.125, 223.125, 223.125); + border-top: 1px solid var(--attack-border-color-body); } .working-with-attack .panel-body p { margin: 24px 0; @@ -1349,18 +1401,18 @@ div#sidebars { } .tip-box { - background: #eaeaea; + background: var(--attack-color-body-alternate-strong); padding: 1rem; } /* Card Blocks */ +.expand-panel .card-block .card-header h5 { + font-family: "Roboto-Regular", sans-serif; +} .expand-panel .card-block .card-header { display: flex; flex-direction: row; } -.expand-panel .card-block .card-header h5 { - font-family: "Roboto-Regular", sans-serif; -} .expand-panel .card-block .card-header :first-child { cursor: pointer; display: inline-block; @@ -1375,7 +1427,7 @@ div#sidebars { display: inline-block; vertical-align: top; background-position: center; - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%2339434C' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E"); + background-image: var(--attack-select-arrow); z-index: 1; transition: all 0.2s ease; } @@ -1412,8 +1464,8 @@ div#sidebars { } /* Footer styling */ .footer { - background-color: #0b2338; - color: white; + background-color: var(--attack-color-footer); + color: var(--attack-on-color-secondary); padding: 1rem 3rem; font-family: "Roboto-Thin", sans-serif; } @@ -1447,10 +1499,10 @@ div#sidebars { justify-content: center; } .footer .footer-link { - color: #87deff; + color: var(--attack-on-color-footer); } .footer .footer-link:hover { - color: rgb(107, 173, 213.5); + color: var(--attack-color-footer-link-hover); } .col .col-footer { @@ -1501,7 +1553,7 @@ div#sidebars { /* Top NAVIGATION */ .navbar { - background-color: #c63f1f; + background-color: var(--attack-color-primary); z-index: 1; } .navbar .navbar-brand { @@ -1511,31 +1563,31 @@ div#sidebars { border-bottom: none; } .navbar .nav-tabs .nav-link { - color: white; + color: var(--attack-on-color-primary); font-family: "Roboto-Light", sans-serif; } .navbar .nav-tabs .nav-link:focus { - color: white; + color: var(--attack-on-color-primary); } .navbar .nav-tabs .nav-link:hover:not(.active) { - color: white; + color: var(--attack-on-color-primary); background-color: transparent; } .navbar .nav-tabs .nav-link.active { - color: white; - background-color: #c63f1f; + color: var(--attack-on-color-primary); + background-color: var(--attack-color-primary); } .navbar .nav-tabs .nav-item.show .nav-link { - color: white; - background-color: #c63f1f; + color: var(--attack-on-color-primary); + background-color: var(--attack-color-primary); border-color: transparent; } .navbar .search-button { padding: 0.3rem 1rem; font-size: 1rem; - border: 0.0625rem solid white; + border: 0.0625rem solid var(--attack-on-color-primary); border-radius: 0.25rem; - color: white; + color: var(--attack-on-color-primary); line-height: 1.5rem; opacity: 0.8; } @@ -1555,21 +1607,84 @@ div#sidebars { } .navbar .search-button .search-icon { cursor: pointer; - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%23FFFFFF' xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E"); + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%23fff' xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E"); } .navbar .search-button .error-icon { cursor: default; - background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg fill='%23FFFFFF' xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 96 960 960' width='24'%3e%3cpath d='M479.982 776q14.018 0 23.518-9.482 9.5-9.483 9.5-23.5 0-14.018-9.482-23.518-9.483-9.5-23.5-9.5-14.018 0-23.518 9.482-9.5 9.483-9.5 23.5 0 14.018 9.482 23.518 9.483 9.5 23.5 9.5ZM453 623h60V370h-60v253Zm27.266 353q-82.734 0-155.5-31.5t-127.266-86q-54.5-54.5-86-127.341Q80 658.319 80 575.5q0-82.819 31.5-155.659Q143 347 197.5 293t127.341-85.5Q397.681 176 480.5 176q82.819 0 155.659 31.5Q709 239 763 293t85.5 127Q880 493 880 575.734q0 82.734-31.5 155.5T763 858.316q-54 54.316-127 86Q563 976 480.266 976Zm.234-60Q622 916 721 816.5t99-241Q820 434 721.188 335 622.375 236 480 236q-141 0-240.5 98.812Q140 433.625 140 576q0 141 99.5 240.5t241 99.5Zm-.5-340Z'/%3e%3c/svg%3e"); + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg fill='%23fff' xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 96 960 960' width='24'%3e%3cpath d='M479.982 776q14.018 0 23.518-9.482 9.5-9.483 9.5-23.5 0-14.018-9.482-23.518-9.483-9.5-23.5-9.5-14.018 0-23.518 9.482-9.5 9.483-9.5 23.5 0 14.018 9.482 23.518 9.483 9.5 23.5 9.5ZM453 623h60V370h-60v253Zm27.266 353q-82.734 0-155.5-31.5t-127.266-86q-54.5-54.5-86-127.341Q80 658.319 80 575.5q0-82.819 31.5-155.659Q143 347 197.5 293t127.341-85.5Q397.681 176 480.5 176q82.819 0 155.659 31.5Q709 239 763 293t85.5 127Q880 493 880 575.734q0 82.734-31.5 155.5T763 858.316q-54 54.316-127 86Q563 976 480.266 976Zm.234-60Q622 916 721 816.5t99-241Q820 434 721.188 335 622.375 236 480 236q-141 0-240.5 98.812Q140 433.625 140 576q0 141 99.5 240.5t241 99.5Zm-.5-340Z'/%3e%3c/svg%3e"); +} +.navbar .theme-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 3.625rem; + min-height: 2.375rem; + margin-right: 0.5rem; + padding: 0.25rem; + border: 0; + color: var(--attack-on-color-primary); +} +.navbar .theme-toggle .theme-toggle-track { + position: relative; + display: inline-flex; + align-items: center; + justify-content: space-between; + width: 3.125rem; + height: 1.75rem; + padding: 0 0.4375rem; + border: 0.0625rem solid var(--attack-on-color-primary); + border-radius: 0.875rem; + background: rgba(0, 0, 0, 0.2); + box-sizing: border-box; + transition: background-color 0.2s ease; +} +.navbar .theme-toggle .theme-toggle-icon { + position: relative; + z-index: 2; + visibility: visible; + display: inline-block; + font-size: 0.75rem; + line-height: 1; +} +.navbar .theme-toggle:hover, .navbar .theme-toggle:focus { + color: var(--attack-on-color-primary); +} +.navbar .theme-toggle:hover .theme-toggle-track, .navbar .theme-toggle:focus .theme-toggle-track { + background: rgba(0, 0, 0, 0.35); + box-shadow: 0 0 0 0.125rem rgba(255, 255, 255, 0.3); +} +.navbar .theme-toggle .theme-toggle-icon-light { + color: var(--attack-color-primary); +} +.navbar .theme-toggle .theme-toggle-thumb { + position: absolute; + top: 0.125rem; + left: 0.125rem; + z-index: 1; + width: 1.375rem; + height: 1.375rem; + border-radius: 50%; + background: var(--attack-on-color-primary); + box-shadow: 0 0.0625rem 0.1875rem rgba(0, 0, 0, 0.35); + transition: transform 0.2s ease; +} +.navbar .theme-toggle[data-theme-effective=dark] .theme-toggle-track { + background: var(--attack-color-secondary); +} +.navbar .theme-toggle[data-theme-effective=dark] .theme-toggle-icon-light { + color: var(--attack-on-color-primary); +} +.navbar .theme-toggle[data-theme-effective=dark] .theme-toggle-icon-dark { + color: var(--attack-color-secondary); +} +.navbar .theme-toggle[data-theme-effective=dark] .theme-toggle-thumb { + transform: translateX(1.375rem); } /* **** */ .nav, .faq { /* NAVIGATION Dropdown */ - /* **** */ - /* Side NAVIGATION */ - border-color: rgb(223.125, 223.125, 223.125) !important; - /* **** */ } .nav .dropdown:hover > .dropdown-menu, .faq .dropdown:hover > .dropdown-menu { @@ -1577,23 +1692,29 @@ div#sidebars { } .nav .dropdown-menu, .faq .dropdown-menu { - background-color: #c63f1f; + background-color: var(--attack-color-primary); } .nav .dropdown-menu .dropdown-item, .faq .dropdown-menu .dropdown-item { - color: white; + color: var(--attack-on-color-primary); } .nav .dropdown-menu .dropdown-item:hover, .nav .dropdown-menu .dropdown-item:focus, .faq .dropdown-menu .dropdown-item:hover, .faq .dropdown-menu .dropdown-item:focus { - color: white; + color: var(--attack-on-color-primary); text-decoration: underline; background-color: transparent; } +.nav, +.faq { + /* **** */ + /* Side NAVIGATION */ + border-color: var(--attack-border-color-body) !important; +} .nav .heading, .faq .heading { font-size: 1.6rem; - color: rgb(106.5, 114, 120.75); + color: var(--attack-on-color-body-deemphasis); letter-spacing: 0.1875rem; pointer-events: none; } @@ -1640,33 +1761,37 @@ div#sidebars { .nav .heading-dropdown, .faq .heading-dropdown { font-size: 1.2rem; - color: #062f4f; + color: var(--attack-color-secondary); letter-spacing: 0.1875rem; } @media screen and (width <= 90.62rem) { .nav .heading, .faq .heading { font-size: 1.2rem; - color: rgb(106.5, 114, 120.75); + color: var(--attack-on-color-body-deemphasis); letter-spacing: 0.1875rem; } .nav .heading-dropdown, .faq .heading-dropdown { font-size: 1rem; - color: #39434c; + color: var(--attack-on-color-body); letter-spacing: 0.0625rem; } } +.nav, +.faq { + /* **** */ +} .nav .nav-link, .faq .nav-link { font-size: 1rem; padding: 0.3rem 1rem; - color: rgb(106.5, 114, 120.75); + color: var(--attack-on-color-body-deemphasis); } .nav .nav-link.expand-title, .faq .nav-link.expand-title { font-size: 1.1rem; - color: #39434c; + color: var(--attack-on-color-body); } .nav .nav-link.side, .faq .nav-link.side { @@ -1675,14 +1800,14 @@ div#sidebars { } .nav .nav-link.side:hover, .faq .nav-link.side:hover { - background-color: #c63f1f; - color: white; + background-color: var(--attack-color-primary); + color: var(--attack-on-color-primary); } .nav .nav-link.side.active, .faq .nav-link.side.active { - color: #c63f1f; - background-color: rgb(242.25, 242.25, 242.25); - border-right: 0.1875rem solid #c63f1f; + color: var(--attack-color-primary); + background-color: var(--attack-color-body-alternate); + border-right: 0.1875rem solid var(--attack-color-primary); } /* **** */ @@ -1693,7 +1818,7 @@ div#sidebars { cursor: col-resize; height: 100%; position: absolute; - background-color: #dfdfdf; + background-color: var(--attack-border-color-body); } .data-sources-menu { @@ -1725,11 +1850,11 @@ div#sidebars { } } .sidebar.nav .sidenav-wrapper .heading { - border-bottom: 1px solid rgb(242.25, 242.25, 242.25); + border-bottom: 1px solid var(--attack-color-body-alternate); flex: 0 1 0; } .sidebar.nav .sidenav-wrapper .checkbox-div { - border-bottom: 1px solid rgb(242.25, 242.25, 242.25); + border-bottom: 1px solid var(--attack-color-body-alternate); flex: 0 1 0; } .sidebar.nav .sidenav-wrapper .sidenav-list { @@ -1748,11 +1873,11 @@ div#sidebars { .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head a, .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head .expand-button { cursor: pointer; - color: black; + color: var(--attack-on-color-body); } .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head a:hover, .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head .expand-button:hover { - background: rgb(242.25, 242.25, 242.25); + background: var(--attack-color-body-alternate); } .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head a, .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head span { @@ -1768,7 +1893,7 @@ div#sidebars { } .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head .expand-button { padding: 5px; - border-left: 1px solid rgb(242.25, 242.25, 242.25); + border-left: 1px solid var(--attack-color-body-alternate); display: inline-block; display: flex; flex-direction: row; @@ -1787,7 +1912,7 @@ div#sidebars { display: inline-block; vertical-align: top; background-position: center; - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%2339434C' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E"); + background-image: var(--attack-select-arrow); z-index: 1; transition: all 0.2s ease; } @@ -1795,9 +1920,9 @@ div#sidebars { transform: rotate(-180deg); } .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head.active, .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head.active > a { - color: #0156b3 !important; + color: var(--attack-color-active) !important; font-weight: bolder; - background: #eaeaea; + background: var(--attack-color-body-alternate-strong); font-family: Roboto-Bold, sans-serif; } .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-body { @@ -1834,8 +1959,8 @@ div#sidebars { } } .search-word-found { - background: yellow; - color: black; + background: var(--attack-color-search-highlight); + color: var(--attack-on-color-search-highlight); } .btn-group-text { @@ -1858,8 +1983,8 @@ div#sidebars { } .overlay.search .overlay-inner { border-radius: 25px; - background: white; - color: #39434c; + background: var(--attack-color-body); + color: var(--attack-on-color-body); width: 100%; height: 100%; display: flex; @@ -1880,8 +2005,8 @@ div#sidebars { line-height: 50px; width: 100%; border: 0; - background: white; - color: #39434c; + background: var(--attack-color-body); + color: var(--attack-on-color-body); } .overlay.search .overlay-inner .search-header .search-input input:focus { outline: none; @@ -1919,10 +2044,10 @@ div#sidebars { .overlay.search .overlay-inner .search-filters button, .overlay.search .overlay-inner .search-filters .search-filter-chip { min-height: 36px; - border: 1px solid rgb(223.125, 223.125, 223.125); + border: 1px solid var(--attack-border-color-body); border-radius: 4px; - background: white; - color: #39434c; + background: var(--attack-color-body); + color: var(--attack-on-color-body); cursor: pointer; } .overlay.search .overlay-inner .search-filters .search-filter-summary { @@ -1935,7 +2060,7 @@ div#sidebars { font-weight: 600; } .overlay.search .overlay-inner .search-filters .search-filter-summary-chip.open { - border-color: #0156b3; + border-color: var(--attack-color-active); } .overlay.search .overlay-inner .search-filters .search-filter-dropdown { position: relative; @@ -1949,9 +2074,9 @@ div#sidebars { min-width: 240px; max-width: min(420px, 100vw - 100px); padding: 12px; - border: 1px solid rgb(223.125, 223.125, 223.125); + border: 1px solid var(--attack-border-color-body); border-radius: 8px; - background: white; + background: var(--attack-color-body); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); } .overlay.search .overlay-inner .search-filters .search-filter-group-heading { @@ -1994,9 +2119,9 @@ div#sidebars { border: 0; } .overlay.search .overlay-inner .search-filters .search-filter-chip.selected { - border-color: #0156b3; - background: #0156b3; - color: #eaeaea; + border-color: var(--attack-color-active); + background: var(--attack-color-active); + color: var(--attack-on-color-active); } .overlay.search .overlay-inner .search-filters .search-filter-count { margin-left: 4px; @@ -2012,7 +2137,7 @@ div#sidebars { flex-direction: column; min-height: 0; padding: 0 50px; - border-top: 1px solid rgb(223.125, 223.125, 223.125); + border-top: 1px solid var(--attack-border-color-body); margin-bottom: 25px; overflow: hidden; scrollbar-gutter: stable; @@ -2037,17 +2162,18 @@ div#sidebars { padding: 2px 8px; border: 1px solid; border-radius: 4px; - color: white; + color: var(--attack-on-color-active); font-size: 0.8rem; font-weight: 700; } .overlay.search .overlay-inner .search-body .results .search-result-badge-page-type { - border-color: #0156b3; - background: rgb(19.975, 92.225, 171.275); + border-color: var(--attack-color-active); + background: var(--attack-color-active-alternate-medium); } .overlay.search .overlay-inner .search-body .results .search-result-badge-domain { - border-color: #303435; - background: #303435; + border-color: var(--attack-color-deemphasis); + background: var(--attack-color-deemphasis); + color: var(--attack-on-color-deemphasis); } .overlay.search .overlay-inner .search-body .results .search-no-results .preview { display: flex; @@ -2072,8 +2198,8 @@ div#sidebars { justify-content: flex-end; min-height: 58px; padding: 12px 0 14px; - border-top: 1px solid rgb(223.125, 223.125, 223.125); - background: white; + border-top: 1px solid var(--attack-border-color-body); + background: var(--attack-color-body); } .overlay.search .overlay-inner .search-body .search-results-pagination:empty { display: none; @@ -2088,17 +2214,17 @@ div#sidebars { .overlay.search .overlay-inner .search-body .search-pagination button { min-height: 36px; padding: 5px 12px; - border: 1px solid rgb(223.125, 223.125, 223.125); + border: 1px solid var(--attack-border-color-body); border-radius: 4px; - background: white; - color: #39434c; + background: var(--attack-color-body); + color: var(--attack-on-color-body); cursor: pointer; font-weight: 700; } .overlay.search .overlay-inner .search-body .search-pagination button.current { - border-color: #0156b3; - background: #0156b3; - color: #eaeaea; + border-color: var(--attack-color-active); + background: var(--attack-color-active); + color: var(--attack-on-color-active); } .overlay.search .overlay-inner .search-body .search-pagination button:disabled { cursor: default; @@ -2121,7 +2247,7 @@ div#sidebars { align-items: center; min-height: 36px; padding: 0 4px; - color: #303435; + color: var(--attack-color-deemphasis); font-weight: 700; } .overlay.search .overlay-inner .search-body .search-pagination-icon { @@ -2208,27 +2334,27 @@ div#sidebars { margin-bottom: 5px !important; } } -.popover { - box-shadow: 0 0 5px 3px white; - border-color: #c63f1f; -} .popover .popover-header { - background: #c63f1f; - color: white; + background: var(--attack-color-primary); + color: var(--attack-on-color-primary); +} +.popover { + box-shadow: 0 0 5px 3px var(--attack-color-body); + border-color: var(--attack-color-primary); } .popover.bs-popover-left .arrow::before { - border-left-color: #c63f1f; + border-left-color: var(--attack-color-primary); } .popover.bs-popover-right .arrow::before { - border-right-color: #c63f1f; + border-right-color: var(--attack-color-primary); } .popover.bs-popover-top .arrow::before { - border-top-color: #c63f1f; + border-top-color: var(--attack-color-primary); } .popover.bs-popover-bottom .arrow::before, .popover.bs-popover-bottom .arrow::after, .popover.bs-popover-bottom .popover-header::before { - border-bottom-color: #c63f1f; + border-bottom-color: var(--attack-color-primary); } .tour-backdrop, @@ -2237,21 +2363,21 @@ div#sidebars { } .matrix-container { - border: 1px solid rgb(223.125, 223.125, 223.125); - background: white; + border: 1px solid var(--attack-border-color-body); + background: var(--attack-color-body); } .matrix-container + .matrix-container { margin-top: 1rem; } .matrix-container .matrix-border { - border-left: 1px solid rgb(223.125, 223.125, 223.125); + border-left: 1px solid var(--attack-border-color-body); padding-left: 0.5rem; display: flex; justify-content: center; align-items: center; } .matrix-container .matrix-title { - border-bottom: 1px solid rgb(223.125, 223.125, 223.125); + border-bottom: 1px solid var(--attack-border-color-body); margin-bottom: 1rem; padding-bottom: 0.5rem; } @@ -2278,35 +2404,35 @@ div#sidebars { right: 0; } .matrix-container .scroll-indicator-group .scroll-indicator.right.show .cover { - background: linear-gradient(to right, rgba(255, 255, 255, 0.001), white); + background: linear-gradient(to right, rgba(255, 255, 255, 0.001), var(--attack-color-body)); } .matrix-container .scroll-indicator-group .scroll-indicator.left .cover { left: 0; } .matrix-container .scroll-indicator-group .scroll-indicator.left.show .cover { - background: linear-gradient(to left, rgba(255, 255, 255, 0.001), white); + background: linear-gradient(to left, rgba(255, 255, 255, 0.001), var(--attack-color-body)); } .matrix { white-space: normal; line-height: 14px; } -.matrix.side .tactic { - padding: 2px 5px; - width: 1%; - vertical-align: top; -} .matrix.side .tactic:first-child { padding: 2px 5px 2px 2px; } .matrix.side .tactic:last-child { padding: 2px 2px 2px 5px; } +.matrix.side .tactic { + padding: 2px 5px; + width: 1%; + vertical-align: top; +} .matrix.side .tactic:hover:not(.name, .count) { - background: rgb(223.125, 223.125, 223.125); + background: var(--attack-background-color-body); } .matrix.side .tactic:hover:not(.name, .count) .sidebar.expanded .angle { - background: rgb(223.125, 223.125, 223.125); + background: var(--attack-background-color-body); } .matrix.side .tactic.name, .matrix.side .tactic.count { text-align: center; @@ -2317,7 +2443,7 @@ div#sidebars { } .matrix.side .tactic.count { font-size: 13px; - border-bottom: 1px solid black; + border-bottom: 1px solid var(--attack-border-color-body); padding-bottom: 5px; margin-bottom: 5px; } @@ -2343,23 +2469,23 @@ div#sidebars { vertical-align: top; } .matrix.side .tactic .supertechnique td.technique { - outline: 1px solid rgb(106.5, 114, 120.75); + outline: 1px solid var(--attack-on-color-body-deemphasis); outline-offset: -1px; } +.matrix.side .tactic .subtechniques.hidden { + display: none; +} .matrix.side .tactic .subtechniques { display: flex; flex-direction: column; height: 100%; margin-left: -1px; - border-left: 2px solid rgb(106.5, 114, 120.75); - outline: 1px solid rgb(106.5, 114, 120.75); + border-left: 2px solid var(--attack-on-color-body-deemphasis); + outline: 1px solid var(--attack-on-color-body-deemphasis); outline-offset: -1px; white-space: nowrap; vertical-align: top; } -.matrix.side .tactic .subtechniques.hidden { - display: none; -} .matrix.side .tactic .subtechniques .subtechnique { height: 100%; flex-grow: 1; @@ -2368,7 +2494,7 @@ div#sidebars { text-align: center; vertical-align: middle; transform: rotate(-90deg); - color: rgb(242.25, 242.25, 242.25); + color: var(--attack-color-body-alternate); width: 12px; height: 12px; font-size: 16px; @@ -2378,7 +2504,7 @@ div#sidebars { min-width: 8px; width: 12px; padding: 0; - background: rgb(106.5, 114, 120.75); + background: var(--attack-on-color-body-deemphasis); cursor: pointer; position: relative; vertical-align: middle; @@ -2391,10 +2517,10 @@ div#sidebars { height: 12px; display: block; position: absolute; - background: white; + background: var(--attack-color-body); } .matrix.side .tactic .sidebar.expanded .angle svg { - fill: rgb(106.5, 114, 120.75); + fill: var(--attack-on-color-body-deemphasis); vertical-align: baseline; } .matrix.side .tactic .sidebar.expanded .angle.top { @@ -2417,7 +2543,7 @@ div#sidebars { } .matrix.flat .tactic.count { font-size: 13px; - border-bottom: 1px solid black; + border-bottom: 1px solid var(--attack-border-color-body); padding-bottom: 5px; margin-bottom: 5px; } @@ -2438,7 +2564,7 @@ div#sidebars { min-width: 8px; width: 12px; padding: 0; - background: rgb(106.5, 114, 120.75); + background: var(--attack-on-color-body-deemphasis); cursor: pointer; vertical-align: middle; } @@ -2446,21 +2572,21 @@ div#sidebars { text-align: center; vertical-align: middle; transform: rotate(-90deg); - color: rgb(242.25, 242.25, 242.25); + color: var(--attack-color-body-alternate); width: 12px; height: 9px; font-size: 16px; line-height: 12px; } .matrix.flat .tactic .supertechnique td.sidebar.subtechniques svg { - fill: rgb(106.5, 114, 120.75); + fill: var(--attack-on-color-body-deemphasis); vertical-align: baseline; } .matrix.flat .tactic .supertechnique td.sidebar { - border-right: 2px solid rgb(106.5, 114, 120.75); + border-right: 2px solid var(--attack-on-color-body-deemphasis); } .matrix.flat .tactic .supertechnique td.technique { - outline: 1px solid rgb(106.5, 114, 120.75); + outline: 1px solid var(--attack-on-color-body-deemphasis); outline-offset: -1px; } .matrix.flat .tactic .more-icon { @@ -2477,11 +2603,9 @@ div#sidebars { height: 100%; display: flex; align-items: center; - background-color: white; + background-color: var(--attack-color-body); font-size: 13px; line-height: 14px; - outline: 1px solid transparent; - outline-offset: -1px; } .matrix .technique-cell a { display: block; @@ -2489,8 +2613,12 @@ div#sidebars { height: 100%; padding: 7px 3px; } +.matrix .technique-cell { + outline: 1px solid transparent; + outline-offset: -1px; +} .matrix .technique-cell:not(.colored):not(.supertechniquecell) { - outline-color: rgb(223.125, 223.125, 223.125); + outline-color: var(--attack-border-color-body); } .matrix-controls { @@ -2499,17 +2627,17 @@ div#sidebars { padding: 1rem; } .matrix-controls button { - border-color: rgb(223.125, 223.125, 223.125); - background: white; - color: #39434c; + border-color: var(--attack-border-color-body); + background: var(--attack-color-body); + color: var(--attack-on-color-body); } .matrix-controls button:hover { - background: rgb(244.8, 244.8, 244.8); + background: var(--attack-color-body-alternate-subtle); } .matrix-controls .layout-button:active { - color: #16181b; + color: var(--attack-on-color-body); text-decoration: none; - background-color: #f8f9fa; + background-color: var(--attack-color-body-alternate-subtle); } .center-controls .matrix-controls .btn-toolbar { @@ -2528,9 +2656,275 @@ div#sidebars { } .version-table .table-break-row { - border-right-color: white; - border-left-color: white; + border-right-color: var(--attack-color-body); + border-left-color: var(--attack-color-body); padding: 1rem 0; } +:root, +:root[data-theme=light] { + --attack-color-primary: #c63f1f; + --attack-on-color-primary: white; + --attack-color-secondary: #062f4f; + --attack-color-secondary-hover: rgb(7.5, 58.75, 98.75); + --attack-on-color-secondary: white; + --attack-color-footer: #0b2338; + --attack-on-color-footer: #87deff; + --attack-color-footer-link-hover: rgb(99, 167, 206.5); + --attack-color-active: #0156b3; + --attack-color-active-alternate-medium: rgb(19.975, 92.225, 171.275); + --attack-on-color-active: #eaeaea; + --attack-color-body: white; + --attack-on-color-body: #39434c; + --attack-on-color-body-emphasis: #1d2226; + --attack-color-property-label: #1d2226; + --attack-on-color-body-deemphasis: #6b7379; + --attack-color-body-alternate-subtle: #f5f5f5; + --attack-color-body-alternate: #f2f2f2; + --attack-color-body-alternate-strong: #e6e6e6; + --attack-color-body-alternate-strongest: #d9d9d9; + --attack-border-color-body: #dfdfdf; + --attack-background-color-body: #dfdfdf; + --attack-color-link: #3f709e; + --attack-color-link-hover: #0056b3; + --attack-color-matrix-header: gray; + --attack-on-color-matrix-header: white; + --attack-color-search-highlight: yellow; + --attack-on-color-search-highlight: black; + --attack-color-deemphasis: #686f75; + --attack-on-color-deemphasis: white; + --attack-color-card-header: rgb(57 67 76 / 3%); + --attack-color-code: #a52f16; + --attack-color-danger: #bd2130; + --attack-color-image-background: white; + --attack-color-banner: #e7f0f6; + --attack-on-color-banner: #263b4a; + --attack-border-color-banner: #c2d5e2; + --attack-select-arrow: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%2339434c' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z'/%3E%3C/svg%3E"); + color-scheme: light; +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme]) { + --attack-color-primary: #c63f1f; + --attack-on-color-primary: white; + --attack-color-secondary: #062f4f; + --attack-color-secondary-hover: rgb(8.4, 65.8, 110.6); + --attack-on-color-secondary: white; + --attack-color-footer: #0b2338; + --attack-on-color-footer: #87deff; + --attack-color-footer-link-hover: #b7edff; + --attack-color-active: #60a9ff; + --attack-color-active-alternate-medium: #3d8cdb; + --attack-on-color-active: #0f171c; + --attack-color-body: #222426; + --attack-on-color-body: #e8e6e3; + --attack-on-color-body-emphasis: #fffaf4; + --attack-color-property-label: #f2d2a4; + --attack-on-color-body-deemphasis: #b7b1a8; + --attack-color-body-alternate-subtle: #272a2c; + --attack-color-body-alternate: #2b2e30; + --attack-color-body-alternate-strong: #303437; + --attack-color-body-alternate-strongest: #353a3d; + --attack-border-color-body: #596166; + --attack-background-color-body: #373d40; + --attack-color-link: #7bb8ee; + --attack-color-link-hover: #b7ddff; + --attack-color-matrix-header: #596166; + --attack-on-color-matrix-header: #fffaf4; + --attack-color-search-highlight: #665a00; + --attack-on-color-search-highlight: #fff4b8; + --attack-color-deemphasis: #b7b1a8; + --attack-on-color-deemphasis: #222426; + --attack-color-card-header: #2b2e30; + --attack-color-code: #ff8f70; + --attack-color-danger: #ff8c96; + --attack-color-image-background: white; + --attack-color-banner: #263a49; + --attack-on-color-banner: #f2f7fa; + --attack-border-color-banner: #3f5d72; + --attack-select-arrow: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%23e8e6e3' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z'/%3E%3C/svg%3E"); + color-scheme: dark; + } +} +:root[data-theme=dark] { + --attack-color-primary: #c63f1f; + --attack-on-color-primary: white; + --attack-color-secondary: #062f4f; + --attack-color-secondary-hover: rgb(8.4, 65.8, 110.6); + --attack-on-color-secondary: white; + --attack-color-footer: #0b2338; + --attack-on-color-footer: #87deff; + --attack-color-footer-link-hover: #b7edff; + --attack-color-active: #60a9ff; + --attack-color-active-alternate-medium: #3d8cdb; + --attack-on-color-active: #0f171c; + --attack-color-body: #222426; + --attack-on-color-body: #e8e6e3; + --attack-on-color-body-emphasis: #fffaf4; + --attack-color-property-label: #f2d2a4; + --attack-on-color-body-deemphasis: #b7b1a8; + --attack-color-body-alternate-subtle: #272a2c; + --attack-color-body-alternate: #2b2e30; + --attack-color-body-alternate-strong: #303437; + --attack-color-body-alternate-strongest: #353a3d; + --attack-border-color-body: #596166; + --attack-background-color-body: #373d40; + --attack-color-link: #7bb8ee; + --attack-color-link-hover: #b7ddff; + --attack-color-matrix-header: #596166; + --attack-on-color-matrix-header: #fffaf4; + --attack-color-search-highlight: #665a00; + --attack-on-color-search-highlight: #fff4b8; + --attack-color-deemphasis: #b7b1a8; + --attack-on-color-deemphasis: #222426; + --attack-color-card-header: #2b2e30; + --attack-color-code: #ff8f70; + --attack-color-danger: #ff8c96; + --attack-color-image-background: white; + --attack-color-banner: #263a49; + --attack-on-color-banner: #f2f7fa; + --attack-border-color-banner: #3f5d72; + --attack-select-arrow: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%23e8e6e3' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z'/%3E%3C/svg%3E"); + color-scheme: dark; +} + +.form-control, +.custom-select, +.bootstrap-select > .dropdown-toggle, +.dropdown-menu, +.list-group-item, +.modal-content, +.popover, +.popover-body, +.page-link, +.input-group-text { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body); + border-color: var(--attack-border-color-body); +} + +.custom-select { + background-image: var(--attack-select-arrow); +} + +.form-control:focus, +.custom-select:focus { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body); + border-color: var(--attack-color-active); + box-shadow: 0 0 0 0.2rem rgba(76, 159, 254, 0.25); +} + +.dropdown-item, +.page-link { + color: var(--attack-color-link); +} + +.dropdown-item:hover, +.dropdown-item:focus, +.page-link:hover, +.page-link:focus { + color: var(--attack-on-color-body-emphasis); + background-color: var(--attack-color-body-alternate); +} + +.form-control:disabled, +.form-control[readonly], +.custom-select:disabled, +.page-item.disabled .page-link { + color: var(--attack-on-color-body-deemphasis); + background-color: var(--attack-color-body-alternate); +} + +.dropdown-divider, +hr { + border-color: var(--attack-border-color-body); +} + +.table { + color: var(--attack-on-color-body); +} + +.text-danger { + color: var(--attack-color-danger) !important; +} + +.btn-outline-secondary { + color: var(--attack-on-color-body); + border-color: var(--attack-on-color-body-deemphasis); +} +.btn-outline-secondary:disabled, .btn-outline-secondary.disabled { + color: var(--attack-on-color-body-deemphasis); + background-color: transparent; +} +.btn-outline-secondary:not(:disabled, .disabled):hover, .btn-outline-secondary:not(:disabled, .disabled):focus, .btn-outline-secondary:not(:disabled, .disabled):active { + color: var(--attack-on-color-active); + background-color: var(--attack-color-active); + border-color: var(--attack-color-active); +} + +.nav-tabs .nav-link { + color: var(--attack-color-link); + border-color: transparent; +} + +.nav-tabs .nav-link:hover, +.nav-tabs .nav-link:focus { + border-color: var(--attack-border-color-body); +} + +.nav-tabs .nav-link.active, +.nav-tabs .nav-item.show .nav-link { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body); + border-color: var(--attack-border-color-body) var(--attack-border-color-body) var(--attack-color-body); +} + +@media print { + :root, + :root[data-theme], + :root:not([data-theme]) { + --attack-color-primary: #c63f1f; + --attack-on-color-primary: white; + --attack-color-secondary: #062f4f; + --attack-color-secondary-hover: rgb(7.5, 58.75, 98.75); + --attack-on-color-secondary: white; + --attack-color-footer: #0b2338; + --attack-on-color-footer: #87deff; + --attack-color-footer-link-hover: rgb(99, 167, 206.5); + --attack-color-active: #0156b3; + --attack-color-active-alternate-medium: rgb(19.975, 92.225, 171.275); + --attack-on-color-active: #eaeaea; + --attack-color-body: white; + --attack-on-color-body: #39434c; + --attack-on-color-body-emphasis: #1d2226; + --attack-color-property-label: #1d2226; + --attack-on-color-body-deemphasis: #6b7379; + --attack-color-body-alternate-subtle: #f5f5f5; + --attack-color-body-alternate: #f2f2f2; + --attack-color-body-alternate-strong: #e6e6e6; + --attack-color-body-alternate-strongest: #d9d9d9; + --attack-border-color-body: #dfdfdf; + --attack-background-color-body: #dfdfdf; + --attack-color-link: #3f709e; + --attack-color-link-hover: #0056b3; + --attack-color-matrix-header: gray; + --attack-on-color-matrix-header: white; + --attack-color-search-highlight: yellow; + --attack-on-color-search-highlight: black; + --attack-color-deemphasis: #686f75; + --attack-on-color-deemphasis: white; + --attack-color-card-header: rgb(57 67 76 / 3%); + --attack-color-code: #a52f16; + --attack-color-danger: #bd2130; + --attack-color-image-background: white; + --attack-color-banner: #e7f0f6; + --attack-on-color-banner: #263b4a; + --attack-border-color-banner: #c2d5e2; + --attack-select-arrow: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%2339434c' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z'/%3E%3C/svg%3E"); + color-scheme: light; + } +} + /*# sourceMappingURL=style-attack.css.map */ diff --git a/attack-theme/static/style-user.css b/attack-theme/static/style-user.css index 0f2df8b35c0..d73da053bb3 100644 --- a/attack-theme/static/style-user.css +++ b/attack-theme/static/style-user.css @@ -37,18 +37,18 @@ src: url("fonts/Roboto/Roboto-Black.ttf"); } .deemphasis { - color: rgb(106.5, 114, 120.75); + color: var(--attack-on-color-body-deemphasis); } .matrix-header { - background-color: gray; - color: white; + background-color: var(--attack-color-matrix-header); + color: var(--attack-on-color-matrix-header); } .table-alternate, .blog-post table, .changelog table, .bg-alternate { - background-color: rgb(242.25, 242.25, 242.25) !important; + background-color: var(--attack-color-body-alternate) !important; } .text-label { @@ -58,7 +58,7 @@ .text-label-small { font-size: 12px; - color: #303435; + color: var(--attack-color-deemphasis); margin-top: -10px; } @@ -69,8 +69,8 @@ html { body { height: 100%; - background: white; - color: #39434c; + background: var(--attack-color-body); + color: var(--attack-on-color-body); font-family: "Roboto-Regular", sans-serif; display: flex; flex-direction: column; @@ -100,19 +100,22 @@ strong { .jumbotron { padding: 0; - background: white; - color: #39434c; + background: var(--attack-color-body); + color: var(--attack-on-color-body); } .jumbotron code { - color: rgb(28.5, 33.5, 38); - background-color: rgb(229.5, 229.5, 229.5); + color: var(--attack-on-color-body-emphasis); + background-color: var(--attack-color-body-alternate-strong); border-radius: 3px; font-family: courier, monospace; padding: 0 3px; } a { - color: #4f7cac; + color: var(--attack-color-link); +} +a:hover { + color: var(--attack-color-link-hover); } a .anchor::before { content: ""; @@ -175,7 +178,23 @@ a .anchor::before { padding: 0.3125rem 0; } .quick-links tr + tr { - border-top: 1px solid rgb(223.125, 223.125, 223.125); + border-top: 1px solid var(--attack-border-color-body); +} +.quick-links .external-link-icon { + margin-left: 0.125rem; + font-size: 0.8125rem; +} +.quick-links .random-page-toggle { + margin-left: 0.375rem; + padding: 0 0.375rem; + border: 1px solid var(--attack-color-active); + color: var(--attack-color-link); + background: var(--attack-color-body); +} +.quick-links .random-page-toggle:hover, .quick-links .random-page-toggle:focus, .quick-links .random-page-toggle[aria-expanded=true] { + border-color: var(--attack-color-secondary); + color: var(--attack-on-color-secondary); + background: var(--attack-color-secondary); } .row-main-page { @@ -191,16 +210,16 @@ a .anchor::before { } } .p-line p { - border-top: 0.0625rem solid #1c2226; + border-top: 0.0625rem solid var(--attack-border-color-body); } .btn-default { - background: white; - border-color: rgb(106.5, 114, 120.75); - color: #39434c; + background: var(--attack-color-body); + border-color: var(--attack-on-color-body-deemphasis); + color: var(--attack-on-color-body); } .btn-default:hover { - color: rgb(28.5, 33.5, 38); + color: var(--attack-on-color-body-emphasis); } .website-button, .slide-button-secondary, .slide-button { @@ -221,9 +240,9 @@ a .anchor::before { } .slide-button { - border-color: #303435; - color: #fff; - background: #303435; + border-color: var(--attack-color-active); + color: var(--attack-on-color-active); + background: var(--attack-color-active); padding: 6px 16px; } a .slide-button { @@ -231,9 +250,9 @@ a .slide-button { } .slide-button-secondary { - color: #303435; - background: #fff; - border-color: #303435; + color: var(--attack-color-active); + background: var(--attack-color-body); + border-color: var(--attack-color-active); padding: 6px 16px; } a .slide-button-secondary { @@ -249,15 +268,21 @@ a .slide-button-secondary { padding-left: 8px; } -.slide-button:hover { - background: #303435; - border-color: #303435; +.slide-button:hover, +.slide-button:focus, +.slide-button:active, +.slide-button[aria-expanded=true] { + background: var(--attack-color-secondary); + border-color: var(--attack-color-secondary); + color: var(--attack-on-color-secondary); } -.slide-button-secondary:hover { - background: #eaeaea; - border-color: #303435; - color: #303435; +.slide-button-secondary:hover, +.slide-button-secondary:focus, +.slide-button-secondary:active { + background: var(--attack-color-secondary); + border-color: var(--attack-color-secondary); + color: var(--attack-on-color-secondary); } .dropdown { @@ -268,7 +293,7 @@ a .slide-button-secondary { .dropdown-content { display: none; position: absolute; - background-color: white; + background-color: var(--attack-color-body); min-width: 160px; box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2); } @@ -278,16 +303,16 @@ a .slide-button-secondary { } .btn-navy:hover, .btn-navy { - color: white; - border-color: 0.0625rem solid #303435; + color: var(--attack-on-color-secondary); + border-color: 0.0625rem solid var(--attack-color-secondary); } .btn-navy { - background-color: #303435; - color: white; + background-color: var(--attack-color-secondary); + color: var(--attack-on-color-secondary); } .btn-navy:hover { - background-color: rgb(57.7188118812, 62.5287128713, 63.7311881188); + background-color: var(--attack-color-secondary-hover); background-image: none; } @@ -312,17 +337,17 @@ a .slide-button-secondary { .changelog table { empty-cells: hide; } +.table td p:last-child, +.blog-post table td p:last-child, +.changelog table td p:last-child { + margin-bottom: 0; +} .table td, .blog-post table td, .changelog table td { padding: 0.75rem; vertical-align: top; } -.table td p:last-child, -.blog-post table td p:last-child, -.changelog table td p:last-child { - margin-bottom: 0; -} .table th, .blog-post table th, .changelog table th { @@ -333,7 +358,7 @@ a .slide-button-secondary { .table .active, .blog-post table .active, .changelog table .active { - color: #303435; + color: var(--attack-color-active); } .blog-post table { @@ -343,13 +368,13 @@ a .slide-button-secondary { .table-alternate tbody, .blog-post table tbody, .changelog table tbody { - background: white; - color: #39434c; + background: var(--attack-color-body); + color: var(--attack-on-color-body); } .table-techniques thead tr { - background: #f2f2f2; - border-bottom: 2px solid #dee2e6; + background: var(--attack-color-body-alternate); + border-bottom: 2px solid var(--attack-border-color-body); } .table-techniques table { border-collapse: collapse; @@ -357,10 +382,10 @@ a .slide-button-secondary { .table-techniques td { vertical-align: top; padding: 10px; - border: 1px solid #dfdfdf; + border: 1px solid var(--attack-border-color-body); } .table-techniques tr:last-child { - border-bottom: 1px solid #dfdfdf; + border-bottom: 1px solid var(--attack-border-color-body); } .table-techniques .sub.technique { border-left: none; @@ -374,10 +399,10 @@ a .slide-button-secondary { border-bottom: none; } .table-techniques .sub.technique td:not(:nth-child(4)) { - color: #4f7cac; + color: var(--attack-color-link); } .table-techniques .technique:not(.sub) td:not(:nth-child(3)) { - color: #4f7cac; + color: var(--attack-color-link); } .techniques-used table { @@ -386,10 +411,10 @@ a .slide-button-secondary { .techniques-used td { vertical-align: top; padding: 10px; - border: 1px solid #dfdfdf; + border: 1px solid var(--attack-border-color-body); } .techniques-used tr:last-child { - border-bottom: 1px solid #dfdfdf; + border-bottom: 1px solid var(--attack-border-color-body); } .techniques-used .sub.technique { border-left: none; @@ -412,24 +437,24 @@ a .slide-button-secondary { } .techniques-used.background thead tr { - background: #f2f2f2; - border-bottom: 2px solid #dee2e6; + background: var(--attack-color-body-alternate); + border-bottom: 2px solid var(--attack-border-color-body); } .datasources-table table { border-collapse: collapse; } .datasources-table thead tr { - background: #f2f2f2; - border-bottom: 2px solid #dee2e6; + background: var(--attack-color-body-alternate); + border-bottom: 2px solid var(--attack-border-color-body); } .datasources-table td { vertical-align: top; padding: 10px; - border: 1px solid #dfdfdf; + border: 1px solid var(--attack-border-color-body); } .datasources-table tr:last-child { - border-bottom: 1px solid #dfdfdf; + border-bottom: 1px solid var(--attack-border-color-body); } .datasources-table .datacomponent.datasource { border-left: none; @@ -465,12 +490,12 @@ a .slide-button-secondary { .changelog table, .changelog table td, .changelog table th { - border: 1px solid rgb(223.125, 223.125, 223.125); + border: 1px solid var(--attack-border-color-body); } .table-bordered th, .blog-post table th, .changelog table th { - border-bottom: 2px solid rgb(223.125, 223.125, 223.125) !important; + border-bottom: 2px solid var(--attack-border-color-body) !important; } .table-matrix { @@ -479,7 +504,7 @@ a .slide-button-secondary { .table-matrix thead th { text-align: center !important; vertical-align: middle !important; - border: 0.0625rem solid rgb(223.125, 223.125, 223.125) !important; + border: 0.0625rem solid var(--attack-border-color-body) !important; } .table-matrix td, .table-matrix th { @@ -491,15 +516,15 @@ a .slide-button-secondary { } .table-matrix td.border, .table-matrix th.border { - border: 0.0625rem solid rgb(223.125, 223.125, 223.125) !important; + border: 0.0625rem solid var(--attack-border-color-body) !important; } .table-matrix td.no-border, .table-matrix th.no-border { border: none !important; } .table-matrix td { - background: white; - color: #39434c; + background: var(--attack-color-body); + color: var(--attack-on-color-body); } .table-mitigations th, @@ -519,18 +544,26 @@ a .slide-button-secondary { } .matrix-tactics-url { - color: white; + color: var(--attack-on-color-primary); } .matrix-tactics-url:visited, .matrix-tactics-url:hover, .matrix-tactics-url:active { - color: white; + color: var(--attack-on-color-primary); } /* **** */ /* BANNER */ .banner-message, .version-banner { - padding: 0.3125rem 0; + padding: 0.4375rem 1rem; + border-top: 1px solid var(--attack-border-color-banner); + border-bottom: 1px solid var(--attack-border-color-banner); text-align: center; - background-color: rgb(229.5, 229.5, 229.5); + color: var(--attack-on-color-banner); + background-color: var(--attack-color-banner); +} +.banner-message a, .version-banner a { + color: inherit; + font-weight: 700; + text-decoration: underline; } /* **** */ @@ -551,22 +584,22 @@ a .slide-button-secondary { /* **** */ /* Pre-block in SIGHTINGS */ pre { - color: #39434c; - background-color: rgb(216.75, 216.75, 216.75); + color: var(--attack-on-color-body); + background-color: var(--attack-color-body-alternate-strongest); border-radius: 5px; padding: 1rem; } code { - color: #c63e1f; + color: var(--attack-color-code); } /* **** */ /* CARDS */ .card { - background: white; - color: #39434c; - border-color: rgb(223.125, 223.125, 223.125); + background: var(--attack-color-body); + color: var(--attack-on-color-body); + border-color: var(--attack-border-color-body); } .button-group { @@ -588,7 +621,7 @@ code { width: 20%; top: 9.3rem; float: right; - background: #eaeaea; + background: var(--attack-color-body-alternate-strong); } @media screen and (width <= 90.62rem) { @@ -609,9 +642,9 @@ code { } .card-header { - color: #39434c; - background: rgba(57, 67, 76, 0.03); - border-bottom-color: rgb(223.125, 223.125, 223.125); + color: var(--attack-on-color-body); + background: var(--attack-color-card-header); + border-bottom-color: var(--attack-border-color-body); } a.partial-underline { @@ -643,21 +676,21 @@ a.partial-underline .hover-line { padding: 1.25rem 1.25rem 0; } .contact-card .card-header.background { - background: #303435; + background: var(--attack-color-footer); color: white; padding-bottom: 1.25rem; } .contact-card .card-header.no-background, .contact-card .card-footer.no-background { - background: white; - color: #39434c; + background: var(--attack-color-body); + color: var(--attack-on-color-body); border: unset; } .contact-card .card-body p { margin: 0; } .contact-card .card-body.background { - background: #eaeaea; + background: var(--attack-color-body-alternate-strong); } .card-body > .table { @@ -696,7 +729,11 @@ a.partial-underline .hover-line { .card-title { font-size: 1.1rem; - color: rgb(28.5, 33.5, 38); + color: var(--attack-on-color-body-emphasis); +} + +.card-data .card-title { + color: var(--attack-color-property-label); } .contact-card-title { @@ -706,7 +743,7 @@ a.partial-underline .hover-line { .card-title-icon { float: right; - color: #303435; + color: var(--attack-color-active); } /* **** */ @@ -716,13 +753,13 @@ a.partial-underline .hover-line { max-width: 100%; height: 30rem; margin: 0 auto; - border: 3px solid #dfdfdf; + border: 3px solid var(--attack-border-color-body); padding: 3px; display: flex; flex-direction: column; } .attack-box iframe { - border: 1px solid rgb(223.125, 223.125, 223.125) !important; + border: 1px solid var(--attack-border-color-body) !important; border-radius: 0.25rem; } @@ -730,13 +767,13 @@ a.partial-underline .hover-line { /* BREADCRUMBS */ .breadcrumb { font-size: 90%; - background-color: white; + background-color: var(--attack-color-body); max-width: 1140px; padding: 0 15px; } .breadcrumb .breadcrumb-item + .breadcrumb-item::before { content: ">"; - color: rgb(106.5, 114, 120.75); + color: var(--attack-on-color-body-deemphasis); } /* **** */ @@ -753,24 +790,24 @@ a.partial-underline .hover-line { counter-increment: item; } -.danger-card { - border-color: #303435; -} .danger-card .card-header { - background: #303435; - color: white; + background: var(--attack-color-primary); + color: var(--attack-on-color-primary); +} +.danger-card { + border-color: var(--attack-color-primary); } /* **** */ /* ATT&CKCON */ .bg-accord-light { - color: #39434c; - background-color: rgb(242.25, 242.25, 242.25); + color: var(--attack-on-color-body); + background-color: var(--attack-color-body-alternate); } .bg-accord-dark { - color: #39434c; - background-color: rgb(216.75, 216.75, 216.75); + color: var(--attack-on-color-body); + background-color: var(--attack-color-body-alternate-strongest); } /* **** */ @@ -786,9 +823,9 @@ a.partial-underline .hover-line { .usa-card__header { font-family: "Roboto-Regular", sans-serif; - color: white; - background: #303435; - border-bottom-color: rgb(223.125, 223.125, 223.125); + color: var(--attack-on-color-secondary); + background: var(--attack-color-secondary); + border-bottom-color: var(--attack-border-color-body); border-radius: 0.3rem 0.3rem 0 0; } @@ -835,7 +872,7 @@ a.partial-underline .hover-line { height: 500px; overflow: auto; padding: 0.9375rem; - background-color: rgb(242.25, 242.25, 242.25); + background-color: var(--attack-color-body-alternate); border-radius: 0.1875rem; } @@ -890,13 +927,13 @@ img.yt-core-image { } /* Ensure the sponsors block is below and not affected by the top image */ /* Card Blocks */ +.card-block .card-header h5 { + font-family: "Roboto-Regular", sans-serif; +} .card-block .card-header { display: flex; flex-direction: row; } -.card-block .card-header h5 { - font-family: "Roboto-Regular", sans-serif; -} .card-block .card-header :first-child { cursor: pointer; display: inline-block; @@ -911,7 +948,7 @@ img.yt-core-image { display: inline-block; vertical-align: top; background-position: center; - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%2339434C' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E"); + background-image: var(--attack-select-arrow); z-index: 1; transition: all 0.2s ease; } @@ -957,7 +994,7 @@ img.yt-core-image { } } .getting-started .getting-started-icon { - color: #303435; + color: var(--attack-color-primary); } .getting-started .video-li { /* don't display the video in the list itself except in mobile layout */ @@ -992,7 +1029,7 @@ img.yt-core-image { padding-left: 0; } .timeline::before { - background: rgb(106.5, 114, 120.75); + background: var(--attack-on-color-body-deemphasis); position: absolute; width: 0.0625rem; height: 100%; @@ -1007,7 +1044,7 @@ img.yt-core-image { height: 300px; } .timeline:not(.show)::after { - background: linear-gradient(to bottom, transparent, white); + background: linear-gradient(to bottom, transparent, var(--attack-color-body)); position: absolute; width: 100%; height: 75px; @@ -1021,7 +1058,7 @@ img.yt-core-image { padding-left: 40px; } .timeline .timeline-card::before { - background: white; + background: var(--attack-color-body); position: absolute; width: 20px; height: 20px; @@ -1029,7 +1066,7 @@ img.yt-core-image { content: ""; display: inline-block; border-radius: 50%; - border: 0.125rem solid rgb(106.5, 114, 120.75); + border: 0.125rem solid var(--attack-on-color-body-deemphasis); left: 10px; } .timeline .timeline-card .timeline-card-header { @@ -1106,7 +1143,7 @@ img.yt-core-image { position: -webkit-sticky; position: sticky; top: 4.125rem; - background: white; + background: var(--attack-color-body); z-index: 100; margin-bottom: 0; } @@ -1129,7 +1166,7 @@ img.yt-core-image { } } .section-view .section-shadow { - border-bottom: 1px solid #dfdfdf !important; + border-bottom: 1px solid var(--attack-border-color-body) !important; } .section-view table { margin-top: -1px; @@ -1143,7 +1180,7 @@ div#sidebars { /* Plus/Minus expand icons */ .expand-icon { font-size: 5%; - color: #39434c; + color: var(--attack-on-color-body); margin-top: 0.4375rem; } @@ -1152,6 +1189,17 @@ div#sidebars { .attackcons { border-top-width: 0; } +.attackcons .attackcon-banner-image { + display: inline-block; + width: 100%; + box-sizing: border-box; +} +.attackcons .attackcon-banner-image.on-light { + padding: 1rem; + border: 1px solid var(--attack-border-color-body); + border-radius: 0.75rem; + background: var(--attack-color-image-background); +} .attackcons .sponsors { flex: 1; padding-left: 25px; @@ -1160,16 +1208,20 @@ div#sidebars { width: 90%; } .attackcons .sponsors-block { - background: #eaeaea; + background: var(--attack-color-image-background); text-align: center; display: flex; justify-content: space-evenly; flex-wrap: wrap; flex-direction: column; width: 200%; + padding: 0.625rem; + border: 0.0625rem solid var(--attack-border-color-body); + border-radius: 0.5rem; + box-sizing: border-box; } .attackcons .sponsors-block .img-container { - margin: 10px; + margin: 0.625rem; flex: 1 1 20%; box-sizing: border-box; } @@ -1186,7 +1238,7 @@ div#sidebars { } .support-box { - background-color: rgb(229.5, 229.5, 229.5); + background-color: var(--attack-color-body-alternate-strong); padding: 1.5em; border-radius: 0.75em; width: fit-content; @@ -1207,7 +1259,7 @@ div#sidebars { display: contents; } .sponsor-square img.sponsor-logo { - background-color: white; + background-color: var(--attack-color-image-background); border-radius: 6px; object-fit: contain; object-position: center; @@ -1216,7 +1268,7 @@ div#sidebars { /* **** */ /* training pages */ .training .exercise { - background: rgb(242.25, 242.25, 242.25); + background: var(--attack-color-body-alternate); } .card-training { @@ -1250,7 +1302,7 @@ div#sidebars { .resource { flex: 1; - background-color: #eaeaea; + background-color: var(--attack-color-body-alternate-strong); padding: 10px; box-sizing: border-box; } @@ -1281,14 +1333,14 @@ div#sidebars { margin-bottom: 16px; } .decorative-panels .decorative-panel .decorative-panel-body.show { - border-top: 1px solid rgb(223.125, 223.125, 223.125); + border-top: 1px solid var(--attack-border-color-body); } .decorative-panels .decorative-panel + .decorative-panel { margin-top: 25px; } .decorative-panels .decorative-panel:nth-of-type(even) { border-radius: 8px; - background: rgb(242.25, 242.25, 242.25); + background: var(--attack-color-body-alternate); } @media screen and (width <= 47.9875rem) { .decorative-panels .row { @@ -1315,16 +1367,16 @@ div#sidebars { object-fit: cover; object-position: left top; border-radius: unset !important; - border: 1px solid rgb(223.125, 223.125, 223.125); + border: 1px solid var(--attack-border-color-body); } .working-with-attack .panel { padding: 0; - border: 1px solid rgb(223.125, 223.125, 223.125); + border: 1px solid var(--attack-border-color-body); border-radius: 0.75em; max-width: 100%; } .working-with-attack img + .panel-body { - border-top: 1px solid rgb(223.125, 223.125, 223.125); + border-top: 1px solid var(--attack-border-color-body); } .working-with-attack .panel-body p { margin: 24px 0; @@ -1349,18 +1401,18 @@ div#sidebars { } .tip-box { - background: #eaeaea; + background: var(--attack-color-body-alternate-strong); padding: 1rem; } /* Card Blocks */ +.expand-panel .card-block .card-header h5 { + font-family: "Roboto-Regular", sans-serif; +} .expand-panel .card-block .card-header { display: flex; flex-direction: row; } -.expand-panel .card-block .card-header h5 { - font-family: "Roboto-Regular", sans-serif; -} .expand-panel .card-block .card-header :first-child { cursor: pointer; display: inline-block; @@ -1375,7 +1427,7 @@ div#sidebars { display: inline-block; vertical-align: top; background-position: center; - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%2339434C' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E"); + background-image: var(--attack-select-arrow); z-index: 1; transition: all 0.2s ease; } @@ -1412,8 +1464,8 @@ div#sidebars { } /* Footer styling */ .footer { - background-color: #303435; - color: white; + background-color: var(--attack-color-footer); + color: var(--attack-on-color-secondary); padding: 1rem 3rem; font-family: "Roboto-Thin", sans-serif; } @@ -1447,10 +1499,10 @@ div#sidebars { justify-content: center; } .footer .footer-link { - color: #87deff; + color: var(--attack-on-color-footer); } .footer .footer-link:hover { - color: rgb(107, 173, 213.5); + color: var(--attack-color-footer-link-hover); } .col .col-footer { @@ -1501,7 +1553,7 @@ div#sidebars { /* Top NAVIGATION */ .navbar { - background-color: #303435; + background-color: var(--attack-color-primary); z-index: 1; } .navbar .navbar-brand { @@ -1511,31 +1563,31 @@ div#sidebars { border-bottom: none; } .navbar .nav-tabs .nav-link { - color: white; + color: var(--attack-on-color-primary); font-family: "Roboto-Light", sans-serif; } .navbar .nav-tabs .nav-link:focus { - color: white; + color: var(--attack-on-color-primary); } .navbar .nav-tabs .nav-link:hover:not(.active) { - color: white; + color: var(--attack-on-color-primary); background-color: transparent; } .navbar .nav-tabs .nav-link.active { - color: white; - background-color: #303435; + color: var(--attack-on-color-primary); + background-color: var(--attack-color-primary); } .navbar .nav-tabs .nav-item.show .nav-link { - color: white; - background-color: #303435; + color: var(--attack-on-color-primary); + background-color: var(--attack-color-primary); border-color: transparent; } .navbar .search-button { padding: 0.3rem 1rem; font-size: 1rem; - border: 0.0625rem solid white; + border: 0.0625rem solid var(--attack-on-color-primary); border-radius: 0.25rem; - color: white; + color: var(--attack-on-color-primary); line-height: 1.5rem; opacity: 0.8; } @@ -1555,21 +1607,84 @@ div#sidebars { } .navbar .search-button .search-icon { cursor: pointer; - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%23FFFFFF' xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E"); + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%23fff' xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E"); } .navbar .search-button .error-icon { cursor: default; - background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg fill='%23FFFFFF' xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 96 960 960' width='24'%3e%3cpath d='M479.982 776q14.018 0 23.518-9.482 9.5-9.483 9.5-23.5 0-14.018-9.482-23.518-9.483-9.5-23.5-9.5-14.018 0-23.518 9.482-9.5 9.483-9.5 23.5 0 14.018 9.482 23.518 9.483 9.5 23.5 9.5ZM453 623h60V370h-60v253Zm27.266 353q-82.734 0-155.5-31.5t-127.266-86q-54.5-54.5-86-127.341Q80 658.319 80 575.5q0-82.819 31.5-155.659Q143 347 197.5 293t127.341-85.5Q397.681 176 480.5 176q82.819 0 155.659 31.5Q709 239 763 293t85.5 127Q880 493 880 575.734q0 82.734-31.5 155.5T763 858.316q-54 54.316-127 86Q563 976 480.266 976Zm.234-60Q622 916 721 816.5t99-241Q820 434 721.188 335 622.375 236 480 236q-141 0-240.5 98.812Q140 433.625 140 576q0 141 99.5 240.5t241 99.5Zm-.5-340Z'/%3e%3c/svg%3e"); + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg fill='%23fff' xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 96 960 960' width='24'%3e%3cpath d='M479.982 776q14.018 0 23.518-9.482 9.5-9.483 9.5-23.5 0-14.018-9.482-23.518-9.483-9.5-23.5-9.5-14.018 0-23.518 9.482-9.5 9.483-9.5 23.5 0 14.018 9.482 23.518 9.483 9.5 23.5 9.5ZM453 623h60V370h-60v253Zm27.266 353q-82.734 0-155.5-31.5t-127.266-86q-54.5-54.5-86-127.341Q80 658.319 80 575.5q0-82.819 31.5-155.659Q143 347 197.5 293t127.341-85.5Q397.681 176 480.5 176q82.819 0 155.659 31.5Q709 239 763 293t85.5 127Q880 493 880 575.734q0 82.734-31.5 155.5T763 858.316q-54 54.316-127 86Q563 976 480.266 976Zm.234-60Q622 916 721 816.5t99-241Q820 434 721.188 335 622.375 236 480 236q-141 0-240.5 98.812Q140 433.625 140 576q0 141 99.5 240.5t241 99.5Zm-.5-340Z'/%3e%3c/svg%3e"); +} +.navbar .theme-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 3.625rem; + min-height: 2.375rem; + margin-right: 0.5rem; + padding: 0.25rem; + border: 0; + color: var(--attack-on-color-primary); +} +.navbar .theme-toggle .theme-toggle-track { + position: relative; + display: inline-flex; + align-items: center; + justify-content: space-between; + width: 3.125rem; + height: 1.75rem; + padding: 0 0.4375rem; + border: 0.0625rem solid var(--attack-on-color-primary); + border-radius: 0.875rem; + background: rgba(0, 0, 0, 0.2); + box-sizing: border-box; + transition: background-color 0.2s ease; +} +.navbar .theme-toggle .theme-toggle-icon { + position: relative; + z-index: 2; + visibility: visible; + display: inline-block; + font-size: 0.75rem; + line-height: 1; +} +.navbar .theme-toggle:hover, .navbar .theme-toggle:focus { + color: var(--attack-on-color-primary); +} +.navbar .theme-toggle:hover .theme-toggle-track, .navbar .theme-toggle:focus .theme-toggle-track { + background: rgba(0, 0, 0, 0.35); + box-shadow: 0 0 0 0.125rem rgba(255, 255, 255, 0.3); +} +.navbar .theme-toggle .theme-toggle-icon-light { + color: var(--attack-color-primary); +} +.navbar .theme-toggle .theme-toggle-thumb { + position: absolute; + top: 0.125rem; + left: 0.125rem; + z-index: 1; + width: 1.375rem; + height: 1.375rem; + border-radius: 50%; + background: var(--attack-on-color-primary); + box-shadow: 0 0.0625rem 0.1875rem rgba(0, 0, 0, 0.35); + transition: transform 0.2s ease; +} +.navbar .theme-toggle[data-theme-effective=dark] .theme-toggle-track { + background: var(--attack-color-secondary); +} +.navbar .theme-toggle[data-theme-effective=dark] .theme-toggle-icon-light { + color: var(--attack-on-color-primary); +} +.navbar .theme-toggle[data-theme-effective=dark] .theme-toggle-icon-dark { + color: var(--attack-color-secondary); +} +.navbar .theme-toggle[data-theme-effective=dark] .theme-toggle-thumb { + transform: translateX(1.375rem); } /* **** */ .nav, .faq { /* NAVIGATION Dropdown */ - /* **** */ - /* Side NAVIGATION */ - border-color: rgb(223.125, 223.125, 223.125) !important; - /* **** */ } .nav .dropdown:hover > .dropdown-menu, .faq .dropdown:hover > .dropdown-menu { @@ -1577,23 +1692,29 @@ div#sidebars { } .nav .dropdown-menu, .faq .dropdown-menu { - background-color: #303435; + background-color: var(--attack-color-primary); } .nav .dropdown-menu .dropdown-item, .faq .dropdown-menu .dropdown-item { - color: white; + color: var(--attack-on-color-primary); } .nav .dropdown-menu .dropdown-item:hover, .nav .dropdown-menu .dropdown-item:focus, .faq .dropdown-menu .dropdown-item:hover, .faq .dropdown-menu .dropdown-item:focus { - color: white; + color: var(--attack-on-color-primary); text-decoration: underline; background-color: transparent; } +.nav, +.faq { + /* **** */ + /* Side NAVIGATION */ + border-color: var(--attack-border-color-body) !important; +} .nav .heading, .faq .heading { font-size: 1.6rem; - color: rgb(106.5, 114, 120.75); + color: var(--attack-on-color-body-deemphasis); letter-spacing: 0.1875rem; pointer-events: none; } @@ -1640,33 +1761,37 @@ div#sidebars { .nav .heading-dropdown, .faq .heading-dropdown { font-size: 1.2rem; - color: #303435; + color: var(--attack-color-secondary); letter-spacing: 0.1875rem; } @media screen and (width <= 90.62rem) { .nav .heading, .faq .heading { font-size: 1.2rem; - color: rgb(106.5, 114, 120.75); + color: var(--attack-on-color-body-deemphasis); letter-spacing: 0.1875rem; } .nav .heading-dropdown, .faq .heading-dropdown { font-size: 1rem; - color: #39434c; + color: var(--attack-on-color-body); letter-spacing: 0.0625rem; } } +.nav, +.faq { + /* **** */ +} .nav .nav-link, .faq .nav-link { font-size: 1rem; padding: 0.3rem 1rem; - color: rgb(106.5, 114, 120.75); + color: var(--attack-on-color-body-deemphasis); } .nav .nav-link.expand-title, .faq .nav-link.expand-title { font-size: 1.1rem; - color: #39434c; + color: var(--attack-on-color-body); } .nav .nav-link.side, .faq .nav-link.side { @@ -1675,14 +1800,14 @@ div#sidebars { } .nav .nav-link.side:hover, .faq .nav-link.side:hover { - background-color: #303435; - color: white; + background-color: var(--attack-color-primary); + color: var(--attack-on-color-primary); } .nav .nav-link.side.active, .faq .nav-link.side.active { - color: #303435; - background-color: rgb(242.25, 242.25, 242.25); - border-right: 0.1875rem solid #303435; + color: var(--attack-color-primary); + background-color: var(--attack-color-body-alternate); + border-right: 0.1875rem solid var(--attack-color-primary); } /* **** */ @@ -1693,7 +1818,7 @@ div#sidebars { cursor: col-resize; height: 100%; position: absolute; - background-color: #dfdfdf; + background-color: var(--attack-border-color-body); } .data-sources-menu { @@ -1725,11 +1850,11 @@ div#sidebars { } } .sidebar.nav .sidenav-wrapper .heading { - border-bottom: 1px solid rgb(242.25, 242.25, 242.25); + border-bottom: 1px solid var(--attack-color-body-alternate); flex: 0 1 0; } .sidebar.nav .sidenav-wrapper .checkbox-div { - border-bottom: 1px solid rgb(242.25, 242.25, 242.25); + border-bottom: 1px solid var(--attack-color-body-alternate); flex: 0 1 0; } .sidebar.nav .sidenav-wrapper .sidenav-list { @@ -1748,11 +1873,11 @@ div#sidebars { .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head a, .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head .expand-button { cursor: pointer; - color: black; + color: var(--attack-on-color-body); } .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head a:hover, .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head .expand-button:hover { - background: rgb(242.25, 242.25, 242.25); + background: var(--attack-color-body-alternate); } .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head a, .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head span { @@ -1768,7 +1893,7 @@ div#sidebars { } .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head .expand-button { padding: 5px; - border-left: 1px solid rgb(242.25, 242.25, 242.25); + border-left: 1px solid var(--attack-color-body-alternate); display: inline-block; display: flex; flex-direction: row; @@ -1787,7 +1912,7 @@ div#sidebars { display: inline-block; vertical-align: top; background-position: center; - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%2339434C' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E"); + background-image: var(--attack-select-arrow); z-index: 1; transition: all 0.2s ease; } @@ -1795,9 +1920,9 @@ div#sidebars { transform: rotate(-180deg); } .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head.active, .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head.active > a { - color: #303435 !important; + color: var(--attack-color-active) !important; font-weight: bolder; - background: #eaeaea; + background: var(--attack-color-body-alternate-strong); font-family: Roboto-Bold, sans-serif; } .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-body { @@ -1834,8 +1959,8 @@ div#sidebars { } } .search-word-found { - background: yellow; - color: black; + background: var(--attack-color-search-highlight); + color: var(--attack-on-color-search-highlight); } .btn-group-text { @@ -1858,8 +1983,8 @@ div#sidebars { } .overlay.search .overlay-inner { border-radius: 25px; - background: white; - color: #39434c; + background: var(--attack-color-body); + color: var(--attack-on-color-body); width: 100%; height: 100%; display: flex; @@ -1880,8 +2005,8 @@ div#sidebars { line-height: 50px; width: 100%; border: 0; - background: white; - color: #39434c; + background: var(--attack-color-body); + color: var(--attack-on-color-body); } .overlay.search .overlay-inner .search-header .search-input input:focus { outline: none; @@ -1919,10 +2044,10 @@ div#sidebars { .overlay.search .overlay-inner .search-filters button, .overlay.search .overlay-inner .search-filters .search-filter-chip { min-height: 36px; - border: 1px solid rgb(223.125, 223.125, 223.125); + border: 1px solid var(--attack-border-color-body); border-radius: 4px; - background: white; - color: #39434c; + background: var(--attack-color-body); + color: var(--attack-on-color-body); cursor: pointer; } .overlay.search .overlay-inner .search-filters .search-filter-summary { @@ -1935,7 +2060,7 @@ div#sidebars { font-weight: 600; } .overlay.search .overlay-inner .search-filters .search-filter-summary-chip.open { - border-color: #303435; + border-color: var(--attack-color-active); } .overlay.search .overlay-inner .search-filters .search-filter-dropdown { position: relative; @@ -1949,9 +2074,9 @@ div#sidebars { min-width: 240px; max-width: min(420px, 100vw - 100px); padding: 12px; - border: 1px solid rgb(223.125, 223.125, 223.125); + border: 1px solid var(--attack-border-color-body); border-radius: 8px; - background: white; + background: var(--attack-color-body); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); } .overlay.search .overlay-inner .search-filters .search-filter-group-heading { @@ -1994,9 +2119,9 @@ div#sidebars { border: 0; } .overlay.search .overlay-inner .search-filters .search-filter-chip.selected { - border-color: #303435; - background: #303435; - color: #eaeaea; + border-color: var(--attack-color-active); + background: var(--attack-color-active); + color: var(--attack-on-color-active); } .overlay.search .overlay-inner .search-filters .search-filter-count { margin-left: 4px; @@ -2012,7 +2137,7 @@ div#sidebars { flex-direction: column; min-height: 0; padding: 0 50px; - border-top: 1px solid rgb(223.125, 223.125, 223.125); + border-top: 1px solid var(--attack-border-color-body); margin-bottom: 25px; overflow: hidden; scrollbar-gutter: stable; @@ -2037,17 +2162,18 @@ div#sidebars { padding: 2px 8px; border: 1px solid; border-radius: 4px; - color: white; + color: var(--attack-on-color-active); font-size: 0.8rem; font-weight: 700; } .overlay.search .overlay-inner .search-body .results .search-result-badge-page-type { - border-color: #303435; - background: rgb(59.925, 63.325, 64.175); + border-color: var(--attack-color-active); + background: var(--attack-color-active-alternate-medium); } .overlay.search .overlay-inner .search-body .results .search-result-badge-domain { - border-color: #303435; - background: #303435; + border-color: var(--attack-color-deemphasis); + background: var(--attack-color-deemphasis); + color: var(--attack-on-color-deemphasis); } .overlay.search .overlay-inner .search-body .results .search-no-results .preview { display: flex; @@ -2072,8 +2198,8 @@ div#sidebars { justify-content: flex-end; min-height: 58px; padding: 12px 0 14px; - border-top: 1px solid rgb(223.125, 223.125, 223.125); - background: white; + border-top: 1px solid var(--attack-border-color-body); + background: var(--attack-color-body); } .overlay.search .overlay-inner .search-body .search-results-pagination:empty { display: none; @@ -2088,17 +2214,17 @@ div#sidebars { .overlay.search .overlay-inner .search-body .search-pagination button { min-height: 36px; padding: 5px 12px; - border: 1px solid rgb(223.125, 223.125, 223.125); + border: 1px solid var(--attack-border-color-body); border-radius: 4px; - background: white; - color: #39434c; + background: var(--attack-color-body); + color: var(--attack-on-color-body); cursor: pointer; font-weight: 700; } .overlay.search .overlay-inner .search-body .search-pagination button.current { - border-color: #303435; - background: #303435; - color: #eaeaea; + border-color: var(--attack-color-active); + background: var(--attack-color-active); + color: var(--attack-on-color-active); } .overlay.search .overlay-inner .search-body .search-pagination button:disabled { cursor: default; @@ -2121,7 +2247,7 @@ div#sidebars { align-items: center; min-height: 36px; padding: 0 4px; - color: #303435; + color: var(--attack-color-deemphasis); font-weight: 700; } .overlay.search .overlay-inner .search-body .search-pagination-icon { @@ -2208,27 +2334,27 @@ div#sidebars { margin-bottom: 5px !important; } } -.popover { - box-shadow: 0 0 5px 3px white; - border-color: #303435; -} .popover .popover-header { - background: #303435; - color: white; + background: var(--attack-color-primary); + color: var(--attack-on-color-primary); +} +.popover { + box-shadow: 0 0 5px 3px var(--attack-color-body); + border-color: var(--attack-color-primary); } .popover.bs-popover-left .arrow::before { - border-left-color: #303435; + border-left-color: var(--attack-color-primary); } .popover.bs-popover-right .arrow::before { - border-right-color: #303435; + border-right-color: var(--attack-color-primary); } .popover.bs-popover-top .arrow::before { - border-top-color: #303435; + border-top-color: var(--attack-color-primary); } .popover.bs-popover-bottom .arrow::before, .popover.bs-popover-bottom .arrow::after, .popover.bs-popover-bottom .popover-header::before { - border-bottom-color: #303435; + border-bottom-color: var(--attack-color-primary); } .tour-backdrop, @@ -2237,21 +2363,21 @@ div#sidebars { } .matrix-container { - border: 1px solid rgb(223.125, 223.125, 223.125); - background: white; + border: 1px solid var(--attack-border-color-body); + background: var(--attack-color-body); } .matrix-container + .matrix-container { margin-top: 1rem; } .matrix-container .matrix-border { - border-left: 1px solid rgb(223.125, 223.125, 223.125); + border-left: 1px solid var(--attack-border-color-body); padding-left: 0.5rem; display: flex; justify-content: center; align-items: center; } .matrix-container .matrix-title { - border-bottom: 1px solid rgb(223.125, 223.125, 223.125); + border-bottom: 1px solid var(--attack-border-color-body); margin-bottom: 1rem; padding-bottom: 0.5rem; } @@ -2278,35 +2404,35 @@ div#sidebars { right: 0; } .matrix-container .scroll-indicator-group .scroll-indicator.right.show .cover { - background: linear-gradient(to right, rgba(255, 255, 255, 0.001), white); + background: linear-gradient(to right, rgba(255, 255, 255, 0.001), var(--attack-color-body)); } .matrix-container .scroll-indicator-group .scroll-indicator.left .cover { left: 0; } .matrix-container .scroll-indicator-group .scroll-indicator.left.show .cover { - background: linear-gradient(to left, rgba(255, 255, 255, 0.001), white); + background: linear-gradient(to left, rgba(255, 255, 255, 0.001), var(--attack-color-body)); } .matrix { white-space: normal; line-height: 14px; } -.matrix.side .tactic { - padding: 2px 5px; - width: 1%; - vertical-align: top; -} .matrix.side .tactic:first-child { padding: 2px 5px 2px 2px; } .matrix.side .tactic:last-child { padding: 2px 2px 2px 5px; } +.matrix.side .tactic { + padding: 2px 5px; + width: 1%; + vertical-align: top; +} .matrix.side .tactic:hover:not(.name, .count) { - background: rgb(223.125, 223.125, 223.125); + background: var(--attack-background-color-body); } .matrix.side .tactic:hover:not(.name, .count) .sidebar.expanded .angle { - background: rgb(223.125, 223.125, 223.125); + background: var(--attack-background-color-body); } .matrix.side .tactic.name, .matrix.side .tactic.count { text-align: center; @@ -2317,7 +2443,7 @@ div#sidebars { } .matrix.side .tactic.count { font-size: 13px; - border-bottom: 1px solid black; + border-bottom: 1px solid var(--attack-border-color-body); padding-bottom: 5px; margin-bottom: 5px; } @@ -2343,23 +2469,23 @@ div#sidebars { vertical-align: top; } .matrix.side .tactic .supertechnique td.technique { - outline: 1px solid rgb(106.5, 114, 120.75); + outline: 1px solid var(--attack-on-color-body-deemphasis); outline-offset: -1px; } +.matrix.side .tactic .subtechniques.hidden { + display: none; +} .matrix.side .tactic .subtechniques { display: flex; flex-direction: column; height: 100%; margin-left: -1px; - border-left: 2px solid rgb(106.5, 114, 120.75); - outline: 1px solid rgb(106.5, 114, 120.75); + border-left: 2px solid var(--attack-on-color-body-deemphasis); + outline: 1px solid var(--attack-on-color-body-deemphasis); outline-offset: -1px; white-space: nowrap; vertical-align: top; } -.matrix.side .tactic .subtechniques.hidden { - display: none; -} .matrix.side .tactic .subtechniques .subtechnique { height: 100%; flex-grow: 1; @@ -2368,7 +2494,7 @@ div#sidebars { text-align: center; vertical-align: middle; transform: rotate(-90deg); - color: rgb(242.25, 242.25, 242.25); + color: var(--attack-color-body-alternate); width: 12px; height: 12px; font-size: 16px; @@ -2378,7 +2504,7 @@ div#sidebars { min-width: 8px; width: 12px; padding: 0; - background: rgb(106.5, 114, 120.75); + background: var(--attack-on-color-body-deemphasis); cursor: pointer; position: relative; vertical-align: middle; @@ -2391,10 +2517,10 @@ div#sidebars { height: 12px; display: block; position: absolute; - background: white; + background: var(--attack-color-body); } .matrix.side .tactic .sidebar.expanded .angle svg { - fill: rgb(106.5, 114, 120.75); + fill: var(--attack-on-color-body-deemphasis); vertical-align: baseline; } .matrix.side .tactic .sidebar.expanded .angle.top { @@ -2417,7 +2543,7 @@ div#sidebars { } .matrix.flat .tactic.count { font-size: 13px; - border-bottom: 1px solid black; + border-bottom: 1px solid var(--attack-border-color-body); padding-bottom: 5px; margin-bottom: 5px; } @@ -2438,7 +2564,7 @@ div#sidebars { min-width: 8px; width: 12px; padding: 0; - background: rgb(106.5, 114, 120.75); + background: var(--attack-on-color-body-deemphasis); cursor: pointer; vertical-align: middle; } @@ -2446,21 +2572,21 @@ div#sidebars { text-align: center; vertical-align: middle; transform: rotate(-90deg); - color: rgb(242.25, 242.25, 242.25); + color: var(--attack-color-body-alternate); width: 12px; height: 9px; font-size: 16px; line-height: 12px; } .matrix.flat .tactic .supertechnique td.sidebar.subtechniques svg { - fill: rgb(106.5, 114, 120.75); + fill: var(--attack-on-color-body-deemphasis); vertical-align: baseline; } .matrix.flat .tactic .supertechnique td.sidebar { - border-right: 2px solid rgb(106.5, 114, 120.75); + border-right: 2px solid var(--attack-on-color-body-deemphasis); } .matrix.flat .tactic .supertechnique td.technique { - outline: 1px solid rgb(106.5, 114, 120.75); + outline: 1px solid var(--attack-on-color-body-deemphasis); outline-offset: -1px; } .matrix.flat .tactic .more-icon { @@ -2477,11 +2603,9 @@ div#sidebars { height: 100%; display: flex; align-items: center; - background-color: white; + background-color: var(--attack-color-body); font-size: 13px; line-height: 14px; - outline: 1px solid transparent; - outline-offset: -1px; } .matrix .technique-cell a { display: block; @@ -2489,8 +2613,12 @@ div#sidebars { height: 100%; padding: 7px 3px; } +.matrix .technique-cell { + outline: 1px solid transparent; + outline-offset: -1px; +} .matrix .technique-cell:not(.colored):not(.supertechniquecell) { - outline-color: rgb(223.125, 223.125, 223.125); + outline-color: var(--attack-border-color-body); } .matrix-controls { @@ -2499,17 +2627,17 @@ div#sidebars { padding: 1rem; } .matrix-controls button { - border-color: rgb(223.125, 223.125, 223.125); - background: white; - color: #39434c; + border-color: var(--attack-border-color-body); + background: var(--attack-color-body); + color: var(--attack-on-color-body); } .matrix-controls button:hover { - background: rgb(244.8, 244.8, 244.8); + background: var(--attack-color-body-alternate-subtle); } .matrix-controls .layout-button:active { - color: #16181b; + color: var(--attack-on-color-body); text-decoration: none; - background-color: #f8f9fa; + background-color: var(--attack-color-body-alternate-subtle); } .center-controls .matrix-controls .btn-toolbar { @@ -2528,9 +2656,275 @@ div#sidebars { } .version-table .table-break-row { - border-right-color: white; - border-left-color: white; + border-right-color: var(--attack-color-body); + border-left-color: var(--attack-color-body); padding: 1rem 0; } +:root, +:root[data-theme=light] { + --attack-color-primary: #303435; + --attack-on-color-primary: white; + --attack-color-secondary: #303435; + --attack-color-secondary-hover: rgb(57.7188118812, 62.5287128713, 63.7311881188); + --attack-on-color-secondary: white; + --attack-color-footer: #303435; + --attack-on-color-footer: #87deff; + --attack-color-footer-link-hover: rgb(99, 167, 206.5); + --attack-color-active: #303435; + --attack-color-active-alternate-medium: rgb(59.925, 63.325, 64.175); + --attack-on-color-active: #eaeaea; + --attack-color-body: white; + --attack-on-color-body: #39434c; + --attack-on-color-body-emphasis: #1d2226; + --attack-color-property-label: #1d2226; + --attack-on-color-body-deemphasis: #6b7379; + --attack-color-body-alternate-subtle: #f5f5f5; + --attack-color-body-alternate: #f2f2f2; + --attack-color-body-alternate-strong: #e6e6e6; + --attack-color-body-alternate-strongest: #d9d9d9; + --attack-border-color-body: #dfdfdf; + --attack-background-color-body: #dfdfdf; + --attack-color-link: #3f709e; + --attack-color-link-hover: #0056b3; + --attack-color-matrix-header: gray; + --attack-on-color-matrix-header: white; + --attack-color-search-highlight: yellow; + --attack-on-color-search-highlight: black; + --attack-color-deemphasis: #686f75; + --attack-on-color-deemphasis: white; + --attack-color-card-header: rgb(57 67 76 / 3%); + --attack-color-code: #a52f16; + --attack-color-danger: #bd2130; + --attack-color-image-background: white; + --attack-color-banner: #e7f0f6; + --attack-on-color-banner: #263b4a; + --attack-border-color-banner: #c2d5e2; + --attack-select-arrow: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%2339434c' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z'/%3E%3C/svg%3E"); + color-scheme: light; +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme]) { + --attack-color-primary: #303435; + --attack-on-color-primary: white; + --attack-color-secondary: #303435; + --attack-color-secondary-hover: rgb(63.5500990099, 68.8459405941, 70.1699009901); + --attack-on-color-secondary: white; + --attack-color-footer: #303435; + --attack-on-color-footer: #87deff; + --attack-color-footer-link-hover: #b7edff; + --attack-color-active: #9aa3a6; + --attack-color-active-alternate-medium: #879195; + --attack-on-color-active: #0f171c; + --attack-color-body: #222426; + --attack-on-color-body: #e8e6e3; + --attack-on-color-body-emphasis: #fffaf4; + --attack-color-property-label: #f2d2a4; + --attack-on-color-body-deemphasis: #b7b1a8; + --attack-color-body-alternate-subtle: #272a2c; + --attack-color-body-alternate: #2b2e30; + --attack-color-body-alternate-strong: #303437; + --attack-color-body-alternate-strongest: #353a3d; + --attack-border-color-body: #596166; + --attack-background-color-body: #373d40; + --attack-color-link: #7bb8ee; + --attack-color-link-hover: #b7ddff; + --attack-color-matrix-header: #596166; + --attack-on-color-matrix-header: #fffaf4; + --attack-color-search-highlight: #665a00; + --attack-on-color-search-highlight: #fff4b8; + --attack-color-deemphasis: #b7b1a8; + --attack-on-color-deemphasis: #222426; + --attack-color-card-header: #2b2e30; + --attack-color-code: #ff8f70; + --attack-color-danger: #ff8c96; + --attack-color-image-background: white; + --attack-color-banner: #263a49; + --attack-on-color-banner: #f2f7fa; + --attack-border-color-banner: #3f5d72; + --attack-select-arrow: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%23e8e6e3' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z'/%3E%3C/svg%3E"); + color-scheme: dark; + } +} +:root[data-theme=dark] { + --attack-color-primary: #303435; + --attack-on-color-primary: white; + --attack-color-secondary: #303435; + --attack-color-secondary-hover: rgb(63.5500990099, 68.8459405941, 70.1699009901); + --attack-on-color-secondary: white; + --attack-color-footer: #303435; + --attack-on-color-footer: #87deff; + --attack-color-footer-link-hover: #b7edff; + --attack-color-active: #9aa3a6; + --attack-color-active-alternate-medium: #879195; + --attack-on-color-active: #0f171c; + --attack-color-body: #222426; + --attack-on-color-body: #e8e6e3; + --attack-on-color-body-emphasis: #fffaf4; + --attack-color-property-label: #f2d2a4; + --attack-on-color-body-deemphasis: #b7b1a8; + --attack-color-body-alternate-subtle: #272a2c; + --attack-color-body-alternate: #2b2e30; + --attack-color-body-alternate-strong: #303437; + --attack-color-body-alternate-strongest: #353a3d; + --attack-border-color-body: #596166; + --attack-background-color-body: #373d40; + --attack-color-link: #7bb8ee; + --attack-color-link-hover: #b7ddff; + --attack-color-matrix-header: #596166; + --attack-on-color-matrix-header: #fffaf4; + --attack-color-search-highlight: #665a00; + --attack-on-color-search-highlight: #fff4b8; + --attack-color-deemphasis: #b7b1a8; + --attack-on-color-deemphasis: #222426; + --attack-color-card-header: #2b2e30; + --attack-color-code: #ff8f70; + --attack-color-danger: #ff8c96; + --attack-color-image-background: white; + --attack-color-banner: #263a49; + --attack-on-color-banner: #f2f7fa; + --attack-border-color-banner: #3f5d72; + --attack-select-arrow: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%23e8e6e3' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z'/%3E%3C/svg%3E"); + color-scheme: dark; +} + +.form-control, +.custom-select, +.bootstrap-select > .dropdown-toggle, +.dropdown-menu, +.list-group-item, +.modal-content, +.popover, +.popover-body, +.page-link, +.input-group-text { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body); + border-color: var(--attack-border-color-body); +} + +.custom-select { + background-image: var(--attack-select-arrow); +} + +.form-control:focus, +.custom-select:focus { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body); + border-color: var(--attack-color-active); + box-shadow: 0 0 0 0.2rem rgba(76, 159, 254, 0.25); +} + +.dropdown-item, +.page-link { + color: var(--attack-color-link); +} + +.dropdown-item:hover, +.dropdown-item:focus, +.page-link:hover, +.page-link:focus { + color: var(--attack-on-color-body-emphasis); + background-color: var(--attack-color-body-alternate); +} + +.form-control:disabled, +.form-control[readonly], +.custom-select:disabled, +.page-item.disabled .page-link { + color: var(--attack-on-color-body-deemphasis); + background-color: var(--attack-color-body-alternate); +} + +.dropdown-divider, +hr { + border-color: var(--attack-border-color-body); +} + +.table { + color: var(--attack-on-color-body); +} + +.text-danger { + color: var(--attack-color-danger) !important; +} + +.btn-outline-secondary { + color: var(--attack-on-color-body); + border-color: var(--attack-on-color-body-deemphasis); +} +.btn-outline-secondary:disabled, .btn-outline-secondary.disabled { + color: var(--attack-on-color-body-deemphasis); + background-color: transparent; +} +.btn-outline-secondary:not(:disabled, .disabled):hover, .btn-outline-secondary:not(:disabled, .disabled):focus, .btn-outline-secondary:not(:disabled, .disabled):active { + color: var(--attack-on-color-active); + background-color: var(--attack-color-active); + border-color: var(--attack-color-active); +} + +.nav-tabs .nav-link { + color: var(--attack-color-link); + border-color: transparent; +} + +.nav-tabs .nav-link:hover, +.nav-tabs .nav-link:focus { + border-color: var(--attack-border-color-body); +} + +.nav-tabs .nav-link.active, +.nav-tabs .nav-item.show .nav-link { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body); + border-color: var(--attack-border-color-body) var(--attack-border-color-body) var(--attack-color-body); +} + +@media print { + :root, + :root[data-theme], + :root:not([data-theme]) { + --attack-color-primary: #303435; + --attack-on-color-primary: white; + --attack-color-secondary: #303435; + --attack-color-secondary-hover: rgb(57.7188118812, 62.5287128713, 63.7311881188); + --attack-on-color-secondary: white; + --attack-color-footer: #303435; + --attack-on-color-footer: #87deff; + --attack-color-footer-link-hover: rgb(99, 167, 206.5); + --attack-color-active: #303435; + --attack-color-active-alternate-medium: rgb(59.925, 63.325, 64.175); + --attack-on-color-active: #eaeaea; + --attack-color-body: white; + --attack-on-color-body: #39434c; + --attack-on-color-body-emphasis: #1d2226; + --attack-color-property-label: #1d2226; + --attack-on-color-body-deemphasis: #6b7379; + --attack-color-body-alternate-subtle: #f5f5f5; + --attack-color-body-alternate: #f2f2f2; + --attack-color-body-alternate-strong: #e6e6e6; + --attack-color-body-alternate-strongest: #d9d9d9; + --attack-border-color-body: #dfdfdf; + --attack-background-color-body: #dfdfdf; + --attack-color-link: #3f709e; + --attack-color-link-hover: #0056b3; + --attack-color-matrix-header: gray; + --attack-on-color-matrix-header: white; + --attack-color-search-highlight: yellow; + --attack-on-color-search-highlight: black; + --attack-color-deemphasis: #686f75; + --attack-on-color-deemphasis: white; + --attack-color-card-header: rgb(57 67 76 / 3%); + --attack-color-code: #a52f16; + --attack-color-danger: #bd2130; + --attack-color-image-background: white; + --attack-color-banner: #e7f0f6; + --attack-on-color-banner: #263b4a; + --attack-border-color-banner: #c2d5e2; + --attack-select-arrow: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%2339434c' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z'/%3E%3C/svg%3E"); + color-scheme: light; + } +} + /*# sourceMappingURL=style-user.css.map */ diff --git a/attack-theme/templates/general/attack-index.html b/attack-theme/templates/general/attack-index.html index 21dd4fbd02d..8aafc84de2a 100644 --- a/attack-theme/templates/general/attack-index.html +++ b/attack-theme/templates/general/attack-index.html @@ -28,7 +28,7 @@ Contribute - Blog External site + Blog External site @@ -40,7 +40,7 @@ Random Page - Toggle Dropdown @@ -115,4 +115,4 @@

{{parsed.matrix_name}}

-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/attack-theme/templates/general/base-template.html b/attack-theme/templates/general/base-template.html index c960c51d76b..ab8e06538de 100644 --- a/attack-theme/templates/general/base-template.html +++ b/attack-theme/templates/general/base-template.html @@ -30,8 +30,10 @@ + {{ title }} + diff --git a/attack-theme/templates/macros/navigation_menu.html b/attack-theme/templates/macros/navigation_menu.html index c842b4d1027..2ef74e9198f 100644 --- a/attack-theme/templates/macros/navigation_menu.html +++ b/attack-theme/templates/macros/navigation_menu.html @@ -17,6 +17,17 @@ {{external_link(element)}} {% elif element.search_bar %} + diff --git a/modules/datasources/templates/datasources-index.html b/modules/datasources/templates/datasources-index.html index 3f42b01f657..fde53545bae 100644 --- a/modules/datasources/templates/datasources-index.html +++ b/modules/datasources/templates/datasources-index.html @@ -90,7 +90,7 @@
{% if row.deprecated %} - ****Deprecation Warning**** + ****Deprecation Warning**** {% endif %} {{clean_output.stixToHTML(row.descr)}} @@ -113,4 +113,4 @@
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/modules/resources/templates/attack-advisory-council-members.html b/modules/resources/templates/attack-advisory-council-members.html index 576c51d6657..373620fefc3 100644 --- a/modules/resources/templates/attack-advisory-council-members.html +++ b/modules/resources/templates/attack-advisory-council-members.html @@ -7,9 +7,10 @@ {{ super() }} @@ -423,7 +425,7 @@
ATT&CK Excel Files by Version {% endif %} -
+
{% for version in versions %} {% set domains = parsed.excel_files_by_version[version] %}
ATT&CK Excel Files by Version -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/modules/resources/templates/attackcon-overview.html b/modules/resources/templates/attackcon-overview.html index fb7e3d13a9a..bd5e5339e98 100644 --- a/modules/resources/templates/attackcon-overview.html +++ b/modules/resources/templates/attackcon-overview.html @@ -25,8 +25,11 @@

{{ con.title }}

{% set ifSponsor = "sponsors_img_list" in con %} {% if "banner_img" in con %}

- ATT&CKcon Banner + {% set light_banner_titles = ["ATT&CKcon 4.0", "ATT&CKcon 5.0", "ATT&CKcon 6.0", "ATT&CKcon 7.0"] %} + + ATT&CKcon Banner +

{% endif %} diff --git a/modules/versions/versions.py b/modules/versions/versions.py index 3f579cf4d9a..0552b84566c 100644 --- a/modules/versions/versions.py +++ b/modules/versions/versions.py @@ -185,18 +185,39 @@ def deploy_previous_version(version_data): if os.path.exists(archive_path): logger.info(f"{version_name}: extracting from local archive {archive_filename}") - extract_tar_gz(archive_path=archive_path, dest_path=dest_path) - return - - if download_archive(url=archive_url, local_path=archive_path): + elif download_archive(url=archive_url, local_path=archive_path): logger.info(f"{version_name}: extracting downloaded archive {archive_filename}") - extract_tar_gz(archive_path=archive_path, dest_path=dest_path) - return - - # this saves the cleaned up version archive for the future - logger.warning(f"{version_name}: download failed, falling back to git archive") - create_version_archive(version_data=version_data, output_dir=archive_dir) + else: + # This saves the cleaned up version archive for the future. + logger.warning(f"{version_name}: download failed, falling back to git archive") + create_version_archive(version_data=version_data, output_dir=archive_dir) extract_tar_gz(archive_path=archive_path, dest_path=dest_path) + add_archive_theme(dest_path) + + +def add_archive_theme(version_path: str) -> None: + """Add appearance support to deployed archived pages without changing the saved archive. + + Parameters + ---------- + version_path : str + Directory containing the extracted historical site. + """ + prefix = "/" + site_config.subdirectory.strip("/") if site_config.subdirectory else "" + theme_assets = ( + f'\n' + f'\n' + ) + for path in Path(version_path).rglob("*.html"): + if not path.is_file(): + continue + html = path.read_text(encoding="utf8") + # Sidebar fragments and redirects have no banner. Newer archives may + # already have native theme support and must not receive a second UI. + if "version-banner" not in html or "data-archive-theme" in html or "data-theme-toggle" in html: + continue + html = re.sub(r"", lambda _: theme_assets + "", html, count=1, flags=re.IGNORECASE) + path.write_text(html, encoding="utf8") def process_html_file(path: str, version_url_path: str, is_current: bool, version_data: dict): From 0c1c87db5c4c309fe6016f7aa90c0fc031ec21c5 Mon Sep 17 00:00:00 2001 From: Jared Ondricek Date: Mon, 14 Sep 2026 14:18:18 -0500 Subject: [PATCH 10/16] fix: update search cache schema version to 4 --- attack-search/src/settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/attack-search/src/settings.js b/attack-search/src/settings.js index 14451cd250b..6218aace463 100644 --- a/attack-search/src/settings.js +++ b/attack-search/src/settings.js @@ -1,7 +1,7 @@ const baseURL = ''; // TODO migrate from base_url (generated via Pelican) const packageJson = require('../package.json'); -const searchCacheSchemaVersion = 3; +const searchCacheSchemaVersion = 4; const flexSearchVersion = packageJson.dependencies.flexsearch.replace(/^[^\d]*/, ''); const searchCacheCompatibilityVersion = `flexsearch-${flexSearchVersion}`; From d95d666b896a983714ae0d24993bc42dccdc61bf Mon Sep 17 00:00:00 2001 From: Jared Ondricek Date: Mon, 14 Sep 2026 14:25:01 -0500 Subject: [PATCH 11/16] fix: update search service tests and adjust attack ID path prefixes --- .../__tests__/search-service.test.js | 24 +++++++++++++++++++ modules/search/search.py | 5 ++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/attack-search/__tests__/search-service.test.js b/attack-search/__tests__/search-service.test.js index 77088b09357..5c5c1c91771 100644 --- a/attack-search/__tests__/search-service.test.js +++ b/attack-search/__tests__/search-service.test.js @@ -194,6 +194,30 @@ describe('SearchService', () => { expect(searchService.allSearchResults.map(result => result.id)).toEqual([2, 5, 3, 4]); }); + test.each(['TA0001', '0001'])('Promotes a tactic page for ATT&CK ID query %s', async (query) => { + const documents = { + 1: { + id: 1, + title: 'A valid reference', + path: '/resources/reference/index.html', + content: 'This page references TA0001.', + }, + 2: { + id: 2, + title: 'Initial Access, Tactic TA0001 - Enterprise', + path: '/tactics/TA0001/index.html', + content: 'The TA0001 tactic.', + attackId: 'TA0001', + }, + }; + searchService.attackIndex.search = jest.fn().mockResolvedValue([{ field: 'title', result: [1, 2] }]); + searchService.resolveSearchResults = jest.fn(async positions => positions.map(position => documents[position])); + + await searchService.query(query); + + expect(searchService.allSearchResults.map(result => result.id)).toEqual([2, 1]); + }); + test('Preserves result ordering for non-ID queries', async () => { const documents = { 1: { id: 1, title: 'First result', path: '/resources/faq/index.html', content: 'Resources' }, diff --git a/modules/search/search.py b/modules/search/search.py index 6faac32e8e0..bd100e9bf38 100644 --- a/modules/search/search.py +++ b/modules/search/search.py @@ -28,8 +28,9 @@ "x-mitre-asset": "assets", "x-mitre-data-component": "datacomponents", "x-mitre-detection-strategy": "detectionstrategies", + "x-mitre-tactic": "tactics", } -searchable_object_path_prefixes = set(object_path_prefixes.values()) | {"techniques"} +attack_id_path_prefixes = set(object_path_prefixes.values()) | {"techniques"} def generate_index(): @@ -145,7 +146,7 @@ def get_search_attack_id(path): return f"{subtechnique_match.group(1)}.{subtechnique_match.group(2)}" object_match = re.fullmatch(r"/([^/]+)/([A-Z]+\d+(?:\.\d+)?)/index\.html", path) - if object_match and object_match.group(1) in searchable_object_path_prefixes: + if object_match and object_match.group(1) in attack_id_path_prefixes: return object_match.group(2) return None From ffd0522211b09bb5abee4c5b95a4ed8b752e3fe2 Mon Sep 17 00:00:00 2001 From: Jared Ondricek Date: Mon, 14 Sep 2026 15:14:00 -0500 Subject: [PATCH 12/16] fix(search): implement cache invalidation for failed search index builds --- CHANGELOG.md | 2 +- attack-search/__tests__/search-events.test.js | 12 +++++++++++- attack-search/src/index.js | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57b7b2c46aa..4ebf354169c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Bug Fixes * Settle the search index write when an IndexedDB write fails, instead of leaving the promise pending and the search spinner up. -* Disable the search controls and explain why when the search index cannot be built, instead of leaving the spinner running for as long as the page is open. A failed restore from the cached index is no longer reported as a successful load. +* Disable the search controls and explain why when the search index cannot be built, instead of leaving the spinner running for as long as the page is open. A failed restore from the cached index is discarded instead of being reported as a successful load or retried after every reload. ## v5.0.0 (2026-08-06) diff --git a/attack-search/__tests__/search-events.test.js b/attack-search/__tests__/search-events.test.js index 5c27afdf8a5..45d30e8c3c7 100644 --- a/attack-search/__tests__/search-events.test.js +++ b/attack-search/__tests__/search-events.test.js @@ -129,12 +129,14 @@ describe('search event bindings', () => { }); test('a failed restore from the cache is not reported as a successful load', async () => { - await loadIndexWithAFailingWarmRestore(); + const { cacheKey, deleteCachedDatabase } = await loadIndexWithAFailingWarmRestore(); // The catch used to set the loaded flag false and the finally set it straight back to // true, so `search` went on to query an index that was never populated. expect(mockJqueryApis['#search-input'].prop).toHaveBeenCalledWith('disabled', true); expect(mockJqueryApis['#search-icon'].addClass).toHaveBeenCalledWith('error-icon'); + expect(global.localStorage.removeItem).toHaveBeenCalledWith(cacheKey); + expect(deleteCachedDatabase).toHaveBeenCalledTimes(1); }); test('a failed index build puts the search controls into their unavailable state', async () => { @@ -172,11 +174,17 @@ async function loadIndexWithAFailingColdStart() { async function loadIndexWithAFailingWarmRestore() { const { searchCacheCompatibilityVersion, searchCacheSchemaVersion } = require('../src/settings'); const version = `${searchCacheSchemaVersion}-${searchCacheCompatibilityVersion}`; + const cacheKey = `saved_uuid_search_schema_${version}`; + const deleteCachedDatabase = jest.fn(() => Promise.resolve()); global.window = { indexedDB: {} }; global.localStorage.getItem.mockReturnValue(`${global.build_uuid}-search-${version}`); jest.doMock('../src/search-service.js', () => class { + constructor() { + this.db = { indexeddb: { delete: deleteCachedDatabase } }; + } + initializeAsync() { return Promise.reject(new Error('cached index is unreadable')); } @@ -184,6 +192,8 @@ async function loadIndexWithAFailingWarmRestore() { require('../src/index'); await new Promise(resolve => setImmediate(resolve)); + + return { cacheKey, deleteCachedDatabase }; } function eventsForSelector(selector) { diff --git a/attack-search/src/index.js b/attack-search/src/index.js index 5ee5c82bbf0..54ad023b402 100644 --- a/attack-search/src/index.js +++ b/attack-search/src/index.js @@ -104,6 +104,23 @@ function markSearchUnavailable(reason) { searchButton.prop('title', reason); } +// Remove a failed cached index so the next page load rebuilds it instead of retrying +// the same restore path. Cache cleanup is best-effort and must not mask the original +// initialization failure or prevent the unavailable UI state from being shown. +async function invalidateSearchCache() { + try { + localStorage.removeItem(searchCacheKey); + } catch (error) { + console.error('Failed to remove the search cache marker:', error); + } + + try { + await searchService.db.indexeddb.delete(); + } catch (error) { + console.error('Failed to delete the cached search index:', error); + } +} + const SEARCH_INDEX_FAILED_MESSAGE = 'The search index could not be built. Reload the page to try again.'; // Initialize the search service @@ -132,6 +149,7 @@ async function initializeSearchService() { } catch (error) { console.error('Failed to initialize SearchService:', error); markSearchUnavailable(SEARCH_INDEX_FAILED_MESSAGE); + await invalidateSearchCache(); } finally { searchParsingIcon.hide(); } From 91264f6ea4f65424964fd403897ec9f0539d79ea Mon Sep 17 00:00:00 2001 From: Jared Ondricek Date: Tue, 15 Sep 2026 09:24:58 -0500 Subject: [PATCH 13/16] feat: implement theme toggle synchronization and update navigation layout --- CHANGELOG.md | 6 ++ attack-search/__tests__/theme.test.js | 52 +++++++++++++- attack-style/themes/README.md | 2 + attack-theme/static/scripts/theme.js | 71 +++++++++++++++++++ .../templates/macros/navigation_menu.html | 12 ++-- 5 files changed, 137 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2039c128b43..882842c1469 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Website Changelog +## Unreleased + +### Features + +* Add dark mode! + ## v5.0.0 (2026-08-06) * Release ATT&CK content version 19.2. diff --git a/attack-search/__tests__/theme.test.js b/attack-search/__tests__/theme.test.js index 55eb51efd55..17b04ad2377 100644 --- a/attack-search/__tests__/theme.test.js +++ b/attack-search/__tests__/theme.test.js @@ -145,6 +145,48 @@ describe('site theme', () => { expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-label', 'Switch to dark mode'); }); + test('synchronizes a theme change from another tab without writing it back', () => { + const fixture = createControllerFixture({ storedPreference: 'light' }); + theme.createThemeController(fixture).init(); + + fixture.storage.getItem.mockReturnValue('dark'); + fixture.document.defaultView.dispatchStorage({ + key: theme.STORAGE_KEY, + storageArea: fixture.storage, + }); + + expect(fixture.document.documentElement.setAttribute).toHaveBeenLastCalledWith('data-theme', 'dark'); + expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-checked', 'true'); + expect(fixture.storage.setItem).not.toHaveBeenCalled(); + fixture.toggle.click(); + expect(fixture.storage.setItem).toHaveBeenCalledWith(theme.STORAGE_KEY, 'light'); + }); + + test.each([null, 'attack-website-theme'])('returns to the system theme after storage removal (%s)', key => { + const fixture = createControllerFixture({ storedPreference: 'dark', systemDark: false }); + theme.createThemeController(fixture).init(); + + fixture.storage.getItem.mockReturnValue(null); + fixture.document.defaultView.dispatchStorage({ key, storageArea: fixture.storage }); + + expect(fixture.document.documentElement.removeAttribute).toHaveBeenCalledWith('data-theme'); + expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-checked', 'false'); + fixture.mediaQuery.matches = true; + fixture.mediaQuery.dispatchChange(); + expect(fixture.toggle.setAttribute).toHaveBeenLastCalledWith('data-theme-effective', 'dark'); + }); + + test('ignores unrelated storage keys and storage areas', () => { + const fixture = createControllerFixture({ storedPreference: 'light' }); + theme.createThemeController(fixture).init(); + fixture.storage.getItem.mockClear(); + + fixture.document.defaultView.dispatchStorage({ key: 'other', storageArea: fixture.storage }); + fixture.document.defaultView.dispatchStorage({ key: theme.STORAGE_KEY, storageArea: createStorage('dark') }); + + expect(fixture.storage.getItem).not.toHaveBeenCalled(); + }); + test('loads the early theme script before styles and renders one toggle before search', () => { const template = fs.readFileSync( path.join(__dirname, '../../attack-theme/templates/general/base-template.html'), @@ -157,7 +199,8 @@ describe('site theme', () => { expect(template).toContain(''); expect(template.indexOf('/theme/scripts/theme.js')).toBeLessThan(template.indexOf('bootstrap.min.css')); - expect(navigation.indexOf('data-theme-toggle')).toBeLessThan(navigation.indexOf('id="search-button"')); + expect(navigation.match(/\bdata-theme-toggle(?=[\s>])/g)).toHaveLength(1); + expect(navigation.indexOf('id="theme-toggle"')).toBeLessThan(navigation.indexOf('id="search-button"')); expect(navigation).toContain('role="switch"'); expect(navigation).toContain('class="theme-toggle-track"'); expect(navigation).toContain('class="theme-toggle-thumb"'); @@ -259,6 +302,7 @@ function createControllerFixture({ storedPreference = null, systemDark = false } const root = createRoot(); const toggle = createElement(); let changeListener; + let storageListener; const mediaQuery = { matches: systemDark, addEventListener: jest.fn((eventName, listener) => { @@ -269,6 +313,12 @@ function createControllerFixture({ storedPreference = null, systemDark = false } const document = { documentElement: root, querySelector: jest.fn(selector => (selector === '[data-theme-toggle]' ? toggle : null)), + defaultView: { + addEventListener: jest.fn((eventName, listener) => { + if (eventName === 'storage') storageListener = listener; + }), + dispatchStorage: event => storageListener(event), + }, }; return { diff --git a/attack-style/themes/README.md b/attack-style/themes/README.md index 3f72b0b21db..e16944148e4 100644 --- a/attack-style/themes/README.md +++ b/attack-style/themes/README.md @@ -21,6 +21,8 @@ The site has two independent theme layers. The brand layer is selected at build The light/dark appearance is selected at runtime. With no `data-theme` attribute on the root element, the stylesheet follows `prefers-color-scheme`. The single navigation toggle stores an explicit `data-theme="light"` or `data-theme="dark"` override after the user first switches themes. +On collapsed navigation, the same toggle moves immediately left of the hamburger when it fits alongside the logo. If space is insufficient, it returns to its menu slot before Search; desktop keeps that original slot. Placement follows Bootstrap's hamburger visibility and measured element widths, including changes to the logo or browser size. Saved preferences synchronize across pages and open tabs on the same origin and browser profile. + Most styling should use semantic color helpers from `abstracts/_color-functions.scss`. Add a token to `_palette.scss` when a component needs a distinct theme-aware surface or foreground instead of embedding a light-only color in that component. ## Preserved Sites diff --git a/attack-theme/static/scripts/theme.js b/attack-theme/static/scripts/theme.js index 7f8365a836d..c61632a9fdc 100644 --- a/attack-theme/static/scripts/theme.js +++ b/attack-theme/static/scripts/theme.js @@ -73,6 +73,64 @@ return mediaQuery && mediaQuery.matches ? 'dark' : 'light'; } + function initNavigationToggle(document, toggle) { + const view = document.defaultView; + const home = document.querySelector('[data-theme-toggle-home]'); + const mobile = document.querySelector('[data-theme-toggle-mobile]'); + if (!view || !home || !mobile || !toggle) return; + + const navbar = mobile.closest('.navbar'); + const brand = navbar.querySelector('.navbar-brand'); + const hamburger = mobile.querySelector('.navbar-toggler'); + let pending = false; + + function outerWidth(element) { + const style = view.getComputedStyle(element); + return element.getBoundingClientRect().width + + parseFloat(style.marginLeft) + parseFloat(style.marginRight); + } + + function updatePlacement() { + pending = false; + const focused = document.activeElement === toggle; + let destination = home; + + // Bootstrap controls the breakpoint; measure only when the hamburger is shown. + if (view.getComputedStyle(hamburger).display !== 'none') { + if (toggle.parentNode !== mobile) mobile.insertBefore(toggle, hamburger); + const style = view.getComputedStyle(navbar); + const availableWidth = navbar.clientWidth + - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight); + if (outerWidth(brand) + mobile.getBoundingClientRect().width <= availableWidth) { + destination = mobile; + } + } + + if (toggle.parentNode !== destination) destination.appendChild(toggle); + home.hidden = destination === mobile; + + // Moving a focused node can blur it. If it now lives in the closed menu, + // leave focus on the button that opens that menu instead. + if (focused) { + const focusTarget = toggle.getClientRects().length ? toggle : hamburger; + focusTarget.focus({ preventScroll: true }); + } + } + + function schedulePlacement() { + if (pending) return; + pending = true; + view.requestAnimationFrame(updatePlacement); + } + + updatePlacement(); + view.addEventListener('resize', schedulePlacement); + if (typeof view.ResizeObserver === 'function') { + const observer = new view.ResizeObserver(schedulePlacement); + [navbar, brand, hamburger, toggle].forEach(element => observer.observe(element)); + } + } + function createThemeController({ document, storage, mediaQuery, archived = false }) { const systemTheme = mediaQuery || { matches: false }; let preference = readStoredPreference(storage); @@ -101,6 +159,15 @@ if (preference === 'system') updateControl(); } + function handleStorageChange(event) { + if (!storage || event.storageArea !== storage + || (event.key !== STORAGE_KEY && event.key !== null)) return; + + preference = readStoredPreference(storage); + applyPreference(document.documentElement, preference); + updateControl(); + } + function init() { applyPreference(document.documentElement, preference); toggle = document.querySelector('[data-theme-toggle]'); @@ -132,6 +199,10 @@ systemTheme.addListener(handleSystemThemeChange); } + if (document.defaultView) { + document.defaultView.addEventListener('storage', handleStorageChange); + } + if (!archived) initNavigationToggle(document, toggle); updateControl(); } diff --git a/attack-theme/templates/macros/navigation_menu.html b/attack-theme/templates/macros/navigation_menu.html index 2ef74e9198f..64756461955 100644 --- a/attack-theme/templates/macros/navigation_menu.html +++ b/attack-theme/templates/macros/navigation_menu.html @@ -1,10 +1,12 @@ {% macro navigation_menu(menu, logo_header, output_file) -%}