Compare commits

..

14 Commits

Author SHA1 Message Date
Disco DeDisco
3800c5bdad fixed attribution of .fa-hand-pointer cursor color scheme to ordering according to token-drop sequence instead of seat sequence; updates to accomodate this throughout apps.epic.models & .views, plus new apps.epic migration; assigned #id_sig_cursor_portal a z-index value corresponding to a high position but still beneath the #id_tray apparatus; minor semantic reordering of INSTALLED_APPS in core.settings
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-04-08 22:53:44 -04:00
Disco DeDisco
12d575a84b fixed seeding problem w. setUp helper causing same FTs to persistently fail
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-04-08 13:34:22 -04:00
Disco DeDisco
c14b6d7062 fixed some old data in two pipeline errors pointing to new Middle Arcana labels still as Minor Arcana
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-04-08 12:51:46 -04:00
Disco DeDisco
a7c5468cbc fixed failing channels FT related to Sig select; FT fix only, code written as intended
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-04-08 12:18:20 -04:00
Disco DeDisco
4da8750c60 fixed tooltip illegibility due to similar color to bg on .sig-overlay when data-polarity='gravity'
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-04-08 11:57:44 -04:00
Disco DeDisco
cf40f626e6 Sig select: _card-deck.scss extract, WS cursor fixes, own-role indicators, role icon refresh
- New _card-deck.scss: sig select styles moved out of _room.scss + _game-kit.scss
- sig-select.js: 3 WS bug fixes — thumbs-up deferred to window.load (layout settled
  before getBoundingClientRect), hover cursor cleared for all cards on reservation
  (not just the reserved card), applyHover guards against already-reserved roles
- Own-role indicators: gamer now sees their own role-coloured card outline + thumbs-up
- Reservation glow: replaced blurry role+ninUser double-shadow with crisp 2px outline
- Gravity qualifier: Graven text set to --terUser (matches Leavened/--quiUser pattern)
- Role card SVGs refreshed; starter-role-Blank removed
- FTs + Jasmine specs extended for sig select WS behaviour
- setup_sig_session management command for multi-browser manual testing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 11:52:49 -04:00
Disco DeDisco
99a826f6c9 FT: pin AppletMenuDismissTest to portrait viewport (800×1200)
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Landscape layout activates sidebar CSS which causes #id_dash_content to
overlap the base-template h2 in CI headless Firefox, triggering
ElementClickInterceptedException. Portrait viewport sidesteps all
landscape breakpoints so the h2 sits safely above #id_dash_content.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 01:26:35 -04:00
Disco DeDisco
51fe2614fa overruling other scss specificity in .btn-disabled 2026-04-07 00:43:26 -04:00
Disco DeDisco
56dc094b45 Jasmine: fix 2 failing specs, drop 5 always-pending touch specs
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- FYI btn is now btn-disabled when caution open; rename test to assert
  disabled click does NOT close caution (old toggle expectation was stale)
- Hover-resets-is-reversed: cloneNode post-init has no mouseenter listener
  (direct binding, not delegation); use mouseleave + re-enter on same card
- Remove 3 touch describe blocks (5 specs total); TouchEvent unavailable
  in desktop Firefox means they never ran; touch behaviour covered by FTs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 00:36:28 -04:00
Disco DeDisco
520fdf7862 Sig select: caution tooltip, FLIP/FYI stat block, keyword display
- TarotCard.cautions JSONField + cautions_json property; migrations
  0027–0029 seed The Schizo (number=1) with 4 rival-interaction cautions
  (Roman-numeral card refs: I. The Pervert / II. The Occultist, etc.)
- Sig-select overlay: FLIP (stat-block toggle) + FYI (caution tooltip)
  buttons; nav PRV/NXT portaled outside tooltip at bottom corners (z-70);
  caution tooltip covers stat block (inset:0, z-60, Gaussian blur);
  tooltip click dismisses; FLIP/FYI fully dead while btn-disabled;
  nav wraps circularly (4/4 → 1/4, 1/4 → 4/4)
- SCSS: btn-disabled specificity fix (!important); btn-nav-left/right
  classes; sig-caution-* layout; stat-face keyword lists
- Jasmine suite expanded: stat block + FLIP (5 specs), caution tooltip
  (16 specs) including wrap-around and disabled-button behaviour
- IT tests: TarotCardCautionsTest (5), SigSelectRenderingTest (8)
- Role-card SVG icons added to static/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 00:22:04 -04:00
Disco DeDisco
e2cc38686f XL landscape: revert tray to landscape style; fix sig-stage stretch
- Remove _tray.scss XL (≥1800px) portrait override block entirely
- _isLandscape() no longer returns false at ≥1800px — tray uses
  landscape slide-from-top at all wide landscape widths
- sig-stage: align-self: stretch (was center) so JS sizeSigCard()
  measures correct stage width; card size no longer collapses
- Position strip: horizontal row at top (was vertical column-reverse)
- sig-overlay/sig-stage/sig-deck-grid layout polish at 1100px/1800px

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 20:11:24 -04:00
Disco DeDisco
0bcc7567bb XL landscape polish: btn-primary sizing, tray from right, footer bg, layout fixes
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- .btn-xl removed; .btn-primary absorbs 4rem sizing (same as PICK SIGS/PICK ROLES)
- Landscape navbar .btn-primary: 3rem → 4rem to match base; XL stays 4rem (consistent)
- _button-pad.scss XL: base .btn ×1.2 (2.4rem); .btn-xl block deleted
- _tray.scss XL (≥1800px): portrait-style tray (slides from right, z-95)
- tray.js: _isLandscape() returns false at ≥1800px; portrait code paths run throughout
- Footer sidebar: background-color added so opaque footer masks tray sliding behind it
- Copyright .footer-container: bottom → top in landscape sidebar
- #id_room_menu: right: 2.5rem override in _room.scss XL block (cascade fix)
- navbar-text XL: 0.65rem × 1.2 = 0.78rem
- All landscape media queries: max-width: 1440px cutoff removed (already done prior)
- btn-xl class stripped from all 5 templates; test_navbar.py assertion updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 03:02:37 -04:00
Disco DeDisco
6654785f25 XL landscape breakpoint (≥1800px): double sidebar widths + scale content
- _base.scss: new @media (orientation:landscape) and (min-width:1800px) block —
  sidebars 4rem→8rem; navbar btn 3rem→5rem; brand h1 1.2rem→2.4rem; navbar-text
  0.65rem→1.3rem; footer icons 1.75rem→3rem; nav gap 3rem→4rem; footer-container
  0.55rem→0.85rem; container margins 4rem→8rem; h2 portrait-style (2rem, centred)
- _applets.scss: gear btn right 0.5rem→2.5rem; menus right 0.5rem→2rem at ≥1800px
- _game-kit.scss: kit btn right 0.5rem→2.5rem at ≥1800px
- _room.scss: sig-overlay padding-left 4rem→8rem at ≥1800px
- _tray.scss: tray wrap left/right 4rem→8rem at ≥1800px
- room.js: sizeSigModal right inset 64px→128px at ≥1800px viewport width

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 01:41:18 -04:00
Disco DeDisco
99a69202b9 landscape layout: remove max-width cutoff; sig-select stage/grid polish
- All landscape @media queries: drop and (max-width: 1440px) — sidebar layout
  now activates for all landscape orientations regardless of viewport width
- _base.scss landscape container: add max-width:none to override the
  @media(min-width:1200px) rule and fill the full space between sidebars
- sig-select sig-deck-grid: landscape now 9×2 @ 3rem cards; 18×1 at ≥1100px
  (bumped from 992px to avoid last-card clip); card text scales with --sig-card-w
- sig-stat-block: flex:1→flex:0 0 auto with width:--sig-card-w so it matches
  preview card dimensions instead of stretching across the full stage
- room.js sizeSigModal: landscape card width clamped to [90px, 160px]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 01:30:31 -04:00
43 changed files with 3406 additions and 736 deletions

View File

@@ -0,0 +1,154 @@
"""
Data migration — Earthman deck:
1. Rename three suit codes (and card names) for Earthman cards:
WANDS → BRANDS (Wands → Brands)
CUPS → GRAILS (Cups → Grails)
SWORDS → BLADES (Swords → Blades)
CROWNS stays CROWNS.
2. Copy keywords_upright / keywords_reversed from the Fiorentine Minchiate
deck to corresponding Earthman cards:
• Major: explicit number-to-number map based on card correspondences.
• Minor/Middle: same number, suit mapped (BRANDS→WANDS, GRAILS→CUPS,
BLADES→SWORDS, CROWNS→PENTACLES). Cards with no Fiorentine counterpart
stay with empty keyword lists.
"""
from django.db import migrations
# ── 1. Suit rename map ────────────────────────────────────────────────────────
SUIT_RENAMES = {
"WANDS": "BRANDS",
"CUPS": "GRAILS",
"SWORDS": "BLADES",
}
# ── 2. Major arcana: Earthman number → Fiorentine number ─────────────────────
# Cards without a Fiorentine counterpart are omitted (keywords stay empty).
MAJOR_KEYWORD_MAP = {
0: 0, # The Schiz → The Fool
1: 1, # Pope I (President) → The Magician
2: 2, # Pope II (Tsar) → The High Priestess
3: 3, # Pope III (Chairman) → The Empress
4: 4, # Pope IV (Emperor) → The Emperor
5: 5, # Pope V (Chancellor) → The Hierophant
6: 8, # Virtue VI (Controlled Folly) → Strength
7: 11, # Virtue VII (Not-Doing) → Justice
8: 14, # Virtue VIII (Losing Self-Importance) → Temperance
# 9: Prudence — no Fiorentine equivalent
10: 10, # Wheel of Fortune → Wheel of Fortune
11: 7, # The Junkboat → The Chariot
12: 12, # The Junkman → The Hanged Man
13: 13, # Death → Death
14: 15, # The Traitor → The Devil
15: 16, # Disco Inferno → The Tower
# 16: Torre Terrestre (Purgatory) — no equivalent
# 17: Fantasia Celestia (Paradise) — no equivalent
18: 6, # Virtue XVIII (Stalking) → The Lovers
# 19: Virtue XIX (Intent / Hope) — no equivalent
# 20: Virtue XX (Dreaming / Faith)— no equivalent
# 2138: Classical Elements + Zodiac — no equivalents
39: 17, # Wanderer XXXIX (Polestar) → The Star
40: 18, # Wanderer XL (Antichthon) → The Moon
41: 19, # Wanderer XLI (Corestar) → The Sun
# 4249: Planets + The Binary — no equivalents
50: 20, # The Eagle → Judgement
51: 21, # Divine Calculus → The World
}
# ── 3. Minor suit map: Earthman (post-rename) → Fiorentine ───────────────────
MINOR_SUIT_MAP = {
"BRANDS": "WANDS",
"GRAILS": "CUPS",
"BLADES": "SWORDS",
"CROWNS": "PENTACLES",
}
def forward(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
try:
earthman = DeckVariant.objects.get(slug="earthman")
fiorentine = DeckVariant.objects.get(slug="fiorentine-minchiate")
except DeckVariant.DoesNotExist:
return # decks not seeded — nothing to do
# ── Step 1: rename Earthman suit codes + card names ───────────────────────
for old_suit, new_suit in SUIT_RENAMES.items():
old_display = old_suit.capitalize() # e.g. "Wands"
new_display = new_suit.capitalize() # e.g. "Brands"
cards = TarotCard.objects.filter(deck_variant=earthman, suit=old_suit)
for card in cards:
card.name = card.name.replace(f" of {old_display}", f" of {new_display}")
card.suit = new_suit
card.save()
# ── Step 2: copy major arcana keywords ───────────────────────────────────
fio_major = {
card.number: card
for card in TarotCard.objects.filter(deck_variant=fiorentine, arcana="MAJOR")
}
for em_num, fio_num in MAJOR_KEYWORD_MAP.items():
fio_card = fio_major.get(fio_num)
if not fio_card:
continue
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=em_num
).update(
keywords_upright=fio_card.keywords_upright,
keywords_reversed=fio_card.keywords_reversed,
)
# ── Step 3: copy minor/middle arcana keywords ─────────────────────────────
for em_suit, fio_suit in MINOR_SUIT_MAP.items():
fio_by_number = {
card.number: card
for card in TarotCard.objects.filter(deck_variant=fiorentine, suit=fio_suit)
}
for em_card in TarotCard.objects.filter(deck_variant=earthman, suit=em_suit):
fio_card = fio_by_number.get(em_card.number)
if fio_card:
em_card.keywords_upright = fio_card.keywords_upright
em_card.keywords_reversed = fio_card.keywords_reversed
em_card.save()
def reverse(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
try:
earthman = DeckVariant.objects.get(slug="earthman")
except DeckVariant.DoesNotExist:
return
# Reverse suit renames
reverse_renames = {new: old for old, new in SUIT_RENAMES.items()}
for new_suit, old_suit in reverse_renames.items():
new_display = new_suit.capitalize()
old_display = old_suit.capitalize()
cards = TarotCard.objects.filter(deck_variant=earthman, suit=new_suit)
for card in cards:
card.name = card.name.replace(f" of {new_display}", f" of {old_display}")
card.suit = old_suit
card.save()
# Clear all Earthman keywords
TarotCard.objects.filter(deck_variant=earthman).update(
keywords_upright=[],
keywords_reversed=[],
)
class Migration(migrations.Migration):
dependencies = [
("epic", "0025_earthman_middle_arcana_and_major_icons"),
]
operations = [
migrations.RunPython(forward, reverse_code=reverse),
]

View File

@@ -0,0 +1,65 @@
"""
Schema + data migration:
1. Add `cautions` JSONField (list, default=[]) to TarotCard.
2. Seed The Schizo (Earthman MAJOR #1) with 4 rival-interaction cautions.
All other cards default to [] — the UI shows a placeholder when empty.
"""
from django.db import migrations, models
SCHIZO_CAUTIONS = [
'This card will reverse into <span class="card-ref">The Pervert</span> when it'
' comes under dominion of <span class="card-ref">The Occultist</span>, which in turn'
' reverses into <span class="card-ref">Pestilence</span>.',
'This card will reverse into <span class="card-ref">The Paranoiac</span> when it'
' comes under dominion of <span class="card-ref">The Despot</span>, which in turn'
' reverses into <span class="card-ref">War</span>.',
'This card will reverse into <span class="card-ref">The Neurotic</span> when it'
' comes under dominion of <span class="card-ref">The Capitalist</span>, which in turn'
' reverses into <span class="card-ref">Famine</span>.',
'This card will reverse into <span class="card-ref">The Suicidal</span> when it'
' comes under dominion of <span class="card-ref">The Fascist</span>, which in turn'
' reverses into <span class="card-ref">Death</span>.',
]
def seed_schizo_cautions(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
try:
earthman = DeckVariant.objects.get(slug="earthman")
except DeckVariant.DoesNotExist:
return
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=1
).update(cautions=SCHIZO_CAUTIONS)
def clear_schizo_cautions(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
try:
earthman = DeckVariant.objects.get(slug="earthman")
except DeckVariant.DoesNotExist:
return
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=1
).update(cautions=[])
class Migration(migrations.Migration):
dependencies = [
("epic", "0026_earthman_suit_renames_and_keywords"),
]
operations = [
migrations.AddField(
model_name="tarotcard",
name="cautions",
field=models.JSONField(default=list),
),
migrations.RunPython(seed_schizo_cautions, reverse_code=clear_schizo_cautions),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0 on 2026-04-07 03:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0027_tarotcard_cautions'),
]
operations = [
migrations.AlterField(
model_name='tarotcard',
name='suit',
field=models.CharField(blank=True, choices=[('WANDS', 'Wands'), ('CUPS', 'Cups'), ('SWORDS', 'Swords'), ('PENTACLES', 'Pentacles'), ('CROWNS', 'Crowns'), ('BRANDS', 'Brands'), ('GRAILS', 'Grails'), ('BLADES', 'Blades')], max_length=10, null=True),
),
]

View File

@@ -0,0 +1,61 @@
"""
Data fix: clear Schizo cautions from The Nomad (number=0) if present,
and ensure they land on The Schizo (number=1).
"""
from django.db import migrations
SCHIZO_CAUTIONS = [
'This card will reverse into <span class="card-ref">I. The Pervert</span> when it'
' comes under dominion of <span class="card-ref">II. The Occultist</span>, which in turn'
' reverses into <span class="card-ref">II. Pestilence</span>.',
'This card will reverse into <span class="card-ref">I. The Paranoiac</span> when it'
' comes under dominion of <span class="card-ref">III. The Despot</span>, which in turn'
' reverses into <span class="card-ref">III. War</span>.',
'This card will reverse into <span class="card-ref">I. The Neurotic</span> when it'
' comes under dominion of <span class="card-ref">IV. The Capitalist</span>, which in turn'
' reverses into <span class="card-ref">IV. Famine</span>.',
'This card will reverse into <span class="card-ref">I. The Suicidal</span> when it'
' comes under dominion of <span class="card-ref">V. The Fascist</span>, which in turn'
' reverses into <span class="card-ref">V. Death</span>.',
]
def forward(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
try:
earthman = DeckVariant.objects.get(slug="earthman")
except DeckVariant.DoesNotExist:
return
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=0
).update(cautions=[])
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=1
).update(cautions=SCHIZO_CAUTIONS)
def reverse(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
try:
earthman = DeckVariant.objects.get(slug="earthman")
except DeckVariant.DoesNotExist:
return
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=1
).update(cautions=[])
class Migration(migrations.Migration):
dependencies = [
("epic", "0028_alter_tarotcard_suit"),
]
operations = [
migrations.RunPython(forward, reverse_code=reverse),
]

View File

@@ -0,0 +1,23 @@
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0029_fix_schizo_cautions'),
]
operations = [
migrations.AddField(
model_name='sigreservation',
name='seat',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='sig_reservation',
to='epic.tableseat',
),
),
]

View File

