Files
GIA/core/templates/pages/security.html
2026-03-07 15:34:23 +00:00

696 lines
31 KiB
HTML

{% extends "base.html" %}
{% block content %}
<section class="section">
<div class="container">
<h1 class="title is-4">Security</h1>
<div class="columns is-desktop is-variable is-8">
<div class="column">
<div class="box">
<h2 class="title is-6">XMPP Channel</h2>
<table class="table is-fullwidth is-size-7">
<tbody>
<tr>
<th>Component JID</th>
<td>{{ xmpp_state.omemo_target_jid|default:"—" }}</td>
</tr>
<tr>
<th>OMEMO</th>
<td>
{% if xmpp_state.omemo_enabled %}
<span class="tag is-success">Active</span>
{% else %}
<span class="tag is-light">{{ xmpp_state.omemo_status|default:"not configured" }}</span>
{% endif %}
</td>
</tr>
<tr>
<th>Status reason</th>
<td>{{ xmpp_state.omemo_status_reason|default:"—" }}</td>
</tr>
<tr>
<th>Component fingerprint</th>
<td><code>{{ xmpp_state.omemo_fingerprint|default:"—" }}</code></td>
</tr>
</tbody>
</table>
<h3 class="title is-7 mt-4 mb-2">Security Policy</h3>
<form method="post">
{% csrf_token %}
<div class="field">
<label class="checkbox">
<input type="checkbox" name="require_omemo"{% if security_settings.require_omemo %} checked{% endif %}>
Require OMEMO encryption — reject plaintext messages from your XMPP client
</label>
<p class="help is-size-7 has-text-grey mt-1">When enabled, any plaintext XMPP message to the gateway is rejected before command routing.</p>
<p class="help is-size-7 has-text-grey">This is separate from command-scope policy checks such as Require Trusted Fingerprint.</p>
</div>
<button class="button is-link is-small" type="submit">Save</button>
</form>
</div>
</div>
<div class="column">
<div class="box">
<h2 class="title is-6">Your XMPP Client</h2>
{% if omemo_row %}
<table class="table is-fullwidth is-size-7">
<tbody>
<tr>
<th>Status</th>
<td>
{% if omemo_row.status == "detected" %}
<span class="tag is-info">{{ omemo_row.status }}</span>
{% elif omemo_row.status == "no_omemo" %}
<span class="tag is-warning">no OMEMO observed</span>
{% elif omemo_row.status == "error" %}
<span class="tag is-danger">{{ omemo_row.status }}</span>
{% else %}
<span class="tag is-light">{{ omemo_row.status }}</span>
{% endif %}
</td>
</tr>
<tr>
<th>Contact key</th>
<td><code>{{ omemo_row.latest_client_key|default:"—" }}</code></td>
</tr>
<tr>
<th>Contact JID</th>
<td>{{ sender_jid.bare|default:"—" }}</td>
</tr>
<tr>
<th>Resource</th>
<td>{{ sender_jid.resource|default:"—" }}</td>
</tr>
<tr>
<th>GIA component</th>
<td>{{ omemo_row.last_target_jid|default:"—" }}</td>
</tr>
<tr>
<th>Last seen</th>
<td>{{ omemo_row.last_seen_at|default:"—" }}</td>
</tr>
<tr>
<th>Updated</th>
<td>{{ omemo_row.updated_at }}</td>
</tr>
</tbody>
</table>
{% else %}
<p class="is-size-7 has-text-grey">No OMEMO observation recorded yet. Send a message via XMPP to populate this.</p>
{% endif %}
</div>
</div>
</div>
<div class="box">
<h2 class="title is-6">Global Scope Override</h2>
<p class="is-size-7 has-text-grey mb-3">
This scope can force settings across all Command Security Scopes.
</p>
<div class="box" style="margin: 0; border: 1px solid rgba(60, 60, 60, 0.12);">
<form method="post">
{% csrf_token %}
<input type="hidden" name="scope_key" value="global.override">
<input type="hidden" name="global_scope_enabled" value="{{ global_override.values.scope_enabled }}" data-global-mode-input="scope_enabled">
<input type="hidden" name="global_require_omemo" value="{{ global_override.values.require_omemo }}" data-global-mode-input="require_omemo">
<input type="hidden" name="global_require_trusted_fingerprint" value="{{ global_override.values.require_trusted_fingerprint }}" data-global-mode-input="require_trusted_fingerprint">
<div class="is-flex is-justify-content-space-between is-align-items-center mb-2">
<div>
<p class="mb-1"><strong>Global Scope Override</strong></p>
<p class="is-size-7 has-text-grey">Remote controls for local scope security checkboxes.</p>
<p class="is-size-7 has-text-grey"><code>global.override</code></p>
</div>
<div class="tags has-addons">
<span class="tag is-dark">Scope Enabled</span>
{% if global_override.values.scope_enabled == "on" %}
<span class="tag is-success">Force On</span>
{% elif global_override.values.scope_enabled == "off" %}
<span class="tag is-light">Force Off</span>
{% else %}
<span class="tag is-info">Per Scope</span>
{% endif %}
</div>
</div>
<div class="field mb-2">
<label class="label is-size-7">Allowed Services Preview</label>
<div class="scope-allowance-capsule" data-capsule>
{% for service in policy_services %}
<span class="scope-allowance-pill" data-service-pill="{{ service }}">{{ service }}</span>
{% endfor %}
</div>
</div>
<div class="field is-grouped is-grouped-multiline mb-2">
<label class="checkbox mr-4">
<input
type="checkbox"
disabled
data-global-visual-checkbox="scope_enabled"
data-global-visual-state="{{ global_override.values.scope_enabled }}"
{% if global_override.values.scope_enabled == "on" %} checked{% endif %}
>
Scope Enabled
</label>
<label class="checkbox mr-4">
<input
type="checkbox"
disabled
data-global-visual-checkbox="require_omemo"
data-global-visual-state="{{ global_override.values.require_omemo }}"
{% if global_override.values.require_omemo == "on" %} checked{% endif %}
>
Require OMEMO
</label>
<label class="checkbox">
<input
type="checkbox"
disabled
data-global-visual-checkbox="require_trusted_fingerprint"
{% if global_override.values.require_trusted_fingerprint == "on" %} checked{% endif %}
>
Require Trusted Fingerprint
</label>
</div>
<p class="help is-size-7 has-text-grey mb-3">Set each field to Per Scope to edit that field inside local scopes.</p>
<div class="field mb-2">
<div class="is-flex is-justify-content-space-between is-align-items-center">
<label class="label is-size-7 mb-1">Scope Enabled</label>
<div class="is-flex is-align-items-center" style="gap:0.5rem;">
<span class="tag is-size-7" data-global-mode-label="scope_enabled"></span>
<button type="button" class="button is-small is-light" data-global-change-toggle="scope_enabled">Change Global</button>
</div>
</div>
<div class="buttons has-addons is-hidden" data-global-mode-picker="scope_enabled">
<button class="button is-small" type="button" data-global-mode-set="scope_enabled" data-mode="per_scope">Per Scope</button>
<button class="button is-small" type="button" data-global-mode-set="scope_enabled" data-mode="on">Force On</button>
<button class="button is-small" type="button" data-global-mode-set="scope_enabled" data-mode="off">Force Off</button>
</div>
</div>
<div class="field mb-2">
<div class="is-flex is-justify-content-space-between is-align-items-center">
<label class="label is-size-7 mb-1">Require OMEMO</label>
<div class="is-flex is-align-items-center" style="gap:0.5rem;">
<span class="tag is-size-7" data-global-mode-label="require_omemo"></span>
<button type="button" class="button is-small is-light" data-global-change-toggle="require_omemo">Change Global</button>
</div>
</div>
<div class="buttons has-addons is-hidden" data-global-mode-picker="require_omemo">
<button class="button is-small" type="button" data-global-mode-set="require_omemo" data-mode="per_scope">Per Scope</button>
<button class="button is-small" type="button" data-global-mode-set="require_omemo" data-mode="on">Force On</button>
<button class="button is-small" type="button" data-global-mode-set="require_omemo" data-mode="off">Force Off</button>
</div>
</div>
<div class="field mb-3">
<div class="is-flex is-justify-content-space-between is-align-items-center">
<label class="label is-size-7 mb-1">Require Trusted Fingerprint</label>
<div class="is-flex is-align-items-center" style="gap:0.5rem;">
<span class="tag is-size-7" data-global-mode-label="require_trusted_fingerprint"></span>
<button type="button" class="button is-small is-light" data-global-change-toggle="require_trusted_fingerprint">Change Global</button>
</div>
</div>
<div class="buttons has-addons is-hidden" data-global-mode-picker="require_trusted_fingerprint">
<button class="button is-small" type="button" data-global-mode-set="require_trusted_fingerprint" data-mode="per_scope">Per Scope</button>
<button class="button is-small" type="button" data-global-mode-set="require_trusted_fingerprint" data-mode="on">Force On</button>
<button class="button is-small" type="button" data-global-mode-set="require_trusted_fingerprint" data-mode="off">Force Off</button>
</div>
</div>
<div class="field mb-2">
<label class="label is-size-7">Allowed Services</label>
<div class="is-flex is-flex-wrap-wrap" style="gap: 0.8rem;">
{% for service in policy_services %}
<label class="checkbox">
<input type="checkbox" name="allowed_services" value="{{ service }}"{% if service in global_override.allowed_services %} checked{% endif %}>
{{ service }}
</label>
{% endfor %}
</div>
<p class="help is-size-7 has-text-grey">Allowed Services: <code>xmpp</code>, <code>whatsapp</code>, <code>signal</code>, <code>instagram</code>, <code>web</code>.</p>
<p class="help is-size-7 has-text-grey">Leave all unchecked to allow all services.</p>
</div>
<div class="field mb-2">
<label class="label is-size-7">Allowed Channels</label>
<div id="channel-rules-global-override" class="channel-rules-list">
{% for rule in global_override.channel_rules %}
<div class="channel-rule-row is-flex is-align-items-center mb-2" style="gap: 0.5rem;">
<div class="select is-small">
<select name="allowed_channel_service">
<option value="*"{% if rule.service == "*" %} selected{% endif %}>any</option>
{% for service in policy_services %}
<option value="{{ service }}"{% if rule.service == service %} selected{% endif %}>{{ service }}</option>
{% endfor %}
</select>
</div>
<input class="input is-small" name="allowed_channel_pattern" value="{{ rule.pattern }}" placeholder="m@zm.is* or 1203*">
<button class="button is-small is-light is-danger channel-rule-remove" type="button">Remove</button>
</div>
{% endfor %}
</div>
<button
class="button is-small is-light channel-rule-add"
type="button"
data-target="channel-rules-global-override"
>
Add Channel Rule
</button>
<p class="help is-size-7 has-text-grey">Leave pattern rows empty to allow all channels for allowed services.</p>
</div>
<button class="button is-link is-small" type="submit">Save Scope</button>
</form>
</div>
</div>
<div class="box">
<h2 class="title is-6">Command Security Scopes</h2>
<p class="is-size-7 has-text-grey mb-3">
Choose a top-level category, expand a scope, then click <strong>Change</strong> to edit that scope.
</p>
<div class="tabs is-toggle is-toggle-rounded is-small mb-3">
<ul>
{% for group in policy_groups %}
<li class="{% if forloop.first %}is-active{% endif %}" data-policy-tab-button="{{ group.key }}">
<a>{{ group.label }}</a>
</li>
{% endfor %}
</ul>
</div>
{% for group in policy_groups %}
<div class="policy-tab-panel{% if forloop.first %} is-active{% endif %}" data-policy-tab-panel="{{ group.key }}">
<div class="is-flex is-flex-direction-column" style="gap: 1rem;">
{% for row in group.rows %}
<details class="box scope-editor-card" style="margin: 0; border: 1px solid rgba(60, 60, 60, 0.12);">
<summary class="scope-summary is-flex is-justify-content-space-between is-align-items-center">
<span>
<strong>{{ row.label }}</strong>
<span class="is-size-7 has-text-grey ml-2"><code>{{ row.scope_key }}</code></span>
</span>
<span class="tags has-addons">
<span class="tag is-dark">Scope Enabled</span>
{% if row.enabled %}
<span class="tag is-success">On</span>
{% else %}
<span class="tag is-light">Off</span>
{% endif %}
</span>
</summary>
<form method="post" class="mt-3" data-scope-form>
{% csrf_token %}
<input type="hidden" name="scope_key" value="{{ row.scope_key }}">
<input type="hidden" name="scope_change_mode" value="0" data-scope-change-mode>
<p class="is-size-7 has-text-grey mb-2">{{ row.description }}</p>
<div class="field mb-2">
<button class="button is-small is-light" type="button" data-scope-edit-toggle>Change</button>
</div>
<div class="field mb-2">
<label class="label is-size-7">Allowed Services Preview</label>
<div class="scope-allowance-capsule" data-capsule>
{% for service in policy_services %}
<span class="scope-allowance-pill" data-service-pill="{{ service }}">{{ service }}</span>
{% endfor %}
</div>
</div>
<div class="field is-grouped is-grouped-multiline mb-2">
<label class="checkbox mr-4" title="{% if row.enabled_locked %}{{ row.lock_help }}{% endif %}">
<input class="scope-editable" data-lock-state="{% if row.enabled_locked %}locked{% else %}free{% endif %}" type="checkbox" name="policy_enabled"{% if row.enabled %} checked{% endif %}{% if row.enabled_locked %} disabled{% endif %}>
Scope Enabled
</label>
<label class="checkbox mr-4" title="{% if row.require_omemo_locked %}{{ row.lock_help }}{% endif %}">
<input class="scope-editable" data-lock-state="{% if row.require_omemo_locked %}locked{% else %}free{% endif %}" type="checkbox" name="policy_require_omemo"{% if row.require_omemo %} checked{% endif %}{% if row.require_omemo_locked %} disabled{% endif %}>
Require OMEMO
</label>
<label class="checkbox" title="{% if row.require_trusted_fingerprint_locked %}{{ row.lock_help }}{% endif %}">
<input class="scope-editable" data-lock-state="{% if row.require_trusted_fingerprint_locked %}locked{% else %}free{% endif %}" type="checkbox" name="policy_require_trusted_fingerprint"{% if row.require_trusted_fingerprint %} checked{% endif %}{% if row.require_trusted_fingerprint_locked %} disabled{% endif %}>
Require Trusted Fingerprint
</label>
</div>
<div class="field mb-2">
<label class="label is-size-7">Allowed Services</label>
<div class="is-flex is-flex-wrap-wrap" style="gap: 0.8rem;">
{% for service in policy_services %}
<label class="checkbox">
<input class="scope-editable" data-lock-state="free" type="checkbox" name="allowed_services" value="{{ service }}"{% if service in row.allowed_services %} checked{% endif %}>
{{ service }}
</label>
{% endfor %}
</div>
<p class="help is-size-7 has-text-grey">Allowed Services: <code>xmpp</code>, <code>whatsapp</code>, <code>signal</code>, <code>instagram</code>, <code>web</code>.</p>
<p class="help is-size-7 has-text-grey">Leave all unchecked to allow all services.</p>
</div>
<div class="field mb-2">
<label class="label is-size-7">Allowed Channels</label>
<div id="channel-rules-{{ row.scope_key|slugify }}" class="channel-rules-list">
{% for rule in row.channel_rules %}
<div class="channel-rule-row is-flex is-align-items-center mb-2" style="gap: 0.5rem;">
<div class="select is-small">
<select class="scope-editable" data-lock-state="free" name="allowed_channel_service">
<option value="*"{% if rule.service == "*" %} selected{% endif %}>any</option>
{% for service in policy_services %}
<option value="{{ service }}"{% if rule.service == service %} selected{% endif %}>{{ service }}</option>
{% endfor %}
</select>
</div>
<input class="input is-small scope-editable" data-lock-state="free" name="allowed_channel_pattern" value="{{ rule.pattern }}" placeholder="m@zm.is* or 1203*">
<button class="button is-small is-light is-danger channel-rule-remove scope-editable" data-lock-state="free" type="button">Remove</button>
</div>
{% endfor %}
</div>
<button
class="button is-small is-light channel-rule-add scope-editable"
data-lock-state="free"
type="button"
data-target="channel-rules-{{ row.scope_key|slugify }}"
>
Add Channel Rule
</button>
<p class="help is-size-7 has-text-grey">Leave pattern rows empty to allow all channels for allowed services.</p>
</div>
<button class="button is-link is-small" type="submit" data-scope-save>Save Scope</button>
</form>
</details>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<div class="box">
<h2 class="title is-6">OMEMO Enablement Plan</h2>
<p class="is-size-7 has-text-grey mb-3">Complete each step to achieve end-to-end encrypted messaging with the gateway.</p>
<table class="table is-fullwidth is-size-7">
<tbody>
{% for step in omemo_plan %}
<tr>
<td style="width:2.5rem">
{% if step.done %}
<span class="tag is-success"></span>
{% else %}
<span class="tag is-warning"></span>
{% endif %}
</td>
<td><strong>{{ step.label }}</strong></td>
<td class="has-text-grey">{{ step.hint }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</section>
<template id="channel-rule-template">
<div class="channel-rule-row is-flex is-align-items-center mb-2" style="gap: 0.5rem;">
<div class="select is-small">
<select class="scope-editable" data-lock-state="free" name="allowed_channel_service">
<option value="*">any</option>
{% for service in policy_services %}
<option value="{{ service }}">{{ service }}</option>
{% endfor %}
</select>
</div>
<input class="input is-small scope-editable" data-lock-state="free" name="allowed_channel_pattern" value="" placeholder="m@zm.is* or 1203*">
<button class="button is-small is-light is-danger channel-rule-remove scope-editable" data-lock-state="free" type="button">Remove</button>
</div>
</template>
<script>
(function () {
function wireRemoveButtons(scope) {
scope.querySelectorAll(".channel-rule-remove").forEach(function (btn) {
btn.onclick = function () {
const row = btn.closest(".channel-rule-row");
if (row) {
row.remove();
}
};
});
}
function refreshScopeCapsule(form) {
if (!form) {
return;
}
const capsule = form.querySelector("[data-capsule]");
if (!capsule) {
return;
}
const enabledInput = form.querySelector('input[name="policy_enabled"]');
let enabled = !!(enabledInput && enabledInput.checked);
if (!enabledInput) {
const globalModeInput = form.querySelector('input[name="global_scope_enabled"]');
if (globalModeInput) {
enabled = String(globalModeInput.value || "").toLowerCase() !== "off";
} else {
enabled = true;
}
}
const serviceChecks = Array.from(
form.querySelectorAll('input[name="allowed_services"]')
);
const selected = serviceChecks
.filter(function (el) { return el.checked; })
.map(function (el) { return String(el.value || "").trim().toLowerCase(); })
.filter(Boolean);
const hasServiceRestriction = selected.length > 0;
capsule.querySelectorAll("[data-service-pill]").forEach(function (pill) {
const service = String(pill.getAttribute("data-service-pill") || "").trim().toLowerCase();
const serviceAllowed = !hasServiceRestriction || selected.includes(service);
const allowed = enabled && serviceAllowed;
pill.classList.remove("is-allowed", "is-blocked");
pill.classList.add(allowed ? "is-allowed" : "is-blocked");
pill.textContent = (service || "service") + (allowed ? " allowed" : " blocked");
});
}
wireRemoveButtons(document);
const template = document.getElementById("channel-rule-template");
document.querySelectorAll(".channel-rule-add").forEach(function (btn) {
btn.addEventListener("click", function () {
const targetId = btn.getAttribute("data-target");
const container = targetId ? document.getElementById(targetId) : null;
if (!container || !template) {
return;
}
const fragment = template.content.cloneNode(true);
container.appendChild(fragment);
wireRemoveButtons(container);
const scopeForm = btn.closest("form[data-scope-form]");
if (scopeForm) {
applyScopeEditState(scopeForm);
}
});
});
document.querySelectorAll("form").forEach(function (form) {
form.querySelectorAll('input[name="policy_enabled"], input[name="allowed_services"]').forEach(function (input) {
input.addEventListener("change", function () {
refreshScopeCapsule(form);
});
});
refreshScopeCapsule(form);
});
function applyScopeEditState(form) {
if (!form || !form.matches("[data-scope-form]")) {
return;
}
const editing = String(form.getAttribute("data-editing") || "0") === "1";
form.querySelectorAll(".scope-editable").forEach(function (input) {
const locked = String(input.getAttribute("data-lock-state") || "").toLowerCase() === "locked";
input.disabled = locked || !editing;
});
const toggle = form.querySelector("[data-scope-edit-toggle]");
if (toggle) {
toggle.textContent = editing ? "Cancel Change" : "Change";
}
const changeMode = form.querySelector("[data-scope-change-mode]");
if (changeMode) {
changeMode.value = editing ? "1" : "0";
}
const saveButton = form.querySelector("[data-scope-save]");
if (saveButton) {
saveButton.disabled = !editing;
}
}
document.querySelectorAll("form[data-scope-form]").forEach(function (form) {
form.setAttribute("data-editing", "0");
applyScopeEditState(form);
const toggle = form.querySelector("[data-scope-edit-toggle]");
if (toggle) {
toggle.addEventListener("click", function () {
const editing = String(form.getAttribute("data-editing") || "0") === "1";
form.setAttribute("data-editing", editing ? "0" : "1");
applyScopeEditState(form);
});
}
});
function updateGlobalModeUI(field) {
const hiddenInput = document.querySelector('input[data-global-mode-input="' + field + '"]');
const checkbox = document.querySelector('input[data-global-visual-checkbox="' + field + '"]');
const label = document.querySelector('[data-global-mode-label="' + field + '"]');
if (!hiddenInput || !checkbox || !label) {
return;
}
const mode = String(hiddenInput.value || "").toLowerCase();
checkbox.checked = mode === "on";
checkbox.indeterminate = mode === "per_scope";
label.classList.remove("is-success", "is-light", "is-info");
if (mode === "on") {
label.classList.add("is-success");
label.textContent = "Force On";
} else if (mode === "off") {
label.classList.add("is-light");
label.textContent = "Force Off";
} else {
label.classList.add("is-info");
label.textContent = "Per Scope";
}
document.querySelectorAll('[data-global-mode-set="' + field + '"]').forEach(function (btn) {
const selected = String(btn.getAttribute("data-mode") || "").toLowerCase() === mode;
btn.classList.toggle("is-link", selected);
});
}
function setGlobalMode(field, mode) {
if (!field) {
return;
}
const hiddenInput = document.querySelector('input[data-global-mode-input="' + field + '"]');
if (!hiddenInput) {
return;
}
hiddenInput.value = mode;
updateGlobalModeUI(field);
const form = hiddenInput.closest("form");
refreshScopeCapsule(form);
}
document.querySelectorAll("[data-global-change-toggle]").forEach(function (btn) {
btn.addEventListener("click", function () {
const field = String(btn.getAttribute("data-global-change-toggle") || "");
const picker = document.querySelector('[data-global-mode-picker="' + field + '"]');
if (!picker) {
return;
}
const currentlyHidden = picker.classList.contains("is-hidden");
document.querySelectorAll("[data-global-mode-picker]").forEach(function (row) {
row.classList.add("is-hidden");
});
if (currentlyHidden) {
picker.classList.remove("is-hidden");
}
});
});
document.querySelectorAll("[data-global-mode-set]").forEach(function (btn) {
btn.addEventListener("click", function () {
const field = String(btn.getAttribute("data-global-mode-set") || "");
const mode = String(btn.getAttribute("data-mode") || "");
setGlobalMode(field, mode);
const picker = document.querySelector('[data-global-mode-picker="' + field + '"]');
if (picker) {
picker.classList.add("is-hidden");
}
});
});
["scope_enabled", "require_omemo", "require_trusted_fingerprint"].forEach(function (field) {
updateGlobalModeUI(field);
});
document.querySelectorAll("[data-policy-tab-button]").forEach(function (tab) {
tab.addEventListener("click", function () {
const key = String(tab.getAttribute("data-policy-tab-button") || "");
if (!key) {
return;
}
document.querySelectorAll("[data-policy-tab-button]").forEach(function (node) {
node.classList.remove("is-active");
});
document.querySelectorAll("[data-policy-tab-panel]").forEach(function (panel) {
panel.classList.remove("is-active");
});
tab.classList.add("is-active");
const target = document.querySelector('[data-policy-tab-panel="' + key + '"]');
if (target) {
target.classList.add("is-active");
}
});
});
document.querySelectorAll(".scope-editor-card").forEach(function (details) {
details.addEventListener("toggle", function () {
if (details.open) {
const form = details.querySelector("form[data-scope-form]");
if (form) {
refreshScopeCapsule(form);
}
}
});
});
})();
</script>
<style>
.policy-tab-panel {
display: none;
}
.policy-tab-panel.is-active {
display: block;
}
.scope-summary {
cursor: pointer;
list-style: none;
}
.scope-summary::-webkit-details-marker {
display: none;
}
.scope-allowance-capsule {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 0.4rem;
}
.scope-allowance-pill {
border-radius: 999px;
padding: 0.28rem 0.5rem;
text-align: center;
font-size: 0.7rem;
font-weight: 600;
border: 1px solid transparent;
white-space: nowrap;
}
.scope-allowance-pill.is-allowed {
background: rgba(72, 199, 142, 0.18);
color: #1f684a;
border-color: rgba(72, 199, 142, 0.28);
}
.scope-allowance-pill.is-blocked {
background: rgba(241, 70, 104, 0.16);
color: #7d1f39;
border-color: rgba(241, 70, 104, 0.24);
}
[data-theme="dark"] .scope-allowance-pill.is-allowed {
background: rgba(72, 199, 142, 0.26);
color: #c7f3df;
border-color: rgba(72, 199, 142, 0.45);
}
[data-theme="dark"] .scope-allowance-pill.is-blocked {
background: rgba(241, 70, 104, 0.24);
color: #ffd0db;
border-color: rgba(241, 70, 104, 0.42);
}
</style>
{% endblock %}