@@ -216,13 +216,19 @@ class TarotCard(models.Model):
CUPS = "CUPS"
SWORDS = "SWORDS"
PENTACLES = "PENTACLES" # Fiorentine 4th suit
CROWNS = "CROWNS" # Earthman 4th suit (renamed from Pentacles)
CROWNS = "CROWNS" # Earthman 4th suit
BRANDS = "BRANDS" # Earthman Wands
GRAILS = "GRAILS" # Earthman Cups
BLADES = "BLADES" # Earthman Swords
SUIT_CHOICES = [
(WANDS, "Wands"),
(CUPS, "Cups"),
(SWORDS, "Swords"),
(PENTACLES, "Pentacles"),
(CROWNS, "Crowns"),
(BRANDS, "Brands"),
(GRAILS, "Grails"),
(BLADES, "Blades"),
]
deck_variant = models.ForeignKey(
@@ -239,6 +245,7 @@ class TarotCard(models.Model):
group = models.CharField(max_length=100, blank=True) # Earthman major grouping
keywords_upright = models.JSONField(default=list)
keywords_reversed = models.JSONField(default=list)
cautions = models.JSONField(default=list)
class Meta:
ordering = ["deck_variant", "arcana", "suit", "number"]
@@ -290,8 +297,16 @@ class TarotCard(models.Model):
self.SWORDS: 'fa-gun',
self.PENTACLES: 'fa-star',
self.CROWNS: 'fa-crown',
self.BRANDS: 'fa-wand-sparkles',
self.GRAILS: 'fa-trophy',
self.BLADES: 'fa-gun',
}.get(self.suit, '')
@property
def cautions_json(self):
import json
return json.dumps(self.cautions)
def __str__(self):
return self.name
@@ -344,6 +359,10 @@ class SigReservation(models.Model):
gamer = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='sig_reservations'
)
seat = models.ForeignKey(
'TableSeat', null=True, blank=True,
on_delete=models.SET_NULL, related_name='sig_reservation',
)
card = models.ForeignKey(
'TarotCard', on_delete=models.CASCADE, related_name='sig_reservations'
)
@@ -369,8 +388,8 @@ class SigReservation(models.Model):
def sig_deck_cards(room):
"""Return 36 TarotCard objects forming the Significator deck (18 unique × 2).
PC/BC pair → WANDS + CROWNS Middle Arcana court cards (1114): 8 unique
SC/AC pair → SWORDS + CUPS Middle Arcana court cards (1114): 8 unique
PC/BC pair → BRANDS/WANDS + CROWNS Middle Arcana court cards (1114): 8 unique
SC/AC pair → BLADES/SWORDS + GRAILS/CUPS Middle Arcana court cards (1114): 8 unique
NC/EC pair → MAJOR arcana numbers 0 and 1: 2 unique
Total: 18 unique × 2 (levity + gravity piles) = 36 cards.
"""
@@ -380,13 +399,13 @@ def sig_deck_cards(room):
wands_crowns = list(TarotCard.objects.filter(
deck_variant=deck_variant,
arcana=TarotCard.MIDDLE,
suit__in=[TarotCard.WANDS, TarotCard.CROWNS],
suit__in=[TarotCard.WANDS, TarotCard.BRANDS, TarotCard.CROWNS],
number__in=[11, 12, 13, 14],
))
swords_cups = list(TarotCard.objects.filter(
deck_variant=deck_variant,
arcana=TarotCard.MIDDLE,
suit__in=[TarotCard.SWORDS, TarotCard.CUPS],
suit__in=[TarotCard.SWORDS, TarotCard.BLADES, TarotCard.CUPS, TarotCard.GRAILS],
number__in=[11, 12, 13, 14],
))
major = list(TarotCard.objects.filter(
@@ -406,13 +425,13 @@ def _sig_unique_cards(room):
wands_crowns = list(TarotCard.objects.filter(
deck_variant=deck_variant,
arcana=TarotCard.MIDDLE,
suit__in=[TarotCard.WANDS, TarotCard.CROWNS],
suit__in=[TarotCard.WANDS, TarotCard.BRANDS, TarotCard.CROWNS],
number__in=[11, 12, 13, 14],
))
swords_cups = list(TarotCard.objects.filter(
deck_variant=deck_variant,
arcana=TarotCard.MIDDLE,
suit__in=[TarotCard.SWORDS, TarotCard.CUPS],
suit__in=[TarotCard.SWORDS, TarotCard.BLADES, TarotCard.CUPS, TarotCard.GRAILS],
number__in=[11, 12, 13, 14],
))
major = list(TarotCard.objects.filter(

View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288 560">
<defs>
<style>
.cls-1 {
fill: #354a9c;
}
.cls-2 {
fill: #381507;
}
.cls-3 {
stroke-width: 2.75px;
}
.cls-3, .cls-4 {
fill: none;
stroke: #381507;
stroke-linecap: round;
stroke-linejoin: round;
}
.cls-5 {
fill: #4f66d4;
}
.cls-6 {
fill: #4258b8;
}
.cls-7 {
fill: #3d180d;
}
.cls-8 {
fill: #3a1709;
}
.cls-4 {
stroke-width: 2.2px;
}
</style>
</defs>
<path class="cls-5" d="M185.31,203.37l.12.54,1.16,5.4-2.22.49c-.08,8.17-.62,15.76-1.54,24.08-.26,2.33.18,7.31,3.04,7.58,4.78.44,9.33-1.88,14.1-2.1,1.59-.07,3.44-.05,4.69-.32l.98,9.54c.24,2.25,2.12,2.5,3.74,3.16,1.99.81,2.31,3.55,3.46,5.16l3.75,5.23c.62.86,1.45,1.66,2.51,1.36-.12,2.28.15,4.61.11,7.14l-.44,1.49c-1.47-.84-2.37,1.63-3.55,1.77l-16.08,1.97-21.66,2.39c4.99,1.47,9.61,1.45,14.55,2.03l23.5,2.78c.22,1.66.49.84,1.47.31,1.01,6.14,2.22,12.45,2.53,19.3,1.2,4.17.5,8.59-3.88,10.35l-3.91,2.35c.65.86,1.46,1.29,1.81,2.27,2.77,1.35,3.4,4.57,3.09,7.75,2.44,5.7,2.42,11.51,1.87,17.57l-.91,9.96c-.14,1.55-1.69,1.51-2.77,1.41l-17.57-1.6c-4.26-.39-8.71.46-12.16-2.84l-5.53-7.04c-.25.2-.35.47-.36.58,0-.11.11-.38.36-.58-.25.2-.35.47-.36.58-.28,3.07-.81,5.92-2.85,8.97l-21.21,1.95c-.12-1.76-.26-3.48-1.01-5.15l-4.42-9.91-3.75-25.3c-.14-.97-.63-1.8-1.07-2.16.43-1.73-.03-4.16-.58-6.18-1.9-4.98-3.59-9.83-4.83-15.35.18-.51-1.33-1.69-.39-2.35l1.62-1.16c1-.71-.63-1.33-.17-2.07.4-.66,1.75-.46,1.97-1.2.33-1.08-.35-2.18-1.64-2.16l-4.2.07-9.67-35.31c-.2-.75-.95-.91-1.34-1.05-.54-.19-1.5,0-1.53.93-.02.66-.64,1.72-1.09,2.78-1.2-1.34-4.74-2.35-5.46-.22l-12.57,37.25-7.39,1.96-1.8.57c-.58.18-.63,2.12,0,2.14,1.81.06,3.73-1.13,5.06.44-1.62.37-4.59.86-3.54,2.77.86,1.55,3.62.48,4.95.31l-4.78,15.62c-.24.78.03,1.76.57,2.34.4.43,1.33.72,2.04-.13,1.08-1.29,1.25-3.2,1.82-4.45.97-.38,2.22.37.73.97-.13,1.21-.23,2.19.18,3.14.23.53.84.92,2.01.61,2.71-6.4,4.7-12.72,6.8-19.73l18.11-2.9,5.96,21.86c.44,1.61,1.47,1.6,2.91,1.32,1.12-.22.51-1.69.51-2.99v-1.31c0-.37.34-.39,1.2-.31-1.04,1.12-.03,2.08.5,3.48.36.96,1.5,1.25,2.74,1.23l-.5,20.05c-.09,3.58-.56,6.95-.22,10.34l-21.98.14-2.92-9.49c2.68-4.69,5.77-9.41,3.04-13.61l-6.91,1.61c-1.59.37.86,2.08.68,2.8l-1.98,7.57c-.27,1.01.11,2.26-.46,3.03-1.37,1.88-6.3,1.61-9.49.15-1.3-2.98,1.24-7.91.16-13.45-.44-2.25-3.52-1.29-5.21-1.15l-8.08.67-6.35-5.57c.37-4.82-6.34-6.88-5.95-12.46.07-.96,1.36-1.31,1.96-2.42l5.08-9.34c.18-1.24-1.67-1.79-2.25-2.13l-3.45-1.97c-1-.57-1.54-1.46-1.61-2.3-.11-1.23,3.32-3.74,6.12-4.08l23.77-2.92c-4.84-1.4-9.6-.46-14.47-.77l-20.58-1.25-1.75-17.45c-.27-2.73-1.52-5.99.31-8.64,1.48-2.15,4.9-2.45,7.04-3.85,3.01-1.97-4.02-5.01-3.67-6.6,1.58-7.04-3.55-9.14-2.92-17.11l1.35-16.91c19.69,3.11,39.13,3.08,58.78.65,2.37-.29,3.87.88,5.27,2.25l.56.54-.56-.54-2.23,1.33c3.23,4.36,4.37,8.99,5.14,13.85l2.71,17.1,3.13,17.22c.82-.18.95-1.1.85-1.6-.94-4.55-.24-9.14-.6-13.84-.32-4.1-.56-7.97.06-12.11.92-6.26,1.04-12.4-.43-18.5-.18-.76-1.4-.95-1.7-.74.48-1.08.49-3.87,2.16-4.43l15.4-1.26,24.35-2.21ZM187.63,257.75c.4.93.52,1.9,1.14,2.91,2.36,3.88,4.35-8.06-7.31-13.58-7.5-3.55-15.29-1.54-21.1,4.83-13.04,14.31-17.28,39.69-4.52,53.8,5.7,6.31,14.78,8.03,22.8,5.87,13.32-3.6,20.27-20.17,13.32-16.11-.83,1.08-1.92.91-1.28-.45.23-1.19-.5-2.25-1.19-2.44-2.65-.75-3.54,8.1-13.46,10.47-5.92,1.42-11.86-1.51-14.55-6.83-4.2-8.32-4.34-17.68-1.05-26.64,3.45-9.39,10.35-17.68,18.54-13.94,4.41,2.01,4.83,8.81,5.68,8.88s1.2-.15,1.64-.32c.27-.1,1.75-2.67.35-6.47.34.07.6.1.97.03Z"/>
<path class="cls-6" d="M142.89,343.17l.29,2.88c.86.07,1.58.05,2.04.23.67.25,1.91,1.3,1.32,2.05-1.46,1.19-1.67,3.08-2.56,5.44l-38.54,2.82c-2.19.16-4,.22-5.07-1.87-.06.09-.12.09-.17.04l-31.82-30.7.34-.51,6.2,1.52c6.89,2.24,14.28,1.74,21.71,1.33,1.56-.09,1.37,2.99,1.43,3.85-.3,7.59-.1,14.62,2.15,21.47.05.41.41.65.83.61.39-.4.95-.17.83-.59l-.37-1.33.4-13.65c.01-.34.23-.76.19-.97-.05-.23.41-.49.79-.42,3.19,1.46,8.11,1.72,9.49-.15.56-.77.19-2.01.46-3.03l1.98-7.57c.19-.72-2.26-2.43-.68-2.8l6.91-1.61c2.74,4.2-.36,8.92-3.04,13.61l2.92,9.49,21.98-.14Z"/>
<path class="cls-6" d="M89.74,321.43c-1.43.12-3.07.88-4.9.94l-9.56.29c-.16,1.21.7,1.9-.37,2.41l-6.2-1.52-.57-.13-.57-.14c.06-.16.14-.32.15-.48l1.45-15.57,1.33-17.08,1.52-13.95,20.58,1.25c4.86.31,9.63-.63,14.47.77l-23.77,2.92c-2.8.34-6.23,2.85-6.12,4.08.07.84.61,1.73,1.61,2.3l3.45,1.97c.58.33,2.43.89,2.25,2.13l-5.08,9.34c-.6,1.11-1.89,1.46-1.96,2.42-.4,5.59,6.32,7.64,5.95,12.46l6.35,5.57Z"/>
<path class="cls-5" d="M185.31,203.37l.41.36,32.83,32.44c.15.15.23.32.17.52l-3.36-.08c-.18,0-.37-.17-.52.11-9.2-3.04-24.62.27-26.12-.81-2.61-1.89,1.62-13.42-2.11-26.59l-1.16-5.4-.12-.54Z"/>
<path class="cls-6" d="M219.09,263.48c-1.07.3-1.89-.5-2.51-1.36l-3.75-5.23c-1.15-1.61-1.47-4.34-3.46-5.16-1.62-.66-3.5-.91-3.74-3.16l-.98-9.54c2.56-.56,5.24-.75,8.24-.37l1.71-.29c.49-.08,1.13-1.43.74-1.77-.18,0-.37-.17-.52.11.15-.28.34-.11.52-.11l3.36.08c.06-.2-.01-.36-.17-.52.15.15.23.32.17.52,2.85,1.17,1.38,7.41,1.07,13.48l-.68,13.31Z"/>
<path class="cls-6" d="M137.02,209.09l5.09,4.92c1.03-.68.94-1.93,1.29-2.74.3-.21,1.51-.02,1.7.74,1.47,6.1,1.35,12.23.43,18.5-.61,4.14-.37,8.01-.06,12.11.37,4.7-.33,9.3.6,13.84.1.5-.03,1.42-.85,1.6l-3.13-17.22-2.71-17.1c-.77-4.87-1.91-9.49-5.14-13.85l2.23-1.33.56.54Z"/>
<path class="cls-1" d="M155.13,354.34l-6.55-5.25c-.5-.4-.58-1.25-1.2-1.09-.28.07-.6.17-.85.32.59-.75-.65-1.8-1.32-2.05-.47-.17-1.18-.15-2.04-.23l-.29-2.88c-.34-3.39.13-6.76.22-10.34l.5-20.05c.35-.26.61-.87,1.26-.95.44.36.93,1.18,1.07,2.16l3.75,25.3,4.42,9.91c.75,1.67.89,3.39,1.01,5.15Z"/>
<path class="cls-1" d="M218.76,272.12c.55,2.96-1.28,5.06-3.13,7.88-.92,1.4,1.16,2.13,1.36,3.36-.98.53-1.25,1.36-1.47-.31l-23.5-2.78c-4.93-.58-9.56-.56-14.55-2.03l21.66-2.39,16.08-1.97c1.17-.14,2.07-2.61,3.55-1.77Z"/>
<path class="cls-8" d="M144.88,311.82c-.66.08-.92.69-1.26.95-1.24.01-2.37-.28-2.74-1.23-.53-1.4-1.54-2.36-.5-3.48-.86-.08-1.19-.06-1.2.31v1.31c0,1.3.6,2.76-.52,2.99-1.43.29-2.47.29-2.91-1.32l-5.96-21.86-18.11,2.9c-2.1,7.01-4.09,13.33-6.8,19.73-1.17.31-1.78-.08-2.01-.61-.41-.95-.31-1.94-.18-3.14,1.5-.6.24-1.35-.73-.97-.56,1.24-.74,3.16-1.82,4.45-.71.85-1.64.57-2.04.13-.54-.58-.8-1.56-.57-2.34l4.78-15.62c-1.33.17-4.1,1.25-4.95-.31-1.05-1.92,1.92-2.4,3.54-2.77-1.34-1.57-3.26-.38-5.06-.44-.62-.02-.58-1.96,0-2.14l1.8-.57,7.39-1.96,12.57-37.25c.72-2.13,4.25-1.12,5.46.22.45-1.06,1.07-2.12,1.09-2.78.03-.94.98-1.12,1.53-.93.39.13,1.13.3,1.34,1.05l9.67,35.31,4.2-.07c1.29-.02,1.98,1.07,1.64,2.16-.23.73-1.57.54-1.97,1.2-.45.74,1.17,1.36.17,2.07l-1.62,1.16c-.93.67.57,1.85.39,2.35,1.24,5.52,2.93,10.37,4.83,15.35.55,2.02,1.01,4.45.58,6.18ZM122.28,263.01l-7.68,21.2,13.37-1.5-5.69-19.69Z"/>
<path class="cls-7" d="M187.63,257.75l-1.21-2.81c-.33-.78-.83-2.11-2.39-2.23l-.53-1.37c-.16-.41-.82-.21-1.64-.58-1.01-.56-1.29.55.06.58,2.16,2.07,3.86,4.01,4.73,6.38,1.4,3.8-.08,6.37-.35,6.47-.45.17-.81.39-1.64.32s-1.27-6.87-5.68-8.88c-8.19-3.73-15.09,4.55-18.54,13.94-3.29,8.97-3.16,18.32,1.05,26.64,2.69,5.33,8.63,8.25,14.55,6.83,9.92-2.37,10.81-11.22,13.46-10.47.69.19,1.42,1.25,1.19,2.44-.64,1.37.45,1.54,1.28.45,6.95-4.06,0,12.51-13.32,16.11-8.02,2.17-17.1.44-22.8-5.87-12.75-14.1-8.52-39.49,4.52-53.8,5.8-6.37,13.59-8.37,21.1-4.83,11.66,5.51,9.67,17.45,7.31,13.58-.61-1.01-.74-1.98-1.14-2.91ZM157.46,302.61l3.6,3.77c.64.67.95.15,1.82.3.52.09-.23-.61-.29-.93-.07-.41-1.13-.1-1.37-.35-1.17-1.26-1.91-3.37-3.77-2.78Z"/>
<path class="cls-1" d="M186.59,209.31c3.74,13.17-.49,24.7,2.11,26.59,1.5,1.08,16.92-2.22,26.12.81.15-.28.34-.11.52-.11.4.33-.25,1.68-.74,1.77l-1.71.29c-3-.38-5.68-.19-8.24.37-1.25.27-3.1.25-4.69.32-4.76.22-9.32,2.54-14.1,2.1-2.86-.26-3.3-5.24-3.04-7.58.92-8.32,1.46-15.9,1.54-24.08l2.22-.49Z"/>
<path class="cls-1" d="M102.87,335.37c-.38-.07-.84.19-.79.42.05.21-.18.63-.19.97l-.4,13.65.37,1.33c.12.42-.44.19-.83.59-.42.03-.78-.2-.83-.61-2.25-6.85-2.45-13.87-2.15-21.47-.06-.86.13-3.93-1.43-3.85-7.43.41-14.82.91-21.71-1.33,1.07-.51.21-1.2.37-2.41l9.56-.29c1.83-.06,3.48-.82,4.9-.94l8.08-.67c1.68-.14,4.77-1.1,5.21,1.15,1.08,5.55-1.46,10.47-.16,13.45Z"/>
<path class="cls-1" d="M187.63,257.75c-.37.07-.64.04-.97-.03-.88-2.38-2.57-4.31-4.73-6.38-1.35-.04-1.07-1.15-.06-.58.82.37,1.48.17,1.64.58l.53,1.37c1.56.12,2.06,1.45,2.39,2.23l1.21,2.81Z"/>
<polygon class="cls-5" points="122.28 263.01 127.97 282.71 114.6 284.21 122.28 263.01"/>
<path class="cls-1" d="M157.46,302.61c1.86-.59,2.59,1.52,3.77,2.78.23.25,1.3-.06,1.37.35.05.32.81,1.02.29.93-.87-.15-1.18.38-1.82-.3l-3.6-3.77Z"/>
<path class="cls-2" d="M210.46,277.3l6.02,1.81c1.38.43.78,2.5-.62,2.11,0,0-6.03-1.81-6.03-1.81-1.97-.86-4.17-1.86-6.07-2.91,1.33.16,5.03.6,6.7.81h0Z"/>
<path class="cls-4" d="M185.31,203.37l-24.35,2.21-15.4,1.26c-1.66.57-1.68,3.35-2.16,4.43-.36.81-.27,2.06-1.29,2.74l-5.09-4.92-.56-.54c-1.41-1.37-2.9-2.54-5.27-2.25-19.66,2.42-39.1,2.45-58.78-.65l-1.35,16.91c-.64,7.97,4.5,10.07,2.92,17.11-.36,1.59,6.68,4.62,3.67,6.6-2.14,1.4-5.56,1.7-7.04,3.85-1.83,2.66-.58,5.92-.31,8.64l1.75,17.45-1.52,13.95-1.33,17.08-1.45,15.57-.15.48"/>
<path class="cls-4" d="M185.32,203.34l.4.39,32.83,32.44c.15.15.23.32.17.52l-3.36-.08c-.18,0-.37-.17-.52.11-9.2-3.04-24.62.27-26.12-.81-2.61-1.89,1.62-13.42-2.11-26.59l-1.16-5.4-.11-.58Z"/>
<path class="cls-4" d="M213.54,317.63c2.77,1.35,3.4,4.57,3.09,7.75,2.44,5.7,2.42,11.51,1.87,17.57l-.91,9.96c-.14,1.55-1.69,1.51-2.77,1.41l-17.57-1.6c-4.26-.39-8.71.46-12.16-2.84l-5.53-7.04c-.25.2-.35.47-.36.58-.28,3.07-.81,5.92-2.85,8.97l-21.21,1.95-6.55-5.25c-.5-.4-.58-1.25-1.2-1.09-.28.07-.6.17-.85.32-1.46,1.19-1.67,3.08-2.56,5.44l-38.54,2.82c-2.19.16-4,.22-5.07-1.87-.62-.8-.65-1.96-.17-3.01-2.25-6.85-2.45-13.87-2.15-21.47-.06-.86.13-3.93-1.43-3.85-7.43.41-14.82.91-21.71-1.33l-6.2-1.52-.57-.13-.57-.14"/>
<polyline class="cls-3" points="100.19 354.76 68.38 324.06 67.57 323.28"/>
</svg>

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288 560">
<defs>
<style>
.cls-1 {
fill: #39170a;
}
.cls-2 {
fill: #6b1f65;
}
.cls-3 {
fill: #852f7e;
}
.cls-4 {
fill: #3d1a0d;
}
.cls-5 {
fill: none;
stroke: #381507;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2.2px;
}
.cls-6 {
fill: #9e3d96;
}
</style>
</defs>
<path class="cls-6" d="M134.56,198.73c-.18.4-.63,1.06-.86,1.01l-1.53-.32,2.73,6.11c3.16,4.21,3.45,9.15,4.36,14.26l5.43,30.42-.31-26.42.69-6.77-.39-15.14c.78.13,1.2.48,1.86.21.09,1.59-.38,3.25-.01,4.91,3.95,1.89,3.31,4.73,5.82,4.74,1.17,0,3.35.51,3.8-.95.88-2.85,3.04-3.98,3.96-8.34,2.01-.62,4.59.12,5.2,2.41l2.94,4.65.47,2.59c.11.63,3.07.5,5.91,4.7.74-2.1,2.63-2.9,4.18-3.47,2.1-.76,3.81.92,5.23,2.46l1.28.36-1.2,10.28c-.16,1.36.06,4.75,1.34,5.26l12.32-1.33c1.3-.14,2.73-.73,4.27-.46-.74,2.06,2.02,3.53,2.3,4.95.63,3.15.89,5.85,3.93,7.95,1.18.81,2.14,2.65,4.4,1.82l2.14,4.34c-1.97,3.85-2.69,7.52-1.18,11.55l-7.95,3.92-9.59,1.55-24.02,4.56,31.72.18c3.86-1.21,7.59-.06,10.11,3.3-3.28,1.87-2.97,4.67-2,7.19,3.17,8.23-.69,15.5-7.22,20.39-2.27,2.33-3.26,5.96-4.66,8.69l-14.97-1.08-1.98.67c-1.17.4-1.4,6.07-1.2,10.28l-5.97.64c.01,1.31.89,2.27,1.35,3.52l-3.55,5.37c-5.53-.62-8.28,7-13.89,10.9-1.98,1.37-3.73-.25-5.2-.67-1.79-.51-3.25-.83-4.12-2.65-1.17.63-2.28.31-3.09,0-.93-.36-1.45-1.42-1.58-2.63l-1.24-12.07-.54-21.41-3.37,26.93-2.32,11.58c-1.35-.6-2.56-.79-3.93-.64-2.87.32-5.45-.38-8.31-.41-1.55-.02-1.88-1.53-3.11-3.07l2.02-2.29c-1.1-2.21-3.48-2.22-5.28-3.2-.75-.41-1.27-1.25-2.14-1.2-2.2.13.67,6.44-2.89,6.37l-6.18-.11c-.97-.02-1.52-.97-1.46-1.38l.36-2.39c-2.16-3.31-6.64-4.97-10.34-6.05l-4.61-1.35,9.71-11.1c1.03-1.17,2.56-2.27,2.2-3.91l-10.97,8.35c-4.57-1.25-8.28-3.55-8.21-8.02-.63-.81-1.9-1.86-1.64-2.9.76-2.95,3.93-2.39,4-6.01.03-1.5-1.05-2.01-2.21-3.07-1.5-1.37-2.59-3.79-4.86-4.28-1.89-.41-6.44-4.24-7.02-6.09-1.01-3.25,2.85-5.63.92-8.2l8.46-2.25,18.21-3.46.55,22.34-4.07-1.09c-.62-.17-1.15.71-1.26,1.11-.71,2.66,6.57,4.49,6.46,4.96-.06.26-.4.99-.55.77-.26-.37-.68-.81-1.29-.89l-2.57-.33c-.63-.08-1.82,2.04-.78,2.4,13.51,4.72,30.52,4.52,40.19-6.04,4.26-4.65,7-10.96,5.44-17.11-1.88-7.43-8.07-12.69-15.99-13.65,4.61-3.97,8.07-9.52,6.06-15.48s-7.82-9.61-13.78-10.33c-13.8-1.66-26.43,7.86-23.9,11.48l1.82.27c.83.77.9,1.54,1.58,1.28l2.21-.86-.14,17.59-10.69.46-11.02-.53c-3.1-.15-5.69-2.07-8.52-1.76l-2.44-19.92c-1.31-2.93-.76-6.46,2.46-7.96,3.42-1.59,4.59-3.37,5.99-2.92.03-2.33-2.37-3.38-4.36-4.69l-.32-6.53c-5.46-9.7-.92-17.59-1.51-28.43l16.23,2.03,23.56-.02,16.48-1.23c2.97-.22,5.24-1.39,7.61.8ZM185.54,293.84c2.42-3.82,5.39-7.88,3.02-9.23s-3.38,6.71-11.7,9.93c-4.18,1.62-8.99.92-12.68-1.81-3.38-2.5-5.13-6.55-6.15-11.09-3.58-15.97,6.89-36.59,18.87-32.85,6.82,2.13,4.36,12.37,8.6,8.12.92-1.52.02-3.6.33-5.64l2.51,3.38c1.54-.91,1.02-2.87.63-4.33-1.61-6.12-7.34-9.87-13.12-11.04-6.3-1.28-11.96,1.46-16.42,6.15-6.23,6.56-9.49,15.21-11.29,23.96-2.95,14.33,3.3,32.61,19.18,34.63,11.19,1.43,22.11-4.68,26.13-15.27.19-.49.04-1.09.04-1.41,0-.73-2.81-.21-2.68-.3-1.54,1.13-1.96,6.72-5.28,6.8Z"/>
<path class="cls-3" d="M74.46,278.72c1.93,2.57-1.93,4.94-.92,8.2.57,1.85,5.13,5.69,7.02,6.09,2.27.49,3.37,2.91,4.86,4.28,1.15,1.06,2.24,1.57,2.21,3.07-.07,3.62-3.24,3.06-4,6.01-.26,1.03,1.01,2.08,1.64,2.9-.07,4.47,3.65,6.77,8.21,8.02l10.97-8.35c.36,1.64-1.18,2.73-2.2,3.91l-9.71,11.1,4.61,1.35c3.7,1.08,8.18,2.75,10.34,6.05l-.36,2.39c-.06.4.49,1.36,1.46,1.38l6.18.11c3.56.07.69-6.24,2.89-6.37.86-.05,1.39.79,2.14,1.2,1.8.99,4.18.99,5.28,3.2l-2.02,2.29c1.23,1.55,1.56,3.06,3.11,3.07,2.87.03,5.44.73,8.31.41,1.37-.15,2.57.04,3.93.64l2.32-11.58,3.37-26.93.54,21.41,1.24,12.07c.12,1.21.65,2.27,1.58,2.63.81.32,1.92.64,3.09,0,.86,1.82,2.33,2.14,4.12,2.65,1.47.42,3.22,2.04,5.2.67,5.6-3.9,8.36-11.52,13.89-10.9l3.55-5.37c-.47-1.25-1.34-2.21-1.35-3.52l5.97-.64.85,18.06-.17,3.08c.67-.03,1.46-.09,2.22.11l-1.57,4.51-.4,1.16-.74-.15-9.48-.44-13.17-.04c-5.51-.02-10.81.2-15.91-2.37-4.17,3.3-8.82,3.85-13.75,4.04l-11.57.44c-4.22.16-8-1.47-12.1-2.44l-9.23-2.18-2.37-2.53-1-1.14c-.19-.22-.42.16-1.49,0l-2.88,3.29c-1.64,3.38-4.27,3.48-7.57,2.93l-8.76-1.47c-1.06-.18-1.68-1.56-2.63-2.24l-.25-10.04c-.09-3.55-2.33-6.92-2.12-10.04l.33-4.87,1.13-9.81c.44-3.77.39-7.85,0-11.53l-.45-4.37c-.88-2.66-.01-5.19,1.15-7.52l3.1-6.18c.48,0,.8.42,1.38.27Z"/>
<path class="cls-3" d="M219.05,227.87l.79.78-.21.25.6.14-.13.88-1.61,30.29-6.75,7.18,6.67,3.15,1.66,35.78c.09,1.99,0,4.05-1.49,5-.19.12-.4.28-.59.29l.1.11-.1-.11-3.17.11c-.29-.45-.37-1.65-1.08-1.62l-5.37.21c-2.72.1-5.69.2-8.32.01,1.39-2.73,2.38-6.36,4.66-8.69,6.53-4.89,10.39-12.17,7.22-20.39-.97-2.52-1.28-5.32,2-7.19-2.53-3.36-6.25-4.51-10.11-3.3l-31.72-.18,24.02-4.56,9.59-1.55,7.95-3.92c-1.51-4.02-.8-7.7,1.18-11.55l-2.14-4.34c-2.26.84-3.21-1-4.4-1.82-3.04-2.1-3.3-4.8-3.93-7.95-.28-1.42-3.04-2.89-2.3-4.95l3.85-.57,8.82.23c.74-.32,1.1.42,1.09-.02-.02-.58.56-1.21.18-1.42l2.43.46.62-.74Z"/>
<path class="cls-3" d="M188.5,197.92c-.14-.14-.26-.35-.42-.36.16.02.28.22.42.36l-.8,2.07.09,1.09-1.96.21c.42,5.17.06,10.03-.5,14.87l-1.28-.36c-1.42-1.53-3.13-3.22-5.23-2.46-1.55.57-3.44,1.37-4.18,3.47-2.84-4.2-5.79-4.07-5.91-4.7l-.47-2.59-2.94-4.65c-.61-2.29-3.19-3.03-5.2-2.41-.92,4.36-3.08,5.48-3.96,8.34-.45,1.46-2.64.96-3.8.95-2.51,0-1.87-2.85-5.82-4.74-.37-1.66.1-3.32.01-4.91-.66.27-1.08-.08-1.86-.21l.39,15.14-.69,6.77.31,26.42-5.43-30.42c-.91-5.11-1.2-10.05-4.36-14.26l-2.73-6.11,1.53.32c.22.05.68-.6.86-1.01,2.07,1.91,4.3,4.1,6.23,6.93l3.69-7.11,37.01-2.68c2.54-1.25,5.05-.41,6.58,1.69.16.02.28.22.42.36Z"/>
<path class="cls-6" d="M217.99,311.6l-.39.42-33.59,33.88-.77.03,1.57-4.51c2.75-7.94,3.11-16.57,1.63-25.15-.21-1.2,1.02-2.43,2.16-2.43l11.01.06c5.33.03,10.37-.9,15.23-2.19l3.17-.11Z"/>
<path class="cls-3" d="M219.05,227.87l-.62.74-2.43-.46c-8.06-2.53-16.09-3.31-24.73-2.08-1.96.28-2.28-4.19-1.56-5.38.71-6.92-.38-13.29-1.92-19.6l-.09-1.09.8-2.07c-.14-.14-.26-.35-.42-.36.16.02.28.22.42.36l30.55,29.94Z"/>
<path class="cls-2" d="M101.08,269.43l.04,3.58-18.21,3.46-8.46,2.25c-.58.15-.9-.27-1.38-.27l2.44-3.67c-.82-2.73-4.33-4.52-4.66-7.18,2.83-.32,5.42,1.61,8.52,1.76l11.02.53,10.69-.46Z"/>
<path class="cls-1" d="M101.08,269.43l.14-17.59-2.21.86c-.67.26-.75-.52-1.58-1.28l-1.82-.27c-2.52-3.62,10.1-13.15,23.9-11.48,5.96.72,11.76,4.34,13.78,10.33s-1.45,11.51-6.06,15.48c7.92.96,14.11,6.22,15.99,13.65,1.56,6.15-1.18,12.46-5.44,17.11-9.67,10.56-26.68,10.76-40.19,6.04-1.04-.36.15-2.48.78-2.4l2.57.33c.61.08,1.03.52,1.29.89.15.22.49-.51.55-.77.11-.47-7.17-2.29-6.46-4.96.11-.41.64-1.28,1.26-1.11l4.07,1.09-.55-22.34-.04-3.58ZM124.8,255.68c1.58-3.1.42-5.63-2.19-7.08-3.93-2.19-8.3-2.63-13.25-1.34v16.49c0,.62.35,1.06.86,1.37.26.15.45-.22,1.12-.47,5.49-1.09,10.87-3.89,13.45-8.97ZM106.69,248.96l-1.41-.03.17,8.81c.23-.03.44-.47.56-.3l.68-8.48ZM129.95,257.42c.65-1.58.8-4.23,1.16-5.86-1.76-.16-.51-1.16-.52-1.51,0-.22-.65-.01-1.42-.09,1.09,3.53.8,7.12-1.99,10.11,1.03.81,1.77-.21,1.99-.75l.77-1.9ZM133.96,284.81c.89-6.65-3.22-11.63-9.46-12.87-5.07-1.01-9.78.28-14.97,1.72l.7,23.53c5.81,1,11.07-.12,16.57-2.3,3.82-1.51,6.61-6.02,7.16-10.08ZM105.85,278.54c-.96,2.12-1.03,4.41.28,6.27.22-1.99.88-3.84-.28-6.27ZM138.89,279.18h-1.19s.2,6.6.2,6.6c.2-.03.41-.45.51-.3.97-1.95.1-4,.47-6.3Z"/>
<path class="cls-4" d="M185.54,293.84c3.32-.08,3.73-5.67,5.28-6.8-.13.1,2.68-.43,2.68.3,0,.32.14.92-.04,1.41-4.02,10.58-14.94,16.69-26.13,15.27-15.88-2.03-22.13-20.3-19.18-34.63,1.8-8.74,5.06-17.4,11.29-23.96,4.45-4.69,10.12-7.43,16.42-6.15,5.78,1.17,11.5,4.93,13.12,11.04.38,1.46.9,3.42-.63,4.33l-2.51-3.38c-.3,2.04.6,4.12-.33,5.64-4.24,4.25-1.78-5.98-8.6-8.12-11.98-3.75-22.45,16.87-18.87,32.85,1.02,4.54,2.77,8.58,6.15,11.09,3.68,2.73,8.49,3.43,12.68,1.81,8.32-3.22,9.32-11.28,11.7-9.93s-.59,5.41-3.02,9.23Z"/>
<path class="cls-2" d="M187.78,201.08c1.54,6.31,2.64,12.68,1.92,19.6-.72,1.2-.4,5.66,1.56,5.38,8.64-1.23,16.67-.45,24.73,2.08.38.2-.2.83-.18,1.42.02.44-.35-.31-1.09.02l-8.82-.23-3.85.57c-1.54-.27-2.98.32-4.27.46l-12.32,1.33c-1.29-.51-1.5-3.9-1.34-5.26l1.2-10.28c.57-4.84.93-9.7.5-14.87l1.96-.21Z"/>
<path class="cls-2" d="M200.05,310.31c2.64.19,5.6.09,8.32-.01l5.37-.21c.71-.03.79,1.17,1.08,1.62-4.86,1.29-9.9,2.22-15.23,2.19l-11.01-.06c-1.13,0-2.36,1.22-2.16,2.43,1.48,8.57,1.13,17.21-1.63,25.15-.76-.2-1.55-.15-2.22-.11l.17-3.08-.85-18.06c-.2-4.21.03-9.88,1.2-10.28l1.98-.67,14.97,1.08Z"/>
<path class="cls-6" d="M133.96,284.81c-.55,4.07-3.34,8.57-7.16,10.08-5.5,2.17-10.76,3.29-16.57,2.3l-.7-23.53c5.2-1.44,9.9-2.72,14.97-1.72,6.24,1.24,10.35,6.22,9.46,12.87Z"/>
<path class="cls-6" d="M124.8,255.68c-2.58,5.08-7.97,7.88-13.45,8.97-.67.25-.86.62-1.12.47-.51-.3-.87-.74-.87-1.37v-16.49c4.96-1.3,9.32-.85,13.25,1.34,2.61,1.45,3.76,3.98,2.19,7.08Z"/>
<path class="cls-2" d="M129.95,257.42l-.77,1.9c-.22.54-.96,1.56-1.99.75,2.79-3,3.08-6.58,1.99-10.11.77.08,1.42-.13,1.42.09,0,.36-1.24,1.36.52,1.51-.36,1.63-.51,4.27-1.16,5.86Z"/>
<path class="cls-6" d="M106.69,248.96l-.68,8.48c-.13-.17-.33.27-.56.3l-.17-8.81,1.41.03Z"/>
<path class="cls-2" d="M138.89,279.18c-.37,2.3.5,4.35-.47,6.3-.1-.15-.32.27-.51.3l-.2-6.6h1.19Z"/>
<path class="cls-2" d="M105.85,278.54c1.16,2.43.51,4.28.28,6.27-1.31-1.86-1.24-4.15-.28-6.27Z"/>
<path class="cls-5" d="M220.24,229.03l-.6-.14-1.21-.28-2.43-.46c-8.06-2.53-16.09-3.31-24.73-2.08-1.96.28-2.28-4.19-1.56-5.38.71-6.92-.38-13.29-1.92-19.6l-.09-1.09"/>
<path class="cls-5" d="M76.87,236.81c.03-2.33-2.37-3.38-4.36-4.69l-.32-6.53c-5.46-9.7-.92-17.59-1.51-28.43l16.23,2.03,23.56-.02,16.48-1.23c2.97-.22,5.24-1.39,7.61.8,2.07,1.91,4.3,4.1,6.23,6.93l3.69-7.11,37.01-2.68c2.54-1.25,5.05-.41,6.58,1.69.16.02.28.22.42.36l30.55,29.94.79.78.4.39"/>
<path class="cls-5" d="M220.24,229.03l-.13.88-1.61,30.29-6.75,7.18,6.67,3.15,1.66,35.78c.09,1.99,0,4.05-1.49,5l-.49.39-.5.31-33.59,33.88-1.17,1.19"/>
<path class="cls-5" d="M182.84,347.08l-.74-.15-9.48-.44-13.17-.04c-5.51-.02-10.81.2-15.91-2.37-4.17,3.3-8.82,3.85-13.75,4.04l-11.57.44c-4.22.16-8-1.47-12.1-2.44l-9.23-2.18-2.37-2.53"/>
<path class="cls-5" d="M182.84,347.08l.4-1.16,1.57-4.51c2.75-7.94,3.11-16.57,1.63-25.15-.21-1.2,1.02-2.43,2.16-2.43l11.01.06c5.33.03,10.37-.9,15.23-2.19l3.17-.11"/>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288 560">
<defs>
<style>
.cls-1 {
fill: #006d30;
}
.cls-2 {
fill: #00873e;
}
.cls-3 {
fill: none;
stroke: #381507;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2.75px;
}
.cls-4 {
fill: #3a160a;
}
.cls-5 {
fill: #3d180d;
}
.cls-6 {
fill: #00a04b;
}
</style>
</defs>
<path class="cls-6" d="M153.66,195.96l-.46-.48-2.25.71,3.07,18.95.16,18.83c.02,1.82-1.11,3.58.04,5.52.86-1.54,1.21-3.1,1.51-4.81l4.63-26.09c.51-2.89.63-6.32-1.68-7.91,3.44-.33,1.92-4.68,4.63-6.02,1.06-.53,2.58-.34,3.85-.57l.57-.11.09,1.97c-1.23.02-2.43-.35-3.03.56,1.72-.21,4.69,1.64,3.78,3.07-1.78,2.8-2.55,5.21-1.44,8.53.2.59-.73,1.95-.05,2.92.42.6,1.37.66,2.45,1.35l.89,1.5c.22.37,1.97.27,1.95-.45-.06-2.39-4.42-4.98-2.14-5.98.89-.39,1.59-.33,2.08.47l3.78,6.21,3.2-1.66,6.99.65c-.45,5.85-2.86,9.72.48,15.56l8.02-1.38c4.46-.77,3.87,5.08,6.83,6.44-.37,2.19.28,4.05,1.64,6.45-6.86,3.73-8.55.88-9.78,1.54-2.84,1.52-2.36,6.78,0,8.07,2.28-5.08,5.7-2.99,8.83-5.88l3.1,3.95c.13.5-.52.62-.77.88-.49.54-1.76-.53-2.23-.28-1.18.63-.37,1.39-.08,1.77.39.5,2.4.95,4.35,1.17.61,1.74-1.58,2.73-2.4,3.52.4,2.18,3.63,1.16,5.22,1.12,1.87-.05,3.1,2.33,5.08,1.26l-.3,8.25c-7.48,2.34-14.03,3.49-21.14,4.16l-23.27,2.2c.09.33.06.66.03.83.7.61,1.85-.06,2.86-.16l30.5,2.09c2.57.18,5.12.05,7.54,1.41-6.47,3.24-11.29,0-12.25.87-.59.54-1.48,2.27-.13,2.72.91.3,1.87.35,2.3.69,1.2.95-1.2,5.83,1.12,6.74.78.3,2.4-.64,2.93.56.31.71.23,2.04-.94,2.69l-1.39,1.83c-1.39.83-1.28,1.3-1.07,2.69.24,1.57,1.51,1.36,3.53.89v3.28s4.12.29,4.12.29l.1,2.95c-4.9.84-9.59,2.46-12.77,6.65l-8.81-1.19c-7.44-1-2.19,11.29-2.46,19.21l-4.89-.16c-2.63-.09-6.35,0-7.58-2.4,1.86-2.75-1.3-5.62-.12-9.66-1.36-.25-2.17-1.48-3.52-1.43-2.58,1.83-5.92,2.68-6.55,5.79-.14.7,1.35,1.6.78,2.39-.88,1.22-5.4.29-5.27,3.28.14,3.32,3.47,1.65,4.91,3.68-1.55,1.93-3.69,4.09-5.51,5.41-.67.04-2.1-.08-2.75-.12l-7.79-.48-1.91-10.68-.87-20.54c-.02-.38.98-.89.38-1.07-.21-.06-.37-.3-.58-.52-.15-.16-.64.62-.67.95l-1.71,16.74c-.72,7.06-2.02,13.71-4.34,20.67-4.06.6-8.21-.87-12.16-2.67-.9-.41-2.41-.75-2.22-2.2.46-.15,1.4-.34,1.49-.82.06-.36.32-1.32-.43-1.33l-9.76-.09c-.43,0-.7-1.15-.36-1.47.61-.58,1.76-.61,2.03-1.46.31-.96.25-1.75.13-3.16l-14.56.05c-3.39.01-6.59.16-10.04-.64l12.59-14.11c-.78-.51-1.89.32-2.63,1.01-4.11,3.83-8.08,7.52-13.71,8.92.38-1.11,1.48-1.61,2.04-2.49l-1.39-4.29c-2.64.06-2.8-2.85-3.9-3.74-2.66-2.14-5.75-3.87-5.86-7.47-.13-4.69,5.58-4.68,4.87-7.46-1.89-.09-2.79-.22-3.53-1.67-2.76-5.4-5.55-10.74-6.13-16.95l.26-3.84c1.78-1.26,3.89-1.22,6.04-1.61l19.34-3.5c-7.89-1.44-15.42-1.63-22.88-4.1l-3.59-.27c-1.87-2.75-1.64-5.92-2.07-9l-1.2-8.62-.43-6.7c-.53-8.33-.99-6.87,3.53-10.65l3.35-2.87.41.38-.41-.38-6.25-5.77,1.99-24.45,29.01,2.92c.3,2.58-.26,4.92.22,7,.52-.09,1.76-.17,2.02.58.15.44.4.77.33,1.31l3.48,3.71c1.73,1.85,4.57,3.61,5.86,5.97,1.68,3.06,3.62,8.54,4.74,8.09.28-.11.94-.29,1.08-1.02.16-.85-.78-2.14-.85-3.37l-.49-9.68c-1.06-2.47-3.15-4.98-2.29-7.94l7.42-2.99.74-.79c.25-.26.12-.53-.1-.59-.25-.07-.86-.46-1.49.09l-.35-1.99,28.3-2.23c1.58-.12,3.1-.91,3.9,1l.46.48ZM186.99,284.59c.05-.8-.21-2.93-.89-3.46-1.03-.8-2.09-.08-2.67.96-2.92,5.19-8.02,9.43-14.12,9.6-10.92.3-16.23-12.43-14.77-24.11,1.5-12,9.42-26.8,19.63-23.04,6.4,2.36,4.16,10.81,8.14,8.64,1.03-.56,1.08-3.75.59-6.65,1.56,1.07,1.13,2.55,1.92,3.73.35.51,1.07.12,1.41-.26,1.54-1.69-1.42-11.67-11.3-14.47-18.44-5.21-31.37,21.26-30.31,39.87.46,8.02,4.55,18.71,12.73,22.74,12.45,6.13,27.46.08,33.05-12.33.29-.63.45-1.54.17-1.99s-.96-1.1-1.65-.81c-.59.25-1.21.83-1.92,1.58ZM101.07,245.27c-.97-.28-1.36,2.51-.27,3.04.84.41,1.82.3,3.43.02l-.02,15.84.52,28.42c.02,1.26-3.05.8-3.99,2.17-.37.54-.43,1.53.14,1.9.48.31,1.1.34,2.1.38l1.74.07c.21,0-.06,1.21-.78.9-1.53-.66-2.37,1.04-2.06,2.1.54,1.85,3.63,1.46,6.98.54.54.72.85,2.18,1.64,2.22.74.04,1.45-.09,1.73-.57.43-.71.32-1.49.26-2.52l13.46-1.99,15.07-1.37c.5-.05,1.21-.93,1.18-1.32-.02-.33-.33-.94-.83-.93l-6.64.11v-.92s7.21-.58,7.21-.58c.67-.05,1.07-1,1.09-1.41s-.74-1.12-1.09-1.32l-2.18-1.26c-7.01-.15-13.57.87-20.29,1.77l-6.33.85-.28-18.72c1.4-.01,2.1-.11,2.8-.21l14.99-2.18c1.84-.27,2.77-2.22,2.67-4.08-1.51-.62-2.4-.02-3.68-.25.35-1.33,1.22-1.8.93-2.28-.46-.75-.97-1.35-1.81-1.22l-16.69,2.55v-17.6c8.76-1.96,17.15-2.99,25.82-3.25.13,0,3.38-2.78,2.38-4.14-.56-.77-2.05-.63-3.18-.4-.1-1.28.99-1.47.68-1.91-.6-.86-1.04-1.38-1.97-1.52-8.39.05-16.34,1.7-24.78,2.69-.47-.45-.8-1.64-1.29-1.61-.68.04-.97.86-1.59,1.72-.59-.64-1.13-1.26-1.75-1.19-.85.11-1.69,0-1.57,1.15.13,1.23-2,1.2-2.96,1.52l-2.84.93c-1.36.44-1.02,1.83-.51,2.86l5.73-.22-1.66.49c-.18.57-.19,1.1-1.51.72Z"/>
<path class="cls-2" d="M189.09,192.96c.05.24.06.17.06-.01l.12.59,1.48,13.12.05,15.42c9.75-1.12,18.49.18,27.17,2.92l.66-.85,1.15,1.22.02.65-.12,6.48c-.06,3.28-.1,6.15-.34,9.4l-1.49,20.28c-.14,1.92.53,4.27-.65,6.12l-5.41,3.97,6.02,6.36,1.49,21.56c.37,5.41,2.59,8.94-3.33,12.4.34.27.29,1.3.03,1.53s-.16.34-.97.66c-.37,1.66-2.22,1.95-3.2,2.95l-20.55,20.92c-1.09,1.11-1.62,2.3-2.99,2.91-1.44.64-2.89,1.38-4.34,2.88l-11.83-.23-21.18-.57c-2.81-.08-5.69-.28-7.16-3.49l-2.05-.16c-3.58,7.25-10.93,5.75-18.03,6.21-3.56.23-7.24,1.2-10.75.28l-13.96-3.64c-3.32-.87-4.74-4.5-6.28-4.44-1.22.05-1.49,1.13-2.18,1.92l-3.06,3.53c-1.81,2.08-4.75,1.11-7.01.74l-7.91-1.31c-.88-.14-1.36-1.64-2.36-2.17.88-6.23-.67-11.82-2.56-17.57-.32-.99.84-5.27,1.01-8.28l.29-4.88.53-6.71.52-27.67,1.82-3.79c.68.02,1.42.1,2.03.75l-.26,3.84c.58,6.2,3.37,11.55,6.13,16.95.74,1.45,1.64,1.58,3.53,1.67.71,2.78-5.01,2.77-4.87,7.46.1,3.59,3.19,5.33,5.86,7.47,1.1.88,1.26,3.8,3.9,3.74l1.39,4.29c-.55.88-1.66,1.38-2.04,2.49,5.64-1.4,9.6-5.08,13.71-8.92.74-.69,1.84-1.52,2.63-1.01l-12.59,14.11c3.44.8,6.65.65,10.04.64l14.56-.05c.11,1.41.17,2.2-.13,3.16-.27.85-1.42.88-2.03,1.46-.33.32-.07,1.46.36,1.47l9.76.09c.76,0,.5.97.43,1.33-.08.47-1.03.66-1.49.82-.2,1.44,1.31,1.79,2.22,2.2,3.95,1.79,8.1,3.27,12.16,2.67,2.32-6.96,3.62-13.61,4.34-20.67l1.71-16.74c.03-.33.52-1.11.67-.95.21.23.36.46.58.52.6.17-.4.69-.38,1.07l.87,20.54,1.91,10.68,7.79.48c.64.04,2.08.16,2.75.12,1.82-1.32,3.96-3.47,5.51-5.41-1.45-2.03-4.77-.36-4.91-3.68-.13-2.98,4.38-2.06,5.27-3.28.57-.79-.92-1.69-.78-2.39.63-3.11,3.98-3.96,6.55-5.79,1.36-.05,2.16,1.19,3.52,1.43-1.18,4.04,1.98,6.9.12,9.66,1.23,2.39,4.95,2.31,7.58,2.4l4.89.16c.28-7.91-4.98-20.21,2.46-19.21l8.81,1.19c3.18-4.19,7.87-5.81,12.77-6.65l-.1-2.95-4.12-.29v-3.28c-2.02.46-3.28.68-3.52-.89-.21-1.38-.32-1.86,1.07-2.69l1.39-1.83c1.17-.65,1.25-1.97.94-2.69-.53-1.2-2.15-.26-2.93-.56-2.32-.9.09-5.78-1.12-6.74-.43-.34-1.39-.39-2.3-.69-1.35-.45-.46-2.18.13-2.72.95-.87,5.78,2.37,12.25-.87-2.42-1.36-4.97-1.23-7.54-1.41l-30.5-2.09c-1,.11-2.15.77-2.86.16.03-.17.07-.51-.03-.83l23.27-2.2c7.11-.67,13.66-1.83,21.14-4.16l.3-8.25c-1.98,1.07-3.21-1.31-5.08-1.26-1.6.04-4.83,1.06-5.22-1.12.82-.79,3.01-1.78,2.4-3.52-1.94-.22-3.96-.67-4.35-1.17-.3-.39-1.1-1.14.08-1.77.47-.25,1.75.82,2.23.28.24-.27.9-.39.77-.88l-3.1-3.95c-3.13,2.9-6.55.8-8.83,5.88-2.36-1.3-2.84-6.55,0-8.07,1.24-.66,2.93,2.18,9.78-1.54-1.36-2.4-2.01-4.25-1.64-6.45-2.96-1.36-2.37-7.21-6.83-6.44l-8.02,1.38c-3.34-5.83-.94-9.71-.48-15.56l-6.99-.65-3.2,1.66-3.78-6.21c-.48-.79-1.18-.86-2.08-.47-2.29,1,2.08,3.59,2.14,5.98.02.72-1.73.82-1.95.45l-.89-1.5c-1.07-.69-2.02-.75-2.45-1.35-.68-.97.25-2.32.05-2.92-1.11-3.33-.34-5.73,1.44-8.53.91-1.43-2.05-3.28-3.78-3.07.6-.91,1.8-.54,3.03-.56l-.09-1.97-.57.11.57-.11,13.46-1.04c2.81-.22,5.44-.4,7.96,0,0,.25,0,.18-.06.01Z"/>
<path class="cls-6" d="M189.09,192.96c.13-.05.29.17.44.4l29.1,30.8-.66.85c-8.68-2.74-17.43-4.04-27.17-2.92l-.05-15.42-1.48-13.12-.12-.59c0,.18-.01.25-.06.01Z"/>
<path class="cls-2" d="M121,196.71l.35,1.99c.63-.55,1.25-.16,1.49-.09.22.06.34.33.1.59l-.74.79-7.42,2.99c-.86,2.97,1.23,5.48,2.29,7.94l.49,9.68c.06,1.23,1,2.52.85,3.37-.14.73-.8.91-1.08,1.02-1.11.45-3.05-5.03-4.74-8.09-1.29-2.35-4.13-4.12-5.86-5.97l-3.48-3.71c.07-.54-.17-.88-.33-1.31-.26-.75-1.5-.67-2.02-.58-.48-2.08.07-4.42-.22-7,3.74.38,6.19,6.49,8.76,5.54,2.12-.78.47-6.55,7.47-6.94l4.09-.23Z"/>
<path class="cls-1" d="M153.66,195.96c1.59,1.69,3.16,3.44,5.02,4.72,2.32,1.59,2.2,5.02,1.68,7.91l-4.63,26.09c-.3,1.71-.65,3.27-1.51,4.81-1.16-1.94-.03-3.7-.04-5.52l-.16-18.83-3.07-18.95,2.25-.71.46.48Z"/>
<path class="cls-1" d="M73.83,272.96c-.61-.65-1.35-.72-2.03-.75l3.58-4.84-2.64-3.89,3.59.27c7.46,2.46,14.99,2.66,22.88,4.1l-19.34,3.5c-2.15.39-4.26.34-6.04,1.61Z"/>
<path class="cls-4" d="M101.07,245.27c1.32.37,1.33-.16,1.51-.72l1.66-.49-5.73.22c-.51-1.03-.85-2.41.51-2.86l2.84-.93c.97-.32,3.09-.29,2.96-1.52-.12-1.15.72-1.05,1.57-1.15.61-.08,1.16.55,1.75,1.19.62-.86.91-1.67,1.59-1.72.49-.03.82,1.16,1.29,1.61,8.43-1,16.39-2.64,24.78-2.69.93.14,1.37.67,1.97,1.52.31.44-.79.63-.68,1.91,1.13-.23,2.61-.37,3.18.4,1,1.35-2.25,4.13-2.38,4.14-8.67.26-17.05,1.29-25.81,3.25v17.6s16.68-2.55,16.68-2.55c.84-.13,1.36.47,1.81,1.22.29.47-.58.95-.93,2.28,1.28.22,2.17-.38,3.68.25.1,1.86-.83,3.81-2.67,4.08l-14.99,2.18c-.7.1-1.4.2-2.8.21l.28,18.72,6.33-.85c6.72-.9,13.28-1.91,20.29-1.77l2.18,1.26c.35.2,1.11.91,1.09,1.32s-.42,1.36-1.09,1.41l-7.21.58v.92s6.65-.11,6.65-.11c.5,0,.81.59.83.93.02.4-.69,1.28-1.18,1.32l-15.07,1.37-13.46,1.99c.07,1.03.17,1.8-.26,2.52-.29.48-.99.61-1.73.57-.79-.04-1.1-1.5-1.64-2.22-3.36.92-6.44,1.31-6.98-.54-.31-1.06.53-2.77,2.06-2.1.72.31,1-.89.78-.9l-1.74-.07c-.99-.04-1.62-.08-2.1-.38-.57-.37-.51-1.36-.14-1.9.94-1.37,4.01-.91,3.99-2.17l-.52-28.42.02-15.84c-1.61.28-2.59.39-3.43-.02-1.08-.53-.7-3.32.27-3.04Z"/>
<path class="cls-5" d="M186.99,284.59c.71-.75,1.33-1.33,1.92-1.58.69-.29,1.36.35,1.65.81s.12,1.36-.17,1.99c-5.59,12.41-20.6,18.47-33.05,12.33-8.18-4.03-12.28-14.72-12.73-22.74-1.06-18.61,11.87-45.08,30.31-39.87,9.88,2.79,12.84,12.78,11.3,14.47-.34.38-1.07.77-1.41.26-.8-1.18-.36-2.66-1.92-3.73.49,2.89.44,6.09-.59,6.65-3.98,2.17-1.74-6.28-8.14-8.64-10.22-3.76-18.14,11.04-19.63,23.04-1.45,11.68,3.85,24.41,14.77,24.11,6.1-.17,11.2-4.41,14.12-9.6.59-1.04,1.65-1.76,2.67-.96.68.53.94,2.66.89,3.46-.24,0-.82-.26-1.15.32l-2.56,4.45.51.54c1.45-1.23,3.14-4.22,3.13-4.06l.07-1.24Z"/>
<path class="cls-1" d="M186.99,284.59l-.07,1.24c0-.16-1.68,2.83-3.13,4.06l-.51-.54,2.56-4.45c.33-.57.91-.32,1.15-.32Z"/>
<path class="cls-3" d="M219.78,225.37l-1.81-.37c-8.68-2.74-17.43-4.04-27.17-2.92l-.05-15.42-1.48-13.12-.19-.58"/>
<path class="cls-3" d="M219.78,225.37l.02.65-.12,6.48c-.06,3.28-.1,6.15-.34,9.4l-1.49,20.28c-.14,1.92.53,4.27-.65,6.12l-5.41,3.97,6.02,6.36,1.49,21.56c.37,5.41,2.59,8.94-3.33,12.4-7.99.64-14.33,1.69-23.27.55-5.4-.69-.74,10.29-4.65,22.7-.68,2.15-.83,4.01.26,5.73"/>
<path class="cls-3" d="M189.15,192.95c-2.52-.41-5.15-.22-7.96,0l-13.46,1.04-.57.11c-1.27.24-2.79.05-3.85.57-2.71,1.35-1.19,5.7-4.63,6.02-1.86-1.28-3.42-3.03-5.02-4.72l-.46-.48c-.8-1.91-2.32-1.12-3.9-1l-28.3,2.23-4.09.23c-7.01.39-5.35,6.16-7.47,6.94-2.57.94-5.02-5.17-8.76-5.54l-29.01-2.92-1.99,24.45,6.25,5.77.41.38.41.37"/>
<polyline class="cls-3" points="189.15 192.95 189.53 193.36 218.63 224.15 219.78 225.37"/>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288 560">
<defs>
<style>
.cls-1 {
fill: #39170a;
}
.cls-2 {
fill: none;
stroke: #381507;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2.75px;
}
.cls-3 {
fill: #3d180b;
}
.cls-4 {
fill: #a88a21;
}
.cls-5 {
fill: #d3ac2c;
}
.cls-6 {
fill: #ffcf34;
}
</style>
</defs>
<path class="cls-6" d="M179.77,205.02l.77,4.04c.04.21.1.45.08.59.02-.14-.04-.38-.08-.59-.85,1.19-1.1-.58-2.67-.25l.79,8.42-.89,10.4c-.41,4.82-1.24,9.97,1.41,14.45,1.05,1.78,9.63-.31,17.16-.75,1.02,1.74,4.84.68,6.63,4.83.88,2.04,2.91,3.45,3.86,5.38,1.4,2.81,2.25,5.73,4.84,7.45,1.02-.82,1.88-.07,2.66-.28l.07,6.27c.03,2.58.2,4.71-.47,6.93-.75.09-2.1-.23-2.94.31-2.87,1.83-6.08,2.33-9.57,2.78l-6.5.84-16.99.61c-3.66.13-7.28-.69-11.12,1.38l16.47,2.61,22.92,3.07,7.37.46c.9,1.94.62,4.11.83,6.29l1.64,17c.53,5.45-8.08,6.66-5.27,8.93,2.01,1.62,5.55,12.81,5.16,16.41l-2.13,19.73c-8.18,1.27-16.4,2.09-24.72,1.88l-6.37-8.74c-.46-.63-1.12-1.06-1.64-1.08-.67-.03-1.56.51-2.14,1.26l-5.23,6.74-16.53,1.49c-2.64.24-5.37.61-7.42-.64l-.56-.34c.37,0,.52-.6.99-.96.23-.18-.52-.27-.45-.87l-1.23-17.67c-.63-9.07-.23-17.95-2.92-26.95l-2.35,17.78-4.21,27.18,3.26.18c-1.16,2.56-3.95,2.42-6.56,2.51l-13.67.49-16.87,1.08c-.05-.45-.49-.46-.65-.67l-.4-.38-.4-.38-.4-.38.58-.54.2.56c2.01-.21.19-3.36.22-9.43,0-2.13-.72-4.28.05-6.44.35-.98,2.07-.69,2.87-.56.43-1.64-1.95-1.67-2.13-2.33l.95-10.42c.09-.94.1-1.71-.31-2.02-1.64-1.25-3.51.15-5.41-.22.06-.7-.29-1.6-.74-2.1-.64-.71-1.77-.71-2.91-.62l-12.92,1.07.18-1.34s-.25-1-.63-1.37l-3.78-3.63c-1.39-1.34-2.03-2.83-1.58-5.16l-3.26-3.67c-1.51-1.7-2.88-4.39-.81-6.29,1.1-.32,3.6.81,4.46-.47,2.86-4.24-1.31-5.58-.49-7.74,1.33-3.48-4.58-6-4.26-8.5l13.01-2.5c3.67-1.59,6.78-3.8,10.59-3.22l.1,32.81c0,1.01,1.11,1.93,1.68,2.01,2.03.27,1.28-3.03,2.08-6.24,2.13.46-1.02,4.51,1.57,8.01.45.61,1.46,0,1.84-.26.62-.41.57-1.41.77-2.39l1.09-.2c.09-.02.41-.44.4-.93l-.43-14.35-.45-23.53c-.01-.52-.02-1.19.34-1.48.58-.46,1.53-.32.75.63l8.86,15.58,13.62,24.51c.37.67,1.86,1.46,2.5,1.41.85-.07,1.58-1.73,1.05-2.73-.13-1.47.35-1.05,1.48-.63,1.09-.52,1.88.67,2.48.34.91-.51,1.18-1.31,1.03-2.48-1.7-13.24-2.12-26.26-1.41-39.55l.91-17.03c.05-.93-.07-1.64-.43-2.01-.49-.49-1.44-.78-1.91-.55-.75.36-1.15,1.14-1.13,2.02l.19,7.68h-.86s-.48-6.72-.48-6.72c-.08-1.09-1.4-1.77-2.14-1.88-1.31-.2-2.03,1.16-2.03,2.59l-.14,40.7c-1.18.02-.63-.49-.97-1.13-7.29-13.26-14.57-26.19-20.99-39.77-.21-.45-.86-.62-1.11-.66-.4-.05-.83.64-1.21.96l-.5-2.31c-.16-.75-1.63-1.47-2.45-.7-1.35,1.27-.5,3.3-.84,4.65-1.25-.1-.52-1.14-.93-1.47-.51-.4-1.14-.55-2.26-.76l-.37,3.69c-2.89-2.03-5.73-4.3-9.04-6.32-.72-.34-2.29.61-2.32,1.39-.03,1.15-.89,2.45.54,3.02l10.66,3.95.41,19.15-14.09-.14c-5.14-.17-9.74-2.39-14.67-4.07-.85.62-1.32,1.36-2.39.69l-1.67-18.92c-.18-2.03.78-3.84,2.65-4.71l7.42-3.44-5.93-5.13.16-5.46c-1.39-3.37-3.58-6.69-3.35-10.39l1.05-16.95c.05-.8-.56-1.46-.02-2.1s1.18.05,2.02.15l15.08,1.79,14.48.61,27.87-1.53c2.78-.15,4.85.21,6.45,2.28-.81,1.03-1.69-.01-3.26,0l4.13,7.93c4.58,11.64,3.97,31.83,8.32,46.37l-.45-23.47,1.02-30.81c-.29-.23-.77.13-1.3.05-.68-.1.4-.6.29-1.64l-.63.29.63-.29,16.21-1.53,9.43-.31c4.27-.14,8.52-1.66,12.53-.39l.66.23c-.03.16-.03.22-.03-.04ZM183.3,260.1c.11.72.67,3.38,2.19,2.35,1.76-7.21-5.95-14.22-14.14-15.14-14.41-1.63-23.52,15.84-26.38,29.41-2.46,11.72.92,27.85,11.7,33.33,3.81,1.94,8.55,3.2,12.6,2.71,17.38-2.11,23.73-18.78,19.2-17.57-1.69.45-1.96,3.32-2.63,3.58-2.37.93.71-2.54.04-4.79-.24-.81-1.17-1.31-1.71-.96-2.42,1.56-3.83,8.02-11.84,10.21-7.31,2.01-14.11-2.14-16.6-9.5-6.23-18.48,5.85-40.89,17.78-36.88,5.29,1.78,5.29,9.24,6.76,9.31,2.39.13,2.79-4.52,2.18-6.12l.85.05Z"/>
<path class="cls-5" d="M97.21,275.95c.18.35.62.79.03.98-3.81-.59-6.93,1.63-10.59,3.22l-13.01,2.5c-.32,2.5,5.59,5.02,4.26,8.5-.82,2.16,3.35,3.5.49,7.74-.86,1.28-3.37.15-4.46.47-2.07,1.9-.69,4.59.81,6.29l3.26,3.67c-.45,2.32.19,3.82,1.58,5.16l3.78,3.63c.38.37.64,1.34.63,1.37l-.18,1.34c-5.9.49-11.78-1.34-17.6-2.56,0,.16,0,.27,0,.02l-.69-.73c.06-1.46-.73-4.12-.11-5.67.88-2.2.34-3.61.48-5.56l1.71-23.37c.25-3.49-1.22-7.09-1.53-10.53,1.07.67,1.54-.07,2.39-.69,4.93,1.69,9.53,3.9,14.67,4.07l14.09.14Z"/>
<path class="cls-6" d="M83.8,320.81l12.92-1.07c1.13-.09,2.26-.09,2.91.62.45.5.8,1.4.74,2.1-.91,10.45-.01,20.93,3.56,30.87l-.58.54-36.74-35.2-.4-.41c5.82,1.22,11.7,3.05,17.6,2.56Z"/>
<path class="cls-6" d="M179.77,205.02c0,.18,0,.24.03.04l34.21,33.44-.49.54-1.92-.35c-.22-.04-.45-.09-.64-.11l-2.66-.27c-13.98-3.98-24.18.77-25.99-.65-.73-.57-.59-1.87-.82-2.93,1.42-8.61.58-16.85-.87-25.09.02-.14-.04-.38-.08-.59l-.77-4.04Z"/>
<path class="cls-5" d="M149.77,353.23l-.56-.34-3.01-2.1c-1.49-1.04-3.18-.66-3.91.8l-3.26-.18,4.21-27.18,2.35-17.78c2.69,9,2.29,17.87,2.92,26.95l1.23,17.67c-.07.59.69.68.45.87-.47.37-.62.97-.99.96l.56.34Z"/>
<path class="cls-5" d="M140.36,207.35l.63-.29c.11,1.04-.97,1.54-.29,1.64.53.08,1.01-.28,1.3-.05l-1.02,30.81.45,23.47c-4.34-14.54-3.74-34.72-8.32-46.37l-4.13-7.93c1.57-.02,2.45,1.02,3.26,0,1.15,1.48,3.05,3.31,5.03,3.99l3.08-5.27Z"/>
<path class="cls-5" d="M213.94,271.89c-1.02,3.42-5.64,5.2-4.81,6.04l3.77,3.83c.8.81.39,1.56.68,2.19l-7.37-.46-22.92-3.07-16.47-2.61c3.84-2.08,7.46-1.25,11.12-1.38l16.99-.61,6.5-.84c3.49-.45,6.7-.95,9.57-2.78.84-.53,2.19-.22,2.94-.31Z"/>
<path class="cls-5" d="M214.34,258.7c-.77.21-1.64-.54-2.66.28-2.59-1.73-3.44-4.65-4.84-7.45-.96-1.92-2.99-3.34-3.86-5.38-1.79-4.15-5.61-3.09-6.63-4.83l14.51-.85c-.03-.62-.15-1.25.1-1.89.19.02.43.07.64.11l1.92.35.49-.54.4.39c.14.14.38.23.4.39l.1.72c.48,3.66-.57,7.44-.57,11.31v7.39Z"/>
<path class="cls-1" d="M96.95,254.77l.37-3.69c1.12.21,1.75.37,2.26.76.42.33-.32,1.37.93,1.47.34-1.34-.52-3.38.84-4.65.82-.77,2.29-.05,2.45.7l.5,2.31c.37-.32.81-1.01,1.21-.96.25.03.9.2,1.11.66,6.43,13.58,13.7,26.51,20.99,39.77.33.64-.22,1.15.97,1.13l.14-40.7c0-1.44.72-2.79,2.03-2.59.75.11,2.07.79,2.14,1.88l.48,6.73h.86s-.19-7.69-.19-7.69c-.02-.88.38-1.66,1.13-2.02.47-.23,1.41.06,1.91.55.36.36.48,1.08.43,2.01l-.91,17.03c-.71,13.29-.29,26.3,1.41,39.55.15,1.17-.12,1.97-1.03,2.48-.6.34-1.39-.86-2.48-.34-1.13-.42-1.61-.84-1.48.63.53,1-.2,2.66-1.05,2.73-.63.05-2.13-.74-2.5-1.41l-13.62-24.51-8.86-15.58c.78-.95-.17-1.09-.75-.63-.36.29-.35.96-.34,1.48l.45,23.53.43,14.35c.01.49-.31.91-.4.93l-1.09.2c-.2.98-.16,1.98-.77,2.39-.38.26-1.39.87-1.84.26-2.59-3.5.56-7.55-1.57-8.01-.8,3.22-.05,6.51-2.08,6.24-.58-.08-1.68-1-1.68-2.01l-.1-32.81c.59-.19.15-.63-.03-.98l-.41-19.15c-.01-.67.09-1.35.16-2.03ZM131.61,294.87c.56.17.86.1,1.15.05l-.14-5.48h-.88s-.12,5.43-.12,5.43Z"/>
<path class="cls-3" d="M183.3,260.1l-.32-2.09c-.11-.72-1.11-1.15-.97-1.75l-3.04-3.05c-.51-.51-.92-.45-1.63-.42,2.53,1.87,4.03,4.39,5.11,7.26.61,1.6.21,6.25-2.18,6.12-1.47-.08-1.46-7.54-6.76-9.31-11.93-4-24.01,18.41-17.78,36.88,2.48,7.36,9.28,11.51,16.6,9.5,8-2.2,9.41-8.66,11.84-10.21.54-.35,1.47.15,1.71.96.66,2.24-2.41,5.72-.04,4.79.67-.26.94-3.13,2.63-3.58,4.53-1.21-1.83,15.46-19.2,17.57-4.05.49-8.79-.77-12.6-2.71-10.78-5.48-14.16-21.61-11.7-33.33,2.86-13.58,11.96-31.05,26.38-29.41,8.2.93,15.9,7.93,14.14,15.14-1.52,1.03-2.08-1.63-2.19-2.35Z"/>
<path class="cls-4" d="M180.62,209.65c1.45,8.24,2.29,16.48.87,25.09.23,1.06.09,2.35.82,2.93,1.81,1.42,12.02-3.33,25.99.65l2.66.27c.19.02.43.07.64.11-.22-.04-.45-.09-.64-.11-.26.64-.13,1.27-.1,1.89l-14.51.85c-7.53.44-16.11,2.54-17.16.75-2.65-4.48-1.82-9.62-1.41-14.45l.89-10.4-.79-8.42c1.56-.33,1.82,1.45,2.67.25.04.21.1.45.08.59Z"/>
<path class="cls-4" d="M100.37,322.45c1.9.37,3.77-1.03,5.41.22.41.31.4,1.07.31,2.02l-.95,10.42c.18.66,2.56.69,2.13,2.33-.8-.12-2.52-.42-2.87.56-.77,2.16-.04,4.31-.05,6.44-.03,6.07,1.79,9.23-.22,9.43l-.2-.56c-3.58-9.94-4.47-20.41-3.56-30.87Z"/>
<path class="cls-5" d="M96.95,254.77c-.07.68-.17,1.36-.16,2.03l-10.66-3.95c-1.43-.57-.57-1.87-.54-3.02.02-.78,1.6-1.73,2.32-1.39,3.31,2.02,6.15,4.3,9.04,6.32Z"/>
<path class="cls-4" d="M183.3,260.1l-.85-.05c-1.08-2.87-2.59-5.4-5.11-7.26.71-.03,1.12-.09,1.63.42l3.04,3.05c-.15.6.86,1.03.97,1.75l.32,2.09Z"/>
<path class="cls-4" d="M131.61,294.87l.12-5.43h.88s.14,5.47.14,5.47c-.29.05-.59.12-1.15-.05Z"/>
<path class="cls-2" d="M179.77,205.02l-.62-.2c-4-1.27-8.26.25-12.53.39l-9.43.31-16.21,1.53-.63.29-3.08,5.27c-1.99-.68-3.89-2.51-5.03-3.99-1.6-2.07-3.66-2.43-6.45-2.28l-27.87,1.53-14.48-.61-15.08-1.79c-.84-.1-1.46-.81-2.02-.15s.07,1.3.02,2.1l-1.05,16.95c-.23,3.7,1.96,7.02,3.35,10.39l-.16,5.46,5.93,5.13-7.42,3.44c-1.86.87-2.83,2.67-2.65,4.71l1.67,18.92c.3,3.43,1.78,7.04,1.53,10.53l-1.71,23.37c-.14,1.96.39,3.36-.48,5.56-.62,1.55.17,4.21.11,5.67l.69.73"/>
<path class="cls-2" d="M214.8,239.27l-1.28-.24-1.92-.35c-.22-.04-.45-.09-.64-.11l-2.66-.27c-13.98-3.98-24.18.77-25.99-.65-.73-.57-.59-1.87-.82-2.93,1.42-8.61.58-16.85-.87-25.09.02-.14-.04-.38-.08-.59l-.77-4.04"/>
<path class="cls-2" d="M104.54,355.01l-.41-1.13-.2-.56c-3.58-9.94-4.47-20.41-3.56-30.87.06-.7-.29-1.6-.74-2.1-.64-.71-1.77-.71-2.91-.62l-12.92,1.07c-5.9.49-11.78-1.34-17.6-2.56"/>
<polyline class="cls-2" points="179.8 205.06 214.01 238.49 214.41 238.88 214.8 239.27"/>
<path class="cls-2" d="M214.8,239.27l.1.72c.48,3.66-.57,7.44-.57,11.31v7.39s.07,6.27.07,6.27c.03,2.58.2,4.71-.47,6.93-1.02,3.42-5.64,5.2-4.81,6.04"/>
<polyline class="cls-2" points="66.21 318.28 66.6 318.66 103.34 353.86 103.74 354.24 104.14 354.63 104.54 355.01"/>
<path class="cls-2" d="M210.79,316.19c2.01,1.62,5.55,12.81,5.16,16.41l-2.13,19.73c-8.18,1.27-16.4,2.09-24.72,1.88l-6.37-8.74c-.46-.63-1.12-1.06-1.64-1.08-.67-.03-1.56.51-2.14,1.26l-5.23,6.74-16.53,1.49c-2.64.24-5.37.61-7.42-.64l-.56-.34-3.01-2.1c-1.49-1.04-3.18-.66-3.91.8-1.16,2.56-3.95,2.42-6.56,2.51l-13.67.49-16.87,1.08-.65-.67"/>
</svg>

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288 560">
<defs>
<style>
.cls-1 {
fill: #9b1f0f;
}
.cls-2 {
fill: #3a160a;
}
.cls-3 {
fill: #e93525;
}
.cls-4 {
fill: #3d180d;
}
.cls-5 {
fill: #c12b1c;
}
.cls-6 {
fill: none;
stroke: #381507;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2.2px;
}
</style>
</defs>
<path class="cls-3" d="M141.35,207.84c-.15-.1-.38-.08-.57-.14-.5.53.56,1.73-.35,2.63,2.2,6.52,2.73,13.17,3.12,20.16l1.18,20.95c.3-.07.67-.04.84-.02.7-.89-.2-2.18-.08-3.4l3.51-37.22c.07-.78-.41-1.44-.5-1.88l19.5-2.14c1.96.84,1.89,4.16,2.43,5.54-.19-.36-1.21.01-1.82.21-.39.13.11.72-.4,1.5.58,10.03-3.74,18.54-4.28,28.36,3.88-6.54,14.81-30.94,15.78-31.51,1.1-.65,1.54.47,2.43.78,1.28.45,3.34-.37,3.73,1.34.25,1.09,1.66.7,2.44,1.56-.46,1.49-1.33,2.83-1.48,4.39,3.42.29,7.69-.88,11.02.3,2.37.84-.91,3.18-1.22,4.37.17,4.04,4.98,4.47,10.21,3.99.82.43.92,2.65,2.23,2.64l-4.61,3.64c4.87-.22,8.3,1.84,11.15,1.44l-.61,4.42c-.69.13-1.97-.34-2.8.24-6.06,4.22-12.42,7.63-18.95,11.04l-6.29,4.39c4.87.92,9.07-1.38,13.76-2.48,3.75-.88,7.71-1.15,11.61-1.17,1.47,0,3.02,1.79,4.47.65l-1.19,12.44c-.25,2.67-.6,5.14-1.34,7.45-.83-.98-1.63-.31-2.63.07-6.58,2.5-13.24,3.89-20.13,5l-17.39,2.78c1.75,1.3,3.52.34,5.24.34h32.08c-1.46,8.73-7.23,6.14-7.99,8.08-1.21,3.1,2.92,5.92,1.74,7.9s-3.15,3.27-4.07,5.7c2.08.02,5.13-3.57,7.39-2.41.77.39,1.39,1.77,1.11,3.19-3.36,4.96-9.42,5.44-8.61,13.35l-.74,3.75c-1.58.06-2.1,1.03-2.21,2.08l-12.24-1.78c-1.47-.21-4.87-.09-4.63,1.94l1.6,13.76c.17,1.43.76,2.77-.25,3.79l-5.27,2.54-2.14.63-1.93-1.16c-6.66,3.78-4.71,7.07-15.02,6.31l-11.95,2.56-1.39-7.83c-3.15-7.67-3.81-15.27-3.95-23.41l-.15-8.71c-.38.04-.44-.34-.87-.53l-1.62,20.27c-.21,2.63-.8,4.53-1.38,7.39l-20.37-.61c-2.55-.08-4.58-3.05-7.89-1.97-.6-2.94-2.88-3.36-5.33-3.82.34-4.53,4.71-6.78,3.51-8.96l-7.49,7.73-1.13-2.09c-1.15-2.13-1.4-5.14-4.15-6.09-.73-1.72-2.1-3.05-4.58-2.9-.17-1.75-2.45-1.79-2.86-2.71-.24-.54.43-.87-.61-.57-.2.06-3.02-3.72-3.62-7.19-3.05-.69-.25-4.22-2.63-4.24,3.16-3.19-3.84-1.11-4.81-3.41-.81-1.91-.04-3.39.74-4.96.26-.51-.56-4.29,2.44-9.1,6.33-2.1,12.4-3.33,18.8-4.23.25-.04.88.64,1.17.5.34-.16.76-.41.89-1.14l-22.09-1.45-1.42-.42c.68.2-2.27-4.82-.93-7.13,1.88-.95,4.57-.3,6.65-.77.49-.11,4.68-5.32,4.6-4.85.15-.81-.35-1.49-.9-1.62-1.08-.25-1.49,2.01-5.22,2.29-1.4.11-3.82-3.52-3.86-3.9-.09-.81.64-.76,1.09-1.56,3.25-5.81,2.03-5.22,1.61-9.67-.13-1.35-1.69-2.4-.19-3.82,2.65-2.49,5.15-5.35,6.94-8.32.25-.1.64-.51,1.11-.47l13.86,1.31c3.55-2.76,1.46-9.93,1.27-15.68l-.49-14.85c.39-.69.33-1.35.37-1.85.06-.7-.98-1.5-1.55-.94l.55-2.93.11-.58.22-1.16-.39.4.39-.34c4.32.66,9.46,1.37,13.92,1.69l14.97,1.06c3.88.27,7.66-.03,11.21,1.08.19.06.42.03.57.14ZM102.09,290.2l.93,21.66c.06,1.44-.52,3.82,1.01,4.64.98.52,2.05,1,2.45.38l1.25-1.93c.34-.52-.14-1.15.09-1.4.18-.19.64-.12.85-.03.32.12.23.16.26.39.08.53-.64.83-.5,1.16.41.96,1.63,1.3,2.11.96.43-.3,1.27-1.06,1.24-1.95l-.87-22.21c8.59-.6,16.52-3.59,22.83-8.98,7.59-6.48,9.09-17.7,2.59-25.38s-21.36-8.59-30.78-5.14c-.19-1.38-.35-2.08-.8-2.15l-2.07-.29-.32,3.56c-9.69,4.25-8.22,8.19-6.55,7.13.37-.23.75-.79,2.02-.94l-1.7,2.61c-.33.5-.28,1.36.15,1.65.29.2,1.12.33,1.58,0l4.17-3.06-.03,24.78c-1.77.34-3.13-1.12-4.55-.4-1.95.98.56,3.2,4.63,4.93ZM183.9,295.43c-2.54-1.64-4.08,8.31-13.8,10.5-5.05,1.14-10.5-1.01-13.47-5.57-4.78-7.32-5.07-16.43-2.58-24.6,3.13-10.28,10.83-20.05,19.27-15.53,5.81,3.12,2.84,9.38,6.76,8.1.76-.25,1.62-3.66.55-6.94,1.78.38,1.35,3.89,2.76,4.16,2.82.54.95-11.97-10.88-15.11-17.71-4.7-30.43,20.87-29.61,38.76.36,7.72,2.8,15.24,8.44,20.4,5.98,5.47,14.37,6.55,22.33,4.17,9.83-2.94,16.54-13.86,14.45-15.95-.52-.53-1.42-.6-1.75-.07l-.88,1.4c-.87.17-.68.78-.87.89-1.52.83,1.03-3.48-.73-4.61Z"/>
<path class="cls-5" d="M100.35,205.56l-.55,2.93c-1.08,5.75-2.71,11.68-2.27,17.86.25,3.54.85,6.96.03,10.68-8.89.06-16.45-2.38-25.75.39-.64.15-.28,1.16-.01,1.44.24.26.66-.03,1.36.22l11.41,1.34c.18.5.36.67.67.55-1.8,2.97-4.29,5.82-6.94,8.32-1.5,1.41.06,2.46.19,3.82.42,4.45,1.64,3.85-1.61,9.67-.45.81-1.18.76-1.09,1.56.04.39,2.46,4.01,3.86,3.9,3.73-.29,4.14-2.55,5.22-2.29.56.13,1.05.81.9,1.62.09-.47-4.1,4.74-4.6,4.85-2.08.48-4.77-.18-6.65.77-1.35,2.31,1.61,7.33.93,7.13l1.42.42,22.09,1.45c-.13.74-.55.98-.89,1.14-.29.14-.91-.53-1.17-.5-6.4.9-12.47,2.12-18.8,4.23-3,4.81-2.18,8.59-2.44,9.1-.78,1.57-1.55,3.04-.74,4.96.97,2.29,7.97.21,4.81,3.41,2.38.02-.42,3.55,2.63,4.24.6,3.47,3.42,7.25,3.62,7.19,1.04-.3.38.02.61.57.4.92,2.69.96,2.86,2.71,2.48-.16,3.85,1.18,4.58,2.9,2.75.95,3,3.96,4.15,6.09l1.13,2.09,7.49-7.73c1.2,2.18-3.16,4.43-3.51,8.96,2.45.45,4.73.87,5.33,3.82,3.31-1.07,5.34,1.9,7.89,1.97l20.37.61c.58-2.85,1.17-4.75,1.38-7.39l1.62-20.27c.43.19.49.57.87.53l.15,8.71c.14,8.14.8,15.74,3.95,23.41l1.39,7.83,11.95-2.56c10.32.76,8.36-2.53,15.02-6.31l1.93,1.16,2.14-.63,5.27-2.54c.4-.02.63.4.83.64.16.18-.17.6-.2,1.04l-.67,10.12c.82-.14,1.5-.45,2.27,0,2.81-6.65,3.02-13.87,2.67-21.4.15-.92-.09-2.34.39-2.85.45-.48,1.84-1.22,2.79-1.11,7.02.82,13.69.47,20.29-1.35,1.03.54,2.17.4,3.18.19.08.61.02,1.31-.5,1.83l-14.08,13.99c-3.04,3.02-6.73,5.26-9.07,8.88l-3.1,3.24c-1.03-1.01-2.03-.45-2.82-.97l-2.06,4.85-35.37-1.32c-4.34-1.79-6.54-8.04-7.73-7.92s-2.8,6.38-5.48,6.54l-10.41.64c-5.38.33-10.15.72-15.49,2.19-3.14.87-7.62.86-10.83.08-2.95-1.18-3.85-5.14-6.2-6.63-2.58.57-3.1,5.36-5.17,6.12s-15.98-.69-16.94-1.64c-1.04-1.02-1.66-2.74-1.93-4.36l-1.46-8.74c-.26-1.57-1.4-2.96-.05-5l.25-3.57c1.08-2.07.21-4.64.52-7.05l1.27-9.71c.24-1.86.5-4.12-.13-5.95l-.38-7.85c-.09-1.91-.47-3.76-.13-5.55l1.4-7.4c.42-2.24,1.09-4.38,1.82-6.51-.7-1.43-2.17-2.49-2.25-3.91l-.41-7.84-.51-7.74-.6-8.71-.3-7.65c-.86-4.02-1.08-8.37,2.47-11.45.28-.11.66-.01.92.02l30.19-31.05.84.55Z"/>
<path class="cls-5" d="M215.61,235.34c-2.85.4-6.28-1.66-11.15-1.44l4.61-3.64c-1.3,0-1.41-2.21-2.23-2.64-5.23.47-10.04.05-10.21-3.99.31-1.19,3.58-3.53,1.22-4.37-3.33-1.18-7.6,0-11.02-.3.14-1.57,1.01-2.91,1.48-4.39-.78-.86-2.19-.47-2.44-1.56-.39-1.71-2.45-.89-3.73-1.34-.89-.32-1.33-1.43-2.43-.78-.97.57-11.91,24.97-15.78,31.51.54-9.82,4.86-18.33,4.28-28.36.51-.78,0-1.37.4-1.5.61-.2,1.63-.57,1.82-.21.52,1.34,1.39,2.32,2.25,3.36l.35.42-.35-.42c2.99-4.01,4.32-9.78,6.79-11.04,1.99-1.01,6.98.63,10.05.99,2.25.26,4.67-.67,7.13.45l5.57.52c4.86.45,13.52,1.2,14.2,1.84.95.89.45,1.58.67,2.86,1.43,8.35.85,16.35-1.48,24.06Z"/>
<path class="cls-5" d="M214.27,272.31c-.4,1.25-2.02,2.41-1.74,4.07l2.61,3.32,1.18,21.98,1.27,18.14c.18,2.6-1.32,4.68-3.5,5.32-1,.22-2.14.35-3.18-.19.51-.44.48-1.12.22-2.05-4.36.28-8.77-.13-13.07-.76.11-1.05.62-2.02,2.21-2.08l.74-3.75c-.81-7.92,5.25-8.4,8.61-13.35.28-1.42-.34-2.79-1.11-3.19-2.26-1.16-5.3,2.44-7.39,2.41.92-2.43,2.85-3.65,4.07-5.7s-2.95-4.8-1.74-7.9c.76-1.94,6.53.65,7.99-8.08h-32.08c-1.72,0-3.48.96-5.24-.34l17.39-2.78c6.89-1.1,13.55-2.49,20.13-5,1-.38,1.8-1.05,2.63-.07Z"/>
<path class="cls-1" d="M148.5,208.91c.09.44.57,1.1.5,1.88l-3.51,37.22c-.12,1.22.79,2.51.08,3.4-.17-.02-.55-.05-.84.02l-1.18-20.95c-.39-6.98-.92-13.64-3.12-20.16.91-.89-.15-2.09.35-2.63.19.06.42.03.57.14,2.12,1.41,4.66,1.34,7.15,1.07Z"/>
<path class="cls-5" d="M215,239.76c-.37,2.69-1.06,4.75-4.09,5.88,0,1.06-1.32,1.19-1.19,1.7.25,1,1.38,1.43,2.04,1.7,1.6.65,5.23,1.29,5.03,3.38-1.45,1.14-3-.66-4.47-.65-3.89.02-7.85.3-11.61,1.17-4.69,1.1-8.9,3.39-13.76,2.48l6.29-4.39c6.53-3.41,12.89-6.83,18.95-11.04.83-.58,2.12-.11,2.8-.24Z"/>
<path class="cls-2" d="M102.09,290.2c-4.07-1.73-6.58-3.95-4.63-4.93,1.42-.72,2.78.75,4.55.4l.03-24.78-4.17,3.06c-.46.33-1.29.2-1.58,0-.43-.29-.48-1.15-.15-1.65l1.7-2.61c-1.27.15-1.65.71-2.02.94-1.68,1.06-3.15-2.89,6.55-7.13l.32-3.56,2.07.29c.45.06.61.77.8,2.15,9.43-3.46,24.09-2.78,30.78,5.14s5,18.9-2.59,25.38c-6.31,5.39-14.24,8.38-22.83,8.98l.87,22.21c.03.89-.81,1.65-1.24,1.95-.48.34-1.7,0-2.11-.96-.15-.34.58-.64.5-1.16-.04-.23.06-.27-.26-.39-.21-.08-.67-.16-.85.03-.23.25.25.88-.09,1.4l-1.25,1.93c-.4.62-1.47.15-2.45-.38-1.53-.82-.95-3.2-1.01-4.64l-.93-21.66ZM131.84,268.69c-.17-9.31-11.39-12.74-20.83-10.93-.29,9.08-1.11,17.3-.24,26.28,9.58-2.04,21.23-6.33,21.06-15.35Z"/>
<path class="cls-4" d="M183.9,295.43c1.76,1.14-.79,5.45.73,4.61.19-.11,0-.72.87-.89l.88-1.4c.33-.52,1.23-.45,1.75.07,2.08,2.09-4.63,13.02-14.45,15.95-7.96,2.38-16.35,1.3-22.33-4.17-5.64-5.17-8.09-12.68-8.44-20.4-.82-17.89,11.9-43.46,29.61-38.76,11.84,3.14,13.7,15.66,10.88,15.11-1.41-.27-.98-3.78-2.76-4.16,1.07,3.28.2,6.69-.55,6.94-3.93,1.28-.95-4.98-6.76-8.1-8.45-4.53-16.14,5.24-19.27,15.53-2.49,8.17-2.2,17.28,2.58,24.6,2.98,4.56,8.43,6.7,13.47,5.57,9.71-2.19,11.26-12.13,13.8-10.5ZM177.02,256.36l3.08,4.12c1.58-.8-.62-4.44-3.08-4.12Z"/>
<path class="cls-1" d="M198.07,322.14c4.3.63,8.71,1.03,13.07.76.25.92.29,1.6-.22,2.05-6.61,1.82-13.27,2.17-20.29,1.35-.95-.11-2.34.63-2.79,1.11-.48.51-.23,1.94-.39,2.85.34,7.53.14,14.75-2.67,21.4-.77-.46-1.44-.14-2.27,0l.67-10.12c.03-.44.36-.86.2-1.04-.2-.24-.43-.66-.83-.64,1.02-1.02.42-2.37.25-3.79l-1.6-13.76c-.24-2.03,3.16-2.15,4.63-1.94l12.24,1.78Z"/>
<path class="cls-1" d="M85.24,240.96c-.31.13-.49-.05-.67-.55l-11.41-1.34c-.71-.24-1.12.04-1.36-.22-.26-.28-.63-1.29.01-1.44,9.3-2.77,16.87-.34,25.75-.39.82-3.72.22-7.14-.03-10.68-.44-6.17,1.19-12.1,2.27-17.86.58-.57,1.61.24,1.55.94-.04.5.02,1.16-.37,1.85l.49,14.85c.19,5.76,2.28,12.92-1.27,15.68l-13.86-1.31c-.47-.04-.87.37-1.11.47Z"/>
<path class="cls-3" d="M131.84,268.69c.17,9.02-11.48,13.31-21.06,15.35-.87-8.98-.05-17.2.24-26.28,9.44-1.81,20.65,1.63,20.83,10.93Z"/>
<path class="cls-1" d="M177.02,256.36c2.46-.32,4.66,3.32,3.08,4.12l-3.08-4.12Z"/>
<path class="cls-6" d="M211.77,249.04c1.6.65,5.23,1.29,5.03,3.38l-1.19,12.44c-.25,2.67-.6,5.14-1.34,7.45-.4,1.25-2.02,2.41-1.74,4.07l2.61,3.32,1.18,21.98,1.27,18.14c.18,2.6-1.32,4.68-3.5,5.32-1,.22-2.14.35-3.18-.19-6.61,1.82-13.27,2.17-20.29,1.35-.95-.11-2.34.63-2.79,1.11-.48.51-.23,1.94-.39,2.85.34,7.53.14,14.75-2.67,21.4-.09.1-.19.28-.26.44l-2.06,4.85-35.37-1.32c-4.34-1.79-6.54-8.04-7.73-7.92s-2.8,6.38-5.48,6.54l-10.41.64c-5.38.33-10.15.72-15.49,2.19-3.14.87-7.62.86-10.83.08-2.95-1.18-3.85-5.14-6.2-6.63"/>
<path class="cls-6" d="M100.69,203.88c4.32.66,9.46,1.37,13.92,1.69l14.97,1.06c3.88.27,7.66-.03,11.21,1.08.19.06.42.03.57.14,2.12,1.41,4.66,1.34,7.15,1.07l19.5-2.14c1.96.84,1.89,4.16,2.43,5.54s1.39,2.32,2.25,3.36l.35.42"/>
<path class="cls-6" d="M100.68,203.82l-.39.4-.78.79-30.19,31.05c.27,1.12,1.58,1.24,2.49,1.36,9.3-2.77,16.87-.34,25.75-.39.82-3.72.22-7.14-.03-10.68-.44-6.17,1.19-12.1,2.27-17.86l.55-2.93.11-.58.22-1.16Z"/>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288 560">
<defs>
<style>
.cls-1 {
fill: #0db3c8;
}
.cls-2 {
fill: #007988;
}
.cls-3 {
fill: #0c96a8;
}
.cls-4 {
fill: #3a170d;
}
.cls-5 {
fill: none;
stroke: #381507;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2.2px;
}
.cls-6 {
fill: #3c1b0d;
}
</style>
</defs>
<path class="cls-1" d="M214.65,239.1l-2.58-.1c-3.79,2.1-7.73,4.08-11.61,6.32l-6.56,3.79c-2.2,1.27-5.02,2.22-6.12,5.03l12.97-3.01,11.9-1.19c.44-.63.06-1.58.21-2.2,1.11.52,3.9,1.16,3.71,3.1l-1.82,18.71c-1.5,1.84-3.42,2.56-4.11,4.45-.39.04-.84-.28-1.1-.57-.2-.23.18-1.3-.21-1.23l-13.32,2.67-24.66,3.99c2.94.91,5.47.34,8.11.31l30.61-.32c.58,0,1.24.32,1.55.45s.06,1.13-.11,1.49l-5.94,12.1-2.34,5.89c1.31.57,2.15-1.03,3.19-1.23,1.71-.34,2.18,1.87,2.29,2.91.19,1.76-.21,2.53-1.63,3.75l-3.61,3.1c-2.25,1.93-2.51,5.48-3.11,8.25l-2.36,4.83-14.21-1.65c-1.16-.13-2.3.56-2.59,1.21-1.13,2.5,3.49,19.87,1.51,29.66l2.38-.32.4,1.88c-.02,1.11-.76,2.05-1.49,2.86-.9,1-2.37.8-3.88.8l-24.17.07c-3.64.01-6.98.15-10.19-1.01l-.55-.2c.38-.35-.12-1.32,1-2.02l-3.02-10.07c-.55-1.82-.65-4.03-.68-6.05l-.38-23.49c0-.35.21-1.3-.12-1.78-.19-.26,1.36-1.02-.61-1.17l-4.19,32.02c-.53,4.08-2.01,7.72-1.64,11.98,1.56-.87,2.59-.64,3.6-.38-1.06.63-1.9,2.74-3.63,3-8.84,1.34-17.96,1.85-26.77.44l-13.88-3.27c-1.56-1.74-2.19-3.54-3.98-4.71l-6.41,7.6c-5.24-.33-10.27-1.31-15.49-2.21-2.15-4.26-.12-10.16-1.62-14.2-1.64-4.39-1.98-8.67-1.04-13.21,1.9-9.16,1.37-17.8.49-27.02-.43-4.52,3.2-9.56,5.02-12.83l.8-.58.55-.55-.55.55-.8.58c.11.87,1.36,1.26,2.07.83.33-.2.87.26,1.48-.62l30.23-5.39c1.79-.32,6.53.08,5.59-1.14-.12-.15-.41-.79-.78-.81l-16.48-.56-11.54.04c-3.89-.56-7.57.1-11.44-1.87.21,1.09-.13,1.57-.76,1.25-.22-.11-.39.11-.92.03l-2.78-19.95c-.32-2.3-1.51-5.82-.08-7.61l6.94-4.85c.64-.45.02-1.82-.8-1.97-2.71-1.37-4.11-3.91-2.9-7.27-2.45-4.5-2.75-9.25-2.39-14.17l.97-13.21c.38-5.16,5.65-2.67,12.92-.43,3.25.54,6.3-.23,9.33-1.63,8.42.55,16.41-.06,24.8-.67l16.36-1.2,8.59,1.05c.2.02.38.14.56.21-.19-.07-.37-.18-.56-.21-.54,4.1,1.56,7.36,1.72,9.19l1.11,13.13.8,17.66-.2,8.37c.25,0,.57-.14.92.02l3.92-47.24-.57.22.57-.22,19.53-1.91c1.57.6,1.43,3.49,2.12,4.44.09.12.09.4.12.6-.03-.2-.03-.48-.12-.6l-2.15.91c.17,8.51-1.44,13.81-3.15,21.63l-2.13,9.75c2.92-2.92,3.58-6.64,5.27-10.02l10.81-21.63c.85-1.69.49-4.05,3.09-4.69-.5-.7-.42-1.53-.6-2.46,1.54.33,3.13-.5,4.85.26l5.72.4,10.88,1.12,6.13.57c2.16.2,6.59-.53,8.1,2.25.4,6.45,1.14,13.48-.1,19.79l-2.24,11.36ZM168.83,303.16c-6.94.18-12.13-5.11-13.68-11.47-1.72-5.03-2.28-10.32-.88-15.55,1.12-10.54,9.73-25.45,20.19-20.69,5.19,2.36,4.68,11.87,7.33,8.34-.13.18.87-3.46,1.06-3.05-.47-1.02-.68-2.77-.56-2.97.38-.62.92-.03.9.33-.01.16.1.13.26.77.43.68.42,1.72.95,1.95,3.46,1.48,1.8-9.41-6.67-13.59-5.47-2.7-10.86-3.08-15.84-.29-7.33,4.11-11.48,11.04-14.58,18.6-3.51,8.58-5.17,17.76-2.65,26.66,1.55,5.5,3.59,11.18,8.36,15.03,6.06,4.89,14.44,6.2,22.41,3.63,13.87-4.48,18.3-21.76,11.29-15.14-.28.27.49.89.09,1.03-.25.08-.69-.15-1.14.58-.32.51.58,1.26-.3,1.42l-1.48.26,1.93-3.85c.54-1.09.15-2.4-.76-2.62-3.3-.79-4.86,10.32-16.21,10.62ZM99.04,299.48l-2.33-4.68-3.47.03c-.04,6.06,3.08,10.63,7.43,14.01,4.93,3.83,10.3,5.14,16.54,4.87,10.18-.44,19.57-6.79,21.53-16.96,1.57-8.14-2.54-15.62-9.31-19.96-7.16-4.59-16.77-5.53-20.09-10.13-1.83-2.53-1.29-5.65.09-8.51,1.08-2.23,3.67-3.76,6.68-4.4,8.68-1.84,13.61,5.15,15,4.52.46-.2,1.46-.79,1.17-1.39l-1.53-3.17c2.36.38,3.63,3.38,4.81,1.76,1.42-1.94-9.35-13.75-24.73-9.15-8.64,2.58-13.65,10.74-11.83,19.53.96,4.64,3.52,8.84,8.3,10.67l12.22,4.68c6.12,2.34,10.3,8.86,8.33,15.46-1.45,4.86-6.35,7.18-11.25,7.51-5.75.39-12.14-2.11-13.82-7.94-.66-2.3-.6-5.38-4.17-5.51-.75,4.05,1.16,6.89.42,8.75ZM103.94,315l-7.75,6.79c.97.96,1.64.88,2.27.52-.28.16,5.09-5.27,5.48-7.31Z"/>
<path class="cls-3" d="M210.63,274c-.34,2.98,4.2,2.59,4.27,3.76l2.51,42.37c.1,1.67-2.35,3.33-3.58,3.16l.11-2.9c-5.22.96-10.62.62-15.97,0l2.36-4.83c.6-2.77.86-6.32,3.11-8.25l3.61-3.1c1.42-1.22,1.82-1.99,1.63-3.75-.11-1.05-.58-3.25-2.29-2.91-1.04.2-1.88,1.81-3.19,1.23l2.34-5.89,5.94-12.1c.18-.36.42-1.36.11-1.49s-.97-.45-1.55-.45l-30.61.32c-2.64.03-5.17.59-8.11-.31l24.66-3.99,13.32-2.67c.39-.08,0,1,.21,1.23.25.29.71.61,1.1.57Z"/>
<path class="cls-1" d="M213.84,323.29c.69.99.45,1.15-.19,1.78l-17.39,17.12c-3.16,3.11-6.44,5.57-9.4,9.05-.38.45-.97.07-1.39-.06l-.4-1.88c1.94-6.68,3.23-13.66,2.17-20.91-.22-1.52-.28-2.52.82-3.51.9-.81,1.78-.11,3.06-.13l14.27-.23c3.03-.05,5.64-1.6,8.45-1.22Z"/>
<path class="cls-3" d="M69.29,278.12c.52.08.69-.14.92-.03.63.32.97-.16.76-1.25,3.87,1.96,7.55,1.31,11.44,1.87l11.54-.04,16.48.56c.38.01.66.65.78.81.93,1.22-3.8.82-5.59,1.14l-30.23,5.39c-.61.87-1.15.42-1.48.62-.71.43-1.96.04-2.07-.83l.8-.58.55-.55-.55.55.55-.55c.13-.13.21-.35.38-.39.47-.12.74-1.1.12-1.45-.29-.17-.73,0-.96-1.07-.92-1.33-3.2-2.39-3.45-4.18Z"/>
<path class="cls-3" d="M145.75,353.91l-.55-.2c-1.16-.42-2.27-1.18-3.54-1.9-1.2-.68-1.89,1.02-2.51.94-1.01-.26-2.05-.49-3.6.38-.38-4.26,1.1-7.9,1.64-11.98l4.19-32.02c1.97.15.43.91.61,1.17.33.47.11,1.42.12,1.78l.38,23.49c.03,2.02.13,4.23.68,6.05l3.02,10.07c-1.11.7-.61,1.67-1,2.02l.55.2Z"/>
<path class="cls-2" d="M181.3,203.36c.18.94.09,1.76.6,2.46-2.6.64-2.24,3-3.09,4.69l-10.81,21.63c-1.69,3.38-2.35,7.1-5.27,10.02l2.13-9.75c1.71-7.83,3.31-13.12,3.15-21.63l2.15-.91c.09.12.09.4.12.6.19,1.19.46,2.37,1.32,3.3.37-.75,1.52-.99,1.94-1.77l3.72-6.88c.54-1,.67-1.36,1.64-1.89,1.02-.56,1.31-.09,2.4.14Z"/>
<path class="cls-3" d="M147.94,207.55l.57-.22-3.92,47.24c-.34-.16-.67-.01-.92-.02l.2-8.37-.8-17.66-1.11-13.13c-.15-1.82-2.26-5.09-1.72-9.19.2.02.38.14.56.21,2.24.82,4.78.99,7.14,1.13Z"/>
<path class="cls-2" d="M214.65,239.1c-.45,2.26-2.11,3.94-3.91,5.65-.84.16-1.05.39-1.13.79-.15.82.08,1.71,1.03,1.66.76-.04,1.5.2,2.23.54-.16.62.22,1.57-.21,2.2l-11.9,1.19-12.97,3.01c1.1-2.81,3.92-3.76,6.12-5.03l6.56-3.79c3.88-2.24,7.82-4.22,11.61-6.32l2.58.1Z"/>
<path class="cls-4" d="M99.04,299.48c.73-1.86-1.18-4.7-.42-8.75,3.57.13,3.51,3.2,4.17,5.51,1.68,5.83,8.07,8.33,13.82,7.94,4.9-.33,9.79-2.65,11.25-7.51,1.97-6.6-2.21-13.11-8.33-15.46l-12.22-4.68c-4.78-1.83-7.34-6.03-8.3-10.67-1.82-8.79,3.19-16.95,11.83-19.53,15.38-4.6,26.15,7.2,24.73,9.15-1.18,1.62-2.45-1.38-4.81-1.76l1.53,3.17c.29.6-.71,1.18-1.17,1.39-1.39.62-6.32-6.36-15-4.52-3.02.64-5.61,2.16-6.68,4.4-1.38,2.86-1.93,5.98-.09,8.51,3.33,4.59,12.93,5.53,20.09,10.13,6.77,4.34,10.89,11.82,9.31,19.96-1.96,10.18-11.36,16.52-21.53,16.96-6.25.27-11.61-1.04-16.54-4.87-4.35-3.38-7.47-7.95-7.43-14.01l3.47-.03,2.33,4.68Z"/>
<path class="cls-6" d="M168.83,303.16c11.35-.3,12.91-11.42,16.21-10.62.9.22,1.3,1.53.76,2.62l-1.93,3.85,1.48-.26c.88-.16-.02-.91.3-1.42.46-.73.9-.5,1.14-.58.4-.14-.37-.76-.09-1.03,7.01-6.62,2.58,10.66-11.29,15.14-7.97,2.58-16.35,1.27-22.41-3.63-4.76-3.85-6.8-9.53-8.36-15.03-2.51-8.9-.86-18.09,2.65-26.66,3.1-7.56,7.25-14.49,14.58-18.6,4.98-2.79,10.38-2.41,15.84.29,8.47,4.18,10.13,15.07,6.67,13.59-.52-.22-.52-1.27-.95-1.95-.16-.64-.27-.62-.26-.77.02-.36-.52-.95-.9-.33-.12.2.09,1.95.56,2.97-.19-.41-1.19,3.23-1.06,3.05-2.65,3.53-2.14-5.98-7.33-8.34-10.46-4.76-19.07,10.15-20.19,20.69-1.4,5.23-.84,10.52.88,15.55,1.55,6.36,6.74,11.66,13.68,11.47Z"/>
<path class="cls-2" d="M197.98,320.39c5.36.62,10.75.96,15.97,0l-.11,2.9c-2.81-.38-5.42,1.17-8.45,1.22l-14.27.23c-1.27.02-2.16-.67-3.06.13-1.1.99-1.04,1.99-.82,3.51,1.06,7.25-.23,14.24-2.17,20.91l-2.38.32c1.98-9.79-2.64-27.16-1.51-29.66.29-.65,1.43-1.35,2.59-1.21l14.21,1.65Z"/>
<path class="cls-3" d="M103.94,315c-.39,2.04-5.76,7.47-5.48,7.31-.63.36-1.3.44-2.27-.52l7.75-6.79Z"/>
<path class="cls-5" d="M210.64,247.2c.76-.04,1.5.2,2.23.54,1.11.52,3.9,1.16,3.71,3.1l-1.82,18.71c-1.5,1.84-3.42,2.56-4.11,4.45-.34,2.98,4.2,2.59,4.27,3.76l2.51,42.37c.1,1.67-2.35,3.33-3.58,3.16-2.81-.38-5.42,1.17-8.45,1.22l-14.27.23c-1.27.02-2.16-.67-3.06.13-1.1.99-1.04,1.99-.82,3.51,1.06,7.25-.23,14.24-2.17,20.91l.4,1.88c-.02,1.11-.76,2.05-1.49,2.86-.9,1-2.37.8-3.88.8l-24.17.07c-3.64.01-6.98.15-10.19-1.01l-.55-.2c-1.16-.42-2.27-1.18-3.54-1.9-1.2-.68-1.89,1.02-2.51.94-1.06.63-1.9,2.74-3.63,3-8.84,1.34-17.96,1.85-26.77.44l-13.88-3.27c-1.56-1.74-2.19-3.54-3.98-4.71l-6.41,7.6c-5.24-.33-10.27-1.31-15.49-2.21-2.15-4.26-.12-10.16-1.62-14.2-1.64-4.39-1.98-8.67-1.04-13.21,1.9-9.16,1.37-17.8.49-27.02-.43-4.52,3.2-9.56,5.02-12.83l.8-.58.55-.55c.13-.13.21-.35.38-.39"/>
<path class="cls-5" d="M172.5,214.73l-.9-.97c-.85-.92-1.12-2.1-1.32-3.3-.03-.2-.03-.48-.12-.6-.7-.95-.55-3.83-2.12-4.44l-19.53,1.91-.57.22c-2.36-.14-4.9-.31-7.14-1.13-.19-.07-.37-.18-.56-.21l-8.59-1.05-16.36,1.2c-8.38.62-16.38,1.23-24.8.67-3.03,1.4-6.08,2.17-9.33,1.63-7.26-2.24-12.54-4.74-12.92.43l-.97,13.21c-.36,4.92-.07,9.67,2.39,14.17-1.22,3.36.19,5.9,2.9,7.27"/>
</svg>

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@@ -61,11 +61,13 @@
}
});
// Landscape: right clears gear/kit buttons (~4rem); bottom is fixed 60px for
// the kit-bag handle strip — tray is ignored so the stage has room to breathe.
// Landscape: right clears gear/kit buttons; bottom is fixed 60px for the
// kit-bag handle strip — tray is ignored so the stage has room to breathe.
// At ≥1800px the right sidebar doubles to 8rem so clear 128px.
if (isLandscape) {
rightInset = Math.max(rightInset, 64); // 4rem
bottomInset = 60; // kit-bag handle
var xlBreak = vw >= 1800;
rightInset = Math.max(rightInset, xlBreak ? 128 : 64);
bottomInset = 60;
}
overlay.style.paddingRight = rightInset + 'px';
@@ -78,7 +80,11 @@
var sw = stageEl.offsetWidth - 24; // subtract padding (0.75rem × 2)
var sh = stageEl.offsetHeight - 24;
if (sw > 0 && sh > 0) {
var cardW = Math.min(sw * 0.4, sh * 0.8 * 5 / 8);
// Clamp between 90px (never tiny in landscape) and 160px (never
// dominant on very wide/tall viewports). In portrait, skip the
// floor so small modals still scale down naturally.
var cardW = Math.min(sw * 0.4, sh * 0.8 * 5 / 8, 160);
if (isLandscape) { cardW = Math.max(cardW, 90); }
overlay.style.setProperty('--sig-card-w', cardW + 'px');
}
}

View File

@@ -5,14 +5,23 @@ var SigSelect = (function () {
gravity: ['BC', 'EC', 'AC'],
};
var overlay, deckGrid, stage, stageCard;
var overlay, deckGrid, stage, stageCard, statBlock;
var cautionEl, cautionEffect, cautionPrev, cautionNext, cautionIndexEl;
var _flipBtn, _cautionBtn, _flipOrigLabel, _cautionOrigLabel;
var reserveUrl, userRole, userPolarity;
var _cautionData = [];
var _cautionIdx = 0;
var _focusedCardEl = null; // card currently shown in stage
var _reservedCardId = null; // card with active reservation
var _stageFrozen = false; // true after OK — stage locks on reserved card
var _requestInFlight = false;
var _floatingCursors = {}; // key: cardId+posClass → portal <i> element (hover)
var _reservedFloats = {}; // key: role → portal <i> element (thumbs-up, frozen)
var _cursorPortal = null;
function getCsrf() {
var m = document.cookie.match(/csrftoken=([^;]+)/);
return m ? m[1] : '';
@@ -20,8 +29,60 @@ var SigSelect = (function () {
// ── Stage ──────────────────────────────────────────────────────────────
function _populateKeywordList(listEl, csv) {
var keywords = csv ? csv.split(',').filter(Boolean) : [];
listEl.innerHTML = keywords.map(function (k) {
return '<li>' + k.trim() + '</li>';
}).join('');
}
// ── Caution tooltip ───────────────────────────────────────────────────
function _renderCaution() {
if (_cautionData.length === 0) {
cautionEffect.innerHTML = '<em>Rival interactions pending.</em>';
cautionPrev.disabled = true;
cautionNext.disabled = true;
cautionIndexEl.textContent = '';
return;
}
cautionEffect.innerHTML = _cautionData[_cautionIdx];
cautionPrev.disabled = (_cautionData.length <= 1);
cautionNext.disabled = (_cautionData.length <= 1);
cautionIndexEl.textContent = _cautionData.length > 1
? (_cautionIdx + 1) + ' / ' + _cautionData.length
: '';
}
function _openCaution() {
if (!_focusedCardEl) return;
try {
_cautionData = JSON.parse(_focusedCardEl.dataset.cautions || '[]');
} catch (e) {
_cautionData = [];
}
_cautionIdx = 0;
_renderCaution();
_flipBtn.classList.add('btn-disabled');
_cautionBtn.classList.add('btn-disabled');
_flipBtn.textContent = '\u00D7';
_cautionBtn.textContent = '\u00D7';
stage.classList.add('sig-caution-open');
}
function _closeCaution() {
stage.classList.remove('sig-caution-open');
if (_flipBtn) {
_flipBtn.classList.remove('btn-disabled');
_cautionBtn.classList.remove('btn-disabled');
_flipBtn.textContent = _flipOrigLabel;
_cautionBtn.textContent = _cautionOrigLabel;
}
}
function updateStage(cardEl) {
if (_stageFrozen) return;
_closeCaution();
if (!cardEl) {
stageCard.style.display = 'none';
stage.classList.remove('sig-stage--active');
@@ -47,9 +108,26 @@ var SigSelect = (function () {
}
});
stageCard.querySelector('.fan-card-name-group').textContent = group;
stageCard.querySelector('.fan-card-name').textContent = title;
stageCard.querySelector('.fan-card-arcana').textContent = arcana;
stageCard.querySelector('.fan-card-correspondence').textContent = corr;
stageCard.querySelector('.fan-card-correspondence').textContent = ''; // shown in game-kit only
var qualifier = userPolarity === 'levity' ? 'Leavened' : 'Graven';
var isMajor = arcana.toLowerCase().indexOf('major') !== -1;
// Major arcana: qualifier sits below the title — append comma so it reads as a subtitle.
stageCard.querySelector('.fan-card-name').textContent = isMajor ? title + ',' : title;
stageCard.querySelector('.sig-qualifier-above').textContent = isMajor ? '' : qualifier;
stageCard.querySelector('.sig-qualifier-below').textContent = isMajor ? qualifier : '';
// Populate stat block keyword faces and reset to upright
statBlock.classList.remove('is-reversed');
_populateKeywordList(
statBlock.querySelector('#id_stat_keywords_upright'),
cardEl.dataset.keywordsUpright
);
_populateKeywordList(
statBlock.querySelector('#id_stat_keywords_reversed'),
cardEl.dataset.keywordsReversed
);
stageCard.style.display = '';
stage.classList.add('sig-stage--active');
@@ -110,6 +188,38 @@ var SigSelect = (function () {
// ── Apply reservation state (local + from WS) ─────────────────────────
function _placeReservedFloat(cardId, cardEl, role) {
// Remove any pre-existing reserved float for this role (e.g. page-load replay)
if (_reservedFloats[role]) { _reservedFloats[role].remove(); }
// Retire ALL hover floats for this role — may be on a different card than reserved
var roles = POLARITY_ROLES[userPolarity] || [];
var idx = roles.indexOf(role);
var posClass = ['--left', '--mid', '--right'][idx] || '--left';
Object.keys(_floatingCursors).forEach(function (key) {
if (key.slice(-posClass.length) === posClass) {
_floatingCursors[key].remove();
var hCid = key.slice(0, key.length - posClass.length);
var hEl = deckGrid.querySelector('.sig-card[data-card-id="' + hCid + '"]');
if (hEl) {
var a = hEl.querySelector('.sig-cursor' + posClass);
if (a) a.classList.remove('active');
}
delete _floatingCursors[key];
}
});
var rect = cardEl.getBoundingClientRect();
var xFractions = [0.15, 0.5, 0.85];
var fc = document.createElement('i');
fc.className = 'fa-solid fa-thumbs-up sig-cursor-float sig-cursor-float--reserved';
fc.dataset.role = role;
fc.style.left = (rect.left + rect.width * xFractions[idx < 0 ? 1 : idx]) + 'px';
fc.style.top = rect.bottom + 'px';
_ensureCursorPortal().appendChild(fc);
_reservedFloats[role] = fc;
}
function applyReservation(cardId, role, reserved) {
var cardEl = deckGrid.querySelector('.sig-card[data-card-id="' + cardId + '"]');
if (!cardEl) return;
@@ -127,6 +237,8 @@ var SigSelect = (function () {
_stageFrozen = true;
stage.classList.add('sig-stage--frozen');
}
// Thumbs-up float for all reservations — own role sees their own indicator too
_placeReservedFloat(cardId, cardEl, role);
} else {
delete cardEl.dataset.reservedBy;
cardEl.classList.remove('sig-reserved', 'sig-reserved--own');
@@ -135,26 +247,65 @@ var SigSelect = (function () {
_stageFrozen = false;
stage.classList.remove('sig-stage--frozen');
}
// Remove thumbs-up float for all releases — own role included
if (_reservedFloats[role]) {
_reservedFloats[role].remove();
delete _reservedFloats[role];
}
}
}
// ── Apply hover cursor (WS only — own hover is CSS :hover) ────────────
//
// Cursor icons are portaled to document root so they escape overflow/clip
// contexts in the deck grid. The in-card anchor elements only carry the
// .active class (for test assertions and the :has() z-index rule).
function _ensureCursorPortal() {
if (!_cursorPortal || !document.body.contains(_cursorPortal)) {
_cursorPortal = document.getElementById('id_sig_cursor_portal');
if (!_cursorPortal) {
_cursorPortal = document.createElement('div');
_cursorPortal.id = 'id_sig_cursor_portal';
document.body.appendChild(_cursorPortal);
}
}
return _cursorPortal;
}
function applyHover(cardId, role, active) {
if (role === userRole) return;
if (_reservedFloats[role]) return; // role already has thumbs-up — ignore hover
var cardEl = deckGrid.querySelector('.sig-card[data-card-id="' + cardId + '"]');
if (!cardEl) return;
var roles = POLARITY_ROLES[userPolarity] || [];
var idx = roles.indexOf(role);
var posClass = ['--left', '--mid', '--right'][idx] || '--left';
var cursor = cardEl.querySelector('.sig-cursor' + posClass);
if (!cursor) return;
var anchor = cardEl.querySelector('.sig-cursor' + posClass);
if (!anchor) return;
var key = cardId + posClass;
if (active) {
cursor.classList.add('active');
anchor.classList.add('active'); // kept for test assertions + :has() z-index
// Place a fixed-position clone in the portal, positioned from card bounds
var rect = cardEl.getBoundingClientRect();
var xFractions = [0.15, 0.5, 0.85];
var fc = document.createElement('i');
fc.className = 'fa-solid fa-hand-pointer sig-cursor-float';
fc.dataset.role = role;
fc.style.left = (rect.left + rect.width * xFractions[idx]) + 'px';
fc.style.top = rect.bottom + 'px';
_ensureCursorPortal().appendChild(fc);
_floatingCursors[key] = fc;
} else {
cursor.classList.remove('active');
anchor.classList.remove('active');
if (_floatingCursors[key]) {
_floatingCursors[key].remove();
delete _floatingCursors[key];
}
}
}
@@ -188,17 +339,64 @@ var SigSelect = (function () {
deckGrid = overlay.querySelector('.sig-deck-grid');
stage = overlay.querySelector('.sig-stage');
stageCard = stage.querySelector('.sig-stage-card');
statBlock = stage.querySelector('.sig-stat-block');
_flipBtn = statBlock.querySelector('.sig-flip-btn');
_cautionBtn = statBlock.querySelector('.sig-caution-btn');
_flipOrigLabel = _flipBtn.textContent;
_cautionOrigLabel = _cautionBtn.textContent;
_flipBtn.addEventListener('click', function () {
if (_flipBtn.classList.contains('btn-disabled')) return;
statBlock.classList.toggle('is-reversed');
});
cautionEl = stage.querySelector('.sig-caution-tooltip');
cautionEffect = cautionEl.querySelector('.sig-caution-effect');
cautionPrev = statBlock.querySelector('.sig-caution-prev');
cautionNext = statBlock.querySelector('.sig-caution-next');
cautionIndexEl = cautionEl.querySelector('.sig-caution-index');
// Clicking the tooltip (not nav buttons) dismisses it
cautionEl.addEventListener('click', function () {
_closeCaution();
});
_cautionBtn.addEventListener('click', function () {
if (_cautionBtn.classList.contains('btn-disabled')) return;
stage.classList.contains('sig-caution-open') ? _closeCaution() : _openCaution();
});
cautionPrev.addEventListener('click', function () {
_cautionIdx = (_cautionIdx - 1 + _cautionData.length) % _cautionData.length;
_renderCaution();
});
cautionNext.addEventListener('click', function () {
_cautionIdx = (_cautionIdx + 1) % _cautionData.length;
_renderCaution();
});
reserveUrl = overlay.dataset.reserveUrl;
userRole = overlay.dataset.userRole;
userPolarity= overlay.dataset.polarity;
// Restore reservations from server-rendered JSON (page-load state)
// Restore reservations from server-rendered JSON (page-load state).
// Deferred to 'load' so sizeSigModal() (also a 'load' listener, registered
// in room.js before this script) has already applied paddingBottom and
// --sig-card-w before _placeReservedFloat calls getBoundingClientRect().
try {
var existing = JSON.parse(overlay.dataset.reservations || '{}');
if (Object.keys(existing).length) {
var _replayReservations = function () {
Object.keys(existing).forEach(function (cardId) {
applyReservation(cardId, existing[cardId], true);
});
};
if (document.readyState === 'complete') {
_replayReservations();
} else {
window.addEventListener('load', _replayReservations, { once: true });
}
}
} catch (e) { /* malformed JSON — ignore */ }
// Hover: update stage preview + broadcast cursor
@@ -264,6 +462,13 @@ var SigSelect = (function () {
_reservedCardId = null;
_stageFrozen = false;
_requestInFlight = false;
_cautionData = [];
_cautionIdx = 0;
Object.keys(_floatingCursors).forEach(function (k) { _floatingCursors[k].remove(); });
_floatingCursors = {};
Object.keys(_reservedFloats).forEach(function (k) { _reservedFloats[k].remove(); });
_reservedFloats = {};
_cursorPortal = null;
init();
},
_setFrozen: function (v) { _stageFrozen = v; },

View File

@@ -14,6 +14,13 @@ var Tray = (function () {
var _tray = null;
var _grid = null;
// Role code → scrawl SVG name mapping for tray card display.
var _ROLE_SCRAWL = {
PC: 'Player', NC: 'Narrator', EC: 'Economist',
SC: 'Shepherd', AC: 'Alchemist', BC: 'Builder'
};
var _roleIconsUrl = null;
// Portrait bounds (X axis)
var _minLeft = 0;
var _maxLeft = 0;
@@ -94,12 +101,7 @@ var Tray = (function () {
// Closed: tray hidden above viewport, handle visible at y=0.
_maxTop = -(gearBtnTop - handleH);
} else {
// Portrait: slide on X axis.
// Wrap width is pinned to viewportW (JS) so its right edge only
// reaches the viewport boundary when left = 0 (fully open).
// This mirrors landscape: the open edge appears only at the last moment.
// Open: left = 0 → wrap right = viewportW exactly.
// Closed: left = viewportW - handleW → tray fully off-screen right.
// Portrait: wrap width = full viewport; handle parks at right edge.
var handleW = _btn.offsetWidth || 48;
if (_wrap) _wrap.style.width = window.innerWidth + 'px';
_minLeft = 0;
@@ -251,7 +253,13 @@ var Tray = (function () {
firstCell.classList.add('tray-role-card');
firstCell.dataset.role = roleCode;
firstCell.textContent = roleCode;
firstCell.textContent = '';
if (_roleIconsUrl) {
var img = document.createElement('img');
img.src = _roleIconsUrl + 'starter-role-' + (_ROLE_SCRAWL[roleCode] || 'Blank') + '.svg';
img.alt = roleCode;
firstCell.appendChild(img);
}
open();
_arcIn(firstCell, function () {
@@ -313,7 +321,7 @@ var Tray = (function () {
}
} else {
if (_tray) _tray.style.display = 'none';
if (_wrap) { _wrap.style.top = ''; _wrap.style.height = ''; }
if (_wrap) { _wrap.style.top = ''; _wrap.style.height = ''; _wrap.style.width = ''; }
_computeBounds();
_applyVerticalBounds();
_computeCellSize();
@@ -331,6 +339,7 @@ var Tray = (function () {
_btn = document.getElementById('id_tray_btn');
_tray = document.getElementById('id_tray');
_grid = document.getElementById('id_tray_grid');
_roleIconsUrl = (_grid && _grid.dataset.roleIconsUrl) || null;
if (!_btn) return;
if (_isLandscape()) {
@@ -342,8 +351,8 @@ var Tray = (function () {
if (_wrap) _wrap.style.top = _maxTop + 'px';
_computeCellSize();
} else {
// Clear landscape's inline top so portrait CSS applies.
if (_wrap) _wrap.style.top = '';
// Clear landscape's inline top/height/width so portrait CSS applies.
if (_wrap) { _wrap.style.top = ''; _wrap.style.width = ''; }
_applyVerticalBounds();
_computeCellSize(); // wrap has correct height after _applyVerticalBounds
_computeBounds();

View File

@@ -269,16 +269,16 @@ class SigDeckCompositionTest(TestCase):
cards = sig_deck_cards(self.room)
self.assertEqual(len(cards), 36)
def test_sc_ac_contribute_court_cards_of_swords_and_cups(self):
def test_sc_ac_contribute_court_cards_of_blades_and_grails(self):
cards = sig_deck_cards(self.room)
sc_ac = [c for c in cards if c.suit in ("SWORDS", "CUPS")]
sc_ac = [c for c in cards if c.suit in ("BLADES", "GRAILS")]
# M/J/Q/K × 2 suits × 2 roles = 16
self.assertEqual(len(sc_ac), 16)
self.assertTrue(all(c.number in (11, 12, 13, 14) for c in sc_ac))
def test_pc_bc_contribute_court_cards_of_wands_and_crowns(self):
def test_pc_bc_contribute_court_cards_of_brands_and_crowns(self):
cards = sig_deck_cards(self.room)
pc_bc = [c for c in cards if c.suit in ("WANDS", "CROWNS")]
pc_bc = [c for c in cards if c.suit in ("BRANDS", "CROWNS")]
self.assertEqual(len(pc_bc), 16)
self.assertTrue(all(c.number in (11, 12, 13, 14) for c in pc_bc))
@@ -342,7 +342,7 @@ class SigCardFieldTest(TestCase):
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
)
self.card = TarotCard.objects.get(
deck_variant=earthman, arcana="MIDDLE", suit="WANDS", number=11,
deck_variant=earthman, arcana="MIDDLE", suit="BRANDS", number=11,
)
owner = User.objects.create(email="owner@test.io")
room = Room.objects.create(name="Field Test", owner=owner)
@@ -479,3 +479,54 @@ class SigCardHelperTest(TestCase):
self.owner.save()
self.assertEqual(levity_sig_cards(self.room), [])
self.assertEqual(gravity_sig_cards(self.room), [])
class TarotCardCautionsTest(TestCase):
"""TarotCard.cautions JSONField — field existence and Schizo seed data."""
def setUp(self):
self.earthman = DeckVariant.objects.get(slug="earthman")
def test_cautions_field_saves_and_retrieves_list(self):
card = TarotCard.objects.create(
deck_variant=self.earthman,
arcana="MINOR",
suit="CROWNS",
number=99,
name="Test Card",
slug="test-card-cautions",
cautions=["First caution.", "Second caution."],
)
card.refresh_from_db()
self.assertEqual(card.cautions, ["First caution.", "Second caution."])
def test_cautions_defaults_to_empty_list(self):
card = TarotCard.objects.create(
deck_variant=self.earthman,
arcana="MINOR",
suit="CROWNS",
number=98,
name="Default Cautions Card",
slug="default-cautions-card",
)
self.assertEqual(card.cautions, [])
def test_schizo_has_4_cautions(self):
schizo = TarotCard.objects.get(
deck_variant=self.earthman, arcana="MAJOR", number=1
)
self.assertEqual(len(schizo.cautions), 4)
def test_schizo_caution_references_the_pervert(self):
schizo = TarotCard.objects.get(
deck_variant=self.earthman, arcana="MAJOR", number=1
)
self.assertIn("The Pervert", schizo.cautions[0])
def test_schizo_cautions_use_reverse_language(self):
schizo = TarotCard.objects.get(
deck_variant=self.earthman, arcana="MAJOR", number=1
)
for caution in schizo.cautions:
self.assertIn("reverse", caution)
self.assertNotIn("transform", caution)

View File

@@ -926,7 +926,7 @@ def _full_sig_setUp(test_case, role_order=None):
room.table_status = Room.SIG_SELECT
room.save()
card_in_deck = TarotCard.objects.get(
deck_variant=earthman, arcana="MIDDLE", suit="WANDS", number=11
deck_variant=earthman, arcana="MIDDLE", suit="BRANDS", number=11
)
test_case.client.force_login(founder)
return room, gamers, earthman, card_in_deck
@@ -963,6 +963,32 @@ class SigSelectRenderingTest(TestCase):
response = self.client.get(self.url)
self.assertNotContains(response, "id_sig_deck")
def test_sig_cards_render_keyword_data_attributes(self):
response = self.client.get(self.url)
content = response.content.decode()
self.assertIn("data-keywords-upright=", content)
self.assertIn("data-keywords-reversed=", content)
def test_sig_stat_block_structure_rendered(self):
response = self.client.get(self.url)
self.assertContains(response, "sig-stat-block")
self.assertContains(response, "sig-flip-btn")
self.assertContains(response, "stat-face--upright")
self.assertContains(response, "stat-face--reversed")
def test_sig_cards_render_cautions_data_attribute(self):
response = self.client.get(self.url)
self.assertContains(response, "data-cautions=")
def test_sig_caution_tooltip_structure_rendered(self):
response = self.client.get(self.url)
self.assertContains(response, "sig-caution-tooltip")
self.assertContains(response, "sig-caution-btn")
self.assertContains(response, "sig-caution-effect")
self.assertContains(response, "sig-caution-index")
self.assertContains(response, "sig-caution-prev")
self.assertContains(response, "sig-caution-next")
class SelectSigCardViewTest(TestCase):
"""select_sig view — records choice, enforces turn order, rejects bad input."""
@@ -1000,8 +1026,8 @@ class SelectSigCardViewTest(TestCase):
def test_select_sig_card_not_in_deck_returns_400(self):
# Create a pip card (number=5) — not in the sig deck (only court 1114 + major 01)
other = TarotCard.objects.create(
deck_variant=self.earthman, arcana="MINOR", suit="WANDS", number=5,
name="Five of Wands Test", slug="five-of-wands-test",
deck_variant=self.earthman, arcana="MINOR", suit="BRANDS", number=5,
name="Five of Brands Test", slug="five-of-brands-test",
keywords_upright=[], keywords_reversed=[],
)
response = self._post(card_id=other.id)
@@ -1188,7 +1214,7 @@ class SigReserveViewTest(TestCase):
def test_reserve_different_card_while_holding_returns_409(self):
"""Cannot OK a different card while holding one — must NVM first."""
card_b = TarotCard.objects.filter(
deck_variant=self.earthman, arcana="MIDDLE", suit="WANDS", number=12
deck_variant=self.earthman, arcana="MIDDLE", suit="BRANDS", number=12
).first()
self._reserve() # PC grabs card A → 200
response = self._reserve(card_id=card_b.id) # tries card B → 409
@@ -1210,7 +1236,7 @@ class SigReserveViewTest(TestCase):
def test_reserve_blocked_then_unblocked_after_release(self):
"""After NVM, a new card can be OK'd."""
card_b = TarotCard.objects.filter(
deck_variant=self.earthman, arcana="MIDDLE", suit="WANDS", number=12
deck_variant=self.earthman, arcana="MIDDLE", suit="BRANDS", number=12
).first()
self._reserve() # hold card A
self._reserve(action="release") # NVM

View File

@@ -10,8 +10,11 @@ from django.shortcuts import redirect, render
from django.utils import timezone
from apps.drama.models import GameEvent, record
from django.db.models import Case, IntegerField, Value, When
from apps.epic.models import (
GateSlot, Room, RoomInvite, SigReservation, TableSeat, TarotCard, TarotDeck,
GateSlot, Room, RoomInvite, SIG_SEAT_ORDER, SigReservation, TableSeat,
TarotCard, TarotDeck,
active_sig_seat, debit_token, levity_sig_cards, gravity_sig_cards,
select_token, sig_deck_cards,
)
@@ -92,6 +95,27 @@ def _notify_sig_reserved(room_id, card_id, role, reserved):
SLOT_ROLE_LABELS = {1: "PC", 2: "NC", 3: "EC", 4: "SC", 5: "AC", 6: "BC"}
_SIG_SEAT_ORDERING = Case(
*[When(role=r, then=Value(i)) for i, r in enumerate(SIG_SEAT_ORDER)],
default=Value(99),
output_field=IntegerField(),
)
def _canonical_user_seat(room, user):
"""Return the user's seat whose role comes first in PC→NC→EC→SC→AC→BC order.
In normal play (one user = one seat) this is equivalent to .first().
For Carte Blanche (one user = all seats) it returns the PC seat, ensuring
sig-select cursor placement is seat-based, not position/slot-based.
"""
return room.table_seats.filter(gamer=user).order_by(_SIG_SEAT_ORDERING).first()
_ROLE_SCRAWL_NAMES = {
"PC": "Player", "NC": "Narrator", "EC": "Economist",
"SC": "Shepherd", "AC": "Alchemist", "BC": "Builder",
}
def _gate_positions(room):
"""Return list of dicts [{slot, role_label, role_assigned}] for _table_positions.html."""
@@ -216,10 +240,16 @@ def _role_select_context(room, user):
if user.is_authenticated else []
)
active_slot = active_seat.slot_number if active_seat else None
_my_role = assigned_seats[0].role if assigned_seats else None
ctx = {
"card_stack_state": card_stack_state,
"starter_roles": starter_roles,
"assigned_seats": assigned_seats,
"my_tray_role": _my_role,
"my_tray_scrawl_static_path": (
f"apps/epic/icons/cards-roles/starter-role-{_ROLE_SCRAWL_NAMES[_my_role]}.svg"
if _my_role else None
),
"user_seat": user_seat,
"user_slots": list(
room.table_seats.filter(gamer=user, role__isnull=True)
@@ -231,7 +261,7 @@ def _role_select_context(room, user):
"slots": room.gate_slots.order_by("slot_number"),
}
if room.table_status == Room.SIG_SELECT:
user_seat = room.table_seats.filter(gamer=user).first() if user.is_authenticated else None
user_seat = _canonical_user_seat(room, user) if user.is_authenticated else None
user_role = user_seat.role if user_seat else None
user_polarity = None
if user_role in _LEVITY_ROLES:
@@ -572,7 +602,7 @@ def sig_reserve(request, room_id):
if room.table_status != Room.SIG_SELECT:
return HttpResponse(status=400)
user_seat = room.table_seats.filter(gamer=request.user).first()
user_seat = _canonical_user_seat(room, request.user)
if not user_seat or not user_seat.role:
return HttpResponse(status=403)
@@ -611,7 +641,7 @@ def sig_reserve(request, room_id):
SigReservation.objects.create(
room=room, gamer=request.user, card=card,
role=user_seat.role, polarity=polarity,
seat=user_seat, role=user_seat.role, polarity=polarity,
)
_notify_sig_reserved(room_id, card.pk, user_seat.role, reserved=True)
return HttpResponse(status=200)

View File

@@ -5,6 +5,7 @@ from . import views as lyric_views
urlpatterns = [
path('send_login_email', lyric_views.send_login_email, name='send_login_email'),
path('login', lyric_views.login, name='login'),
path('logout', auth_views.LogoutView.as_view(next_page='/'), name='logout')
path('logout', auth_views.LogoutView.as_view(next_page='/'), name='logout'),
path('dev-login/<str:session_key>/', lyric_views.dev_login, name='dev_login'),
]

View File

@@ -1,4 +1,6 @@
from django.conf import settings
from django.contrib import auth, messages
from django.http import Http404
from django.shortcuts import redirect
from django.urls import reverse
@@ -27,3 +29,13 @@ def login(request):
else:
messages.error(request, "Invalid login link!—please request another")
return redirect("/")
def dev_login(request, session_key):
"""DEBUG-only: set session cookie and redirect. Used by setup_sig_session command."""
if not settings.DEBUG:
raise Http404
next_url = request.GET.get("next", "/")
response = redirect(next_url)
response.set_cookie(settings.SESSION_COOKIE_NAME, session_key, httponly=True)
return response

View File

@@ -57,13 +57,13 @@ INSTALLED_APPS = [
# Board apps
'apps.dashboard',
'apps.gameboard',
'apps.billboard',
# Gamer apps
'apps.lyric',
'apps.epic',
'apps.drama',
'apps.billboard',
'apps.ap',
# Custom apps
'apps.ap',
'apps.api',
'apps.applets',
'functional_tests',

View File

@@ -0,0 +1,128 @@
"""
Management command for manual multi-user sig-select testing.
Creates (or reuses) a room with all 6 gate slots filled, roles assigned,
and table_status=SIG_SELECT. Prints one pre-auth URL per gamer so you can
paste them into 6 Firefox Multi-Account Container tabs.
Usage:
python src/manage.py setup_sig_session
python src/manage.py setup_sig_session --base-url http://localhost:8000
python src/manage.py setup_sig_session --room <uuid> # reuse existing room
"""
from django.contrib.auth import BACKEND_SESSION_KEY, HASH_SESSION_KEY, SESSION_KEY
from django.contrib.sessions.backends.db import SessionStore
from django.core.management.base import BaseCommand
from apps.epic.models import DeckVariant, GateSlot, Room, TableSeat, TarotCard
from apps.lyric.models import User
GAMERS = [
("founder@test.io", "discoman"),
("amigo@test.io", "amigo"),
("bud@test.io", "bud"),
("pal@test.io", "pal"),
("dude@test.io", "dude"),
("bro@test.io", "bro"),
]
ROLES = ["PC", "NC", "EC", "SC", "AC", "BC"]
def _ensure_earthman():
"""Return (or create) the Earthman DeckVariant with enough sig-deck cards seeded."""
earthman, _ = DeckVariant.objects.get_or_create(
slug="earthman",
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
)
_NAME = {11: "Maid", 12: "Jack", 13: "Queen", 14: "King"}
for suit in ("WANDS", "PENTACLES", "SWORDS", "CUPS"):
for number in (11, 12, 13, 14):
TarotCard.objects.get_or_create(
deck_variant=earthman,
slug=f"{_NAME[number].lower()}-of-{suit.lower()}-em",
defaults={
"arcana": "MINOR",
"suit": suit,
"number": number,
"name": f"{_NAME[number]} of {suit.capitalize()}",
},
)
return earthman
def _make_session(user):
session = SessionStore()
session[SESSION_KEY] = str(user.pk)
session[BACKEND_SESSION_KEY] = "apps.lyric.authentication.PasswordlessAuthenticationBackend"
session[HASH_SESSION_KEY] = user.get_session_auth_hash()
session.save()
return session.session_key
class Command(BaseCommand):
help = "Set up a SIG_SELECT room and print pre-auth URLs for all six gamers"
def add_arguments(self, parser):
parser.add_argument("--base-url", default="http://localhost:8000")
parser.add_argument("--room", default=None, help="UUID of an existing room to reuse")
def handle(self, *args, **options):
base_url = options["base_url"].rstrip("/")
earthman = _ensure_earthman()
# ── Users ────────────────────────────────────────────────────────────
users = []
for email, _ in GAMERS:
user, _ = User.objects.get_or_create(email=email)
user.is_staff = True
user.is_superuser = True
if not user.equipped_deck:
user.equipped_deck = earthman
user.save()
users.append(user)
# ── Room ─────────────────────────────────────────────────────────────
if options["room"]:
room = Room.objects.get(pk=options["room"])
else:
room = Room.objects.create(
name="Sig Select Test Room",
owner=users[0],
visibility=Room.PUBLIC,
)
# ── Gate slots ───────────────────────────────────────────────────────
for i, user in enumerate(users, start=1):
slot = room.gate_slots.get(slot_number=i)
slot.gamer = user
slot.status = GateSlot.FILLED
slot.save()
room.gate_status = Room.OPEN
room.save()
# ── Table seats + roles ──────────────────────────────────────────────
for i, (user, role) in enumerate(zip(users, ROLES), start=1):
TableSeat.objects.update_or_create(
room=room, slot_number=i,
defaults={"gamer": user, "role": role, "role_revealed": True},
)
room.table_status = Room.SIG_SELECT
room.save()
# ── Print URLs ───────────────────────────────────────────────────────
room_path = f"/gameboard/room/{room.pk}/"
self.stdout.write(f"\nRoom: {base_url}{room_path}\n")
self.stdout.write(f"{'Container':<12} {'Email':<22} {'Role':<6} URL")
self.stdout.write("" * 100)
for (email, container), user, role in zip(GAMERS, users, ROLES):
session_key = _make_session(user)
url = f"{base_url}/lyric/dev-login/{session_key}/?next={room_path}"
self.stdout.write(f"{container:<12} {email:<22} {role:<6} {url}")
self.stdout.write("")

View File

@@ -119,6 +119,9 @@ class DashboardMaintenanceTest(FunctionalTest):
class AppletMenuDismissTest(FunctionalTest):
def setUp(self):
super().setUp()
# Portrait viewport: sidebars don't activate, h2 sits safely above
# #id_dash_content and can't be obscured by it regardless of font metrics.
self.browser.set_window_size(800, 1200)
Applet.objects.get_or_create(slug="username", defaults={"name": "Username"})
Applet.objects.get_or_create(slug="palette", defaults={"name": "Palette"})
self.create_pre_authenticated_session("discoman@example.com")

View File

@@ -113,7 +113,7 @@ class NavbarByeTest(FunctionalTest):
class NavbarContGameTest(FunctionalTest):
"""
When the authenticated user has at least one room with a game event the
CONT GAME btn-primary btn-xl appears in the navbar and navigates to that
CONT GAME btn-primary appears in the navbar and navigates to that
room on confirmation. Its tooltip must also appear below the button.
"""
@@ -139,7 +139,6 @@ class NavbarContGameTest(FunctionalTest):
)
btn = self.browser.find_element(By.ID, "id_cont_game")
self.assertIn("btn-primary", btn.get_attribute("class"))
self.assertIn("btn-xl", btn.get_attribute("class"))
# ------------------------------------------------------------------ #
# T6 — CONT GAME tooltip appears below btn #

View File

@@ -34,14 +34,15 @@ def _assign_all_roles(room, role_order=None):
slug="earthman",
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
)
# Seed the 18 sig deck cards (migration data is flushed in TransactionTestCase FTs)
# Seed the 18 sig deck cards (migration data is flushed in TransactionTestCase FTs).
# _sig_unique_cards() filters arcana=MIDDLE, suits BRANDS/CROWNS/BLADES/GRAILS (Earthman).
_NAME = {11: "Maid", 12: "Jack", 13: "Queen", 14: "King"}
for suit in ("WANDS", "PENTACLES", "SWORDS", "CUPS"):
for suit in ("BRANDS", "CROWNS", "BLADES", "GRAILS"):
for number in (11, 12, 13, 14):
TarotCard.objects.get_or_create(
deck_variant=earthman,
slug=f"{_NAME[number].lower()}-of-{suit.lower()}-em",
defaults={"arcana": "MINOR", "suit": suit, "number": number,
defaults={"arcana": "MIDDLE", "suit": suit, "number": number,
"name": f"{_NAME[number]} of {suit.capitalize()}"},
)
for number, name, slug in [
@@ -138,3 +139,234 @@ class SigSelectChannelsTest(ChannelsFunctionalTest):
))
return b
def _setup_sig_select_room(self):
"""Create a full SIG_SELECT room; return (room, [user_pc, user_nc, ...])."""
emails = [
"founder@test.io", "amigo@test.io", "bud@test.io",
"pal@test.io", "dude@test.io", "bro@test.io",
]
founder, _ = User.objects.get_or_create(email=emails[0])
room = Room.objects.create(name="Cursor Colour Test", owner=founder)
gamers = _fill_room_via_orm(room, emails)
_assign_all_roles(room)
return room, gamers
# ── SC1: NC hover → PC sees mid cursor active, coloured --priYl ────────── #
@tag('channels')
def test_nc_hover_activates_mid_cursor_in_pc_browser(self):
"""
When NC (levity mid) hovers a card, PC (levity left) must see the
--mid cursor become active, coloured --priYl (rgb 255 207 52).
Verifies: WS broadcast pipeline + JS applyHover + CSS role colouring.
"""
room, gamers = self._setup_sig_select_room()
room_url = self.live_server_url + f"/gameboard/room/{room.pk}/"
# ── Browser 1: PC (founder) ───────────────────────────────────────────
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(room_url)
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
# ── Browser 2: NC (amigo) ─────────────────────────────────────────────
browser2 = self._make_browser2("amigo@test.io")
try:
browser2.get(room_url)
self.wait_for(lambda: browser2.find_element(By.CSS_SELECTOR, ".sig-overlay"))
# Grab the first card ID visible in browser2's deck
first_card = browser2.find_element(By.CSS_SELECTOR, ".sig-card")
card_id = first_card.get_attribute("data-card-id")
# Hover over it — triggers sendHover() → WS broadcast
from selenium.webdriver.common.action_chains import ActionChains
ActionChains(browser2).move_to_element(first_card).perform()
# ── Browser 1 should see --mid cursor go active (anchor carries class) ─
mid_cursor_sel = f'.sig-card[data-card-id="{card_id}"] .sig-cursor--mid'
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, mid_cursor_sel + ".active"
)
)
# CSS colour check: portal float has data-role="NC" → --priYl = 255, 207, 52
portal_sel = '.sig-cursor-float[data-role="NC"]'
portal_cursor = self.browser.find_element(By.CSS_SELECTOR, portal_sel)
color = self.browser.execute_script(
"return window.getComputedStyle(arguments[0]).color",
portal_cursor,
)
self.assertEqual(color, "rgb(255, 207, 52)", f"Expected --priYl colour for NC cursor, got {color}")
# ── Mouse-off: anchor class removed, portal float gone ────────────
ActionChains(browser2).move_to_element(
browser2.find_element(By.CSS_SELECTOR, ".sig-stage")
).perform()
self.wait_for(
lambda: not self.browser.find_elements(
By.CSS_SELECTOR, mid_cursor_sel + ".active"
)
)
finally:
browser2.quit()
# ── SC2: NC reserves → PC sees card border coloured --priYl ──────────── #
@tag('channels')
def test_nc_reservation_glows_priYl_in_pc_browser(self):
"""
When NC (levity mid) clicks OK on a card, PC must see that card's border
coloured --priYl (rgb 255 207 52) via the data-reserved-by CSS selector.
Verifies: sig_reserve view → WS broadcast → applyReservation → CSS glow.
"""
room, gamers = self._setup_sig_select_room()
room_url = self.live_server_url + f"/gameboard/room/{room.pk}/"
# ── Browser 1: PC (founder) ───────────────────────────────────────────
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(room_url)
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
# ── Browser 2: NC (amigo) ─────────────────────────────────────────────
browser2 = self._make_browser2("amigo@test.io")
try:
browser2.get(room_url)
self.wait_for(lambda: browser2.find_element(By.CSS_SELECTOR, ".sig-overlay"))
# Get first card in B2's deck
first_card = browser2.find_element(By.CSS_SELECTOR, ".sig-card")
card_id = first_card.get_attribute("data-card-id")
# Click card body → .sig-focused → OK button appears
from selenium.webdriver.common.action_chains import ActionChains
ActionChains(browser2).move_to_element(first_card).perform()
first_card.click()
ok_btn = self.wait_for(
lambda: browser2.find_element(By.CSS_SELECTOR, ".sig-focused .sig-ok-btn")
)
ok_btn.click()
# ── B1 should see the card's border turn --priYl ──────────────────
reserved_card_sel = f'.sig-card[data-card-id="{card_id}"]'
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, reserved_card_sel + '[data-reserved-by="NC"]'
)
)
reserved_card = self.browser.find_element(By.CSS_SELECTOR, reserved_card_sel)
box_shadow = self.browser.execute_script(
"return window.getComputedStyle(arguments[0]).boxShadow",
reserved_card,
)
self.assertIn(
"255, 207, 52", box_shadow,
f"Expected --priYl (255,207,52) in box-shadow for NC reservation, got {box_shadow}",
)
finally:
browser2.quit()
# ── Polarity theming: qualifier text + no correspondence ─────────────────────
class SigSelectThemeTest(FunctionalTest):
"""Polarity-qualifier display (Graven/Leavened) and correspondence suppression.
No WebSocket needed — stage updates are local; uses plain FunctionalTest."""
EMAILS = [
"founder@test.io", "amigo@test.io", "bud@test.io",
"pal@test.io", "dude@test.io", "bro@test.io",
]
def setUp(self):
super().setUp()
self.browser.set_window_size(800, 1200)
Applet.objects.get_or_create(slug="new-game", defaults={"name": "New Game", "context": "gameboard"})
Applet.objects.get_or_create(slug="my-games", defaults={"name": "My Games", "context": "gameboard"})
def _setup_sig_room(self):
founder, _ = User.objects.get_or_create(email=self.EMAILS[0])
room = Room.objects.create(name="Theme Test", owner=founder)
_fill_room_via_orm(room, self.EMAILS)
_assign_all_roles(room)
return room
def _hover_card(self, css):
from selenium.webdriver.common.action_chains import ActionChains
card = self.browser.find_element(By.CSS_SELECTOR, css)
ActionChains(self.browser).move_to_element(card).perform()
return card
# ── ST1: Levity (Leavened) qualifier ──────────────────────────────────── #
def test_levity_non_major_card_shows_leavened_above(self):
"""Hovering a non-major card in the levity overlay shows 'Leavened' in
qualifier-above and nothing in qualifier-below."""
room = self._setup_sig_room()
self.create_pre_authenticated_session("founder@test.io") # PC = levity
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
self._hover_card('.sig-card[data-arcana="Middle Arcana"]')
above = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above")
)
self.assertEqual(above.text, "Leavened")
below = self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-below")
self.assertEqual(below.text, "")
def test_levity_major_card_shows_leavened_below(self):
"""Hovering a major arcana card in the levity overlay shows 'Leavened' in
qualifier-below and nothing in qualifier-above."""
room = self._setup_sig_room()
self.create_pre_authenticated_session("founder@test.io") # PC = levity
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
self._hover_card('.sig-card[data-arcana="Major Arcana"]')
below = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-below")
)
self.assertEqual(below.text, "Leavened")
above = self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above")
self.assertEqual(above.text, "")
# ── ST2: Gravity (Graven) qualifier ───────────────────────────────────── #
def test_gravity_non_major_card_shows_graven_above(self):
"""EC (bud) sees the gravity overlay; hovering a non-major card shows 'Graven'."""
room = self._setup_sig_room()
self.create_pre_authenticated_session("bud@test.io") # EC = gravity
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
self._hover_card('.sig-card[data-arcana="Middle Arcana"]')
above = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above")
)
self.assertEqual(above.text, "Graven")
# ── ST3: Correspondence not shown ─────────────────────────────────────── #
def test_correspondence_not_shown_in_sig_select(self):
"""The Minchiate-equivalence field must always be blank on the stage card."""
room = self._setup_sig_room()
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
# Hover any card — correspondence should remain empty regardless
self._hover_card(".sig-card")
self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".sig-stage-card"
))
corr = self.browser.find_element(By.CSS_SELECTOR, ".fan-card-correspondence")
self.assertEqual(corr.text, "")

View File

@@ -1,12 +1,12 @@
describe("SigSelect", () => {
let testDiv, stageCard, card;
let testDiv, stageCard, card, statBlock;
function makeFixture({ reservations = '{}' } = {}) {
function makeFixture({ reservations = '{}', cardCautions = '[]', polarity = 'levity', userRole = 'PC' } = {}) {
testDiv = document.createElement("div");
testDiv.innerHTML = `
<div class="sig-overlay"
data-polarity="levity"
data-user-role="PC"
data-polarity="${polarity}"
data-user-role="${userRole}"
data-reserve-url="/epic/room/test/sig-reserve"
data-reservations="${reservations.replace(/"/g, '&quot;')}">
<div class="sig-modal">
@@ -15,10 +15,35 @@ describe("SigSelect", () => {
<span class="fan-corner-rank"></span>
<i class="stage-suit-icon"></i>
<p class="fan-card-name-group"></p>
<p class="sig-qualifier-above"></p>
<h3 class="fan-card-name"></h3>
<p class="sig-qualifier-below"></p>
<p class="fan-card-arcana"></p>
<p class="fan-card-correspondence"></p>
</div>
<div class="sig-stat-block">
<button class="btn btn-reverse sig-flip-btn" type="button">FLIP</button>
<button class="btn btn-caution sig-caution-btn" type="button">!!</button>
<div class="stat-face stat-face--upright">
<p class="stat-face-label">Upright</p>
<ul class="stat-keywords" id="id_stat_keywords_upright"></ul>
</div>
<div class="stat-face stat-face--reversed">
<p class="stat-face-label">Reversed</p>
<ul class="stat-keywords" id="id_stat_keywords_reversed"></ul>
</div>
<button class="btn btn-nav-left sig-caution-prev" type="button">&#9664;</button>
<button class="btn btn-nav-right sig-caution-next" type="button">&#9654;</button>
<div class="sig-caution-tooltip" id="id_sig_caution">
<div class="sig-caution-header">
<h4 class="sig-caution-title">Caution!</h4>
<span class="sig-caution-type">Rival Interaction</span>
</div>
<p class="sig-caution-shoptalk">[Shoptalk forthcoming]</p>
<p class="sig-caution-effect"></p>
<span class="sig-caution-index"></span>
</div>
</div>
</div>
<div class="sig-deck-grid">
<div class="sig-card"
@@ -28,7 +53,10 @@ describe("SigSelect", () => {
data-name-group="Pentacles"
data-name-title="King of Pentacles"
data-arcana="Minor Arcana"
data-correspondence="">
data-correspondence=""
data-keywords-upright="action,impulsiveness,ambition"
data-keywords-reversed="no direction,disregard for consequences"
data-cautions="${cardCautions.replace(/"/g, '&quot;')}">
<div class="fan-card-corner fan-card-corner--tl">
<span class="fan-corner-rank">K</span>
</div>
@@ -48,6 +76,7 @@ describe("SigSelect", () => {
`;
document.body.appendChild(testDiv);
stageCard = testDiv.querySelector(".sig-stage-card");
statBlock = testDiv.querySelector(".sig-stat-block");
card = testDiv.querySelector(".sig-card");
window.fetch = jasmine.createSpy("fetch").and.returnValue(
Promise.resolve({ ok: true })
@@ -107,80 +136,6 @@ describe("SigSelect", () => {
});
});
// ── Touch: OK btn tap allows synthetic click through ──────────────── //
describe("touch on OK button", () => {
beforeEach(() => {
if (typeof TouchEvent === 'undefined') { pending('TouchEvent unavailable in desktop Firefox'); return; }
makeFixture();
});
it("touchstart on OK btn does not call preventDefault (allows synthetic click)", () => {
// First tap the card body to show OK
card.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(card.classList.contains("sig-focused")).toBe(true);
// Now tap the OK button — touchstart should NOT preventDefault
var okBtn = card.querySelector(".sig-ok-btn");
var touchEvent = new TouchEvent("touchstart", {
bubbles: true,
cancelable: true,
touches: [new Touch({ identifier: 1, target: okBtn })],
});
okBtn.dispatchEvent(touchEvent);
expect(touchEvent.defaultPrevented).toBe(false);
});
it("touchstart on card body (not OK btn) calls preventDefault", () => {
var touchEvent = new TouchEvent("touchstart", {
bubbles: true,
cancelable: true,
touches: [new Touch({ identifier: 1, target: card })],
});
card.dispatchEvent(touchEvent);
expect(touchEvent.defaultPrevented).toBe(true);
});
});
// ── Touch outside grid dismisses stage (mobile) ───────────────────── //
describe("touch outside grid", () => {
beforeEach(() => {
if (typeof TouchEvent === 'undefined') { pending('TouchEvent unavailable in desktop Firefox'); return; }
makeFixture();
});
it("dismisses stage preview when touching outside the grid (unfocused state)", () => {
// Focus a card first
card.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(stageCard.style.display).toBe("");
// Touch on the sig-stage (outside the grid)
var stage = testDiv.querySelector(".sig-stage");
stage.dispatchEvent(new TouchEvent("touchstart", {
bubbles: true,
cancelable: true,
touches: [new Touch({ identifier: 2, target: stage })],
}));
expect(stageCard.style.display).toBe("none");
expect(card.classList.contains("sig-focused")).toBe(false);
});
it("does NOT dismiss stage preview when frozen (card reserved)", () => {
card.dispatchEvent(new MouseEvent("click", { bubbles: true }));
SigSelect._setFrozen(true);
// _focusedCardEl is set but frozen — use internal state trick via _setFrozen
// We also need a focused card; simulate it by setting frozen after focus
var stage = testDiv.querySelector(".sig-stage");
stage.dispatchEvent(new TouchEvent("touchstart", {
bubbles: true,
cancelable: true,
touches: [new Touch({ identifier: 3, target: stage })],
}));
expect(stageCard.style.display).toBe("");
});
});
// ── Lock after reservation ─────────────────────────────────────────── //
describe("lock after reservation", () => {
@@ -200,18 +155,6 @@ describe("SigSelect", () => {
expect(window.fetch).not.toHaveBeenCalled();
});
it("does not call preventDefault on touchstart while a card is reserved", () => {
if (typeof TouchEvent === 'undefined') { pending('TouchEvent unavailable in desktop Firefox'); return; }
SigSelect._setReservedCardId("99");
var touchEvent = new TouchEvent("touchstart", {
bubbles: true,
cancelable: true,
touches: [new Touch({ identifier: 1, target: card })],
});
card.dispatchEvent(touchEvent);
expect(touchEvent.defaultPrevented).toBe(false);
});
it("allows focus again after reservation is cleared", () => {
SigSelect._setReservedCardId("99");
SigSelect._setReservedCardId(null);
@@ -252,4 +195,414 @@ describe("SigSelect", () => {
expect(card.classList.contains("sig-focused")).toBe(true);
});
});
// ── Caution tooltip (!!) ──────────────────────────────────────────── //
describe("caution tooltip", () => {
var cautionTooltip, cautionEffect, cautionPrev, cautionNext, cautionBtn;
beforeEach(() => {
makeFixture();
cautionTooltip = testDiv.querySelector(".sig-caution-tooltip");
cautionEffect = testDiv.querySelector(".sig-caution-effect");
cautionPrev = testDiv.querySelector(".sig-caution-prev");
cautionNext = testDiv.querySelector(".sig-caution-next");
cautionBtn = testDiv.querySelector(".sig-caution-btn");
});
function hover() {
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
}
function openCaution() {
hover();
cautionBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
}
it("!! click adds .sig-caution-open to the stage", () => {
openCaution();
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
});
it("FYI click when btn-disabled does not close caution", () => {
openCaution();
expect(cautionBtn.classList.contains("btn-disabled")).toBe(true);
cautionBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
});
it("shows placeholder text when cautions list is empty", () => {
card.dataset.cautions = "[]";
openCaution();
expect(cautionEffect.innerHTML).toContain("pending");
});
it("renders first caution effect HTML including .card-ref spans", () => {
card.dataset.cautions = JSON.stringify(['First <span class="card-ref">Card</span> effect.']);
openCaution();
expect(cautionEffect.querySelector(".card-ref")).not.toBeNull();
expect(cautionEffect.querySelector(".card-ref").textContent).toBe("Card");
});
it("with 1 caution both nav arrows are disabled", () => {
card.dataset.cautions = JSON.stringify(["Single caution."]);
openCaution();
expect(cautionPrev.disabled).toBe(true);
expect(cautionNext.disabled).toBe(true);
});
it("with multiple cautions both nav arrows are always enabled", () => {
card.dataset.cautions = JSON.stringify(["C1", "C2", "C3", "C4"]);
openCaution();
expect(cautionPrev.disabled).toBe(false);
expect(cautionNext.disabled).toBe(false);
});
it("next click advances to second caution", () => {
card.dataset.cautions = JSON.stringify(["First", "Second"]);
openCaution();
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(cautionEffect.innerHTML).toContain("Second");
});
it("next wraps from last caution back to first", () => {
card.dataset.cautions = JSON.stringify(["First", "Last"]);
openCaution();
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(cautionEffect.innerHTML).toContain("First");
});
it("prev click goes back to first caution", () => {
card.dataset.cautions = JSON.stringify(["First", "Second"]);
openCaution();
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
cautionPrev.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(cautionEffect.innerHTML).toContain("First");
});
it("prev wraps from first caution to last", () => {
card.dataset.cautions = JSON.stringify(["First", "Middle", "Last"]);
openCaution();
cautionPrev.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(cautionEffect.innerHTML).toContain("Last");
});
it("index label shows n / total when multiple cautions", () => {
card.dataset.cautions = JSON.stringify(["C1", "C2", "C3"]);
openCaution();
expect(testDiv.querySelector(".sig-caution-index").textContent).toBe("1 / 3");
});
it("index label is empty when only 1 caution", () => {
card.dataset.cautions = JSON.stringify(["Only one."]);
openCaution();
expect(testDiv.querySelector(".sig-caution-index").textContent).toBe("");
});
it("card mouseleave closes the caution", () => {
openCaution();
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(false);
});
it("opening again resets to first caution", () => {
card.dataset.cautions = JSON.stringify(["First", "Second"]);
openCaution();
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
// Close and reopen
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
openCaution();
expect(cautionEffect.innerHTML).toContain("First");
});
it("opening caution adds .btn-disabled and swaps labels to ×", () => {
openCaution();
var flipBtn = testDiv.querySelector(".sig-flip-btn");
expect(flipBtn.classList.contains("btn-disabled")).toBe(true);
expect(cautionBtn.classList.contains("btn-disabled")).toBe(true);
expect(flipBtn.textContent).toBe("\u00D7");
expect(cautionBtn.textContent).toBe("\u00D7");
});
it("closing caution removes .btn-disabled and restores original labels", () => {
var flipBtn = testDiv.querySelector(".sig-flip-btn");
var origFlip = flipBtn.textContent;
var origCaution = cautionBtn.textContent;
openCaution();
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
expect(flipBtn.classList.contains("btn-disabled")).toBe(false);
expect(cautionBtn.classList.contains("btn-disabled")).toBe(false);
expect(flipBtn.textContent).toBe(origFlip);
expect(cautionBtn.textContent).toBe(origCaution);
});
it("clicking the tooltip closes caution", () => {
openCaution();
cautionEffect.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(false);
});
it("FLIP click when caution open (btn-disabled) does nothing", () => {
openCaution();
var flipBtn = testDiv.querySelector(".sig-flip-btn");
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
expect(statBlock.classList.contains("is-reversed")).toBe(false);
});
});
// ── Stat block: keyword population and FLIP toggle ────────────────── //
describe("stat block and FLIP", () => {
beforeEach(() => makeFixture());
it("populates upright keywords when a card is hovered", () => {
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
var items = statBlock.querySelectorAll("#id_stat_keywords_upright li");
expect(items.length).toBe(3);
expect(items[0].textContent).toBe("action");
expect(items[1].textContent).toBe("impulsiveness");
expect(items[2].textContent).toBe("ambition");
});
it("populates reversed keywords when a card is hovered", () => {
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
var items = statBlock.querySelectorAll("#id_stat_keywords_reversed li");
expect(items.length).toBe(2);
expect(items[0].textContent).toBe("no direction");
expect(items[1].textContent).toBe("disregard for consequences");
});
it("FLIP click adds .is-reversed to the stat block", () => {
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
var flipBtn = statBlock.querySelector(".sig-flip-btn");
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(statBlock.classList.contains("is-reversed")).toBe(true);
});
it("second FLIP click removes .is-reversed", () => {
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
var flipBtn = statBlock.querySelector(".sig-flip-btn");
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(statBlock.classList.contains("is-reversed")).toBe(false);
});
it("hovering a new card resets .is-reversed", () => {
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
statBlock.querySelector(".sig-flip-btn").dispatchEvent(
new MouseEvent("click", { bubbles: true })
);
expect(statBlock.classList.contains("is-reversed")).toBe(true);
// Leave and re-enter (simulates moving to a different card)
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(statBlock.classList.contains("is-reversed")).toBe(false);
});
it("card with no keywords yields empty lists", () => {
card.dataset.keywordsUpright = "";
card.dataset.keywordsReversed = "";
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(statBlock.querySelectorAll("#id_stat_keywords_upright li").length).toBe(0);
expect(statBlock.querySelectorAll("#id_stat_keywords_reversed li").length).toBe(0);
});
});
// ── WS cursor hover (applyHover) ──────────────────────────────────────── //
//
// Fixture polarity = levity, userRole = PC.
// POLARITY_ROLES: levity → [PC, NC, SC] = [left, mid, right]
//
// Only tests the JS position mapping — colour is CSS-only.
describe("WS cursor hover", () => {
beforeEach(() => makeFixture());
it("NC hover activates the --mid cursor", () => {
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "NC", active: true },
}));
expect(card.querySelector(".sig-cursor--mid").classList.contains("active")).toBe(true);
});
it("SC hover activates the --right cursor", () => {
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "SC", active: true },
}));
expect(card.querySelector(".sig-cursor--right").classList.contains("active")).toBe(true);
});
it("own role (PC) hover event is ignored — no cursor activates", () => {
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "PC", active: true },
}));
expect(card.querySelectorAll(".sig-cursor.active").length).toBe(0);
});
it("hover-off removes .active from the cursor", () => {
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "NC", active: true },
}));
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "NC", active: false },
}));
expect(card.querySelector(".sig-cursor--mid").classList.contains("active")).toBe(false);
});
it("hover on unknown card_id is a no-op", () => {
expect(() => {
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 9999, role: "NC", active: true },
}));
}).not.toThrow();
});
});
// ── WS reservation — data-reserved-by attribute ───────────────────────── //
//
// applyReservation() sets data-reserved-by so the CSS can glow the card in
// the reserving gamer's role colour. These tests assert the attribute, not
// the colour (CSS variables aren't resolvable in the SpecRunner context).
describe("WS reservation sets data-reserved-by", () => {
beforeEach(() => makeFixture());
it("peer reservation sets data-reserved-by to the reserving role", () => {
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: true },
}));
expect(card.dataset.reservedBy).toBe("NC");
});
it("peer reservation also adds .sig-reserved class", () => {
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: true },
}));
expect(card.classList.contains("sig-reserved")).toBe(true);
});
it("release removes data-reserved-by", () => {
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: true },
}));
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: false },
}));
expect(card.dataset.reservedBy).toBeUndefined();
});
it("own reservation (PC) sets data-reserved-by AND .sig-reserved--own", () => {
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "PC", reserved: true },
}));
expect(card.dataset.reservedBy).toBe("PC");
expect(card.classList.contains("sig-reserved--own")).toBe(true);
});
it("peer reservation places a thumbs-up float and removes any hand-pointer float", () => {
// First, a hover float exists for NC (mid cursor)
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "NC", active: true },
}));
expect(document.querySelector('.sig-cursor-float[data-role="NC"]')).not.toBeNull();
expect(document.querySelector('.fa-hand-pointer[data-role="NC"]')).not.toBeNull();
// NC then clicks OK — reservation arrives
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: true },
}));
// Thumbs-up replaces hand-pointer
const floatEl = document.querySelector('.sig-cursor-float[data-role="NC"]');
expect(floatEl).not.toBeNull();
expect(floatEl.classList.contains("fa-thumbs-up")).toBe(true);
expect(floatEl.classList.contains("fa-hand-pointer")).toBe(false);
});
it("peer release removes the thumbs-up float", () => {
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: true },
}));
expect(document.querySelector('.sig-cursor-float--reserved[data-role="NC"]')).not.toBeNull();
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: false },
}));
expect(document.querySelector('.sig-cursor-float--reserved[data-role="NC"]')).toBeNull();
});
});
// ── Polarity theming — stage qualifier text ────────────────────────────── //
//
// On mouseenter, updateStage() injects "Leavened" or "Graven" into the
// sig-qualifier-above (non-major) or sig-qualifier-below (major arcana) slot.
// Correspondence field is never populated in sig-select context.
describe("polarity theming — stage qualifier", () => {
it("levity non-major card puts 'Leavened' in qualifier-above, qualifier-below empty", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
// data-arcana defaults to "Minor Arcana" in fixture → non-major
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("Leavened");
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("");
});
it("levity major arcana card puts 'Leavened' in qualifier-below, qualifier-above empty", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
card.dataset.arcana = "Major Arcana";
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("");
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("Leavened");
});
it("major arcana title gets a trailing comma (qualifier reads as subtitle)", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
card.dataset.arcana = "Major Arcana";
card.dataset.nameTitle = "The Schizo";
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".fan-card-name").textContent).toBe("The Schizo,");
});
it("non-major arcana title has no trailing comma", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
// fixture default: Minor Arcana, "King of Pentacles"
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".fan-card-name").textContent).toBe("King of Pentacles");
});
it("gravity non-major card puts 'Graven' in qualifier-above", () => {
makeFixture({ polarity: 'gravity', userRole: 'BC' });
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("Graven");
});
it("gravity major arcana card puts 'Graven' in qualifier-below", () => {
makeFixture({ polarity: 'gravity', userRole: 'BC' });
card.dataset.arcana = "Major Arcana";
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("Graven");
});
it("hovering clears qualifier slots from the previous card", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
card.dataset.arcana = "Major Arcana";
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
// Now major — above should be empty, below filled
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("");
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("Leavened");
});
it("correspondence field is never populated", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
card.dataset.correspondence = "Il Bagatto (Minchiate)";
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".fan-card-correspondence").textContent).toBe("");
});
});
});

View File

@@ -110,7 +110,8 @@
}
// In landscape: shift gear btn and applet menus left of the footer right sidebar
@media (orientation: landscape) and (max-width: 1440px) {
// XL override below doubles sidebar to 8rem — centre items in the wider column.
@media (orientation: landscape) {
$sidebar-w: 4rem;
.gameboard-page,
@@ -119,7 +120,7 @@
.room-page,
.billboard-page {
> .gear-btn {
right: 0.5rem;
right: 1rem;
bottom: 3.95rem; // same gap above kit btn as portrait; no page-specific overrides needed
top: auto;
}
@@ -131,12 +132,30 @@
#id_wallet_applet_menu,
#id_room_menu,
#id_billboard_applet_menu {
right: 0.5rem;
right: 1rem;
bottom: 6.6rem;
top: auto;
}
}
@media (orientation: landscape) and (min-width: 1800px) {
// Centre gear btn and menus in the doubled 8rem sidebar (was 0.5rem from right edge)
.gameboard-page,
.dashboard-page,
.wallet-page,
.room-page,
.billboard-page {
> .gear-btn { right: 2.5rem; }
}
#id_dash_applet_menu,
#id_game_applet_menu,
#id_game_kit_menu,
#id_wallet_applet_menu,
#id_room_menu,
#id_billboard_applet_menu { right: 2.5rem; }
}
// ── Applet box visual shell (reusable outside the grid) ────
%applet-box {
border:
@@ -215,6 +234,16 @@
black 99%,
transparent 100%
);
margin-left: 1rem;
margin-top: 1rem;
@media (orientation: landscape) and (min-width: 900px) {
margin-left: 2rem;
margin-top: 2rem;
}
@media (orientation: landscape) and (min-width: 1800px) {
margin-left: 4rem;
margin-top: 4rem;
}
section {
@extend %applet-box;

View File

@@ -35,6 +35,7 @@ body {
}
}
.container-fluid {
display: flex;
align-items: center;
@@ -192,8 +193,18 @@ body {
}
}
@media (orientation: landscape) and (max-width: 1440px) {
$sidebar-w: 4rem;
@media (orientation: landscape) and (max-width: 1100px) {
body .container {
.navbar {
h1 {
font-size: 1rem !important;
}
}
}
}
@media (orientation: landscape) {
$sidebar-w: 5rem;
// ── Sidebar layout: navbar ← left, footer → right ────────────────────────────
body {
@@ -264,13 +275,12 @@ body {
.navbar-label { opacity: 0.7; }
}
.btn-primary {
width: 3rem;
height: 3rem;
font-size: 0.75rem;
border-width: 0.125rem;
// margin-left: 0.75rem;
}
// .btn-primary {
// width: 4rem;
// height: 4rem;
// font-size: 0.875rem;
// border-width: 0.21rem;
// }
// Login form: offset from fixed sidebars in landscape
.input-group {
@@ -288,26 +298,35 @@ body {
}
}
// Container: fill center, compensate for fixed sidebars on both sides
// Container: fill center, compensate for fixed sidebars on both sides.
// max-width: none overrides the @media (min-width: 1200px) rule above so the
// container fills all available space between the two sidebars on wide screens.
body .container {
flex: 1;
min-width: 0;
max-width: none;
margin-left: $sidebar-w;
margin-right: $sidebar-w;
padding: 0 0.5rem;
}
// Header row: compact in landscape
// Header row: h2 rotates into the left gutter (just right of the navbar border).
// position:fixed takes h2 out of flow; .row collapses to zero height automatically.
body .container .row {
padding: 0.25rem 0;
.col-lg-6 h2 {
font-size: 1.5rem;
margin: 0 0 0.25rem;
letter-spacing: 0.4em;
text-align: center;
text-align-last: left;
padding: 0;
margin: 0;
}
body .container .row .col-lg-6 h2 {
position: fixed;
left: 5rem; // $sidebar-w — flush with the navbar right border
top: 50%;
transform: translateY(-50%) rotate(180deg);
writing-mode: vertical-rl;
font-size: 1.5rem;
letter-spacing: 0.4em;
margin: 0;
z-index: 85;
pointer-events: none;
}
// Footer → fixed right sidebar (mirrors navbar approach — explicit right boundary)
@@ -324,6 +343,7 @@ body {
align-items: center;
border-top: none;
border-left: 0.1rem solid rgba(var(--secUser), 0.3);
background-color: rgba(var(--priUser), 1); // opaque: masks tray sliding behind it
padding: 1rem 0;
gap: 0;
z-index: 100;
@@ -332,7 +352,8 @@ body {
flex-direction: column-reverse;
width: auto;
max-width: none;
gap: 3rem;
gap: 1.5rem !important;
margin-bottom: 4rem;
a {
font-size: 1.75rem;
@@ -344,13 +365,104 @@ body {
.footer-container {
position: absolute;
bottom: 0.75rem;
top: 0.25rem;
text-align: center;
font-size: 0.55rem;
line-height: 1.4;
color: rgba(var(--secUser), 0.5);
line-height: 0.75 !important;
color: rgba(var(--secUser), 1);
br { display: block; }
small {
font-size: 0.75rem !important;
}
}
}
}
@media (orientation: landscape) and (min-width: 700px) {
body .container .row .col-lg-6 h2 {
@media (min-height: 400px) {
font-size: 2.5rem;
}
@media (min-height: 500px) {
font-size: 3rem;
}
}
body #id_footer {
#id_footer_nav {
gap: 3rem !important;
a {
font-size: 1.75rem;
display: flex;
justify-content: center;
align-items: center;
}
}
.footer-container {
line-height: 1;
margin-top: 0.5rem;
small {
font-size: 1rem;
}
}
}
}
// ── XL landscape (≥1800px): double sidebar widths and scale content ────────────
@media (orientation: landscape) and (min-width: 1800px) {
$sidebar-xl: 8rem;
body .container .navbar {
width: $sidebar-xl;
.container-fluid {
gap: 2rem;
padding: 0 0.5rem;
}
.navbar-brand h1 { font-size: 2.4rem; }
.navbar-text { font-size: 0.78rem; } // 0.65rem × 1.2
// .btn-primary { width: 4rem; height: 4rem; font-size: 0.875rem; }
.input-group {
left: $sidebar-xl;
right: $sidebar-xl;
}
}
body .container {
margin-left: $sidebar-xl;
margin-right: $sidebar-xl;
}
// h2 page title: keep vertical rotation; shift left to clear the wider XL navbar.
body .container .row .col-lg-6 h2 {
left: 8rem; // $sidebar-xl
@media (min-height: 800px) {
font-size: 4.5rem;
}
}
body #id_footer {
width: $sidebar-xl;
#id_footer_nav {
gap: 8rem !important;
a { font-size: 3rem; }
}
.footer-container {
font-size: 0.85rem;
margin-top: 1rem;
small {
font-size: 1.2rem;
}
}
}
}
@@ -363,13 +475,6 @@ body {
.navbar-brand h1 {
font-size: 1.2rem;
}
.btn-primary {
width: 3rem;
height: 3rem;
font-size: 0.75rem;
border-width: 0.125rem;
}
}
.row .col-lg-6 h2 {
@@ -436,8 +541,8 @@ body {
br { display: none; }
small {
font-size: 0.7rem;
opacity: 0.6;
font-size: 0.75rem;
opacity: 1;
}
}
}

View File

@@ -25,6 +25,10 @@
}
&.btn-primary {
width: 4rem;
height: 4rem;
font-size: 0.875rem;
border-width: 0.21rem;
color: rgba(var(--quaUser), 1);
border-color: rgba(var(--quaUser), 1);
background-color: rgba(var(--quiUser), 1);
@@ -34,37 +38,6 @@
0.25rem 0.25rem 0.25rem rgba(var(--quiUser), 0.12)
;
&:hover {
text-shadow:
0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25),
0 0 1rem rgba(var(--quaUser), 1)
;
box-shadow:
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--quaUser), 0.12)
;
}
&:active {
border: 0.18rem solid rgba(var(--quaUser), 1);
text-shadow:
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25),
0 0 0.12rem rgba(var(--quaUser), 1)
;
box-shadow:
-0.1rem -0.1rem 0.12rem rgba(var(--quiUser), 0.25),
-0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--quaUser), 0.12)
;
}
}
&.btn-xl {
width: 4rem;
height: 4rem;
font-size: 0.875rem;
border-width: 0.21rem;
&:hover {
text-shadow:
0.2rem 0.2rem 0.2rem rgba(0, 0, 0, 0.25),
@@ -72,7 +45,7 @@
;
box-shadow:
0.24rem 0.24rem 0.5rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--quaUser), 22)
0 0 0.5rem rgba(var(--quaUser), 0.22)
;
}
@@ -88,6 +61,13 @@
0 0 0.5rem rgba(var(--quaUser), 0.22)
;
}
@media (orientation: landscape) and (max-width: 1100px) {
width: 2.75rem !important;
height: 2.75rem !important;
font-size: 0.625rem !important;
border-width: 0.125rem !important;
}
}
&.btn-abandon {
@@ -300,13 +280,20 @@
}
}
@media (orientation: landscape) and (min-width: 1800px) {
width: 2.4rem; // 2rem × 1.2
height: 2.4rem;
font-size: 0.75rem; // 0.63rem × 1.2
}
&.btn-disabled {
cursor: default !important;
pointer-events: none;
font-size: 1.2rem;
padding-bottom: 0.1rem;
color: rgba(var(--secUser), 0.25);
background-color: rgba(var(--priUser), 1);
border-color: rgba(var(--secUser), 0.25);
color: rgba(var(--secUser), 0.25) !important;
background-color: rgba(var(--priUser), 1) !important;
border-color: rgba(var(--secUser), 0.25) !important;
box-shadow:
0.1rem 0.1rem 0.12rem rgba(var(--priUser), 0.5),
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
@@ -336,4 +323,144 @@
;
}
}
&.btn-nav-left {
color: rgba(var(--priFs), 1);
border-color: rgba(var(--priFs), 1);
background-color: rgba(var(--terFs), 1);
box-shadow:
0.1rem 0.1rem 0.12rem rgba(var(--terFs), 0.25),
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
0.25rem 0.25rem 0.25rem rgba(var(--terFs), 0.12)
;
&:hover {
text-shadow:
0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25),
0 0 1rem rgba(var(--priFs), 1)
;
box-shadow:
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--priFs), 0.12)
;
}
&:active {
border: 0.18rem solid rgba(var(--priFs), 1);
text-shadow:
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25),
0 0 0.12rem rgba(var(--priFs), 1)
;
box-shadow:
-0.1rem -0.1rem 0.12rem rgba(var(--terFs), 0.25),
-0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--priFs), 0.12)
;
}
}
&.btn-nav-right {
color: rgba(var(--priLm), 1);
border-color: rgba(var(--priLm), 1);
background-color: rgba(var(--terLm), 1);
box-shadow:
0.1rem 0.1rem 0.12rem rgba(var(--terLm), 0.25),
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
0.25rem 0.25rem 0.25rem rgba(var(--terLm), 0.12)
;
&:hover {
text-shadow:
0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25),
0 0 1rem rgba(var(--priLm), 1)
;
box-shadow:
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--priLm), 0.12)
;
}
&:active {
border: 0.18rem solid rgba(var(--priLm), 1);
text-shadow:
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25),
0 0 0.12rem rgba(var(--priLm), 1)
;
box-shadow:
-0.1rem -0.1rem 0.12rem rgba(var(--terLm), 0.25),
-0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--priLm), 0.12)
;
}
}
&.btn-reverse {
color: rgba(var(--priCy), 1);
border-color: rgba(var(--priCy), 1);
background-color: rgba(var(--terCy), 1);
box-shadow:
0.1rem 0.1rem 0.12rem rgba(var(--terCy), 0.25),
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
0.25rem 0.25rem 0.25rem rgba(var(--terCy), 0.12)
;
&:hover {
text-shadow:
0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25),
0 0 1rem rgba(var(--priCy), 1)
;
box-shadow:
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--priCy), 0.12)
;
}
&:active {
border: 0.18rem solid rgba(var(--priCy), 1);
text-shadow:
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25),
0 0 0.12rem rgba(var(--priCy), 1)
;
box-shadow:
-0.1rem -0.1rem 0.12rem rgba(var(--terCy), 0.25),
-0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--priCy), 0.12)
;
}
}
&.btn-tip {
color: rgba(var(--priLm), 1);
border-color: rgba(var(--priLm), 1);
background-color: rgba(var(--terLm), 1);
box-shadow:
0.1rem 0.1rem 0.12rem rgba(var(--terLm), 0.25),
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
0.25rem 0.25rem 0.25rem rgba(var(--terLm), 0.12)
;
&:hover {
text-shadow:
0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25),
0 0 1rem rgba(var(--priLm), 1)
;
box-shadow:
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--priLm), 0.12)
;
}
&:active {
border: 0.18rem solid rgba(var(--priLm), 1);
text-shadow:
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25),
0 0 0.12rem rgba(var(--priLm), 1)
;
box-shadow:
-0.1rem -0.1rem 0.12rem rgba(var(--terLm), 0.25),
-0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--priLm), 0.12)
;
}
}
}

View File

@@ -0,0 +1,672 @@
// ─── Card deck primitives — fan cards + sig-select overlay ─────────────────────
//
// Shared card display classes (.fan-card, .fan-card-corner, .fan-card-face, .fan-nav)
// extracted from _game-kit.scss; sig-select overlay extracted from _room.scss.
// ── Tarot fan modal ──────────────────────────────────────────────────────────
#id_tarot_fan_dialog {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
max-width: none;
max-height: none;
margin: 0;
padding: 0;
border: none;
background: rgba(0, 0, 0, 0.88);
overflow: hidden;
&::backdrop { display: none; } // Dialog IS the backdrop
}
.tarot-fan-wrap {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
perspective: 900px;
button {
box-shadow: none;
&:hover, &.active {
box-shadow: none;
}
}
}
.tarot-fan {
position: relative;
width: 220px;
height: 340px;
}
.fan-card {
position: absolute;
inset: 0;
width: 220px;
height: 340px;
border-radius: 0.75rem;
background: rgba(var(--priUser), 1);
border: 0.1rem solid rgba(var(--secUser), 0.4);
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.25s ease, opacity 0.25s ease;
transform-style: preserve-3d;
&--active {
border-color: rgba(var(--secUser), 1);
box-shadow: 0 0 2rem rgba(var(--secUser), 0.3);
}
}
.fan-card-corner {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.15rem;
line-height: 1;
color: rgba(var(--secUser), 0.75);
&--tl { top: 0.4rem; left: 0.4rem; }
&--br { bottom: 0.4rem; right: 0.4rem; transform: rotate(180deg); }
.fan-corner-rank {
font-size: 1.5rem;
font-weight: bold;
padding: 0.18rem 0;
}
i { font-size: 1.5rem; }
}
.fan-card-face {
padding: 1.25rem;
text-align: center;
display: flex;
flex-direction: column;
gap: 0.5rem;
.fan-card-number { font-size: 0.65rem; }
.fan-card-name-group { font-size: 0.65rem; margin: 0; text-transform: uppercase; letter-spacing: 0.08em; color: rgba(var(--secUser), 1); }
.fan-card-name { font-size: 0.95rem; font-weight: bold; margin: 0; color: rgba(var(--terUser), 1); }
.fan-card-arcana { font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.1em; color: rgba(var(--secUser), 1); }
.fan-card-correspondence { font-size: 0.6rem; font-style: italic; color: rgba(var(--secUser), 0.5); }
}
.fan-nav {
position: absolute;
z-index: 20;
font-size: 3rem;
line-height: 1;
background: none;
border: none;
color: rgba(var(--secUser), 0.6);
cursor: pointer;
padding: 1rem;
transition: color 0.15s;
pointer-events: auto;
&:hover { color: rgba(var(--secUser), 1); }
// Suppress browser focus ring on mouse/touch clicks; retain it for keyboard nav
&:focus:not(:focus-visible) { outline: none; box-shadow: none; }
&--prev { left: 1rem; }
&--next { right: 1rem; }
}
// ─── Sig Select overlay (SIG_SELECT phase) ────────────────────────────────────
//
// Two overlays (levity / gravity) run in parallel, one per polarity group.
// Layout mirrors the gatekeeper: dark Gaussian backdrop + centred modal.
// Inside the modal: upper stage (card preview) + lower mini card grid (no scroll).
html:has(.sig-backdrop) {
overflow: hidden;
}
.sig-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(5px);
z-index: 100;
pointer-events: none;
}
.sig-overlay {
position: fixed;
inset: 0;
display: flex;
align-items: stretch;
justify-content: center;
z-index: 120;
pointer-events: none;
}
.sig-modal {
pointer-events: auto;
display: flex;
flex-direction: column;
width: 100%; // respects overlay padding-right set by JS
max-width: 420px;
max-height: 100%; // respects overlay padding-bottom set by JS
}
// ─── Stage ────────────────────────────────────────────────────────────────────
// flex: 1 — fills all space above the card grid; no background (backdrop blur).
// Row layout: preview card bottom-left, stat block fills the right.
// Card width is set by sizeSigCard() in room.js (smaller of 40% stage width or
// 80% stage height × 5/8) via --sig-card-w CSS variable — libsass can't handle
// container query units inside min().
.sig-stage {
flex: 1;
min-height: 0;
position: relative;
display: flex;
flex-direction: row;
align-items: flex-end;
padding-left: 1.5rem;
gap: 0.75rem;
// Preview card — width driven by JS via --sig-card-w; aspect-ratio derives height.
.sig-stage-card {
flex-shrink: 0;
width: var(--sig-card-w, 120px);
height: auto;
aspect-ratio: 5 / 8;
border-radius: 0.5rem;
background: rgba(var(--priUser), 1);
border: 0.15rem solid rgba(var(--secUser), 0.6);
display: flex;
flex-direction: column;
position: relative;
padding: 0.25rem;
overflow: hidden;
// game-kit sets .fan-card-corner { position: absolute; top/left offsets }
// so these just need display/font overrides; the corners land at the card edges.
// All font-sizes scale with --sig-card-w (ratio = original-rem × 16 / 120).
.fan-card-corner--tl {
display: flex;
flex-direction: column;
align-items: center;
line-height: 1.1;
gap: 0.1rem;
.fan-corner-rank { font-size: calc(var(--sig-card-w, 120px) * 0.133); font-weight: 700; }
i { font-size: calc(var(--sig-card-w, 120px) * 0.1); }
}
.fan-card-corner--br {
display: flex;
flex-direction: column;
align-items: center;
line-height: 1.1;
gap: 0.1rem;
.fan-corner-rank { font-size: calc(var(--sig-card-w, 120px) * 0.12); font-weight: 700; }
i { font-size: calc(var(--sig-card-w, 120px) * 0.1); }
}
.fan-card-face {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 0.25rem 0.15rem;
gap: 0.2rem;
.fan-card-name-group { font-size: calc(var(--sig-card-w, 120px) * 0.073); opacity: 0.6; }
.sig-qualifier-above,
.sig-qualifier-below { font-size: calc(var(--sig-card-w, 120px) * 0.093); font-weight: 600; }
.fan-card-name { font-size: calc(var(--sig-card-w, 120px) * 0.093); font-weight: 600; }
.fan-card-arcana { font-size: calc(var(--sig-card-w, 120px) * 0.067); text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.5; }
.fan-card-correspondence{ display: none; } // Minchiate equivalence shown in game-kit only
}
}
// Stat block — same dimensions as the preview card (width × 5:8 aspect).
// flex: 0 0 auto so it doesn't stretch to fill the stage; the rest of the
// stage row is simply empty, giving the card room to breathe.
.sig-stat-block {
flex: 0 0 auto;
width: var(--sig-card-w, 120px);
height: calc(var(--sig-card-w, 120px) * 8 / 5);
align-self: flex-end;
background: rgba(var(--priUser), 0.5);
border-radius: 0.4rem;
border: 0.1rem solid rgba(var(--terUser), 0.15);
display: none;
position: relative;
.sig-flip-btn {
position: absolute;
top: -1rem;
right: -1rem;
margin: 0;
z-index: 50;
}
.sig-caution-btn {
position: absolute;
top: 1.25rem;
right: -1rem;
margin: 0;
z-index: 50;
}
// Caution tooltip — covers the entire stat block (inset: 0), z-index above buttons.
.sig-caution-tooltip {
display: none;
position: absolute;
inset: 0;
z-index: 60;
background-color: rgba(var(--tooltip-bg), 0.6);
backdrop-filter: blur(6px);
border-radius: 0.4rem;
border: 0.1rem solid rgba(var(--priYl), 0.35);
padding: 0.75rem;
flex-direction: column;
gap: 0.4rem;
overflow-y: auto;
}
.sig-caution-header {
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.sig-caution-title {
font-size: calc(var(--sig-card-w, 120px) * 0.093);
font-weight: 700;
margin: 0;
color: rgba(var(--priYl), 1);
}
.sig-caution-type {
font-size: calc(var(--sig-card-w, 120px) * 0.058);
opacity: 0.7;
text-transform: uppercase;
letter-spacing: 0.05em;
flex-shrink: 0;
}
.sig-caution-shoptalk {
font-size: calc(var(--sig-card-w, 120px) * 0.063);
opacity: 0.55;
margin: 0;
font-style: italic;
}
.sig-caution-effect {
flex: 1;
font-size: calc(var(--sig-card-w, 120px) * 0.075);
margin: 0;
line-height: 1.55;
.card-ref {
color: rgba(var(--terUser), 1);
font-weight: 600;
}
}
.sig-caution-index {
font-size: calc(var(--sig-card-w, 120px) * 0.063);
opacity: 0.55;
}
// Nav arrows portaled out of tooltip — sit at bottom corners above tooltip (z-70)
.sig-caution-prev,
.sig-caution-next {
display: none;
position: absolute;
bottom: -1rem;
margin: 0;
z-index: 70;
}
.sig-caution-prev { left: -1rem; }
.sig-caution-next { right: -1rem; }
.stat-face {
display: none;
padding: calc(var(--sig-card-w, 120px) * 0.37) calc(var(--sig-card-w, 120px) * 0.1) calc(var(--sig-card-w, 120px) * 0.08);
&--upright { display: block; }
}
&.is-reversed {
.stat-face--upright { display: none; }
.stat-face--reversed { display: block; }
}
.stat-face-label {
font-size: calc(var(--sig-card-w, 120px) * 0.063);
text-transform: uppercase;
letter-spacing: 0.09em;
opacity: 0.4;
margin: 0 0 calc(var(--sig-card-w, 120px) * 0.07);
}
.stat-keywords {
list-style: none;
padding: 0;
margin: 0;
li {
font-size: calc(var(--sig-card-w, 120px) * 0.083);
padding: calc(var(--sig-card-w, 120px) * 0.042) 0;
opacity: 0.85;
border-bottom: 0.05rem solid rgba(var(--terUser), 0.12);
&:last-child { border-bottom: none; }
}
}
}
&.sig-stage--frozen .sig-stat-block { display: block; }
&.sig-caution-open .sig-stat-block {
.sig-caution-tooltip { display: flex; }
.sig-caution-prev, .sig-caution-next { display: inline-flex; }
}
}
// ─── Mini card grid ───────────────────────────────────────────────────────────
// flex: 0 0 auto — shrinks to card content; no background (backdrop blur).
// align-content: start prevents CSS grid from distributing extra height between rows.
.sig-deck-grid {
flex: 0 0 auto;
display: grid;
grid-template-columns: repeat(6, 1fr);
align-content: start;
gap: 2px;
padding: 4px;
overflow: hidden;
margin: 0 1rem 5rem 4rem;
}
.sig-card {
aspect-ratio: 5 / 8;
border-radius: 0.4rem;
background: rgba(var(--priUser), 0.97);
border: 1px solid rgba(var(--secUser), 0.3);
position: relative;
cursor: grab;
transition: border-color 0.15s, box-shadow 0.15s;
overflow: hidden;
// game-kit sets .fan-card-corner { position:absolute; top:0.4rem; left:0.4rem }
// Override: center the element within the card instead.
.fan-card-corner--tl {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
gap: 0; // game-kit has gap:0.15rem — too large at 0.5rem font-size
.fan-corner-rank { font-size: 1rem; font-weight: 700; }
i { font-size: 0.75rem; }
}
// OK / NVM overlay — appears on click (focused) or own reservation
.sig-card-actions {
position: absolute;
inset: 0;
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
background: rgba(var(--priUser), 0.92);
border-radius: inherit;
.sig-nvm-btn { display: none; }
}
&.sig-focused .sig-card-actions { display: flex; }
&.sig-reserved--own .sig-card-actions {
display: flex;
.sig-ok-btn { display: none; }
.sig-nvm-btn { display: flex; }
}
// Cursor strip — hangs below the card bottom edge; overflow: visible allows this.
.sig-card-cursors {
position: absolute;
bottom: -0.6rem;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
padding: 0 2px;
}
// Rise above DOM-order siblings when a peer's cursor is active on this card.
// Without this, later cards in the grid paint over the overflowing cursor icons.
&:has(.sig-cursor.active) { z-index: 5; }
&:hover:not([data-reserved-by]) {
border-color: rgba(var(--secUser), 0.8);
box-shadow: 0 0 4px rgba(var(--secUser), 0.25);
}
&.sig-reserved {
cursor: not-allowed;
}
// Role-coloured reservation glow — border/shadow matches the reserving gamer's role.
// data-reserved-by is set by applyReservation() in sig-select.js.
// Own reservation also shows role colour (same as peers see), not a separate style.
&.sig-reserved {
&[data-reserved-by="PC"] { border-color: rgba(var(--priRd), 1); box-shadow: 0 0 0 2px rgba(var(--priRd), 1); }
&[data-reserved-by="NC"] { border-color: rgba(var(--priYl), 1); box-shadow: 0 0 0 2px rgba(var(--priYl), 1); }
&[data-reserved-by="EC"] { border-color: rgba(var(--priGn), 1); box-shadow: 0 0 0 2px rgba(var(--priGn), 1); }
&[data-reserved-by="SC"] { border-color: rgba(var(--priCy), 1); box-shadow: 0 0 0 2px rgba(var(--priCy), 1); }
&[data-reserved-by="AC"] { border-color: rgba(var(--priId), 1); box-shadow: 0 0 0 2px rgba(var(--priId), 1); }
&[data-reserved-by="BC"] { border-color: rgba(var(--priFs), 1); box-shadow: 0 0 0 2px rgba(var(--priFs), 1); }
}
&.sig-reserved--own {
cursor: grabbing;
}
}
// ─── Cursor anchors ───────────────────────────────────────────────────────────
//
// Three tiny dots along the bottom of each mini card, one per role in the group.
// Inactive: invisible. Active (another gamer is hovering): role-coloured dot.
// Position order is fixed per polarity (POLARITY_ROLES in sig-select.js):
// levity (PC / NC / SC) → left / mid / right
// gravity (BC / EC / AC) → left / mid / right
// In-card cursor elements — invisible anchors only.
// Visible icons are portaled to document root by applyHover() in sig-select.js.
.sig-cursor {
display: block;
font-size: 0; // zero-size: no layout impact, just carries .active class
color: transparent;
pointer-events: none;
}
// ─── Floating cursor portal ───────────────────────────────────────────────────
//
// sig-select.js creates these <i> elements inside #id_sig_cursor_portal, a
// position:fixed root-level container, so they escape all overflow/clip contexts.
// Positioned via getBoundingClientRect() on the card element.
#id_sig_cursor_portal {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 200; // above sig-overlay (120), below tray (310)
overflow: visible;
}
.sig-cursor-float {
position: absolute;
font-size: 1.5rem;
line-height: 1;
transform: translateX(-50%); // centre on the x coordinate from JS
pointer-events: none;
}
// Role-specific colour + outline shadow + ninUser glow
.sig-cursor-float[data-role="PC"] {
color: rgba(var(--priRd), 1);
text-shadow: 2px 0 0 rgba(var(--priOr),1), -2px 0 0 rgba(var(--priOr),1),
0 2px 0 rgba(var(--priOr),1), 0 -2px 0 rgba(var(--priOr),1),
0 0 6px rgba(0, 0, 0, 0.5);
}
.sig-cursor-float[data-role="NC"] {
color: rgba(var(--priYl), 1);
text-shadow: 2px 0 0 rgba(var(--priLm),1), -2px 0 0 rgba(var(--priLm),1),
0 2px 0 rgba(var(--priLm),1), 0 -2px 0 rgba(var(--priLm),1),
0 0 6px rgba(0, 0, 0, 0.5);
}
.sig-cursor-float[data-role="EC"] {
color: rgba(var(--priGn), 1);
text-shadow: 2px 0 0 rgba(var(--priTk),1), -2px 0 0 rgba(var(--priTk),1),
0 2px 0 rgba(var(--priTk),1), 0 -2px 0 rgba(var(--priTk),1),
0 0 6px rgba(0, 0, 0, 0.5);
}
.sig-cursor-float[data-role="SC"] {
color: rgba(var(--priCy), 1);
text-shadow: 2px 0 0 rgba(var(--priBl),1), -2px 0 0 rgba(var(--priBl),1),
0 2px 0 rgba(var(--priBl),1), 0 -2px 0 rgba(var(--priBl),1),
0 0 6px rgba(0, 0, 0, 0.5);
}
.sig-cursor-float[data-role="AC"] {
color: rgba(var(--priId), 1);
text-shadow: 2px 0 0 rgba(var(--priVt),1), -2px 0 0 rgba(var(--priVt),1),
0 2px 0 rgba(var(--priVt),1), 0 -2px 0 rgba(var(--priVt),1),
0 0 6px rgba(0, 0, 0, 0.5);
}
.sig-cursor-float[data-role="BC"] {
color: rgba(var(--priFs), 1);
text-shadow: 2px 0 0 rgba(var(--priMe),1), -2px 0 0 rgba(var(--priMe),1),
0 2px 0 rgba(var(--priMe),1), 0 -2px 0 rgba(var(--priMe),1),
0 0 6px rgba(0, 0, 0, 0.5);
}
// ─── Polarity theming — card colour inversion ────────────────────────────────
//
// Gravity (Graven): --priUser bg / --secUser text — standard dark palette.
// Levity (Leavened): --secUser bg / --priUser text — inverted, lighter feel.
// Both mini-cards and the stage preview card follow the same rule.
.sig-overlay[data-polarity="levity"] {
// Mini card: inverted palette. game-kit sets explicit colours on .fan-card-name
// and .fan-card-corner that out-specifc the parent color, so re-target them here.
.sig-card {
background: rgba(var(--secUser), 0.97);
border-color: rgba(var(--priUser), 0.3);
color: rgba(var(--priUser), 1);
.fan-card-corner { color: rgba(var(--priUser), 0.75); }
.fan-card-name { color: rgba(var(--quiUser), 1); }
// OK / NVM overlay — must match the inverted card background
.sig-card-actions { background: rgba(var(--secUser), 0.92); }
}
// Stage preview card: same inversion + title colour.
// .fan-card-name-group and .fan-card-arcana have explicit color in the base
// .fan-card-face rule (specificity 0,2,0) — must re-target them here (0,3,0).
// Opacity dim is still applied by the nested sig-stage-card rule.
.sig-stage-card {
background: rgba(var(--secUser), 1);
border-color: rgba(var(--priUser), 0.6);
color: rgba(var(--priUser), 1);
.fan-card-corner { color: rgba(var(--priUser), 0.75); }
.fan-card-name-group{ color: rgba(var(--priUser), 1); }
.fan-card-name { color: rgba(var(--quiUser), 1); }
.fan-card-arcana { color: rgba(var(--priUser), 1); }
}
// Polarity qualifier: same colour as the card title in this context
.sig-qualifier-above,
.sig-qualifier-below { color: rgba(var(--quiUser), 1); }
// card-ref spans inside the caution tooltip — must match the base rule's
// .sig-stat-block .sig-caution-effect .card-ref specificity (0,3,0) to win.
.sig-caution-effect .card-ref { color: rgba(var(--quiUser), 1); }
// Cursor colours live in .sig-cursor-float[data-role] rules (portal elements)
}
.sig-overlay[data-polarity="gravity"] {
// Stat block: invert priUser/secUser so gravity gets the same stark contrast as leavened cards
.sig-stat-block {
background: rgba(var(--secUser), 0.75);
color: rgba(var(--priUser), 1);
border-color: rgba(var(--priUser), 0.15);
}
// Caution tooltip: --tooltip-bg is black so priUser text (dark) would be invisible —
// override to secUser (light) so body text reads against the dark backdrop.
.sig-caution-tooltip { color: rgba(var(--secUser), 1); }
// Polarity qualifier: terUser for gravity (quiUser is levity's equivalent)
.sig-qualifier-above,
.sig-qualifier-below { color: rgba(var(--terUser), 1); }
// Cursor colours live in .sig-cursor-float[data-role] rules (portal elements)
}
// ─── Sig select: landscape overrides ─────────────────────────────────────────
// Landscape base: 9×2 grid of 3rem cards. At ≥992px (wide enough for 18 cards
// at 3rem + 4rem left + ~4rem right): collapse to a single 18×1 row so the
// stage preview gets maximum vertical real-estate.
// padding-left clears the fixed left navbar (JS sets right/bottom but not left).
// Grid margins reset to 0 — overlay padding handles all edge clearance.
@media (orientation: landscape) {
.sig-modal {
max-width: none;
flex-direction: row; // grid to the right, stage + card preview to the left
margin-left: 4rem;
margin-right: 3rem;
}
.sig-stage {
min-width: 0; // allow shrinking in row layout; align-items:flex-end already set
}
.sig-deck-grid {
grid-template-columns: repeat(6, 2.5rem);
margin: 0;
align-self: flex-end; // sit at the bottom of the modal row
}
}
@media (orientation: landscape) and (min-width: 900px) {
// Wide landscape: revert to stacked layout (stage top, 18-card row grid bottom).
.sig-modal {
flex-direction: column;
align-items: stretch;
}
.sig-stage {
min-width: auto;
align-self: stretch; // fill full modal width so JS sizeSigCard() gets correct stageWidth
margin-left: 3rem;
}
.sig-deck-grid {
grid-template-columns: repeat(18, 3rem);
align-self: center;
}
}
@media (orientation: landscape) and (min-width: 1800px) {
// Sig overlay: clear doubled sidebars (8rem each instead of 4rem/6rem)
.sig-overlay { padding-left: 8rem; padding-right: 8rem; }
.sig-stage {
align-self: stretch; // fill full modal width so JS sizeSigCard() gets correct stageWidth
margin-left: 3rem;
}
.sig-deck-grid {
grid-template-columns: repeat(18, 5rem);
align-self: center;
}
// Room menu: base right: 0.5rem (same-specificity ID rule) overrides _applets.scss
// XL block because _card-deck.scss is imported after _applets.scss. Re-declare here to win the cascade.
#id_room_menu { right: 2.5rem; }
}

View File

@@ -149,7 +149,7 @@ body.page-dashboard {
}
}
@media (orientation: landscape) and (max-width: 1440px) {
@media (orientation: landscape) {
// Reset the 666px min-width so #id_dash_content shrinks to fit within the
// sidebar-bounded container rather than overflowing into the footer sidebar.
#id_dash_content {

View File

@@ -3,12 +3,16 @@
bottom: 0.5rem;
right: 0.5rem;
@media (orientation: landscape) and (max-width: 1440px) {
right: 0.5rem;
@media (orientation: landscape) {
right: 1rem;
bottom: 0.5rem;
top: auto;
}
@media (orientation: landscape) and (min-width: 1800px) {
right: 2.5rem; // centre in doubled 8rem sidebar
}
z-index: 318;
font-size: 1.75rem;
cursor: pointer;
@@ -45,7 +49,7 @@
z-index: 316;
overflow: hidden;
@media (orientation: landscape) and (max-width: 1440px) {
@media (orientation: landscape) {
$sidebar-w: 4rem;
// left: $sidebar-w;
right: $sidebar-w;
@@ -208,118 +212,3 @@
opacity: 0.45;
}
// ── Tarot fan modal ──────────────────────────────────────────────────────────
#id_tarot_fan_dialog {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
max-width: none;
max-height: none;
margin: 0;
padding: 0;
border: none;
background: rgba(0, 0, 0, 0.88);
overflow: hidden;
&::backdrop { display: none; } // Dialog IS the backdrop
}
.tarot-fan-wrap {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
perspective: 900px;
button {
box-shadow: none;
&:hover, &.active {
box-shadow: none;
}
}
}
.tarot-fan {
position: relative;
width: 220px;
height: 340px;
}
.fan-card {
position: absolute;
inset: 0;
width: 220px;
height: 340px;
border-radius: 0.75rem;
background: rgba(var(--priUser), 1);
border: 0.1rem solid rgba(var(--secUser), 0.4);
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.25s ease, opacity 0.25s ease;
transform-style: preserve-3d;
&--active {
border-color: rgba(var(--secUser), 1);
box-shadow: 0 0 2rem rgba(var(--secUser), 0.3);
}
}
.fan-card-corner {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.15rem;
line-height: 1;
color: rgba(var(--secUser), 0.75);
&--tl { top: 0.4rem; left: 0.4rem; }
&--br { bottom: 0.4rem; right: 0.4rem; transform: rotate(180deg); }
.fan-corner-rank {
font-size: 1.5rem;
font-weight: bold;
padding: 0.18rem 0;
}
i { font-size: 1.5rem; }
}
.fan-card-face {
padding: 1.25rem;
text-align: center;
display: flex;
flex-direction: column;
gap: 0.5rem;
.fan-card-number { font-size: 0.65rem; }
.fan-card-name-group { font-size: 0.65rem; margin: 0; text-transform: uppercase; letter-spacing: 0.08em; color: rgba(var(--secUser), 1); }
.fan-card-name { font-size: 0.95rem; font-weight: bold; margin: 0; color: rgba(var(--terUser), 1); }
.fan-card-arcana { font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.1em; color: rgba(var(--secUser), 1); }
.fan-card-correspondence { font-size: 0.6rem; font-style: italic; color: rgba(var(--secUser), 0.5); }
}
.fan-nav {
position: absolute;
z-index: 20;
font-size: 3rem;
line-height: 1;
background: none;
border: none;
color: rgba(var(--secUser), 0.6);
cursor: pointer;
padding: 1rem;
transition: color 0.15s;
pointer-events: auto;
&:hover { color: rgba(var(--secUser), 1); }
// Suppress browser focus ring on mouse/touch clicks; retain it for keyboard nav
&:focus:not(:focus-visible) { outline: none; box-shadow: none; }
&--prev { left: 1rem; }
&--next { right: 1rem; }
}

View File

@@ -46,7 +46,7 @@ body.page-gameboard {
}
}
@media (orientation: landscape) and (max-width: 1440px) {
@media (orientation: landscape) {
// Restore clip in landscape — overrides the >738px overflow:visible above,
// preventing the gameboard applets from bleeding into the footer sidebar.
body.page-gameboard .container {

View File

@@ -800,291 +800,43 @@ $card-h: 60px;
}
// Landscape mobile — aggressively scale down to fit short viewport
@media (orientation: landscape) and (max-width: 1440px) {
// Sink navbar below gate/role-select overlays when a modal is open.
// Landscape navbar z-index is 100 (_base.scss); gate-backdrop/overlay are
// 100/120 — same level causes paint-order ties so we drop it to 50.
@media (orientation: landscape) {
// Sink navbar + footer sidebar below any modal backdrop when open.
// Landscape navbar and footer sidebar are both z-index:100 (_base.scss).
// Gate/role-select/sig backdrops are also z-index:100 — DOM paint-order ties
// let the footer (later in DOM) bleed through. Drop both to 50.
html:has(.gate-backdrop) body .container .navbar,
html:has(.role-select-backdrop) body .container .navbar {
html:has(.role-select-backdrop) body .container .navbar,
html:has(.sig-backdrop) body .container .navbar {
z-index: 50;
}
html:has(.gate-backdrop) body #id_footer,
html:has(.role-select-backdrop) body #id_footer,
html:has(.sig-backdrop) body #id_footer {
z-index: 50;
}
// Reflow position strip into a vertical column along the left edge,
// reversed so 6 is at top, 1 at bottom, below the GAMEROOM title.
// Position strip: horizontal row across the top, slots 1-6 in order.
// Offset from both sidebars (5rem each) and centred with gap.
.position-strip {
flex-direction: column-reverse;
top: 3rem;
left: 0.5rem;
right: auto;
flex-direction: row;
top: 2.5rem;
left: 5rem;
right: 5rem;
justify-content: center;
gap: round($gate-gap * 0.4);
}
// Shallow landscape (phones): wrap into two columns — left: 6,5,4 / right: 3,2,1
// Columns grow rightward (wrap, not wrap-reverse) so overflow: hidden doesn't clip.
// order: -1 on slots 46 pulls them to the front of the flex sequence; combined
// with column-reverse they land in the left column reading 6,5,4 top-to-bottom.
// Small landscape (phones ≤550px tall): strip stays horizontal — no two-column
// trick needed now that the h2 is in the gutter. Just clear any order overrides.
@media (max-height: 550px) {
.position-strip {
flex-wrap: wrap;
// cap height to exactly 3 circles so the 4th wraps to a new column
max-height: #{3 * round($gate-node * 0.75) + 2 * round($gate-gap * 0.4)};
.gate-slot[data-slot="4"],
.gate-slot[data-slot="5"],
.gate-slot[data-slot="6"] { order: -1; }
.gate-slot { order: 0; }
top: 1rem;
}
}
}
// ─── Sig Select overlay (SIG_SELECT phase) ────────────────────────────────────
//
// Two overlays (levity / gravity) run in parallel, one per polarity group.
// Layout mirrors the gatekeeper: dark Gaussian backdrop + centred modal.
// Inside the modal: upper stage (card preview) + lower mini card grid (no scroll).
html:has(.sig-backdrop) {
overflow: hidden;
}
.sig-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(5px);
z-index: 100;
pointer-events: none;
}
.sig-overlay {
position: fixed;
inset: 0;
display: flex;
align-items: stretch;
justify-content: center;
z-index: 120;
pointer-events: none;
}
.sig-modal {
pointer-events: auto;
display: flex;
flex-direction: column;
width: 100%; // respects overlay padding-right set by JS
max-width: 420px;
max-height: 100%; // respects overlay padding-bottom set by JS
}
// ─── Stage ────────────────────────────────────────────────────────────────────
// flex: 1 — fills all space above the card grid; no background (backdrop blur).
// Row layout: preview card bottom-left, stat block fills the right.
// Card width is set by sizeSigCard() in room.js (smaller of 40% stage width or
// 80% stage height × 5/8) via --sig-card-w CSS variable — libsass can't handle
// container query units inside min().
.sig-stage {
flex: 1;
min-height: 0;
display: flex;
flex-direction: row;
align-items: flex-end;
padding-left: 1.5rem;
gap: 0.75rem;
// Preview card — width driven by JS via --sig-card-w; aspect-ratio derives height.
.sig-stage-card {
flex-shrink: 0;
width: var(--sig-card-w, 120px);
height: auto;
aspect-ratio: 5 / 8;
border-radius: 0.5rem;
background: rgba(var(--priUser), 1);
border: 0.15rem solid rgba(var(--secUser), 0.6);
display: flex;
flex-direction: column;
position: relative;
padding: 0.25rem;
overflow: hidden;
// game-kit sets .fan-card-corner { position: absolute; top/left offsets }
// so these just need display/font overrides; the corners land at the card edges.
.fan-card-corner--tl {
display: flex;
flex-direction: column;
align-items: center;
line-height: 1.1;
gap: 0.1rem;
.fan-corner-rank { font-size: 1rem; font-weight: 700; }
i { font-size: 0.75rem; }
}
.fan-card-corner--br {
display: flex;
flex-direction: column;
align-items: center;
line-height: 1.1;
gap: 0.1rem;
.fan-corner-rank { font-size: 0.9rem; font-weight: 700; }
i { font-size: 0.75rem; }
}
.fan-card-face {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 0.25rem 0.15rem;
gap: 0.2rem;
.fan-card-name-group { font-size: 0.55rem; opacity: 0.6; }
.fan-card-name { font-size: 0.7rem; font-weight: 600; }
.fan-card-arcana { font-size: 0.5rem; text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.5; }
.fan-card-correspondence{ font-size: 0.5rem; opacity: 0.5; }
}
}
// Stat block — same height as the preview card; fills remaining stage width.
// Height derived from the JS-computed --sig-card-w via aspect ratio 5:8.
.sig-stat-block {
flex: 1;
height: calc(var(--sig-card-w, 120px) * 8 / 5);
align-self: flex-end;
background: rgba(var(--priUser), 0.25);
border-radius: 0.4rem;
border: 0.1rem solid rgba(var(--terUser), 0.15);
display: none;
}
&.sig-stage--frozen .sig-stat-block { display: block; }
}
// ─── Mini card grid ───────────────────────────────────────────────────────────
// flex: 0 0 auto — shrinks to card content; no background (backdrop blur).
// align-content: start prevents CSS grid from distributing extra height between rows.
.sig-deck-grid {
flex: 0 0 auto;
display: grid;
grid-template-columns: repeat(6, 1fr);
align-content: start;
gap: 2px;
padding: 4px;
overflow: hidden;
margin: 0 1rem 5rem 4rem;
}
.sig-card {
aspect-ratio: 5 / 8;
border-radius: 0.4rem;
background: rgba(var(--priUser), 0.97);
border: 1px solid rgba(var(--secUser), 0.3);
position: relative;
cursor: grab;
transition: border-color 0.15s, box-shadow 0.15s;
overflow: hidden;
// game-kit sets .fan-card-corner { position:absolute; top:0.4rem; left:0.4rem }
// Override: center the element within the card instead.
.fan-card-corner--tl {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
gap: 0; // game-kit has gap:0.15rem — too large at 0.5rem font-size
.fan-corner-rank { font-size: 1rem; font-weight: 700; }
i { font-size: 0.75rem; }
}
// OK / NVM overlay — appears on click (focused) or own reservation
.sig-card-actions {
position: absolute;
inset: 0;
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
background: rgba(var(--priUser), 0.92);
border-radius: inherit;
.sig-nvm-btn { display: none; }
}
&.sig-focused .sig-card-actions { display: flex; }
&.sig-reserved--own .sig-card-actions {
display: flex;
.sig-ok-btn { display: none; }
.sig-nvm-btn { display: flex; }
}
// Cursor anchors strip — bottom of card
.sig-card-cursors {
position: absolute;
bottom: 2px;
left: 2px;
right: 2px;
display: flex;
justify-content: space-between;
}
&:hover:not([data-reserved-by]) {
border-color: rgba(var(--secUser), 0.8);
box-shadow: 0 0 4px rgba(var(--secUser), 0.25);
}
&.sig-reserved {
border-color: rgba(var(--terUser), 1);
box-shadow:
0 0 0.4rem rgba(var(--terUser), 0.7),
0 0 1rem rgba(var(--ninUser), 0.4);
cursor: not-allowed;
}
&.sig-reserved--own {
border-color: rgba(var(--secUser), 1);
box-shadow:
0 0 0.4rem rgba(var(--secUser), 0.7),
0 0 1rem rgba(var(--ninUser), 0.5);
cursor: grabbing;
}
}
// ─── Cursor anchors ───────────────────────────────────────────────────────────
//
// Three tiny dots along the bottom of each mini card, one per role in the group.
// Inactive: invisible. Active (another gamer is hovering): coloured dot.
.sig-cursor {
display: block;
width: 5px;
height: 5px;
border-radius: 50%;
background: transparent;
transition: background 0.1s;
&.active {
background: rgba(var(--terUser), 1);
box-shadow: 0 0 3px rgba(var(--ninUser), 0.8);
}
}
// ─── Sig select: landscape overrides ─────────────────────────────────────────
// Wider viewport → 2 rows of 9 cards; modal fills full available width.
// padding-left clears the fixed left navbar (JS sets right/bottom but not left).
// Grid margins reset to 0 — overlay padding handles all edge clearance in landscape.
@media (orientation: landscape) {
.sig-overlay { padding-left: 4rem; }
.sig-modal { max-width: none; }
.sig-deck-grid {
grid-template-columns: repeat(9, minmax(0, 90px));
margin: 0;
}
}
// ─── Seat tray — see _tray.scss ─────────────────────────────────────────────

View File

@@ -121,24 +121,29 @@ $handle-r: 1rem;
&::before { border-color: rgba(var(--quaUser), 1); }
}
// ─── Role card: arc-in animation (portrait) ─────────────────────────────────
// ─── Role card: scrawl fade-in ───────────────────────────────────────────────
@keyframes tray-role-arc-in {
from { opacity: 0; transform: scale(0.3) translate(-40%, -40%); }
to { opacity: 1; transform: scale(1) translate(0, 0); }
from { opacity: 0; }
to { opacity: 1; }
}
.tray-role-card {
background: rgba(var(--quaUser), 0.25);
display: flex;
align-items: flex-start;
justify-content: flex-start;
padding: 0.2em;
font-size: 0.65rem;
color: rgba(var(--quaUser), 1);
font-weight: 600;
padding: 0;
overflow: hidden;
background: transparent;
&.arc-in {
animation: tray-role-arc-in 1.0s cubic-bezier(0.22, 1, 0.36, 1) forwards;
img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
transform: scale(1.4); // crop SVG's internal margins
}
// Cell stays static; only the scrawl image fades in.
&.arc-in img {
animation: tray-role-arc-in 1s ease forwards;
}
}
@@ -301,15 +306,7 @@ $handle-r: 1rem;
border-bottom: none;
}
// Role card arc-in for landscape
@keyframes tray-role-arc-in-landscape {
from { opacity: 0; transform: scale(0.3) translate(-40%, 40%); }
to { opacity: 1; transform: scale(1) translate(0, 0); }
}
.tray-role-card.arc-in {
animation: tray-role-arc-in-landscape 1.0s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
// Role card: same fade-in in landscape — no override needed.
@keyframes tray-wobble-landscape {
0%, 100% { transform: translateY(0); }
@@ -328,3 +325,5 @@ $handle-r: 1rem;
80% { transform: translateY(-3px); }
}
}
// ≥1800px uses the same landscape tray rules as narrower landscape — no override block needed.

View File

@@ -6,6 +6,7 @@
@import 'gameboard';
@import 'palette-picker';
@import 'room';
@import 'card-deck';
@import 'tray';
@import 'billboard';
@import 'game-kit';

View File

@@ -1,12 +1,12 @@
describe("SigSelect", () => {
let testDiv, stageCard, card;
let testDiv, stageCard, card, statBlock;
function makeFixture({ reservations = '{}' } = {}) {
function makeFixture({ reservations = '{}', cardCautions = '[]', polarity = 'levity', userRole = 'PC' } = {}) {
testDiv = document.createElement("div");
testDiv.innerHTML = `
<div class="sig-overlay"
data-polarity="levity"
data-user-role="PC"
data-polarity="${polarity}"
data-user-role="${userRole}"
data-reserve-url="/epic/room/test/sig-reserve"
data-reservations="${reservations.replace(/"/g, '&quot;')}">
<div class="sig-modal">
@@ -15,10 +15,35 @@ describe("SigSelect", () => {
<span class="fan-corner-rank"></span>
<i class="stage-suit-icon"></i>
<p class="fan-card-name-group"></p>
<p class="sig-qualifier-above"></p>
<h3 class="fan-card-name"></h3>
<p class="sig-qualifier-below"></p>
<p class="fan-card-arcana"></p>
<p class="fan-card-correspondence"></p>
</div>
<div class="sig-stat-block">
<button class="btn btn-reverse sig-flip-btn" type="button">FLIP</button>
<button class="btn btn-caution sig-caution-btn" type="button">!!</button>
<div class="stat-face stat-face--upright">
<p class="stat-face-label">Upright</p>
<ul class="stat-keywords" id="id_stat_keywords_upright"></ul>
</div>
<div class="stat-face stat-face--reversed">
<p class="stat-face-label">Reversed</p>
<ul class="stat-keywords" id="id_stat_keywords_reversed"></ul>
</div>
<button class="btn btn-nav-left sig-caution-prev" type="button">&#9664;</button>
<button class="btn btn-nav-right sig-caution-next" type="button">&#9654;</button>
<div class="sig-caution-tooltip" id="id_sig_caution">
<div class="sig-caution-header">
<h4 class="sig-caution-title">Caution!</h4>
<span class="sig-caution-type">Rival Interaction</span>
</div>
<p class="sig-caution-shoptalk">[Shoptalk forthcoming]</p>
<p class="sig-caution-effect"></p>
<span class="sig-caution-index"></span>
</div>
</div>
</div>
<div class="sig-deck-grid">
<div class="sig-card"
@@ -28,7 +53,10 @@ describe("SigSelect", () => {
data-name-group="Pentacles"
data-name-title="King of Pentacles"
data-arcana="Minor Arcana"
data-correspondence="">
data-correspondence=""
data-keywords-upright="action,impulsiveness,ambition"
data-keywords-reversed="no direction,disregard for consequences"
data-cautions="${cardCautions.replace(/"/g, '&quot;')}">
<div class="fan-card-corner fan-card-corner--tl">
<span class="fan-corner-rank">K</span>
</div>
@@ -48,6 +76,7 @@ describe("SigSelect", () => {
`;
document.body.appendChild(testDiv);
stageCard = testDiv.querySelector(".sig-stage-card");
statBlock = testDiv.querySelector(".sig-stat-block");
card = testDiv.querySelector(".sig-card");
window.fetch = jasmine.createSpy("fetch").and.returnValue(
Promise.resolve({ ok: true })
@@ -107,80 +136,6 @@ describe("SigSelect", () => {
});
});
// ── Touch: OK btn tap allows synthetic click through ──────────────── //
describe("touch on OK button", () => {
beforeEach(() => {
if (typeof TouchEvent === 'undefined') { pending('TouchEvent unavailable in desktop Firefox'); return; }
makeFixture();
});
it("touchstart on OK btn does not call preventDefault (allows synthetic click)", () => {
// First tap the card body to show OK
card.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(card.classList.contains("sig-focused")).toBe(true);
// Now tap the OK button — touchstart should NOT preventDefault
var okBtn = card.querySelector(".sig-ok-btn");
var touchEvent = new TouchEvent("touchstart", {
bubbles: true,
cancelable: true,
touches: [new Touch({ identifier: 1, target: okBtn })],
});
okBtn.dispatchEvent(touchEvent);
expect(touchEvent.defaultPrevented).toBe(false);
});
it("touchstart on card body (not OK btn) calls preventDefault", () => {
var touchEvent = new TouchEvent("touchstart", {
bubbles: true,
cancelable: true,
touches: [new Touch({ identifier: 1, target: card })],
});
card.dispatchEvent(touchEvent);
expect(touchEvent.defaultPrevented).toBe(true);
});
});
// ── Touch outside grid dismisses stage (mobile) ───────────────────── //
describe("touch outside grid", () => {
beforeEach(() => {
if (typeof TouchEvent === 'undefined') { pending('TouchEvent unavailable in desktop Firefox'); return; }
makeFixture();
});
it("dismisses stage preview when touching outside the grid (unfocused state)", () => {
// Focus a card first
card.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(stageCard.style.display).toBe("");
// Touch on the sig-stage (outside the grid)
var stage = testDiv.querySelector(".sig-stage");
stage.dispatchEvent(new TouchEvent("touchstart", {
bubbles: true,
cancelable: true,
touches: [new Touch({ identifier: 2, target: stage })],
}));
expect(stageCard.style.display).toBe("none");
expect(card.classList.contains("sig-focused")).toBe(false);
});
it("does NOT dismiss stage preview when frozen (card reserved)", () => {
card.dispatchEvent(new MouseEvent("click", { bubbles: true }));
SigSelect._setFrozen(true);
// _focusedCardEl is set but frozen — use internal state trick via _setFrozen
// We also need a focused card; simulate it by setting frozen after focus
var stage = testDiv.querySelector(".sig-stage");
stage.dispatchEvent(new TouchEvent("touchstart", {
bubbles: true,
cancelable: true,
touches: [new Touch({ identifier: 3, target: stage })],
}));
expect(stageCard.style.display).toBe("");
});
});
// ── Lock after reservation ─────────────────────────────────────────── //
describe("lock after reservation", () => {
@@ -200,18 +155,6 @@ describe("SigSelect", () => {
expect(window.fetch).not.toHaveBeenCalled();
});
it("does not call preventDefault on touchstart while a card is reserved", () => {
if (typeof TouchEvent === 'undefined') { pending('TouchEvent unavailable in desktop Firefox'); return; }
SigSelect._setReservedCardId("99");
var touchEvent = new TouchEvent("touchstart", {
bubbles: true,
cancelable: true,
touches: [new Touch({ identifier: 1, target: card })],
});
card.dispatchEvent(touchEvent);
expect(touchEvent.defaultPrevented).toBe(false);
});
it("allows focus again after reservation is cleared", () => {
SigSelect._setReservedCardId("99");
SigSelect._setReservedCardId(null);
@@ -252,4 +195,414 @@ describe("SigSelect", () => {
expect(card.classList.contains("sig-focused")).toBe(true);
});
});
// ── Caution tooltip (!!) ──────────────────────────────────────────── //
describe("caution tooltip", () => {
var cautionTooltip, cautionEffect, cautionPrev, cautionNext, cautionBtn;
beforeEach(() => {
makeFixture();
cautionTooltip = testDiv.querySelector(".sig-caution-tooltip");
cautionEffect = testDiv.querySelector(".sig-caution-effect");
cautionPrev = testDiv.querySelector(".sig-caution-prev");
cautionNext = testDiv.querySelector(".sig-caution-next");
cautionBtn = testDiv.querySelector(".sig-caution-btn");
});
function hover() {
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
}
function openCaution() {
hover();
cautionBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
}
it("!! click adds .sig-caution-open to the stage", () => {
openCaution();
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
});
it("FYI click when btn-disabled does not close caution", () => {
openCaution();
expect(cautionBtn.classList.contains("btn-disabled")).toBe(true);
cautionBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
});
it("shows placeholder text when cautions list is empty", () => {
card.dataset.cautions = "[]";
openCaution();
expect(cautionEffect.innerHTML).toContain("pending");
});
it("renders first caution effect HTML including .card-ref spans", () => {
card.dataset.cautions = JSON.stringify(['First <span class="card-ref">Card</span> effect.']);
openCaution();
expect(cautionEffect.querySelector(".card-ref")).not.toBeNull();
expect(cautionEffect.querySelector(".card-ref").textContent).toBe("Card");
});
it("with 1 caution both nav arrows are disabled", () => {
card.dataset.cautions = JSON.stringify(["Single caution."]);
openCaution();
expect(cautionPrev.disabled).toBe(true);
expect(cautionNext.disabled).toBe(true);
});
it("with multiple cautions both nav arrows are always enabled", () => {
card.dataset.cautions = JSON.stringify(["C1", "C2", "C3", "C4"]);
openCaution();
expect(cautionPrev.disabled).toBe(false);
expect(cautionNext.disabled).toBe(false);
});
it("next click advances to second caution", () => {
card.dataset.cautions = JSON.stringify(["First", "Second"]);
openCaution();
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(cautionEffect.innerHTML).toContain("Second");
});
it("next wraps from last caution back to first", () => {
card.dataset.cautions = JSON.stringify(["First", "Last"]);
openCaution();
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(cautionEffect.innerHTML).toContain("First");
});
it("prev click goes back to first caution", () => {
card.dataset.cautions = JSON.stringify(["First", "Second"]);
openCaution();
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
cautionPrev.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(cautionEffect.innerHTML).toContain("First");
});
it("prev wraps from first caution to last", () => {
card.dataset.cautions = JSON.stringify(["First", "Middle", "Last"]);
openCaution();
cautionPrev.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(cautionEffect.innerHTML).toContain("Last");
});
it("index label shows n / total when multiple cautions", () => {
card.dataset.cautions = JSON.stringify(["C1", "C2", "C3"]);
openCaution();
expect(testDiv.querySelector(".sig-caution-index").textContent).toBe("1 / 3");
});
it("index label is empty when only 1 caution", () => {
card.dataset.cautions = JSON.stringify(["Only one."]);
openCaution();
expect(testDiv.querySelector(".sig-caution-index").textContent).toBe("");
});
it("card mouseleave closes the caution", () => {
openCaution();
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(false);
});
it("opening again resets to first caution", () => {
card.dataset.cautions = JSON.stringify(["First", "Second"]);
openCaution();
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
// Close and reopen
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
openCaution();
expect(cautionEffect.innerHTML).toContain("First");
});
it("opening caution adds .btn-disabled and swaps labels to ×", () => {
openCaution();
var flipBtn = testDiv.querySelector(".sig-flip-btn");
expect(flipBtn.classList.contains("btn-disabled")).toBe(true);
expect(cautionBtn.classList.contains("btn-disabled")).toBe(true);
expect(flipBtn.textContent).toBe("\u00D7");
expect(cautionBtn.textContent).toBe("\u00D7");
});
it("closing caution removes .btn-disabled and restores original labels", () => {
var flipBtn = testDiv.querySelector(".sig-flip-btn");
var origFlip = flipBtn.textContent;
var origCaution = cautionBtn.textContent;
openCaution();
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
expect(flipBtn.classList.contains("btn-disabled")).toBe(false);
expect(cautionBtn.classList.contains("btn-disabled")).toBe(false);
expect(flipBtn.textContent).toBe(origFlip);
expect(cautionBtn.textContent).toBe(origCaution);
});
it("clicking the tooltip closes caution", () => {
openCaution();
cautionEffect.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(false);
});
it("FLIP click when caution open (btn-disabled) does nothing", () => {
openCaution();
var flipBtn = testDiv.querySelector(".sig-flip-btn");
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
expect(statBlock.classList.contains("is-reversed")).toBe(false);
});
});
// ── Stat block: keyword population and FLIP toggle ────────────────── //
describe("stat block and FLIP", () => {
beforeEach(() => makeFixture());
it("populates upright keywords when a card is hovered", () => {
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
var items = statBlock.querySelectorAll("#id_stat_keywords_upright li");
expect(items.length).toBe(3);
expect(items[0].textContent).toBe("action");
expect(items[1].textContent).toBe("impulsiveness");
expect(items[2].textContent).toBe("ambition");
});
it("populates reversed keywords when a card is hovered", () => {
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
var items = statBlock.querySelectorAll("#id_stat_keywords_reversed li");
expect(items.length).toBe(2);
expect(items[0].textContent).toBe("no direction");
expect(items[1].textContent).toBe("disregard for consequences");
});
it("FLIP click adds .is-reversed to the stat block", () => {
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
var flipBtn = statBlock.querySelector(".sig-flip-btn");
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(statBlock.classList.contains("is-reversed")).toBe(true);
});
it("second FLIP click removes .is-reversed", () => {
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
var flipBtn = statBlock.querySelector(".sig-flip-btn");
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(statBlock.classList.contains("is-reversed")).toBe(false);
});
it("hovering a new card resets .is-reversed", () => {
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
statBlock.querySelector(".sig-flip-btn").dispatchEvent(
new MouseEvent("click", { bubbles: true })
);
expect(statBlock.classList.contains("is-reversed")).toBe(true);
// Leave and re-enter (simulates moving to a different card)
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(statBlock.classList.contains("is-reversed")).toBe(false);
});
it("card with no keywords yields empty lists", () => {
card.dataset.keywordsUpright = "";
card.dataset.keywordsReversed = "";
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(statBlock.querySelectorAll("#id_stat_keywords_upright li").length).toBe(0);
expect(statBlock.querySelectorAll("#id_stat_keywords_reversed li").length).toBe(0);
});
});
// ── WS cursor hover (applyHover) ──────────────────────────────────────── //
//
// Fixture polarity = levity, userRole = PC.
// POLARITY_ROLES: levity → [PC, NC, SC] = [left, mid, right]
//
// Only tests the JS position mapping — colour is CSS-only.
describe("WS cursor hover", () => {
beforeEach(() => makeFixture());
it("NC hover activates the --mid cursor", () => {
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "NC", active: true },
}));
expect(card.querySelector(".sig-cursor--mid").classList.contains("active")).toBe(true);
});
it("SC hover activates the --right cursor", () => {
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "SC", active: true },
}));
expect(card.querySelector(".sig-cursor--right").classList.contains("active")).toBe(true);
});
it("own role (PC) hover event is ignored — no cursor activates", () => {
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "PC", active: true },
}));
expect(card.querySelectorAll(".sig-cursor.active").length).toBe(0);
});
it("hover-off removes .active from the cursor", () => {
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "NC", active: true },
}));
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "NC", active: false },
}));
expect(card.querySelector(".sig-cursor--mid").classList.contains("active")).toBe(false);
});
it("hover on unknown card_id is a no-op", () => {
expect(() => {
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 9999, role: "NC", active: true },
}));
}).not.toThrow();
});
});
// ── WS reservation — data-reserved-by attribute ───────────────────────── //
//
// applyReservation() sets data-reserved-by so the CSS can glow the card in
// the reserving gamer's role colour. These tests assert the attribute, not
// the colour (CSS variables aren't resolvable in the SpecRunner context).
describe("WS reservation sets data-reserved-by", () => {
beforeEach(() => makeFixture());
it("peer reservation sets data-reserved-by to the reserving role", () => {
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: true },
}));
expect(card.dataset.reservedBy).toBe("NC");
});
it("peer reservation also adds .sig-reserved class", () => {
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: true },
}));
expect(card.classList.contains("sig-reserved")).toBe(true);
});
it("release removes data-reserved-by", () => {
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: true },
}));
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: false },
}));
expect(card.dataset.reservedBy).toBeUndefined();
});
it("own reservation (PC) sets data-reserved-by AND .sig-reserved--own", () => {
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "PC", reserved: true },
}));
expect(card.dataset.reservedBy).toBe("PC");
expect(card.classList.contains("sig-reserved--own")).toBe(true);
});
it("peer reservation places a thumbs-up float and removes any hand-pointer float", () => {
// First, a hover float exists for NC (mid cursor)
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "NC", active: true },
}));
expect(document.querySelector('.sig-cursor-float[data-role="NC"]')).not.toBeNull();
expect(document.querySelector('.fa-hand-pointer[data-role="NC"]')).not.toBeNull();
// NC then clicks OK — reservation arrives
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: true },
}));
// Thumbs-up replaces hand-pointer
const floatEl = document.querySelector('.sig-cursor-float[data-role="NC"]');
expect(floatEl).not.toBeNull();
expect(floatEl.classList.contains("fa-thumbs-up")).toBe(true);
expect(floatEl.classList.contains("fa-hand-pointer")).toBe(false);
});
it("peer release removes the thumbs-up float", () => {
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: true },
}));
expect(document.querySelector('.sig-cursor-float--reserved[data-role="NC"]')).not.toBeNull();
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: false },
}));
expect(document.querySelector('.sig-cursor-float--reserved[data-role="NC"]')).toBeNull();
});
});
// ── Polarity theming — stage qualifier text ────────────────────────────── //
//
// On mouseenter, updateStage() injects "Leavened" or "Graven" into the
// sig-qualifier-above (non-major) or sig-qualifier-below (major arcana) slot.
// Correspondence field is never populated in sig-select context.
describe("polarity theming — stage qualifier", () => {
it("levity non-major card puts 'Leavened' in qualifier-above, qualifier-below empty", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
// data-arcana defaults to "Minor Arcana" in fixture → non-major
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("Leavened");
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("");
});
it("levity major arcana card puts 'Leavened' in qualifier-below, qualifier-above empty", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
card.dataset.arcana = "Major Arcana";
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("");
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("Leavened");
});
it("major arcana title gets a trailing comma (qualifier reads as subtitle)", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
card.dataset.arcana = "Major Arcana";
card.dataset.nameTitle = "The Schizo";
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".fan-card-name").textContent).toBe("The Schizo,");
});
it("non-major arcana title has no trailing comma", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
// fixture default: Minor Arcana, "King of Pentacles"
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".fan-card-name").textContent).toBe("King of Pentacles");
});
it("gravity non-major card puts 'Graven' in qualifier-above", () => {
makeFixture({ polarity: 'gravity', userRole: 'BC' });
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("Graven");
});
it("gravity major arcana card puts 'Graven' in qualifier-below", () => {
makeFixture({ polarity: 'gravity', userRole: 'BC' });
card.dataset.arcana = "Major Arcana";
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("Graven");
});
it("hovering clears qualifier slots from the previous card", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
card.dataset.arcana = "Major Arcana";
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
// Now major — above should be empty, below filled
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("");
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("Leavened");
});
it("correspondence field is never populated", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
card.dataset.correspondence = "Il Bagatto (Minchiate)";
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".fan-card-correspondence").textContent).toBe("");
});
});
});

View File

@@ -41,7 +41,7 @@
</div>
{% endif %}
<button type="submit" class="btn btn-primary btn-xl">Share</button>
<button type="submit" class="btn btn-primary">Share</button>
</form>
<small>Note shared with:
{% for user in note.shared_with.all %}

View File

@@ -53,7 +53,7 @@
{% if room.gate_status == 'OPEN' %}
<form method="POST" action="{% url 'epic:pick_roles' room.id %}" style="display:contents">
{% csrf_token %}
<button type="submit" class="launch-game-btn btn btn-primary btn-xl">PICK ROLES</button>
<button type="submit" class="launch-game-btn btn btn-primary">PICK ROLES</button>
</form>
{% endif %}
</div>

View File

@@ -21,16 +21,40 @@ Context: sig_cards, user_polarity, user_seat, sig_reserve_url, sig_reservations_
</div>
<div class="fan-card-face">
<p class="fan-card-name-group"></p>
<p class="sig-qualifier-above"></p>
<h3 class="fan-card-name"></h3>
<p class="sig-qualifier-below"></p>
<p class="fan-card-arcana"></p>
<p class="fan-card-correspondence"></p>
<p class="fan-card-correspondence"></p>{# not shown in sig-select — game-kit only #}
</div>
<div class="fan-card-corner fan-card-corner--br">
<span class="fan-corner-rank"></span>
<i class="fa-solid stage-suit-icon" style="display:none"></i>
</div>
</div>
<div class="sig-stat-block"></div>
<div class="sig-stat-block">
<button class="btn btn-reverse sig-flip-btn" type="button">FLIP</button>
<button class="btn btn-caution sig-caution-btn" type="button">FYI</button>
<div class="stat-face stat-face--upright">
<p class="stat-face-label">Upright</p>
<ul class="stat-keywords" id="id_stat_keywords_upright"></ul>
</div>
<div class="stat-face stat-face--reversed">
<p class="stat-face-label">Reversed</p>
<ul class="stat-keywords" id="id_stat_keywords_reversed"></ul>
</div>
<div class="sig-caution-tooltip" id="id_sig_caution">
<div class="sig-caution-header">
<h4 class="sig-caution-title">Caution!</h4>
<p class="sig-caution-type">Rival Interaction</p>
</div>
<p class="sig-caution-shoptalk">[Shoptalk forthcoming]</p>
<p class="sig-caution-effect"></p>
<span class="sig-caution-index"></span>
</div>
<button class="btn btn-nav-left sig-caution-prev" type="button">PRV</button>
<button class="btn btn-nav-right sig-caution-next" type="button">NXT</button>
</div>
</div>
<div class="sig-deck-grid" id="id_sig_deck">
@@ -42,7 +66,10 @@ Context: sig_cards, user_polarity, user_seat, sig_reserve_url, sig_reservations_
data-name-group="{{ card.name_group }}"
data-name-title="{{ card.name_title }}"
data-arcana="{{ card.get_arcana_display }}"
data-correspondence="{{ card.correspondence|default:'' }}">
data-correspondence="{{ card.correspondence|default:'' }}"
data-keywords-upright="{{ card.keywords_upright|join:',' }}"
data-keywords-reversed="{{ card.keywords_reversed|join:',' }}"
data-cautions="{{ card.cautions_json }}">
<div class="fan-card-corner fan-card-corner--tl">
<span class="fan-corner-rank">{{ card.corner_rank }}</span>
{% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %}
@@ -52,9 +79,9 @@ Context: sig_cards, user_polarity, user_seat, sig_reserve_url, sig_reservations_
<button class="sig-nvm-btn btn btn-cancel" type="button">NVM</button>
</div>
<div class="sig-card-cursors">
<span class="sig-cursor sig-cursor--left"></span>
<span class="sig-cursor sig-cursor--mid"></span>
<span class="sig-cursor sig-cursor--right"></span>
<i class="fa-solid fa-hand-pointer sig-cursor sig-cursor--left"></i>
<i class="fa-solid fa-hand-pointer sig-cursor sig-cursor--mid"></i>
<i class="fa-solid fa-hand-pointer sig-cursor sig-cursor--right"></i>
</div>
</div>
{% endfor %}

View File

@@ -8,7 +8,7 @@
<form method="POST" action="{% url 'epic:confirm_token' room.id %}">
{% csrf_token %}
{% if is_last_slot %}
<button type="submit" class="btn btn-primary btn-xl">PICK ROLES</button>
<button type="submit" class="btn btn-primary">PICK ROLES</button>
{% else %}
<button type="submit" class="btn btn-confirm">OK</button>
{% endif %}

View File

@@ -14,7 +14,7 @@
<div id="id_pick_sigs_wrap"{% if starter_roles|length < 6 %} style="display:none"{% endif %}>
<form method="POST" action="{% url 'epic:pick_sigs' room.id %}">
{% csrf_token %}
<button id="id_pick_sigs_btn" type="submit" class="btn btn-primary btn-xl">PICK<br>SIGS</button>
<button id="id_pick_sigs_btn" type="submit" class="btn btn-primary">PICK<br>SIGS</button>
</form>
</div>
{% endif %}
@@ -73,7 +73,7 @@
<i class="fa-solid fa-dice-d20"></i>
</button>
</div>
<div id="id_tray" style="display:none"><div id="id_tray_grid">{% for i in "12345678" %}<div class="tray-cell"></div>{% endfor %}</div></div>
<div id="id_tray" style="display:none"><div id="id_tray_grid" data-role-icons-url="{% static 'apps/epic/icons/cards-roles/' %}">{% if my_tray_role %}<div class="tray-cell tray-role-card" data-role="{{ my_tray_role }}"><img src="{% static my_tray_scrawl_static_path %}" alt="{{ my_tray_role }}"></div>{% else %}<div class="tray-cell"></div>{% endif %}{% for i in "2345678" %}<div class="tray-cell"></div>{% endfor %}</div></div>
</div>
{% endif %}
{% include "apps/gameboard/_partials/_room_gear.html" %}

View File

@@ -24,7 +24,7 @@
{% if navbar_recent_room_url %}
<button
id="id_cont_game"
class="btn btn-primary btn-xl"
class="btn btn-primary"
type="button"
data-confirm="Continue game?"
data-href="{{ navbar_recent_room_url }}"