Start

Claude Code is a terminal agent that reads and writes the files on your machine. This site is how one person actually runs it: a rulebook so it behaves the same every session, skills so it gains abilities, connectors so it reaches your accounts, and a knowledge store so it remembers. Start here, go deeper only when you need to.

Claude Code Basics

Claude Code is Anthropic's agentic AI tool.

Claude Code is Anthropic's agentic AI tool. It is not a classic chatbot AI that only answers in a browser window. You install it on your computer, and it takes real control of your machine: it reads your files, edits your code, and runs commands on your system — with your permission.

In this tutorial we'll install it, walk through the core concepts, and make a first real change to a project.

Installation

You need Node.js 18+ installed. First, open a terminal: press the Windows key, type cmd and press Enter (on Mac, open Terminal). Then install Claude Code (docs) with this command:

npm install -g @anthropic-ai/claude-code

Start Your First Session

⚡ read this first — where do I type commands?

Open the folder you want to build in. Click the File Explorer address bar, type cmd, press Enter. In the terminal that opens:

claude --dangerously-skip-permissions

⚠ That flag lets Claude edit files and run commands without asking. Use it only in projects you trust and can restore with git. Against a company database, give it its own user: write rights in its own schema, read-only everywhere else.

Rename your session

Claude Code names each session by itself — usually from your first message. To change that name later:

Claude Code prompt
/rename my-project-name

How do you return to a session you opened before?

Go to your project folder. It must be the same folder you started the session in, otherwise Claude cannot find your old session. Click its address bar in File Explorer, type cmd and press Enter to open the terminal again. Then:

in your normal terminal (cmd)
claude --resume --dangerously-skip-permissions

This lists all your sessions in that folder — pick one and continue where you left off.

Getting to Know Claude's Folders

Claude Code keeps everything on your own computer:

Where your data lives

Claude Code keeps everything on your own computer:

    Everything below sits under C:\Users\<you>\.claude\. On Mac and Linux it is ~/.claude/.

The two you will open by hand

  • CLAUDE.md — your global rulebook, loaded into every session in every project.
  • settings.json — permissions, environment variables, hooks. settings.local.json holds machine-only overrides.

Where your work is kept

  • projects\ — every conversation, one .jsonl per session, inside a folder named after the directory you launched from. This is the big one: mine is 261 MB.
  • projects\<folder>\memory\ — the notes Claude keeps to remember things about that project.
  • history.jsonl — every prompt you have typed, with the project it was typed in.
  • file-history\ — snapshots of files Claude changed, per session. What an undo would read.

What you install

  • skills\ — installed skills. Installers often put the real files in ~/.agents/skills/ and leave a symlink here, so one copy serves every agent.
  • plugins\ — plugins, the marketplaces they came from, and their cache.

Caches you can ignore until you cannot

  • paste-cache\ and image-cache\ — text and images you pasted into a chat. Pasted images live only here; they are not in the transcript, so once this is cleaned they are gone.
  • shell-snapshots\, session-env\, sessions\, state\, cache\ — terminal state, environment and model metadata. Leave them alone.
  • backups\ — copies Claude took before editing its own config files.

When a Change Takes Effect

You changed a rule file or a setting. Do open sessions need a restart?

Usually not. It depends on what changed:

What changedWhen open sessions see it
CLAUDE.md (global or project)Loaded once, when a session starts.
To use it in the same session, tell it “Re-read CLAUDE.md”.
Other MDsNothing to do. Claude reads them from disk each time it needs them.
Hooks (settings.json)Loaded when a session starts; restart the open session.
An env in settings.json (hidden Playwright, for example)Applies to new sessions. In an open session, reconnect that MCP with /mcp, Reconnect.
A new pluginType /reload-plugins.

The only file Claude Code loads on its own, in every session, in every project. Everything else on this site is optional; this is not.

CLAUDE.md — The Master Rulebook

If you take one thing from this site, take this: CLAUDE.md is the master rule set of your project.

If you take one thing from this site, take this: CLAUDE.md is the master rule set of your project. It defines how Claude Code behaves in your repo — every session, every task, without you asking. In short: Claude learns how you take your coffee.

⚡ the one rule that matters

Claude Code automatically reads only CLAUDE.md. No other markdown file in your repo is loaded on its own.

Your other .MD docs — memory.md, design.md, admin.md, specs, handoffs — are read only if CLAUDE.md references them. A brilliant doc that CLAUDE.md never mentions is invisible to Claude Code — unless you mention it specifically in your prompt.

The global CLAUDE.md does not come ready-made

Installing Claude Code does not create a global CLAUDE.md. You create it once, yourself — the easiest way is plain language:

Claude Code prompt — just ask
create a global CLAUDE.md for me

The word "global" matters: if you leave it out, Claude creates the file for the project you are working in, not the global one. Always say global when you mean the file that applies everywhere. Claude creates it at C:\Users\<you>\.claude\CLAUDE.md. From then on it is loaded automatically in every project, every session.

If you do want a CLAUDE.md for the current project, there is a dedicated command for that — prefer it:

Claude Code prompt
/init

/init scans your project and generates a CLAUDE.md for that project only.

How the loading actually works

At the start of every session, Claude Code loads the CLAUDE.md files it finds (project root, plus your global ~/.claude/CLAUDE.md). Every time you ask a question, this is what actually gets sent to the LLM:

your question + your global CLAUDE.md + your project CLAUDE.md

That is why a very long CLAUDE.md makes you spend more tokens.

Everything else is read on demand. Claude Code will open docs/architecture.md only when something points it there: a line in CLAUDE.md, or you asking for it in the conversation.

Coding Discipline

Think first, keep it simple, stay surgical.

EN
## Coding discipline
Apply on every coding, review, or refactor task.
* **Think before coding.** State assumptions explicitly. If multiple interpretations exist, present them instead of silently picking one. If a simpler approach exists, say so.
* **Simplicity first.** The minimum code that solves the problem. No speculative features, abstractions, configurability, or error handling for impossible cases.
* **Surgical changes.** Touch only what the request needs. Do not refactor or reformat unrelated code. Match existing style. Only remove orphans your own change created.
* **Goal-driven.** Turn each task into a verifiable success criterion (a test or a concrete check) and loop until it passes.
* **A UI change is not done until it has been seen in the browser.** Open the app and check the thing that changed: measure behaviour (clicks, keys, state) in JS, but for anything **visual take a screenshot and look at it** — measurements say "14px matched" while the user sees no change at all. Say so plainly when a change was not verified. Undo any test data and delete the screenshots afterwards.
TR
## Coding discipline
Her kodlama, inceleme veya refactor görevinde uygula.
* **Kodlamadan önce düşün.** Varsayımları açıkça söyle. Birden fazla yorum varsa, sessizce birini seçmek yerine hepsini sun. Daha basit bir yol varsa söyle.
* **Önce sadelik.** Problemi çözen en az kod. Spekülatif özellik, soyutlama, yapılandırılabilirlik ya da imkânsız durumlar için hata yönetimi yok.
* **Cerrahi değişiklik.** Sadece talebin gerektirdiğine dokun. İlgisiz kodu refactor etme ya da yeniden biçimlendirme. Mevcut üsluba uy. Sadece kendi değişikliğinin yarattığı artıkları temizle.
* **Hedef odaklı.** Her görevi doğrulanabilir bir başarı ölçütüne (bir test ya da somut bir kontrol) çevir ve geçene kadar döngüye gir.
* **Bir arayüz değişikliği, tarayıcıda görülene kadar bitmiş değildir.** Uygulamayı aç ve değişen şeyi kontrol et: davranışı (tıklamalar, tuşlar, durum) JS ile ölç, ama **görsel olan her şey için ekran görüntüsü al ve ona bak** — ölçümler "14px tuttu" derken kullanıcı hiçbir değişiklik görmüyor olabilir. Bir değişiklik doğrulanmadıysa bunu açıkça söyle. Sonrasında test verilerini geri al ve ekran görüntülerini sil.

Commands

+name

Adds the rule to name.md. The project's file if it has one, otherwise the global one.

EN
When a message contains a `+<name>` or `+<name>.md` token (e.g. `+claude`, `+design.md`, `+ dashboard.md`), it means: take the rule/fact stated in that sentence and add it to the markdown file `<name>.md`. Spacing after the `+` is flexible.

* **Target resolution:** (1) if the current project has a `<name>.md` (project root or `.claude/`), add the rule there. (2) Otherwise add it to `G:\My Drive\mindmap\<name>.md`, creating it if missing and adding its reference line to the `mindmap.md` hub in the same commit.

* Write every rule, anywhere, as the shortest wording that still pins down what it requires and forbids. Match the file's existing style and language, under the most fitting existing section. Create a new section only if none fits. Never duplicate: if an equivalent rule already exists, refine or extend that rule instead of adding a second copy.

* **PROPOSE FIRST, WRITE ONLY AFTER APPROVAL.** Never edit the md straight from the message. Show me the **verbatim text that will land in the file** — not a restatement of what you understood, not a summary of the rule. One line above it names the target: file, and the section (existing, or "new section `<title>`"). Below the English text give a Turkish rendering of it for my reading only; the file itself stays English. Then, in one or two sentences, say **what the rule will change in practice** — what it makes you do or stop doing. **Revising an existing rule: print the current text first under OLD, then the replacement under NEW**, so I compare two blocks instead of a description of a change. Add a single line naming anything you had to **invent** — a value, a threshold, a scope boundary my message did not state. Close by asking whether to write it. Then **stop and wait.** On approval, write it; if I amend it, write the amended version.

* **The gate applies to the md write only.** Any other task in the same message proceeds normally while the proposed rule text waits.

* **Landing report.** After writing, confirm briefly what was added and to which file and section.
TR
Bir mesajda `+<name>` veya `+<name>.md` token'ı geçtiğinde (ör. `+claude`, `+design.md`, `+ dashboard.md`), şu anlama gelir: o cümlede söylenen kuralı/bilgiyi al ve `<name>.md` markdown dosyasına ekle. `+` işaretinden sonraki boşluk esnektir.

* **Hedef çözümleme:** (1) mevcut projede bir `<name>.md` varsa (proje kökü veya `.claude/`), kuralı oraya ekle. (2) Yoksa `G:\My Drive\mindmap\<name>.md` dosyasına ekle, yoksa oluştur ve referans satırını aynı commit'te `mindmap.md` hub'ına ekle.

* Her kuralı, nerede olursa olsun, neyi gerektirip neyi yasakladığını hâlâ net tutan en kısa ifadeyle yaz. Dosyanın mevcut üslubuna ve diline uy, en uygun mevcut bölümün altına koy. Hiçbiri uymuyorsa yeni bölüm aç. Asla tekrarlama: eşdeğer bir kural zaten varsa ikinci bir kopya eklemek yerine o kuralı iyileştir veya genişlet.

* **ÖNCE ÖNER, ANCAK ONAYDAN SONRA YAZ.** Mesajdan doğrudan md'yi asla düzenleme. Bana **dosyaya inecek metnin birebir kendisini** göster; ne anladığının yeniden anlatımını değil, kuralın özetini değil. Üstündeki tek satır hedefi söyler: dosya ve bölüm (mevcut ya da "yeni bölüm `<başlık>`"). İngilizce metnin altına sadece benim okumam için Türkçesini ver; dosyanın kendisi İngilizce kalır. Sonra bir iki cümleyle **kuralın pratikte neyi değiştireceğini** söyle: seni ne yapmaya ya da ne yapmamaya zorluyor. **Mevcut bir kuralı revize ediyorsan önce ESKİ başlığı altında şimdiki metni, sonra YENİ başlığı altında yerine geçecek metni yaz** ki değişikliğin tarifini değil iki bloğu karşılaştırayım. **Uydurmak** zorunda kaldığın her şeyi tek satırda söyle: mesajımda geçmeyen bir değer, eşik veya kapsam sınırı. Yazayım mı diye sorarak bitir. Sonra **dur ve bekle.** Onay gelince yaz; değiştirirsem değiştirilmiş halini yaz.

* **Kapı sadece md yazımı için geçerlidir.** Aynı mesajdaki diğer işler, önerilen kural metni beklerken normal şekilde ilerler.

* **İniş raporu.** Yazdıktan sonra neyin hangi dosya ve bölüme eklendiğini kısaca teyit et.
Claude Code prompt — a normal chat message
Numeric table columns should always be right-aligned. +design

+claude

Adds it to the current project's CLAUDE.md.

EN
**Target resolution** (3) `+claude` writes to the **current project's** `CLAUDE.md` (create it if missing, kept as a pointer file per section D).
TR
**Hedef çözümleme** (3) `+claude` **mevcut projenin** `CLAUDE.md` dosyasına yazar (yoksa oluştur, bölüm D'ye göre işaretçi dosya olarak tutulur).

+claude g

Adds it to the global CLAUDE.md instead.

EN
`+claude g` or `+claude global` writes to the user-level `C:\Users\firat\.claude\CLAUDE.md`. `C:\Users\firat\.claude\` is for behavior rules only, never for knowledge.
TR
`+claude g` veya `+claude global` kullanıcı seviyesindeki `C:\Users\firat\.claude\CLAUDE.md` dosyasına yazar. `C:\Users\firat\.claude\` sadece davranış kuralları içindir, asla bilgi için değil.

+md

No name. Claude works out the file and shows you the target before writing.

EN
**Bare `+md` — no name, you find the target.** Route it yourself with the tests already in section D: a rule about how you behave goes to a `CLAUDE.md`, a fact goes to the global knowledge mds, and a fact that changes when the code changes goes to the project's `memory.md`. Then match it against the candidate files' `## Scope` sections and name the single best one. If none fits, propose a new file with its name and Scope rather than forcing it into an existing one. Never ask me which file; deciding is the point of the token. The propose-first gate below still applies in full.
TR
**Yalın `+md` — isim yok, hedefi sen bul.** Bölüm D'deki testlerle kendin yönlendir: nasıl davrandığınla ilgili bir kural bir `CLAUDE.md` dosyasına, bir bilgi global knowledge md'lere, kod değiştiğinde değişen bir bilgi ise projenin `memory.md` dosyasına gider. Sonra bunu aday dosyaların `## Scope` bölümleriyle eşleştir ve tek en iyi olanı söyle. Hiçbiri uymuyorsa, mevcut birine zorla sokmak yerine adı ve Scope'u ile yeni bir dosya öner. Bana hangi dosya olduğunu asla sorma; karar vermek zaten token'ın amacı. Aşağıdaki önce-öner kapısı tam olarak geçerli kalır.
Claude Code prompt — no filename, no folder
Vite binds IPv6 only here, so Chrome fails on 127.0.0.1. +md

+md g

Forces a file outside the project, memory.md ruled out.

EN
* **`+md g` forces a global file.** Every project file is ruled out, so a behaviour rule lands in `C:\Users\firat\.claude\CLAUDE.md` and a fact lands in the `mindmap` tree. Without `g` you make that call too.

* **`g` / `global` means the global mds** — every md that lives outside a project: `C:\Users\firat\.claude\CLAUDE.md` and everything under `G:\My Drive\mindmap`. It is not a synonym for `CLAUDE.md`. Adding it (`+design.md g`, `+design.md global`) forces the global copy even when the project has its own `<name>.md`, and rules out every project file including `memory.md`.
TR
* **`+md g` global bir dosyayı zorunlu kılar.** Bütün proje dosyaları devre dışı kalır, yani bir davranış kuralı `C:\Users\firat\.claude\CLAUDE.md` dosyasına, bir bilgi ise `mindmap` ağacına iner. `g` yoksa bu kararı da sen verirsin.

* **`g` / `global` global md'ler demektir** — bir projenin dışında duran her md: `C:\Users\firat\.claude\CLAUDE.md` ve `G:\My Drive\mindmap` altındaki her şey. `CLAUDE.md` ile eş anlamlı değildir. Eklenmesi (`+design.md g`, `+design.md global`), projenin kendi `<name>.md` dosyası olsa bile global kopyayı zorunlu kılar ve `memory.md` dahil bütün proje dosyalarını devre dışı bırakır.

apply name.md

Checks a subject against a rules md and reports every violation before touching anything.

EN
Check a subject against a spec md, report non-compliances first, fix them only after approval. Triggers: `apply <name>.md`, `apply <name>`, `apply <name> g`, `apply <name> global`. Formerly called `audit`; that word now belongs to the different command below, so `apply` is the only trigger.
1. **Audit (read-only, no edits yet):** read the rules md in full, go rule by rule and classify the subject as PASS / VIOLATION / PARTIAL / N/A (rule does not apply, with the reason). Every VIOLATION and PARTIAL cites the rule (section plus short quote) and where it fails (`file:line` or component). No silent omissions.
2. **Report and get approval:** list every VIOLATION and PARTIAL first, most severe first, each as rule ref → what is wrong → what compliance needs. Then a tally and the N/A list. Then ask before changing anything, never fix unbidden.
3. **Fix (only after approval):** edit the subject until it complies. Report what was fixed and anything that could NOT be, each with a root cause: **deferred by me** (say so plainly) or **weak rule in the md** (ambiguous, incomplete, underspecified).
4. **Propose md revisions:** for every gap caused by a weak rule, propose concrete tightened rule text. Propose only; do not edit the md without approval.
TR
Bir konuyu bir kural md'sine göre denetle, önce uyumsuzlukları raporla, sadece onaydan sonra düzelt. Tetikleyiciler: `apply <name>.md`, `apply <name>`, `apply <name> g`, `apply <name> global`. Eskiden `audit` deniyordu; o kelime artık aşağıdaki farklı komuta ait, bu yüzden tek tetikleyici `apply`.

1. **Denetim (sadece okuma, henüz düzenleme yok):** kural md'sini baştan sona oku, kural kural git ve konuyu PASS / VIOLATION / PARTIAL / N/A (kural uygulanmıyor, gerekçesiyle) olarak sınıflandır. Her VIOLATION ve PARTIAL kuralı (bölüm ve kısa alıntı) ve nerede kaldığını (`file:line` veya bileşen) gösterir. Sessizce atlanan hiçbir şey yok.
2. **Raporla ve onay al:** önce bütün VIOLATION ve PARTIAL'ları en ağırdan başlayarak listele, her biri kural referansı → sorun ne → uyum için ne gerekiyor biçiminde. Sonra sayım ve N/A listesi. Sonra hiçbir şeyi değiştirmeden önce sor, istenmeden asla düzeltme.
3. **Düzeltme (sadece onaydan sonra):** konuyu uyumlu hale gelene kadar düzenle. Neyin düzeltildiğini ve düzeltilemeyenleri kök nedeniyle raporla: **benim ertelediğim** (açıkça söyle) ya da **md'deki zayıf kural** (belirsiz, eksik, yetersiz tanımlı).
4. **Md revizyonları öner:** zayıf bir kuraldan kaynaklanan her boşluk için somut, sıkılaştırılmış kural metni öner. Sadece öner; onay almadan md'yi düzenleme.

audit rule

Starts from what went wrong and audits the rule instead of the output. Searches every rule file.

EN
The reverse of `apply`: it starts from something that went wrong and audits **the rule itself**. The input is the rest of the message — a complaint, a piece of output, "you did X again".

Two forms, identical in every step below. The only difference is where you look:
* **`audit rule`** — search every rule file: the global `CLAUDE.md`, the project's `CLAUDE.md`, and the global knowledge mds.


1. **Find the rule.** Name every rule that bears on the failure, with its file and section.
2. **Diagnose, and say which of these five it is.** Only the second and third are fixed by editing rule text:
   * **No rule exists** → propose one.
   * **The rule is ambiguous or incomplete** → tighten it.
   * **It contradicts another rule** → say which wins, and fix the loser.
   * **The rule was never loaded** (it sits in a global knowledge md that is read on demand and nothing pointed at it) → the fix is a pointer, not more words.
   * **The rule is clear and correct and I did not follow it** → say so plainly. Do not propose a text change; padding the file fixes nothing and hides the real cause.
3. **Propose and stop.** Show before/after when text changes. Proposing no change is a valid outcome.
4. **Write on approval, then the landing report.**

Scope: rule files only. The output is a diagnosis and at most a wording change, never a code change.
TR
`apply`'ın tersi: ters giden bir şeyden başlar ve **kuralın kendisini** denetler. Girdi mesajın geri kalanıdır: bir şikâyet, bir çıktı parçası, "yine X yaptın".

İki biçim, aşağıdaki her adımda aynı. Tek fark nereye baktığın:

* **`audit rule`** — bütün kural dosyalarını ara: global `CLAUDE.md`, projenin `CLAUDE.md` dosyası ve global knowledge md'ler.


1. **Kuralı bul.** Hataya değen her kuralı dosyası ve bölümüyle birlikte söyle.
2. **Teşhis et ve bu beşten hangisi olduğunu söyle.** Sadece ikincisi ve üçüncüsü kural metnini düzenleyerek çözülür:
  * **Kural yok** → bir tane öner.
  * **Kural belirsiz veya eksik** → sıkılaştır.
  * **Başka bir kuralla çelişiyor** → hangisinin kazandığını söyle ve kaybedeni düzelt.
  * **Kural hiç yüklenmemiş** (ihtiyaç anında okunan bir global knowledge md'de duruyor ve hiçbir şey oraya işaret etmemiş) → çözüm daha fazla kelime değil, bir işaretçi.
  * **Kural net ve doğru, ben uymadım** → bunu açıkça söyle. Metin değişikliği önerme; dosyayı şişirmek hiçbir şeyi çözmez, gerçek nedeni gizler.
3. **Öner ve dur.** Metin değişiyorsa öncesini ve sonrasını göster. Hiçbir değişiklik önermemek de geçerli bir sonuçtur.
4. **Onay gelince yaz, sonra iniş raporu.**

Kapsam: sadece kural dosyaları. Çıktı bir teşhis ve en fazla bir ifade değişikliğidir, asla bir kod değişikliği değil.
Claude Code prompt — the complaint, then the token
You gave me a command to run again. audit rule

audit name.md

The same audit, straight to one file. Use it when you know where the rule lives.

EN
**`audit <name>.md`** — go straight to `<name>.md`. Use it when you already know which file holds the rule; it saves the search and nothing else. If that file turns out to hold no rule bearing on the failure, say so rather than stretching one to fit, and offer to widen to the full search.
TR
**`audit <name>.md`** — doğrudan `<name>.md` dosyasına git. Kuralın hangi dosyada olduğunu zaten biliyorsan kullan; sadece aramayı atlatır, başka bir şeyi değil. O dosyada hataya değen bir kural çıkmazsa, bir kuralı zorla uydurmak yerine bunu söyle ve tam aramaya genişletmeyi öner.

harvest

At the end of a project, sweeps what was learned out of the session and into the knowledge docs.

EN
Trigger: a plain-language message containing "harvest", "harvest <name>", or "project harvest" (NOT a slash command). **Disambiguation: if the message also contains "full" or "tam", it is `full harvest`, not this command.** The archive-wide command always wins the overlap. Purpose: at the end of a project, sweep everything learned and persist the durable pieces into the global knowledge mds. This is the batch, end-of-project counterpart to the single-fact `+<name>.md` shorthand. A handoff is a transient state document; a harvest writes durable knowledge and leaves the code and the handoff untouched.

**Target root: `G:\My Drive\mindmap`.** Every md the harvest writes lives in this folder, regardless of which project the session was working in. Its hub is `G:\My Drive\mindmap\mindmap.md`; create it on the first harvest, born with its `Parent:` line, a `## Scope` section, and a `## Reference Docs` index.

1. **Gather sources — BOTH of them, always:**
   * **The conversation history.** Distill it with `G:\My Drive\mindmap\tools\distill.js` (`node distill.js <projectFolder> <outFile>`), which orders sessions, walks the `parentUuid` chain, and strips plumbing while KEEPING the content of file-writing tool calls. Never re-truncate those: the first full harvest lost every file a session wrote by capping tool inputs at 300 characters. Read the distilled file end to end, in sequential chunks, never sampled.
   * **The project's own code.** Open it and check the transcript's claims against it. The conversation says what was intended; the code says what shipped. An atom derived only from a conversation is a claim, not a fact, and must be marked as unverified if the code was not consulted.
2. **Extract durable knowledge atoms** worth keeping long term. Skip session-specific or ephemeral notes, anything trivially re-derivable from the code, and secrets. Apply the routing question from "Where a fact lives" above to every atom: anything that changes when the code changes belongs in that project's `memory.md`, not in the global knowledge mds. Report those separately instead of writing them here.
   * **Do not cap the atom count.** Extract what is there. A long project yields more atoms than a short one, and an artificial ceiling silently drops the tail.
   * **Images are a known blind spot.** Attached screenshots and pasted images are referenced in the transcript but their content is not stored, so any decision made visually is invisible to a harvest. The distiller marks each one. When a marked gap sits on a topic that matters, say so in the report rather than pretending the topic is covered.
3. **One child md per topic.** Every new topic gets its OWN `.md` file in the target folder, born with its `Parent:` line and `## Scope` section, and its reference line is added to the hub in the same commit (Reference Doc Rule, section C). Never write a topic's detail inline into the hub; the hub holds only the pointer list.
4. **Routing:** match each atom against the existing children's `## Scope` sections and write it into the single best-fitting one. If no child fits, create a new child md for that topic. If a child is itself a hub, recurse one level down until you reach a leaf.
5. **Compound decomposition.** A finding that carries several independent facts is split into atoms, and each atom lands in exactly one leaf. Never write the same meaning at two levels; ancestors rely on links, not copies.
6. **Dedup:** if an equivalent fact already exists, refine or extend it rather than adding a second copy.
7. **Preview is mandatory.** Before writing anything, present the proposed additions as an `atom → target md` list, explicitly marking which target files do not exist yet and will be created. Get my approval first.
8. **Landing report:** after writing, list every doc touched and every doc created.
9. **Never fabricate.** If a fact's source or a technical detail cannot be determined from the history or the code, ask me instead of guessing.
TR
Tetikleyici: içinde "harvest", "harvest <name>" veya "project harvest" geçen düz bir mesaj (slash komutu DEĞİL). **Ayrım: mesajda ayrıca "full" veya "tam" geçiyorsa bu komut değil `full harvest`'tir.** Örtüşmede her zaman arşiv geneli komut kazanır. Amaç: bir projenin sonunda öğrenilen her şeyi süpürmek ve kalıcı parçaları global knowledge md'lere işlemek. Bu, tek bilgilik `+<name>.md` kısayolunun toplu, proje sonu karşılığıdır. Handoff geçici bir durum dokümanıdır; harvest kalıcı bilgi yazar ve kodu da handoff'u da ellemez.

**Hedef kök: `G:\My Drive\mindmap`.** Harvest'in yazdığı her md, session hangi projede çalışıyor olursa olsun bu klasörde yaşar. Hub'ı `G:\My Drive\mindmap\mindmap.md`; ilk harvest'te oluştur, `Parent:` satırı, bir `## Scope` bölümü ve bir `## Reference Docs` dizini ile doğsun.

1. **Kaynakları topla — her zaman İKİSİNİ birden:**
  * **Konuşma geçmişi.** Onu `G:\My Drive\mindmap\tools\distill.js` ile damıt (`node distill.js <projectFolder> <outFile>`); bu araç session'ları sıralar, `parentUuid` zincirini yürür ve tesisatı ayıklarken dosyaya yazan araç çağrılarının içeriğini KORUR. Onları asla yeniden kırpma: ilk full harvest, araç girdilerini 300 karakterde keserek bir session'ın yazdığı her dosyayı kaybetti. Damıtılmış dosyayı baştan sona, sıralı parçalar halinde oku, asla örnekleyerek değil.
  * **Projenin kendi kodu.** Onu aç ve transkriptin iddialarını koda karşı kontrol et. Konuşma neyin amaçlandığını söyler; kod neyin çıktığını. Sadece konuşmadan türetilmiş bir atom bilgi değil iddiadır ve koda bakılmadıysa doğrulanmamış olarak işaretlenmelidir.
2. **Uzun vadede tutmaya değer kalıcı bilgi atomlarını çıkar.** Session'a özel veya geçici notları, koddan zahmetsizce yeniden türetilebilecek her şeyi ve secret'ları atla. Yukarıdaki "Bir bilgi nerede yaşar" yönlendirme sorusunu her atoma uygula: kod değiştiğinde değişen her şey global knowledge md'lere değil o projenin `memory.md` dosyasına aittir. Onları buraya yazmak yerine ayrıca raporla.
  * **Atom sayısına üst sınır koyma.** Orada ne varsa çıkar. Uzun bir proje kısa olandan daha fazla atom verir ve yapay bir tavan kuyruğu sessizce düşürür.
  * **Görseller bilinen bir kör nokta.** Eklenen ekran görüntüleri ve yapıştırılan görseller transkriptte anılır ama içerikleri saklanmaz, yani görsel olarak verilen her karar bir harvest'e görünmezdir. Damıtıcı her birini işaretler. İşaretli bir boşluk önemli bir konuya denk geliyorsa, konu kapsanmış gibi davranmak yerine bunu raporda söyle.
3. **Konu başına bir çocuk md.** Her yeni konu hedef klasörde KENDİ `.md` dosyasını alır, `Parent:` satırı ve `## Scope` bölümüyle doğar ve referans satırı aynı commit'te hub'a eklenir (Referans Doküman Kuralı, bölüm C). Bir konunun detayını asla hub'ın içine yazma; hub sadece işaretçi listesini tutar.
4. **Yönlendirme:** her atomu mevcut çocukların `## Scope` bölümleriyle eşleştir ve tek en iyi uyana yaz. Hiçbir çocuk uymuyorsa o konu için yeni bir çocuk md oluştur. Bir çocuk kendisi hub ise, bir yaprağa ulaşana kadar bir seviye daha in.
5. **Bileşik ayrıştırma.** Birden fazla bağımsız bilgi taşıyan bir bulgu atomlara bölünür ve her atom tam olarak bir yaprağa iner. Aynı anlamı asla iki seviyede yazma; üst seviyeler kopyaya değil linke dayanır.
6. **Tekrar önleme:** eşdeğer bir bilgi zaten varsa ikinci bir kopya eklemek yerine onu iyileştir veya genişlet.
7. **Önizleme zorunlu.** Hiçbir şey yazmadan önce, önerilen eklemeleri `atom → hedef md` listesi olarak sun ve hangi hedef dosyaların henüz olmadığını, oluşturulacağını açıkça işaretle. Önce onayımı al.
8. **İniş raporu:** yazdıktan sonra dokunulan ve oluşturulan her dokümanı listele.
9. **Asla uydurma.** Bir bilginin kaynağı veya bir teknik ayrıntı geçmişten ya da koddan belirlenemiyorsa tahmin etmek yerine bana sor.

full harvest

The same sweep over every session ever recorded, one project at a time.

EN
Trigger: a plain-language message containing "full harvest", "harvest full", or "tam harvest" (NOT a slash command). It is the archive-wide version of `harvest`: it sweeps **every** session ever recorded, not just the current project's. Do not confuse it with `handoff full`, which writes a `handoff.md` into a project root; `full harvest` writes durable knowledge into the `G:\My Drive\mindmap` tree and touches nothing else.

**Scope:** every project folder under `C:\Users\firat\.claude\projects` and every `.jsonl` inside it. The directory the session happens to be running in does NOT narrow the scope. `history.jsonl` is out of scope.

**Project is the primary axis, chronology lives inside a project.** Never merge messages from different projects into one global timeline: interleaving them by timestamp destroys each conversation's chain of reasoning. Read one project fully, start to finish, as a single narrative before moving to the next.
* Within a project, order sessions by timestamp and messages by the `parentUuid` chain.
* If one project was opened from more than one path (different slugs, same `cwd`), treat those folders as one project.
* Read large files in chunks with a running summary. Skip tool-call plumbing, cancelled attempts and error spam; keep decisions, outcomes and rationale. Redact secrets.
* **Parallel reading:** one subagent per project folder is authorized here, since each agent reads one project end to end and the narrative stays intact. Parallelism happens between projects, never inside one.

**Writing:** identical to `harvest` above (one child md per topic, born with `Parent:` and `## Scope`, hub reference line in the same commit, atoms land in exactly one leaf, dedup by refining the existing entry, mandatory preview, landing report, never fabricate). Additionally, **every knowledge atom records which project it came from**, because the same topic may have resolved differently in two projects and that difference is itself the knowledge.

**Cross-project pass:** only after every project has been harvested individually, do a second pass extracting the patterns and lessons common across projects. The comparison sits above the projects, never inside them.

**Incremental by default.** The hub `G:\My Drive\mindmap\mindmap.md` carries a `## Harvest log` section with one row per session: project slug, session id, the uuid and timestamp of the last message processed, and the date it was processed. Each run reads that log and classifies every session as never seen (read fully), grown since last time (resume from the recorded uuid), or unchanged (skip). A session in the log whose file no longer exists is reported as a lost session, never silently skipped. "full harvest rebuild" forces a complete re-scan ignoring the log.

Because the input arrives in increments, the dedup rule is critical here: a later run may extend or contradict a decision recorded earlier, so refine the existing entry instead of letting two versions of the same topic sit side by side.
TR
Tetikleyici: içinde "full harvest", "harvest full" veya "tam harvest" geçen düz bir mesaj (slash komutu DEĞİL). `harvest`'in arşiv geneli sürümüdür: sadece mevcut projenin değil, kaydedilmiş **bütün** session'ları süpürür. Proje köküne bir `handoff.md` yazan `handoff full` ile karıştırma; `full harvest` kalıcı bilgiyi `G:\My Drive\mindmap` ağacına yazar ve başka hiçbir şeye dokunmaz.

**Kapsam:** `C:\Users\firat\.claude\projects` altındaki her proje klasörü ve içindeki her `.jsonl`. Session'ın hangi dizinde çalıştığı kapsamı DARALTMAZ. `history.jsonl` kapsam dışıdır.

**Ana eksen projedir, kronoloji bir projenin içinde yaşar.** Farklı projelerin mesajlarını asla tek bir global zaman çizelgesinde birleştirme: onları zaman damgasına göre iç içe geçirmek her konuşmanın düşünce zincirini yok eder. Bir sonrakine geçmeden önce bir projeyi baştan sona, tek bir anlatı olarak oku.

* Bir projenin içinde session'ları zaman damgasına, mesajları `parentUuid` zincirine göre sırala.
* Bir proje birden fazla yoldan açıldıysa (farklı slug'lar, aynı `cwd`), o klasörleri tek bir proje say.
* Büyük dosyaları ilerleyen bir özetle parça parça oku. Araç çağrısı tesisatını, iptal edilmiş denemeleri ve hata gürültüsünü atla; kararları, sonuçları ve gerekçeleri tut. Secret'ları gizle.
* **Paralel okuma:** burada proje klasörü başına bir subagent yetkilidir, çünkü her agent bir projeyi baştan sona okur ve anlatı bozulmaz. Paralellik projeler arasında olur, asla bir projenin içinde değil.

**Yazma:** yukarıdaki `harvest` ile aynı (konu başına bir çocuk md, `Parent:` ve `## Scope` ile doğar, aynı commit'te hub referans satırı, atomlar tam olarak bir yaprağa iner, mevcut kaydı iyileştirerek tekrar önleme, zorunlu önizleme, iniş raporu, asla uydurma). Ek olarak, **her bilgi atomu hangi projeden geldiğini kaydeder**, çünkü aynı konu iki projede farklı sonuçlanmış olabilir ve bu fark başlı başına bilgidir.

**Projeler arası geçiş:** ancak her proje tek tek harvest edildikten sonra, projeler arasında ortak olan örüntüleri ve dersleri çıkaran ikinci bir tur yap. Karşılaştırma projelerin üstünde durur, asla içlerinde değil.

**Varsayılan olarak artımlı.** Hub `G:\My Drive\mindmap\mindmap.md` içinde session başına bir satır taşıyan bir `## Harvest log` bölümü bulunur: proje slug'ı, session id'si, işlenen son mesajın uuid'i ve zaman damgası ve işlendiği tarih. Her çalışma bu günlüğü okur ve her session'ı hiç görülmemiş (baştan sona okunur), geçen seferden beri büyümüş (kayıtlı uuid'den devam edilir) veya değişmemiş (atlanır) olarak sınıflandırır. Günlükte olup dosyası artık bulunmayan bir session kayıp session olarak raporlanır, asla sessizce atlanmaz. "full harvest rebuild" günlüğü yok sayarak tam bir yeniden tarama yaptırır.

Girdi parça parça geldiği için tekrar önleme kuralı burada kritiktir: sonraki bir çalışma daha önce kaydedilmiş bir kararı genişletebilir veya onunla çelişebilir, o yüzden aynı konunun iki sürümünü yan yana bırakmak yerine mevcut kaydı iyileştir.

handoff full

Writes handoff.md from the project's whole history, not just the open context window.

EN
Trigger: a plain-language message containing "handoff full", "handoff project", "full handoff", or "project handoff" (NOT a slash command; never confuse it with any `/handoff` skill and never modify that skill). Produce a handoff from the project's entire conversation history, not just the active context window.
* **Input:** derive the project's session folder under `~/.claude/projects/` from the working directory (path flattened to the folder name) and read ALL `*.jsonl` files there, including other folders if the project was opened from more than one path (match on the `cwd` field). Order files by timestamp and follow the `parentUuid` chain to reconstruct true message order. Read large files in chunks and accumulate a running summary; do not assume it fits one context window. Skip tool-call plumbing, cancelled attempts and error spam; keep decisions, outcomes and rationale. Redact secrets.
* **Output:** Goal → Detailed Project → Current State → Files in flight → Changed → Failed attempts → Next step, plus a **Decision Timeline** (key decisions and turning points in order) and the abandoned approaches a single-session handoff would miss. Save to `handoff.md` in the project root, overwriting any existing one. Never create a separate `handoff-full.md`.
TR
Tetikleyici: içinde "handoff full", "handoff project", "full handoff" veya "project handoff" geçen düz bir mesaj (slash komutu DEĞİL; herhangi bir `/handoff` skill'iyle asla karıştırma ve o skill'i asla değiştirme). Sadece aktif bağlam penceresinden değil, projenin bütün konuşma geçmişinden bir handoff üret.

* **Girdi:** projenin `~/.claude/projects/` altındaki session klasörünü çalışma dizininden türet (yol, klasör adına düzleştirilmiş) ve oradaki BÜTÜN `*.jsonl` dosyalarını oku; proje birden fazla yoldan açıldıysa diğer klasörler de dahil (`cwd` alanından eşleştir). Dosyaları zaman damgasına göre sırala ve gerçek mesaj sırasını yeniden kurmak için `parentUuid` zincirini takip et. Büyük dosyaları parça parça oku ve ilerleyen bir özet biriktir; tek bağlam penceresine sığdığını varsayma. Araç çağrısı tesisatını, iptal edilmiş denemeleri ve hata gürültüsünü atla; kararları, sonuçları ve gerekçeleri tut. Secret'ları gizle.
* **Çıktı:** Hedef → Detaylı Proje → Mevcut Durum → Havadaki dosyalar → Değişenler → Başarısız denemeler → Sonraki adım, ayrıca bir **Karar Zaman Çizelgesi** (önemli kararlar ve dönüm noktaları sırayla) ve tek session'lık bir handoff'un kaçıracağı terk edilmiş yaklaşımlar. Proje kökündeki `handoff.md` dosyasına kaydet, varsa üzerine yaz. Asla ayrı bir `handoff-full.md` oluşturma.

soru: / question:

Answer only. Nothing is done until I ask for it.

EN
If I start with "soru:" or "question:" it means that do not take any action, just answer my question and wait for my permission to take action.
TR
"soru:" veya "question:" ile başlarsam hiçbir işlem yapma, sadece sorumu cevapla ve işlem yapmak için iznimi bekle demektir.

got it?

Explain it back first, assumptions included. Nothing happens until I confirm.

EN
If I say "got it?" it means: explain back to me what you understood from what I described, including your interpretation, assumptions and planned approach, and take NO action until I explicitly confirm.
TR
"got it?" dersem şu demektir: anlattığım şeyden ne anladığını, yorumunu, varsayımlarını ve planladığın yaklaşımı bana geri anlat ve ben açıkça onaylayana kadar HİÇBİR işlem yapma.

best min

Say it in the fewest, plainest words that still do the job.

EN
**`best min` — the fewest, plainest words.** When a message contains `best min`, write the requested
text in as few words as it can carry, using the ordinary word rather than the technical or clever
one. Brevity stops where the text stops doing its job: a heading still has to pull, a button still
has to say what it does, an instruction still has to be followable. Give one version, not a menu;
a second only when it is genuinely a coin toss. It applies to the text I asked for, not to the rest
of your reply.
TR
Bir mesajda `best min` geçtiğinde, istenen metni taşıyabileceği en az kelimeyle yaz; teknik veya zekice olanı değil sıradan kelimeyi kullan. Kısalık, metnin işini görmeyi bıraktığı yerde biter: bir başlık hâlâ çekmeli, bir buton hâlâ ne yaptığını söylemeli, bir talimat hâlâ uygulanabilir olmalı. Menü değil tek bir sürüm ver; ikincisini ancak gerçekten yazı tura ise. Bu kural istediğim metin için geçerlidir, cevabının geri kalanı için değil.

project:

A project name and a colon in front of the message says which project it is about.

EN
If I write "tranferet_web:" it means that prompt is related with transferet web project, if I write "transferet_mobile:" it means that prompt is related with transferet mobile project. if I write admin_panel then it is related with C:\Users\firat\apps\transferet-admin project.
TR
"tranferet_web:" yazarsam o prompt transferet web projesiyle ilgilidir, "transferet_mobile:" yazarsam transferet mobile projesiyle ilgilidir. admin_panel yazarsam C:\Users\firat\apps\transferet-admin projesiyle ilgilidir.

Every rule in my global CLAUDE.md

Copy any of them. Each card is the exact text from the file.

Working style & approvals

Run commands yourself

Never hand me a command. You run it.

EN
If you can do something in PowerShell or cmd, do it yourself. **Never** hand me a command to run and never ask whether you should run it. If a command can be executed by you, you execute it. The only exception is something you genuinely cannot do (an interactive login, a GUI step). Only then give it to me as a **single-line command in its own code block**, one command per block, no line continuations and no heredocs, using the syntax of the shell I am actually in (cmd-native commands for cmd.exe, PowerShell cmdlets only in a PowerShell prompt).
TR
PowerShell veya cmd ile yapabileceğin bir şey varsa kendin yap. Bana çalıştırmam için **asla** komut verme ve çalıştırıp çalıştırmayacağını sorma. Bir komutu sen çalıştırabiliyorsan sen çalıştırırsın. Tek istisna, gerçekten yapamayacağın bir şeydir (interaktif bir giriş, bir arayüz adımı). Ancak o zaman bana **kendi kod bloğunda tek satırlık bir komut** olarak ver, blok başına bir komut, satır devamı ve heredoc olmadan, gerçekten içinde olduğum shell'in söz dizimiyle (cmd.exe için cmd komutları, PowerShell cmdlet'leri sadece PowerShell prompt'unda).

Start the dev server yourself

Start it and restart it in the background, unasked.

EN
When I ask you to start, run, or restart a project or its dev server (e.g. "expoyu başlat", "projeyi çalıştır/başlat", "run the app"), start it yourself directly in the background. I should NEVER have to type a command in the terminal myself. Never tell me to run it; never ask for approval; just start it. The same applies to restarting a backend or dev server mid-task: do it yourself, do not request it from me.
TR
Bir projeyi ya da dev server'ını başlatmamı, çalıştırmamı veya yeniden başlatmamı istediğimde (ör. "expoyu başlat", "projeyi çalıştır/başlat", "run the app"), onu doğrudan arka planda kendin başlat. Terminale komut yazmak ASLA bana düşmemeli. Bana çalıştırmamı söyleme; onay isteme; sadece başlat. Aynısı görev ortasında bir backend veya dev server'ı yeniden başlatmak için de geçerli: kendin yap, benden isteme.

Do not ask for approval

Act. The carve-outs are named.

EN
Act. Three things are gated, and only three: deleting a table from the hosted backend, writing to a markdown rule file (the propose-first gate, section C), and anything leaving my machine (the deploy gate below). When something is genuinely unclear, do not stop to ask either: state the assumption you are proceeding on, do the work, and say what you assumed in your report.
TR
Harekete geç. Üç şey onaya tabidir, sadece üç: barındırılan backend'den tablo silmek, bir markdown kural dosyasına yazmak (önce-öner kapısı, bölüm C) ve makinemden çıkan her şey (aşağıdaki deploy kapısı). Bir şey gerçekten belirsizse de durup sorma: hangi varsayımla ilerlediğini söyle, işi yap ve raporunda ne varsaydığını belirt.

Pushing to a live environment is gated

Anything leaving my machine needs a yes first.

EN
Anything leaving my machine needs my explicit approval first. Free without asking: local servers, staging and preview deploys, reads, and writing or committing code.

"Live" covers the hosted backend, not just websites: Supabase migrations, edge function deploys, secrets, auth and storage config, creating a hosted project. Without a separate staging, the one hosted environment is production, including before launch and while nothing calls the change yet. "Harmless right now" and "keeping repo and deployment in sync" are not exemptions. If it cannot be shown locally (OAuth callback, edge function, cron job), ask; never proceed.
TR
Makinemden çıkan her şey önce açık onayımı ister. Sormadan serbest olanlar: yerel sunucular, staging ve preview deploy'ları, okumalar, kod yazmak veya commit'lemek.

"Canlı" sadece web sitelerini değil, barındırılan backend'i de kapsar: Supabase migration'ları, edge function deploy'ları, secret'lar, auth ve storage ayarları, barındırılan bir proje oluşturmak. Ayrı bir staging yoksa tek barındırılan ortam production'dır; lansman öncesi de, değişikliği henüz hiçbir şey çağırmıyorken de. "Şu an zararsız" ve "repo ile deployment'ı senkron tutmak" muafiyet değildir. Yerelde gösterilemiyorsa (OAuth callback, edge function, cron job) sor; asla kendiliğinden ilerleme.

Preserve my ability to change my mind

Make a setting stick without pinning it.

EN
When I ask for a setting or behavior to "stick", I mean *whatever I choose from now on should persist*, not *pin it to a fixed value*. A solution that enforces a value by configuration or policy is the wrong shape even when it stops the symptom, because it takes the choice away from me. Offer the agency-preserving fix first; if only an enforcing one exists, say so plainly and get a decision before applying it.
TR
Bir ayarın veya davranışın "kalıcı olmasını" istediğimde kastettiğim *bundan sonra ne seçersem o kalsın*, *sabit bir değere sabitlensin* değil. Bir değeri konfigürasyon veya politika ile dayatan çözüm, belirtiyi durdursa bile yanlış biçimdir, çünkü seçimi benden alır. Önce tercihimi koruyan çözümü öner; sadece dayatan bir çözüm varsa bunu açıkça söyle ve uygulamadan önce karar al.

Porting a design to another platform needs an agreed parity boundary

Say what stays 1:1 and what is replaced.

EN
Before building, state which parts will be 1:1 with the original and which are being intentionally replaced, and why — then get agreement. Do not silently ship a simplified version.
TR
Yapmaya başlamadan önce hangi kısımların orijinaliyle birebir olacağını, hangilerinin bilerek değiştirildiğini ve nedenini söyle, sonra mutabakat al. Sessizce basitleştirilmiş bir sürüm çıkarma.

Language & communication

Output language

English for anyone but me.

EN
Anything a person other than me will read is written in **English**: app UI (buttons, labels, toasts, modals, badges, tooltips), emails, presentations, reports, and anything shared outside this chat. Documents written for me (memory.md, handoff.md, technical notes, reference docs) and the chat conversation itself may stay in Turkish.
TR
Benden başka birinin okuyacağı her şey **İngilizce** yazılır: uygulama arayüzü (butonlar, etiketler, toast'lar, modal'lar, badge'ler, tooltip'ler), e-postalar, sunumlar, raporlar ve bu sohbetin dışına çıkan her şey. Benim için yazılan dokümanlar (memory.md, handoff.md, teknik notlar, başvuru dokümanları) ve sohbetin kendisi Türkçe kalabilir.

Drafting text I will send

Short, plain, corporate-polite, no dashes.

EN
(email, chat, requests): write it in English and follow all of these.
* Short and results-oriented. Fewest words possible, cut anything not needed. Corporate-polite.
* Confirm-understanding structure when relevant: state the understanding and ask to confirm, phrased naturally ("Just to confirm our understanding:"), never a clunky literal like "We understood this:". Then any questions clearly, then the ask.
* **No dashes as punctuation.** No em dash, no en dash, no hyphen used as a pause. Use commas, periods, parentheses, or rewrite the sentence.
* Simple, user-friendly words and plain sentence structure, not fancy or idiomatic phrasing.
* **Never open with "I am."** It reads as amateur. Use a participial opener instead ("Currently serving as…", "As the…").
* **Never use the word "simply."** It irritates the reader.
* Soften ownership the corporate way ("I had the opportunity to build", "I would welcome the chance to contribute") rather than making blunt claims.
* A motivation statement is short and framed as the experience matching the position's requirements, not as personal enthusiasm.
TR
(e-posta, sohbet, talepler): İngilizce yaz ve bunların hepsine uy.

* Kısa ve sonuç odaklı. Mümkün olan en az kelime, gereksiz her şey çıkarılmış. Kurumsal-nazik.
* Uygun olduğunda anlaşıldığını teyit eden yapı: anlaşılanı söyle ve teyit iste, doğal bir ifadeyle ("Just to confirm our understanding:"), "We understood this:" gibi hantal bir çeviriyle asla. Sonra varsa sorular net şekilde, sonra talep.
* **Noktalama olarak tire yok.** Uzun tire yok, kısa tire yok, duraklama için kullanılan kısa çizgi yok. Virgül, nokta, parantez kullan ya da cümleyi yeniden yaz.
* Basit, kullanıcı dostu kelimeler ve düz cümle yapısı; süslü veya deyimsel anlatım değil.
* **Asla "I am" ile başlama.** Acemice duruyor. Onun yerine ortaç ile başla ("Currently serving as…", "As the…").
* **"simply" kelimesini asla kullanma.** Okuyanı rahatsız ediyor.
* Sahipliği kurumsal biçimde yumuşat ("I had the opportunity to build", "I would welcome the chance to contribute"), sert iddialar kurma.
* Motivasyon cümlesi kısadır ve kişisel heves olarak değil, deneyimin pozisyonun gerekleriyle örtüşmesi olarak kurulur.

No blockquote for copy-paste text

The left bar corrupts copy-paste.

EN
Any block of text meant for me to copy out of the chat and reuse elsewhere must be output as plain text, never a markdown blockquote (`>`). The vertical bar a blockquote renders on the left corrupts copy-paste. This is NOT limited to messages I will send: it covers presentation and speech scripts, talking points, verbatim snippets, and any other copyable text. When structure must be preserved, use a fenced code block, never the `>` bar.
TR
Sohbetten kopyalayıp başka yerde kullanacağım her metin bloğu düz metin olarak çıkmalı, asla markdown blok alıntısı (`>`) olarak değil. Blok alıntının solda çizdiği dikey çizgi kopyalamayı bozuyor. Bu SADECE göndereceğim mesajlarla sınırlı değil: sunum ve konuşma metinlerini, konuşma başlıklarını, birebir alıntıları ve kopyalanabilir her metni kapsar. Yapı korunmak zorundaysa çitli kod bloğu kullan, asla `>` çizgisini değil.

Terminology consistency

One term per concept, everywhere.

EN
Within a single project or deliverable, use one consistent term for each concept everywhere it appears (labels, notes, tooltips, headings). Never mix synonyms for the same thing. Conversely, genuinely different concepts keep distinct terms so they are not confused.
TR
Tek bir proje veya teslimat içinde, her kavram için geçtiği her yerde aynı terimi kullan (etiketler, notlar, tooltip'ler, başlıklar). Aynı şey için asla eş anlamlıları karıştırma. Tersine, gerçekten farklı kavramlar ayrı terimlerini korur ki karışmasınlar.

Questions aimed at different people are grouped separately

Separate them and label every one.

EN
When a document raises open questions or comments for more than one audience, never present them as one mixed list. Separate each audience visually (its own margin side where the format has margin comments, its own heading in plain text), and label every single comment with whose it is, so the attribution survives being copied out of its position.
TR
Bir doküman birden fazla okuyucuya açık soru veya yorum yöneltiyorsa, bunları asla tek bir karışık liste olarak sunma. Her okuyucuyu görsel olarak ayır (kenar yorumu olan formatlarda kendi kenarında, düz metinde kendi başlığı altında) ve her yorumu kimin olduğuyla etiketle ki atıf, yorum yerinden kopyalandığında da kaybolmasın.

Documentation tree & the +<name> shorthand

Reference Doc Rule

A doc nobody links to is never read.

EN
Whenever a new `.md` documentation file is created, add a one-line reference to it, in the same commit, in the CLAUDE.md or root doc of its level. Format: `- [\`filename.md\`](./filename.md) — one-line description of what it covers.` A doc that no CLAUDE.md ever mentions will never be read.

Every new knowledge/reference md opens with a top `Parent:` line (its lineage plus a one-line teaser of what it covers), then a `## Scope` section as the first `##` right after it: the authoritative definition of what the doc owns, with explicit boundary lines against its sibling docs. Both are created in the same commit as the file. Exempt: throwaway artifacts like handoffs and specs.

**Always announce the folder.** Whenever you create or write any `.md` file, state in your reply which folder you put it in (its full path), so I always know where it landed.

**Docs-first pointer wording.** When a CLAUDE.md delegates a project's rules or knowledge to a reference doc, never scope the pointer to changes only ("read it before any change" is too narrow, questions then get answered from the code instead of the doc). Always word it as docs first, code second, for ALL work: "read it before any change AND before answering any question about how something works or should work, the code is only the implementation of those rules."

**A new branch joins the index in the same commit.** Add its **name** to the knowledge index at the top of this file, and its reference line to its parent. Nothing else: the teaser comes from the doc's own `Parent:` line, which the session hook reads straight from the file. A branch no index names is a branch nothing will look for — `language.md` and `tooltip.md` sat outside every index and were rediscovered by grep, after their rules had already been rebuilt from scratch.

**Moving a rule between docs.** If you move a rule from one md to another, leave a pointer to its new home in the source md.

**A new topic md collects what already exists.** When a new topic md is created, in the same commit search the whole tree for that topic and move every matching section into it, leaving a one-line pointer in each source. Update the neighbouring docs' `## Scope` boundary lines to name the new file. Move the text whole (generalised if it can be), never summarised, and never leave the same rule living in two docs.
TR
Yeni bir `.md` doküman dosyası oluşturulduğunda, aynı commit'te kendi seviyesindeki CLAUDE.md veya kök dokümana ona tek satırlık bir referans ekle. Biçim: `- [\](./filename.md)`filename.md\`— neyi kapsadığının tek satırlık açıklaması.` Hiçbir CLAUDE.md'nin anmadığı bir doküman asla okunmayacaktır.

Her yeni bilgi/başvuru md'si en üstte bir `Parent:` satırıyla açılır (soy zinciri ve neyi kapsadığının tek satırlık tanıtımı), hemen ardından ilk `##` olarak bir `## Scope` bölümü gelir: dokümanın neye sahip olduğunun bağlayıcı tanımı ve kardeş dokümanlara karşı açık sınır satırları. İkisi de dosyayla aynı commit'te oluşturulur. Muaf: handoff ve spec gibi tek kullanımlık ürünler.

**Klasörü her zaman söyle.** Herhangi bir `.md` dosyası oluşturduğunda veya yazdığında, cevabında onu hangi klasöre koyduğunu (tam yolunu) söyle ki nereye indiğini her zaman bileyim.

**Önce doküman işaretçisinin ifadesi.** Bir CLAUDE.md bir projenin kurallarını veya bilgisini bir başvuru dokümanına devrettiğinde, işaretçiyi asla sadece değişikliklerle sınırlama ("herhangi bir değişiklikten önce oku" çok dardır, sorular o zaman dokümandan değil koddan cevaplanır). Her zaman TÜM işler için önce doküman, sonra kod biçiminde yaz: "herhangi bir değişiklikten önce VE bir şeyin nasıl çalıştığı ya da nasıl çalışması gerektiği hakkındaki herhangi bir soruyu cevaplamadan önce oku, kod sadece o kuralların uygulamasıdır."

**Yeni bir dal aynı commit'te dizine katılır.** Bu dosyanın en üstündeki bilgi dizinine **adını**, ebeveynine de referans satırını ekle. Fazlası yok: tanıtım, dokümanın kendi `Parent:` satırından gelir ve session hook'u onu doğrudan dosyadan okur. Hiçbir dizinin adını anmadığı bir dal, hiç kimsenin aramayacağı bir daldır — `language.md` ve `tooltip.md` her dizinin dışında kaldı ve kuralları sıfırdan yeniden kurulduktan sonra grep ile bulundu.

**Bir kuralı dokümanlar arasında taşımak.** Bir kuralı bir md'den diğerine taşırsan, kaynak md'de yeni adresine bir işaretçi bırak.

**Yeni bir konu md'si hâlihazırda var olanı toplar.** Yeni bir konu md'si oluşturulduğunda, aynı commit'te bütün ağacı o konu için tara ve eşleşen her bölümü içine taşı, her kaynakta tek satırlık bir işaretçi bırak. Komşu dokümanların `## Scope` sınır satırlarını yeni dosyayı anacak şekilde güncelle. Metni bütün olarak taşı (genelleştirilebiliyorsa genelleştirerek), asla özetleyerek değil ve aynı kuralı asla iki dokümanda yaşatma.

A rule that is a procedure gets pseudocode, not prose alone

Prose states a verdict well and a procedure badly.

EN
If a rule is an **order of operations** — several options tried in turn, conditions deciding which
one wins, a loop, an argument for why it terminates — write 10 to 20 lines of **pseudocode** beside
the prose that explains why. Prose states a verdict well and a procedure badly: derived from
sentences, the order comes out wrong, and a wrong order fails **silently**, because the result still
looks plausible.

**The pseudocode carries the order and the conditions, nothing else.** No values, no styling, no
framework, no language. Numbers, thresholds and reasons stay in the prose, so changing a value never
touches the block.

**Never put a whole implementation in a knowledge doc**, in prose or in a fenced block or as a file
beside it. That is a second source of truth: it drifts from the rule silently, and the next project
starts from the drifted copy while appearing to obey the rule. Code is also the least portable thing
in the doc — the rules survive a change of stack, an implementation does not.

**Test:** if the rule can be stated in one sentence without *then*, *otherwise* or *repeat*, it does
not need pseudocode.
TR
Bir kural bir **işlem sırası** ise — sırayla denenen birkaç seçenek, hangisinin kazandığına karar
veren koşullar, bir döngü, neden sonlandığına dair bir gerekçe — nedenini anlatan düz yazının yanına
10 ila 20 satır **sözde kod** yaz. Düz yazı bir hükmü iyi, bir prosedürü kötü anlatır: cümlelerden
türetildiğinde sıra yanlış çıkar ve yanlış bir sıra **sessizce** başarısız olur, çünkü sonuç yine de
makul görünür.

**Sözde kod sırayı ve koşulları taşır, başka hiçbir şeyi.** Değer yok, biçim yok, çerçeve yok, dil
yok. Sayılar, eşikler ve gerekçeler düz yazıda kalır, böylece bir değeri değiştirmek bloğa hiç
dokunmaz.

**Bir bilgi dokümanına asla bütün bir uygulamayı koyma**, ne düz yazıda, ne çitli bir blokta, ne de
yanına bir dosya olarak. O, ikinci bir doğruluk kaynağıdır: kuraldan sessizce uzaklaşır ve bir
sonraki proje, kurala uyuyormuş gibi görünerek sapmış kopyadan başlar. Kod aynı zamanda dokümandaki
en az taşınabilir şeydir — kurallar bir teknoloji değişikliğinden sağ çıkar, bir uygulama çıkmaz.

**Test:** kural *sonra*, *aksi hâlde* ya da *tekrarla* kullanmadan tek bir cümlede söylenebiliyorsa,
sözde koda ihtiyacı yoktur.

Project hygiene & handoffs

Never write to another project's code from a foreign session

Reading is fine. Writes are the boundary.

EN
A session working on project A must never modify project B's code, config, or deployment, no matter how well B seems documented and even when the change looks small and safe. Reading B's files to understand or reference them is fine; **writes are the boundary.** A foreign session lacks the target project's loaded context (its CLAUDE.md, handoffs, consumer map, gotchas) and documentation is never the whole picture.

Instead: export a handoff md into the target project's folder describing the needed change (the trigger, the exact requirement, the evidence gathered, files likely involved, and acceptance criteria), then make the change in that project's own session. This rule exists because a cross-project "small and verified" fix once missed a client consumer the project's own session would have known about and took a live site down.
TR
A projesi üzerinde çalışan bir session, B projesinin kodunu, konfigürasyonunu veya deployment'ını asla değiştirmemeli; B ne kadar iyi dokümante görünürse görünsün ve değişiklik ne kadar küçük ve güvenli görünürse görünsün. B'nin dosyalarını anlamak veya referans almak için okumak serbesttir; **sınır yazmaktır.** Yabancı bir session, hedef projenin yüklü bağlamından (CLAUDE.md'si, handoff'ları, tüketici haritası, tuzakları) yoksundur ve dokümantasyon hiçbir zaman resmin tamamı değildir.

Onun yerine: hedef projenin klasörüne gereken değişikliği anlatan bir handoff md'si çıkar (tetikleyici, tam gereksinim, toplanan kanıt, muhtemel dosyalar ve kabul kriterleri), sonra değişikliği o projenin kendi session'ında yap. Bu kural var çünkü projeler arası "küçük ve doğrulanmış" bir düzeltme bir keresinde projenin kendi session'ının bileceği bir istemci tüketicisini atladı ve canlı bir siteyi düşürdü.

Where a fact lives: the project, or the global knowledge mds

Project memory, or the global mds. One test decides.

EN
Both the global CLAUDE.md and a project's own CLAUDE.md load into context automatically; the `mindmap` tree is read on demand. So the same fact written in two places costs context twice AND rots, because the two copies drift and nothing says which one is right. One test decides where a fact belongs:

**Does this fact change when the code changes?**

* **Yes → it lives with the code**, in the project's `memory.md`. Ports, commands, file paths, current state, data model, which file does what, what is built and what is not. **Never copy these into the global knowledge mds**: a copy starts going stale the moment it is written.
* **No → it lives in the global knowledge mds.** Why a decision was made, the alternatives that were rejected, platform limits, product logic, lessons learned. These stay true after the code changes and after the project dies, and you usually read them while standing in some *other* folder.

Three files, three jobs:
* **`<project>/CLAUDE.md`** is auto-loaded, so it is a **pointer file, not a knowledge store**: one line on what the project is, how to run it, and links to `memory.md` and to the relevant mindmap branches. Keep it under about twenty lines.
* **`<project>/memory.md`** is the operational truth, read on demand, updated with the code.
* **`mindmap/<project>.md`** is the outside view: identity, decisions and their reasons, roadmap thinking.

Two rules keep the split from collapsing:
* **Reusable technique notes never go in a project doc.** An Expo build trap, an overlay geometry gotcha, a Word COM recipe: these live in the mindmap topic doc and the project's CLAUDE.md links to it. When the same trap appears in a second project, it must already be in exactly one place.
* **Every harvest asks the routing question before writing an atom.** If the atom would change when the code changes, it belongs in that project's `memory.md`, not in the global knowledge mds. Note it for the project instead of writing it into mindmap.
TR
Hem global CLAUDE.md hem projenin kendi CLAUDE.md'si bağlama otomatik yüklenir; `mindmap` ağacı ise ihtiyaç anında okunur. Yani aynı bilginin iki yere yazılması bağlamda iki kere yer kaplar VE çürür, çünkü iki kopya birbirinden uzaklaşır ve hangisinin doğru olduğunu hiçbir şey söylemez. Bir bilginin nereye ait olduğuna tek bir test karar verir:

**Kod değiştiğinde bu bilgi değişir mi?**

* **Evet → kodla birlikte yaşar**, projenin `memory.md` dosyasında. Port'lar, komutlar, dosya yolları, mevcut durum, veri modeli, hangi dosya ne yapıyor, neyin yapıldığı ve neyin yapılmadığı. **Bunları global knowledge md'lere asla kopyalama**: bir kopya yazıldığı anda bayatlamaya başlar.
* **Hayır → global knowledge md'lerde yaşar.** Bir kararın neden verildiği, reddedilen alternatifler, platform sınırları, ürün mantığı, alınan dersler. Bunlar kod değiştikten ve proje öldükten sonra da doğru kalır ve genelde bambaşka bir klasörde dururken okunur.

Üç dosya, üç iş:

* **`<project>/CLAUDE.md`** otomatik yüklenir, bu yüzden **bilgi deposu değil, işaretçi dosyadır**: projenin ne olduğuna dair tek satır, nasıl çalıştırıldığı ve `memory.md` ile ilgili mindmap dallarına linkler. Yaklaşık yirmi satırın altında tut.
* **`<project>/memory.md`** işletme gerçeğidir, ihtiyaç anında okunur, kodla birlikte güncellenir.
* **`mindmap/<project>.md`** dışarıdan bakıştır: kimlik, kararlar ve gerekçeleri, yol haritası düşüncesi.

İki kural bu ayrımın çökmesini engeller:

* **Yeniden kullanılabilir teknik notlar asla proje dokümanına girmez.** Bir Expo build tuzağı, bir overlay geometri tuzağı, bir Word COM tarifi: bunlar mindmap konu dokümanında yaşar ve projenin CLAUDE.md'si oraya link verir. Aynı tuzak ikinci bir projede karşınıza çıktığında, zaten tek bir yerde duruyor olmalı.
* **Her harvest bir atomu yazmadan önce yönlendirme sorusunu sorar.** Atom kod değiştiğinde değişecekse, global knowledge md'lere değil o projenin `memory.md` dosyasına aittir. Mindmap'e yazmak yerine proje için not düş.

Project memory file

Ports, data model, gotchas, kept current.

EN
Every project should have a `memory.md` at its root: a condensed, human-readable project reference covering ports and commands, data model, architecture, design tokens and decisions, and known gotchas. When you create or update it, add a reference to it from the project's `CLAUDE.md` near the top so it surfaces when CLAUDE.md loads. Keep it current as the project evolves. This is separate from the `~/.claude/projects/.../memory/` auto-memory system.
TR
Her projenin kökünde bir `memory.md` olmalı: port'ları ve komutları, veri modelini, mimariyi, tasarım token'larını ve kararları ve bilinen tuzakları kapsayan, yoğunlaştırılmış ve insan tarafından okunabilir bir proje başvurusu. Oluşturduğunda veya güncellediğinde projenin `CLAUDE.md` dosyasında üst taraflara ona bir referans ekle ki CLAUDE.md yüklendiğinde görünür olsun. Proje geliştikçe güncel tut. Bu, `~/.claude/projects/.../memory/` otomatik hafıza sisteminden ayrıdır.

Handoff docs cover the WHOLE project

Not just the last session's task.

EN
When writing a handoff, onboarding, or summary document, describe the entire project (what it is, the stack, how it fits together, referencing existing docs instead of duplicating them), not only the current session's task. Put the last session's work in a clearly separate "current focus / in progress" section. Never let the top-level Goal read as if the session's narrow task is the whole project.
TR
Bir handoff, onboarding veya özet dokümanı yazarken sadece mevcut session'ın görevini değil, projenin tamamını anlat (ne olduğu, teknoloji yığını, parçaların nasıl birleştiği, mevcut dokümanları tekrarlamak yerine onlara referans vererek). Son session'ın işini açıkça ayrı bir "mevcut odak / devam eden" bölümüne koy. Üstteki Hedef bölümü, session'ın dar görevi tüm projeymiş gibi okunmasın.

Local dev servers (Windows), launch and restart without piling up windows

One port, one check, one window.

EN
* **Per-port guard, never "both".** Start a service ONLY if ITS OWN port is free (`if (-not (Get-NetTCPConnection -LocalPort <p> -State Listen)) { Start-Process … }`), checked per service. NEVER use `if (-not (portA -and portB))`: when one is down it relaunches both.
* **Live means reuse.** If the port is already listening and no restart is needed, do nothing. When you DO relaunch (it is down, a dead `cmd /k` leftover lingers, or a backend change needs picking up), first close that service's own window and its node process (match by the service subpath in the process CommandLine), then start one fresh. **Never blanket `Stop-Process node`**: it orphans `/k` windows and can kill other apps' node.
* Frontend rarely needs a restart (Vite hot-reloads), but a backend run as `tsx src/app.ts` is NOT watch mode, so backend code changes do need one.
* Clean up a pile-up by killing `cmd.exe` / `conhost.exe` / `node.exe` whose CommandLine matches the project path, scoped to that project.
TR
* **Port başına kontrol, asla "ikisi birden" değil.** Bir servisi SADECE KENDİ port'u boşsa başlat (`if (-not (Get-NetTCPConnection -LocalPort <p> -State Listen)) { Start-Process … }`), her servis için ayrı kontrol ederek. `if (-not (portA -and portB))` ASLA kullanma: biri kapalıyken ikisini birden yeniden başlatır.
* **Ayakta olan yeniden kullanılır.** Port zaten dinliyorsa ve yeniden başlatma gerekmiyorsa hiçbir şey yapma. Yeniden başlatman GEREKTİĞİNDE (kapalıysa, ölü bir `cmd /k` kalıntısı duruyorsa ya da bir backend değişikliğinin alınması gerekiyorsa), önce o servisin kendi penceresini ve node process'ini kapat (process CommandLine'ındaki servis alt yolundan eşleştir), sonra bir tane taze başlat. **Asla toptan `Stop-Process node` yapma**: `/k` pencerelerini öksüz bırakır ve başka uygulamaların node'unu öldürebilir.
* Frontend nadiren yeniden başlatma ister (Vite sıcak yükler), ama `tsx src/app.ts` olarak çalışan bir backend izleme modunda DEĞİLDİR, dolayısıyla backend kod değişiklikleri yeniden başlatma ister.
* Yığılmayı temizlemek için CommandLine'ı proje yoluyla eşleşen `cmd.exe` / `conhost.exe` / `node.exe` süreçlerini, sadece o projeyle sınırlı kalarak öldür.

Moving a project and re-homing its Claude session

Move the folder, then re-home the session.

EN
* **The move:** stop any running dev server first (it holds locks on build dirs), then `Move-Item <src> <dst>` (a same-volume move is instant even with a huge `node_modules`). Gotcha: a file literally named `nul` (a Windows reserved device name) makes `Move-Item` fail with "Access denied"; delete it first with `cmd /c del "\\?\<path>\nul"`.
* **Before `git add`:** audit the project root for secret files (plain `.env`, which the default Next.js `.gitignore` does NOT cover since it ignores only `.env*.local`, plus credential files, `*.pem`, `*.key`) and heavy build artifacts (`.next/`, `node_modules/`, `tsconfig.tsbuildinfo`). Add them to `.gitignore`, then run `git add -n <folder>` and check the output before the real add.
* **Re-home the session:** a session is keyed to the directory `claude` was launched in. Manually copying the transcript `.jsonl` into the new slug folder does NOT work and editing its `cwd` fields is a dead end. The correct fix is to run `/cd <new-absolute-path>` inside the live session. Gotcha: `/cd` is not available in the VSCode/IDE window, run it from a real terminal session. Fallback: a session is always resumable from its original launch directory.
TR
* **Taşıma:** önce çalışan dev server'ları durdur (build dizinlerinde kilit tutuyorlar), sonra `Move-Item <src> <dst>` (aynı disk içindeki taşıma, devasa bir `node_modules` ile bile anlıktır). Tuzak: adı birebir `nul` olan bir dosya (Windows'un ayrılmış aygıt adı) `Move-Item` komutunu "Access denied" ile düşürür; önce `cmd /c del "\\?\<path>\nul"` ile sil.
* **`git add` öncesi:** proje kökünü secret dosyalar (düz `.env`, ki varsayılan Next.js `.gitignore` bunu KAPSAMAZ çünkü sadece `.env*.local` dosyalarını yok sayar, ayrıca kimlik bilgisi dosyaları, `*.pem`, `*.key`) ve ağır build çıktıları (`.next/`, `node_modules/`, `tsconfig.tsbuildinfo`) için denetle. Bunları `.gitignore` dosyasına ekle, sonra `git add -n <folder>` çalıştır ve gerçek ekleme öncesi çıktıyı kontrol et.
* **Session'ı yeni adresine alma:** bir session, `claude`'un başlatıldığı dizine bağlıdır. Transkript `.jsonl` dosyasını elle yeni slug klasörüne kopyalamak ÇALIŞMAZ ve `cwd` alanlarını düzenlemek çıkmaz sokaktır. Doğru çözüm, canlı session'ın içinde `/cd <yeni-mutlak-yol>` çalıştırmaktır. Tuzak: `/cd` VSCode/IDE penceresinde yoktur, gerçek bir terminal session'ından çalıştır. Yedek çözüm: bir session her zaman ilk başlatıldığı dizinden devam ettirilebilir.

Coding & git discipline

Auto-commit policy

Commit each working unit. Never push.

EN
Commit automatically after each completed and verified unit of work, without asking me first.
* Commit only when the change is coherent and working (build, tests, or a manual check pass). Never commit half-finished or knowingly broken code.
* Group by logical unit of work (one feature or fix per commit) with a clear message. Not one commit per tiny edit.
* Commit to the current branch, local only. **Never `push`**, and never add or change a remote, without my explicit approval each time.
* **Never `git add -A` or `git add .`.** Stage only the files this unit of work actually changed. Do not sweep in unrelated changes or stray/temp files.
* **Never commit secrets** (`.env`, files containing DB passwords, tokens, or keys). Exclude them and warn me if a tracked file already contains secrets.
* If on a shared, protected, or default branch (e.g. `main`), branch first before committing.
TR
Tamamlanmış ve doğrulanmış her iş biriminden sonra bana sormadan otomatik commit'le.

* Sadece değişiklik tutarlı ve çalışır durumdayken commit'le (build, testler veya elle bir kontrol geçmiş olmalı). Yarım kalmış veya bilerek bozuk kodu asla commit'leme.
* Mantıksal iş birimine göre grupla (commit başına bir özellik veya düzeltme), net bir mesajla. Her küçük düzenleme için bir commit değil.
* Mevcut branch'e, sadece yerel olarak commit'le. Her seferinde açık onayım olmadan **asla `push` etme** ve asla bir remote ekleme veya değiştirme.
* **Asla `git add -A` veya `git add .` yapma.** Sadece bu iş biriminin gerçekten değiştirdiği dosyaları stage'le. İlgisiz değişiklikleri veya başıboş/geçici dosyaları süpürüp alma.
* **Secret'ları asla commit'leme** (`.env`, veritabanı şifresi, token veya anahtar içeren dosyalar). Onları dışarıda bırak ve takip edilen bir dosya zaten secret içeriyorsa beni uyar.
* Paylaşılan, korumalı veya varsayılan bir branch'teysen (ör. `main`), commit'lemeden önce branch aç.

UI rules

Design user flows for zero effort ("armut piş ağzıma düş")

Bring the action to where the user already is.

EN
Bring every required action to where the user already is. The full rule is in `design.md`.
TR
Gereken her eylemi kullanıcının zaten olduğu yere getir. Kuralın tamamı `design.md` dosyasındadır.

The keyboard never covers the field

Always visible, with room to spare.

EN
**A text input is never covered by the on-screen keyboard.** The field being edited stays visible with a comfortable gap above the keyboard, and this is handled by default on every new input without being asked. The platform recipe — `adjustResize`, `imePadding()`, why `WindowInsets.ime` reads 0 under it, and the `BringIntoViewRequester` gap — is in `compose-ui.md`.
TR
**Bir metin alanı asla ekran klavyesinin altında kalmaz.** Düzenlenen alan, klavyenin üstünde rahat bir boşlukla görünür kalır ve bu, her yeni input'ta istenmeden varsayılan olarak halledilir. Platform tarifi — `adjustResize`, `imePadding()`, altında `WindowInsets.ime`'in neden 0 okuduğu ve `BringIntoViewRequester` boşluğu — `compose-ui.md` dosyasındadır.

Case conversion follows the language of the text, not of the page

Uppercase against the text's language, never the page's.

EN
When a UI switches language it must update the document language (`document.documentElement.lang` or the platform equivalent), but every element whose own language does NOT change must then carry its own `lang`. This is mandatory on anything styled with `text-transform: uppercase / lowercase / capitalize`, because casing is locale sensitive: under `lang="tr"` an English label uppercases `i` to `İ` ("ARCHİTECTURE"). The same bug class exists in code, so convert case against the text's locale and never against the ambient default (in Kotlin `uppercase(Locale.ROOT)`, not bare `uppercase()`).
TR
Bir arayüz dil değiştirdiğinde doküman dilini güncellemeli (`document.documentElement.lang` veya platform karşılığı), ama kendi dili değişMEyen her öğe o zaman kendi `lang` değerini taşımalı. Bu, `text-transform: uppercase / lowercase / capitalize` ile biçimlendirilmiş her şeyde zorunludur, çünkü harf dönüşümü yerel ayara duyarlıdır: `lang="tr"` altında İngilizce bir etiket `i` harfini `İ` yapar ("ARCHİTECTURE"). Aynı hata sınıfı kodda da vardır, o yüzden harf dönüşümünü metnin yerel ayarına göre yap, asla ortamın varsayılanına göre değil (Kotlin'de `uppercase(Locale.ROOT)`, yalın `uppercase()` değil).

A skill is a packaged set of instructions, not a data source. It teaches Claude how to do something; if it promises numbers or live data, there is an API or an MCP server behind it and installing the skill alone gives you nothing.

search the registry — in your normal terminal
npx skills search keyword research
install anything you find
npx skills add <owner>/<repo>@<skill>

Files land in ~/.agents/skills/ and are symlinked into ~/.claude/skills/, so one copy serves every agent. If an install says no skills found, the repo's SKILL.md is missing its name or description — the skill is there, the frontmatter is not.

⚡ the -g flag

-g installs a skill for all projects. Without it, only for the current folder.

Anthropic Official Skills

anthropics/skills

Anthropic's official skill collection — document skills (PDF, Word, Excel, PowerPoint), frontend design, skill creator, webapp testing and more.

Anthropic's official skill collection — document skills (PDF, Word, Excel, PowerPoint), frontend design, skill creator, webapp testing and more. One download gives Claude all of them.

install
Claude Code prompt — just ask in plain language
connect to https://github.com/anthropics/skills and download all the skills to my global claude folder
invoke

They start on their own when the task needs them. For example, this runs the xlsx skill:

Claude Code prompt — example
Make an Excel file of these figures, with a chart.

Type / to see the full list.

what's inside
  • xlsx — Excel
  • pptx — PowerPoint
  • pdf — PDF files
  • docx — Word
  • frontend-design
  • skill-creator
  • webapp-testing
  • canvas-design
  • theme-factory
  • brand-guidelines
  • mcp-builder
  • web-artifacts-builder
  • claude-api
  • doc-coauthoring
  • internal-comms
  • slack-gif-creator
  • algorithmic-art

The stars of the set are the document skills — xlsx (Excel), pptx (PowerPoint) and pdf. The pdf skill alone replaces a PDF editor: edit, merge, split, fill forms, add watermarks — you just describe what you want and Claude does it on the file.

Frontend Design

anthropics/skills

Anthropic's official skill for building distinctive, polished web UI that avoids the generic "AI look".

Anthropic's official skill for building distinctive, polished web UI that avoids the generic "AI look".

install
Claude Code prompt
1
/plugin marketplace add anthropics/skills
2
/plugin install frontend-design@claude-plugins-official
invoke
Claude Code prompt
/frontend-design
or just mention it in plain language, e.g.
Use the frontend-design skill while building this page.

Andrej Karpathy Skill

forrestchang/andrej-karpathy-skills

Instead of using this as a separate skill, I find it more effective to add its rules directly to CLAUDE.md — see why in the CLAUDE.md tutorial.

Instead of using this as a separate skill, I find it more effective to add its rules directly to CLAUDE.md — see why in the CLAUDE.md tutorial.

Skill Creator

anthropics/skills

Lets you build your own skills.

Lets you build your own skills. Think of it like defining a function in SQL: instead of explaining a long procedure to Claude every time, you wrap it once as a skill — and from then on it runs with one short command.

install
Claude Code prompt
1
/plugin install skill-creator@claude-plugins-official
2
/reload-plugins
invoke
Claude Code prompt — example
Use skill creator to turn my fancy-linkedin.md file into a design skill I can call with /design-fancy-linkedin

Design intelligence for web and mobile: 50+ styles, color palettes, font pairings and UX guidelines, applied to your UI work.

Design intelligence for web and mobile: 50+ styles, color palettes, font pairings and UX guidelines, applied to your UI work.

install
Claude Code prompt
Install the UI/UX Pro Max skill from nextlevelbuilder/ui-ux-pro-max-skill, globally.

Claude runs npm install -g ui-ux-pro-max-cli and uipro init --ai claude --global. It installs to ~/.claude/skills/, so it works in every project. Needs Python 3.

invoke

Starts on its own when you ask for UI work. Describe what you want:

Claude Code prompt — example
Use UI/UX Pro Max to build a landing page for my SaaS product.
styles

Superpowers

obra/superpowers

A full workflow toolkit: brainstorming before coding, systematic debugging, test-driven development, plan writing and more.

A full workflow toolkit: brainstorming before coding, systematic debugging, test-driven development, plan writing and more.

install
Claude Code prompt
Install the Superpowers plugin.

Claude runs claude plugin install superpowers@claude-plugins-official.

invoke

Starts on its own: it brainstorms before a new feature and debugs step by step when something breaks. For example:

Claude Code prompt — example
Use Superpowers to brainstorm a CSV export for the reports page before writing any code.

Emil Design Eng — motion and polish

emilkowalski/skill 7 animation tips

This skill adds small, minimal motions to your design — every time the mouse moves over an element, it reacts.

This skill adds small, minimal motions to your design — every time the mouse moves over an element, it reacts. It gives your UI energy and a real sense of touch. When all your frontend work is done, ask it to revise your design as the final polish. Samples at animations.dev.

install
Claude Code prompt
Install Emil Kowalski's skills from emilkowalski/skill, globally.

Claude runs npx skills add https://github.com/emilkowalski/skill -g.

invoke
Claude Code prompt — example
Use the /emil-design-eng skill to improve one aspect of the design, then show me exactly what you changed.
one install, 13 skills
  • /emil-design-eng the philosophy: UI polish, component design, the invisible details
  • /animate builds a web animation, deciding in the order that makes it feel right
  • /animate-expo the same for React Native and Expo: Reanimated, gestures, haptics
  • /review-animations critiques the motion you already have
  • /improve-animations audits a whole codebase's motion and writes the fix plans
  • /find-animation-opportunities finds what should animate and rejects what should not
  • /animation-vocabulary “the bouncy thing when a popover opens” → Pop in. Names the effect
  • /apple-design Apple's gestures, springs, materials and typography, for the web
  • /mobile-native the CSS and meta fixes that stop a web app feeling like a website on a phone
  • /pick-ui-library chooses the component library for the job
  • /prototype builds the throwaway version first
  • /ask-sonner Sonner toasts: wiring, promise toasts, why yours do not appear
  • /write-swift modern Swift: value types, Swift 6 concurrency, API design

Runs your question through a council of 5 AI advisors that debate, peer-review each other and give a final verdict.

Runs your question through a council of 5 AI advisors that debate, peer-review each other and give a final verdict. A skill version of Karpathy's llm-council.

install
Claude Code prompt
Install the LLM Council skill from aiwithremy/claude-skills-llm-council, globally.

Claude runs npx skills add with the skill's GitHub address and -g.

invoke
Claude Code prompt — say things like:
council this
pressure-test this
debate this

Forty-one diagram types — architecture, flowchart, sequence, ER, Sankey, Gantt, swimlane, org chart, funnel, heatmap — as self-contained HTML with…

Forty-one diagram types — architecture, flowchart, sequence, ER, Sankey, Gantt, swimlane, org chart, funnel, heatmap — as self-contained HTML with inline SVG. Editorial design system, no shadows, no Mermaid. Imports .drawio and .excalidraw.

install
Claude Code prompt
Install the Diagram Design skill from cathrynlavery/diagram-design, globally.

Claude runs npx skills add cathrynlavery/diagram-design -g.

invoke
Claude Code prompt — just describe what to draw
draw the sync architecture as a diagram
flowchart the four stages of the loan process

On the first diagram in a project it stops and asks whether to customise the style guide, so it never ships a default-skinned diagram into a branded project. Target density is 4/10 — past nine nodes it will tell you it is two diagrams.

Find Skills

vercel-labs/skills

Searches the open skill ecosystem when you ask "how do I do X" or "is there a skill for X", then installs what it finds.

Searches the open skill ecosystem when you ask "how do I do X" or "is there a skill for X", then installs what it finds. The meta-skill: the one that fetches the others.

install
Claude Code prompt
Install the find-skills skill from vercel-labs/skills, globally.

Claude runs npx skills add vercel-labs/skills@find-skills -g.

invoke

Ask in plain language:

Claude Code prompt — example
Is there a skill for keyword research?
search by hand
the same registry, from the terminal
npx skills search keyword research
npx skills add <owner>/<repo>@<skill>

Two things worth knowing before you trust a result. A skill is a prompt, not a data source — if it promises numbers it needs an API or MCP server behind it, and installing it alone gives you nothing. And plenty of published skills are broken: if SKILL.md is missing its name or description, the installer says "no skills found" even though the skill is right there.

Writes a handoff document when you stop working, so the next session (or the next person) knows exactly where you left off.

Writes a handoff document when you stop working, so the next session (or the next person) knows exactly where you left off.

install
Claude Code prompt
Install the handoff skill from mattpocock/skills (skills/productivity/handoff), globally.

Claude puts its SKILL.md in ~/.claude/skills/handoff.

invoke
Claude Code prompt
/handoff

Humanizer

blader/humanizer

Removes the signs of AI-generated writing from text, based on Wikipedia's "Signs of AI writing" guide.

Removes the signs of AI-generated writing from text, based on Wikipedia's "Signs of AI writing" guide.

install
Claude Code prompt
Install the Humanizer skill from blader/humanizer, globally.

Claude clones it into ~/.claude/skills/humanizer.

invoke
Claude Code prompt — just ask
Humanize this text
make this sound less AI-written

Very effective frontend skill: design, critique, polish and improve any interface.

Very effective frontend skill: design, critique, polish and improve any interface. It interviews you first, then designs.

install
Claude Code prompt
Install the Impeccable skill from pbakaus/impeccable, globally.

Claude runs npx skills add https://github.com/pbakaus/impeccable.git -g.

invoke
Claude Code prompt
/impeccable init
asks you questions, then builds the design
on a project that exists
  1. /impeccable document record what is already there
  2. /impeccable critique <page> find what is wrong
  3. /impeccable polish <page> fix it
  4. /impeccable audit <page> check before shipping
all 25 commands
make
  • /impeccable impeccable describe what you want and it designs it
  • /impeccable shape turns an idea into a design brief you can build from
judge
  • /impeccable critique what is holding the design back, and what to fix first
  • /impeccable audit implementation problems: accessibility, performance, code quality
refine
  • /impeccable typeset readability: headings, sizing, line length
  • /impeccable layout arranges elements for hierarchy and clarity
  • /impeccable colorize colour that clarifies actions, states and character
  • /impeccable animate motion that explains changes and relationships
  • /impeccable bolder gives an existing section more presence
  • /impeccable quieter removes visual noise, keeps the personality
  • /impeccable delight makes the moments that matter surprising
  • /impeccable overdrive pushes past expected into extraordinary
simplify
  • /impeccable distill cuts clutter so the important thing shows
  • /impeccable clarify tells the user what happened and what to do next
  • /impeccable adapt makes it work on other screens and platforms
harden
  • /impeccable harden keeps it usable when data, network or actions fail
  • /impeccable onboard walks a new user to their first real result
  • /impeccable optimize finds the performance problem, fixes it, measures it
  • /impeccable polish makes an existing page feel finished
system
  • /impeccable init asks who uses this and why, writes PRODUCT.md
  • /impeccable document records the existing visual system into DESIGN.md
  • /impeccable extract turns repeated patterns into shared components
  • /impeccable generate names an element, compares variants, keeps the winner
  • /impeccable live explore page variations and pick one
  • /impeccable hooks on checks every UI edit and reports back automatically

Design Taste Frontend

Leonxlnx/taste-skill tasteskill.dev

Anti-slop frontend skill for landing pages, portfolios and redesigns — ships interfaces that don't look templated.

Anti-slop frontend skill for landing pages, portfolios and redesigns — ships interfaces that don't look templated.

install
Claude Code prompt
Install the design-taste-frontend skill from Leonxlnx/taste-skill, globally.

Claude runs npx skills add https://github.com/Leonxlnx/taste-skill --skill "design-taste-frontend" -g.

invoke

Starts on its own on frontend design work:

Claude Code prompt — example
Use the design-taste-frontend skill to design a portfolio landing page for a photographer.
how it works
  • reads the room industry, audience, mood, motion depth, layout approach — before generating anything
  • maps a design system Material, Fluent, Radix, shadcn and the rest, chosen from that reading
  • dual mode by protocol dark and light are designed together, not one then the other
  • pre-flight check a hard checklist it has to pass before it hands the output over
pick a variant
one variant per install — swap the name at the end
npx skills add https://github.com/Leonxlnx/taste-skill --skill "design-taste-frontend" -g
the 13 variants
  • design-taste-frontend the default v2: brief inference, design-system map, pre-flight check
  • design-taste-frontend-v1 the original, kept for when v2 overreaches
  • redesign-existing-projects audits what is there first, then fixes it
  • image-to-code starts from a picture instead of a brief
  • high-end-visual-design premium and softer: the expensive-looking end
  • minimalist-ui editorial and restrained
  • industrial-brutalist-ui hard, mechanical visual language
  • full-output-enforcement guarantees the whole output, no truncation
  • gpt-taste a stricter variant for GPT and Codex
  • stitch-design-taste compatible with Google Stitch
  • brandkit brand kit generation
  • imagegen-frontend-web generates web UI imagery
  • imagegen-frontend-mobile generates mobile UI imagery

Hallmark

nutlope/hallmark

Paste a design you admire — a screenshot or a URL.

Paste a design you admire — a screenshot or a URL. Hallmark reads its structure, not its pixels, and returns a DNA card you can build against. Or say "lock the DNA" to write a portable design.md you can hand to another AI tool. Never copies pixels. Tighter refusal layer for the portable spec than for the diagnosis.

install
Claude Code prompt
Install the Hallmark skill from nutlope/hallmark, globally.

Claude runs npx skills add nutlope/hallmark -g.

invoke

Starts on its own on design work, or call it by name:

Claude Code prompt — example
Use hallmark to redesign this page.

Looking for more? Browse the community skill directory at skillsmp.com.

Three words that get mixed up. A skill teaches Claude how to do something. A connector gives it access to an outside account — Gmail, Drive, Supabase. A plugin bundles commands, skills and connectors together and installs them in one step.

Connectors

Connectors — Connect Claude to Your Other Tools

Connectors link Claude to all the other tools you already use — Canva, Asana, Atlassian (Jira/Confluence), Box, Figma, GitHub, HubSpot and many more.

Connectors link Claude to all the other tools you already use — Canva, Asana, Atlassian (Jira/Confluence), Box, Figma, GitHub, HubSpot and many more. Once connected, Claude can read from and work with those tools directly.

Step 1 — In the Claude desktop app, switch to the Code tab and click Customize:

Claude desktop app: Code tab with Customize menu item

Step 2 — In the Customize menu, choose Connectors (Skills and Plugins are also managed here):

Customize menu: Skills, Connectors, Plugins

Step 3 — The connector list opens. Click Connect next to any tool and follow the login flow:

Connectors list with Canva, Asana, Atlassian, Figma, GitHub and more

After connecting, just mention the tool in your prompt — e.g. "create a Jira ticket for this bug" or "pull my latest Figma design".

CLI Tools — Supabase, GitHub, Netlify, Vercel

Some services connect to Claude Code through their own CLI tools (command-line programs).

Some services connect to Claude Code through their own CLI tools (command-line programs). Install the CLI once, log in once — from then on Claude uses it for you. You just say what you want in plain language, and Claude runs the right commands: deploy, configure, manage.

Supabase CLI

Supabase is the online backend of your projects — your online database / DWH, auth and storage in one service.

in your normal terminal (PowerShell)
1
npm install -g supabase
2
supabase login

GitHub CLI

GitHub is the secure online storage of your code — every project lives there as a repository.

in your normal terminal (PowerShell)
1
winget install GitHub.cli
2
gh auth login

Netlify CLI

Netlify is a hosting provider — this site itself is deployed on it.

in your normal terminal (PowerShell)
1
npm install -g netlify-cli
2
netlify login

Vercel CLI

Vercel is another popular hosting provider, especially strong for Next.js projects.

in your normal terminal (PowerShell)
1
npm install -g vercel
2
vercel login

Then just talk

Claude Code prompt — examples, one at a time
Deploy this site to Netlify.
Create a GitHub repo for this project and push the code.
Create a new table in my Supabase database for user feedback.

Claude connects through the CLI automatically and handles the whole flow — one natural-language command is enough to deploy and manage all of them.

Plugins

Claude Code Official Plugins

claude.com/plugins

Anthropic's official plugin directory — browse plugins that bundle MCPs, skills and tools into a single download. Look for the "Anthropic Verified" badge, and only install plugins from developers you trust.

install
Claude Code prompt
/plugin install frontend-design@claude-plugins-official
invoke
Claude Code prompt — opens the plugin menu (browse, install, manage)
/plugin

Note: run /reload-plugins to use a freshly installed plugin in the same session.

Alongside this plugin marketplace, we also recommend installing the Anthropic Official Skills. Many abilities overlap between the two — the same skill often exists both as an official plugin and inside the official skill set.

One of the most important features. After you build something, Claude can test it like a real user: it opens your project in a browser automatically, clicks through the pages, fills in data, reads console errors and takes screenshots. You can also ask it to write full test scenarios for a new feature.

install
Claude Code prompt
Install the Playwright plugin.

Claude runs claude plugin install playwright@claude-plugins-official.

invoke
Claude Code prompt
/webapp-testing
or just ask in plain language, e.g.
Test the login page in the browser.
run it hidden

Every test opens a browser window on your screen. To keep it hidden, ask Claude:

Claude Code prompt
Run the Playwright browser hidden from now on.

Claude adds "env": { "PLAYWRIGHT_MCP_HEADLESS": "true" } to ~/.claude/settings.json, and it applies from the next session. Claude still sees every page through its screenshots; only the window goes away. Ask it to show the browser again whenever you want to watch.

tip — prompt like a feedback loop

The best prompts tell Claude how to check its own work. Compare:

# weak
Build the shipping calculator.

# strong — a feedback loop
Build the shipping calculator.
After every change, run npm test.
Fix the errors before saying it's done.

You can also make this permanent by adding it to your project's CLAUDE.md:

# After every change
Run `npm run test` and fix failures
Run `npm run lint` and fix issues

▶ Watch the demo video

Official MongoDB plugin — query and manage your MongoDB databases in plain language.

install
Claude Code prompt
Install the MongoDB plugin.

Claude runs claude plugin install mongodb@claude-plugins-official.

invoke
Claude Code prompt — example
Use the MongoDB plugin to show the collections in my database and how many documents each has.

Add-ons

Free Claude Code — Run Claude Code on Free Models

github.com/Alishahryar1/free-claude-code

A free, open-source app that sits between Claude Code and the model. Claude Code keeps working as usual, but its requests go to the provider you pick: free tiers such as NVIDIA NIM, paid APIs, or a model running on your own machine. Not a Claude Code skill, and not made by Anthropic.

what it gives you
  • free tokens dozens of providers and their free monthly tokens, in one model list
  • fallback when a provider fails, it moves on to your next model without restarting the turn
  • other agents the same models in Codex, OpenCode, Cline, Aider and more
  • admin page add keys and switch models in the browser
install
Claude Code prompt
Install Free Claude Code from github.com/Alishahryar1/free-claude-code.

Claude runs its installer. By hand instead:

in your normal terminal (PowerShell)
& ([scriptblock]::Create((irm "https://raw.githubusercontent.com/Alishahryar1/free-claude-code/main/scripts/install.ps1")))

Then open Free Claude Code from the Start menu, paste a free NVIDIA NIM key on its admin page and click Apply.

invoke
in your normal terminal, instead of claude
fcc-claude

Free limits are set by each provider and can change. The code is under the AGPL licence.

Codex — OpenAI's Agent Inside Claude Code

github.com/openai/codex-plugin-cc

OpenAI's official plugin. Claude hands work to Codex, OpenAI's coding agent: a second code review, a fix, or an image. It runs on your ChatGPT plan (sign in with your ChatGPT account, no API key needed) or on an OpenAI API key.

install
Claude Code prompt
Install the Codex plugin from openai/codex-plugin-cc.

Then sign in once with your ChatGPT account, and reload the plugins:

Claude Code prompt
!codex login
/reload-plugins

Claude installs the Codex CLI (npm install -g @openai/codex) and the plugin (claude plugin install codex@openai-codex). Needs Node.js 18.18 or later.

use

No need to learn the commands. Say what Codex should do:

Claude Code prompt
Ask Codex to review my changes.
Ask Codex to find out why the tests fail.
commands
  • /codex:review a read-only review of your uncommitted changes, or of your branch with --base main
  • /codex:adversarial-review a review that challenges the design; add what to question
  • /codex:rescue hands a task to Codex: a bug to find, a fix, an image
  • /codex:transfer moves this conversation into Codex to continue there
  • /codex:status jobs running in the background; /codex:result and /codex:cancel go with it
limits

Every run counts toward your Codex usage limits, which are separate from ChatGPT's chat limits. Codex sees your code: it is sent to OpenAI.

Browser Add-ons

Mobile View — Mobile Simulator

Chrome Web Store

A Chrome extension that shows how your website looks on phones and tablets. Preview any site on iPhone, Android and tablet screens, up to 5 devices at once, live next to the desktop view. Useful for checking the responsive layout of what Claude builds without opening each device by hand.

what it looks like
Mobile View toolbar: device picker, rotate, screenshot, recording and zoom This site previewed in Mobile View on an iPhone frame
install

Open the Chrome Web Store page and click Add to Chrome.

use

Open your site (a local dev server works too), click the extension icon in the toolbar and pick the devices to preview.

Claude forgets everything when a session ends. These are the two ways to stop paying that tax: a vault it can read, and a rule that tells it to.

Claude + Obsidian — Your Tool for Central Context Management

CLAUDE.md works best as an index: it links to every other doc in your setup.

CLAUDE.md works best as an index: it links to every other doc in your setup. Obsidian can open the same folder as a vault. The result: your Claude Code rules become a visual knowledge graph, with CLAUDE.md at the center.

One thing makes this work: link format. Obsidian builds its graph from links. Claude Code follows links too. But they prefer different formats:

  • Markdown links — [design.md](./design.md) — work everywhere: Claude Code, Obsidian, GitHub, any editor.
  • Wikilinks — [[design]] — work only in Obsidian and similar note tools. Claude Code sees plain text.

So the base rule is simple: use markdown links in any file that Claude Code will read. But Obsidian creates wikilinks by default. To keep both worlds connected, add this rule to your global CLAUDE.md:

Rule 1 — Wikilink Compatibility

EN
## Wikilink Compatibility
If a `.md` file I'm already editing contains a `[[wikilink]]` and I can
resolve the target file, add a markdown link next to it pointing to the
same target: `[[design]] ([design.md](./design.md))`. If the target can't
be resolved, leave the wikilink untouched. Never sweep unrelated files
for this.
TR
## Wikilink Uyumluluğu
Zaten düzenlemekte olduğum bir `.md` dosyasında `[[wikilink]]` varsa ve
hedef dosyayı çözebiliyorsam, yanına aynı hedefe giden bir markdown
linki ekle: `[[design]] ([design.md](./design.md))`. Hedef çözülemiyorsa
wikilink'e dokunma. Bunun için asla ilgisiz dosyaları tarama.

What it does: every time Claude Code touches a file with a wikilink, it quietly adds the markdown twin next to it. Over time, your notes become readable by both tools — without one big risky cleanup.

"Open with Obsidian" — why it doesn't just work

Right-click a .md file → "Open with Obsidian" often does nothing. The reason: Obsidian is vault-based, not file-based. It can only open a file that lives inside a folder you have already added as a vault. A file outside every vault is invisible to it.

The one-time setup

  1. Open Obsidian → click the vault icon (bottom left)
  2. Choose "Open folder as vault"
  3. Select the folder that holds your markdown files (e.g. ~/.claude)

After this, every .md in that folder opens in Obsidian — and the graph view shows your CLAUDE.md hub with all its links.

How to organize vaults

  • One vault per project — clean graph per project. Adding a vault takes 30 seconds; Obsidian remembers all of them and you switch from the vault icon.
  • One parent-folder vault — works only if your projects sit under one folder (like C:\Projects\). Never make your whole user folder a vault: indexing gets huge and slow.

A practical setup: make ~/.claude a vault first — the global rules hub is where the graph has real value. Add project vaults only when you actually work on their notes. Tip: exclude noisy subfolders (like plugins/) via Settings → Files & Links → Excluded files.

Turns any input (code, docs, papers) into a persistent knowledge graph you can query.

install
Claude Code prompt
Install the Graphify skill from safishamsi/graphify, globally.

Claude clones it into ~/.claude/skills/graphify.

invoke
Claude Code prompt
/graphify

Persistent memory for Claude Code: it remembers past sessions, injects relevant context automatically, and can answer "how did we solve this last time?"

install
Claude Code prompt
Install claude-mem and start it.

Claude runs npx claude-mem install, then npx claude-mem start, which starts the memory service. To stop it later, say “Stop claude-mem”.

invoke

Works automatically once started.

Claude Code prompt — useful extras
mem-search
"did we already solve this?"
learn-codebase
reads the whole codebase into memory (~5 min)
timeline-report
a story of how the project evolved
notes
Runs at http://localhost:37777. It does NOT auto-start after a
reboot — run "npx claude-mem start" again (or set it up as a
Windows service).

Taken further, this becomes a knowledge tree with its own capture and sweep commands. That system is documented in full at mindmap.firatada.com.

Material you come back to rather than read once: the full rule set, where files live, and the smaller collections.

Useful Commands to Know

Type the word ultrathink anywhere in your message (it turns colorful in the terminal).

Deep thinking: ultrathink

Claude Code prompt — just include the word in your message
ultrathink — design the database schema for this app

Type the word ultrathink anywhere in your message (it turns colorful in the terminal). Claude then spends more time reasoning before it answers. Use it for hard, multi-step problems: architecture decisions, tricky bugs, planning. Skip it for simple questions.

For professions

Login screens were specified in login.md, which is NOT in the global knowledge mds

login.md was not imported — it was a team doc covering LDAP bind, JWT sessions and a three-layer whitelist that do not apply here.

EN
## Login screens were specified in `login.md`, which is NOT in the global knowledge mds

**`login.md` was not imported** — it was a team doc covering LDAP bind, JWT sessions and a
three-layer whitelist that do not apply here. What survives from it, and what still holds when you
build a sign-in page: logo centered **above** the card, "Sign in with your Windows credentials"
style wording matched to the actual auth method, a **Forgot password?** link to the provider's own
change-password URL, a plain-text username input (never `type="email"`), a generic `first.last`
placeholder, nothing below the card, and never flash the form to someone already signed in.

The original rule, kept for its reasoning: The login page is the one screen whose layout, wording
and behaviour are fixed by the authentication doc: logo centered above the card, *"Sign in with your
Windows credentials"*, **Forgot password?** beside the Password label pointing at Microsoft's own
change-password page, a plain-text username field (never `type="email"`, which rejects a bare
username), a generic `first.last` placeholder rather than a real person's name, nothing under the
card, the session resolved server-side so an already-signed-in user never sees the form flash, and a
real navigation after success so the browser offers to save the credentials.

This pointer exists because the natural instinct is to treat "how should the login page look?" as a
design question and come here — and the two rules this file does carry about that screen (the
[logo asset](#afiniti-logos--two-distinct-assets-never-interchange-them) and the
[auth-flash rule](#auth-dependent-screens--resolve-the-session-on-the-server-never-flash-a-wrong-state))
are findable only if you already know to look for them.
TR
## Login ekranları `login.md` dosyasında tanımlıydı, o dosya global knowledge md'lerde YOK

**`login.md` içe aktarılmadı** — LDAP bind, JWT session'ları ve burada geçerli olmayan üç
katmanlı bir beyaz listeyi kapsayan bir ekip dokümanıydı. Ondan ayakta kalan ve bir giriş sayfası
kurarken hâlâ geçerli olan kısım: logo kartın **üstünde** ortalanmış, gerçek kimlik doğrulama
yöntemine uydurulmuş "Sign in with your Windows credentials" tarzı bir ifade, sağlayıcının kendi
şifre değiştirme adresine giden bir **Forgot password?** linki, düz metin kullanıcı adı input'u
(asla `type="email"`), genel bir `first.last` placeholder'ı, kartın altında hiçbir şey, ve zaten
giriş yapmış birine formu bir an bile gösterme.

Orijinal kural, gerekçesi için korundu: Giriş sayfası, yerleşimi, ifadesi ve davranışı kimlik
doğrulama dokümanıyla sabitlenmiş tek ekrandır: logo kartın üstünde ortalanmış, *"Sign in with your
Windows credentials"*, Password etiketinin yanında Microsoft'un kendi şifre değiştirme sayfasına
işaret eden **Forgot password?**, düz metin kullanıcı adı alanı (asla `type="email"`, çünkü yalın
bir kullanıcı adını reddeder), gerçek bir kişinin adı yerine genel bir `first.last` placeholder'ı,
kartın altında hiçbir şey, session'ın sunucu tarafında çözülmesi ki zaten giriş yapmış bir kullanıcı
formu bir an bile görmesin, ve başarıdan sonra gerçek bir yönlendirme ki tarayıcı kimlik bilgilerini
kaydetmeyi önersin.

Bu işaretçi var çünkü doğal içgüdü "giriş sayfası nasıl görünmeli?" sorusunu bir tasarım sorusu
sayıp buraya gelmek — ve bu dosyanın o ekran hakkında taşıdığı iki kural
([logo görseli](#afiniti-logos--two-distinct-assets-never-interchange-them) ve
[auth-flash kuralı](#auth-dependent-screens--resolve-the-session-on-the-server-never-flash-a-wrong-state))
ancak aramayı zaten biliyorsan bulunabilir.

Style References

calendly.md — Calendly.com style reference (light, quiet professional calendar theme).

EN
## Style References
- [`calendly.md`](./design_themes/calendly/calendly.md) — Calendly.com style reference (light, quiet professional calendar theme).
- [`fancy-linkedin.md`](./design_themes/fancy_linkedin/fancy-linkedin.md) — "Fancy LinkedIn" creator-dashboard design system (floating windows, blue→purple accents).
- `notifications.md` — tool-notification design across channels (mail / MS Teams / Webex); first note: **dual-Outlook (new + classic) HTML-email compatibility** (table layout, inline CSS, `bgcolor`+style, VML buttons, MSO DPI fix). Read before building any email/notification.
- [`design_themes/spark_theme/spark_theme.md`](./design_themes/spark_theme/spark_theme.md) — "Afiniti Spark" energetic dashboard theme (blue→cyan brand gradients, gold reward accents, color-matched glow buttons, glassmorphism topbar, gamification widgets). Ships a ready-to-link stylesheet [`spark-theme.css`](./design_themes/spark_theme/spark-theme.css). Use for internal tools, portals, and gamified/community dashboards.
- `formula_tooltip.md` — Formula tooltip pattern: Lucide Info icon, two-panel popover ("How is this calculated?" + Technical Definition), React/Radix implementation, copy-paste template. Use whenever a column or metric needs a formula explanation.
- `formula_tooltip.md` — Formula tooltip pattern: when a column/metric needs a "How is this calculated?" tooltip, use this pattern.
- [`design-filters.md`](./design-filters.md) — Filter & sort rules, and it **owns every filter rule in the estate** — table columns *and* view-wide (bar) filter controls: every column filterable + sortable, click-the-name to open a **popover** that starts with everything selected and **applies only on Apply** (outside-click / `Esc` discard), a **range** instead of a value list on numeric columns, a **field chooser** on stacked cells, option lists **shipped with the page and cascaded** by every other active filter (plus the coarse-⊃-fine hierarchy gotcha), the sort icon's 3-click cycle back to the table's default sort, active-filter indication, bar filters that **open on hover**, the "All …" first option and its scope-defining exception, and the ban on bar time filters at or below the charts' drawn granularity. Read it before building any filter or sort — `design-dashboard.md` delegates here rather than restating it.
- [`design-form-validation.md`](./design-form-validation.md) — Mandatory-field validation: on submit, highlight invalid required fields **on the fields themselves** (red border/ring), never via a message elsewhere; keep the submit button clickable and block the submit until fixed. Read before building any form/modal with required fields.
TR
## Stil Referansları
- [`calendly.md`](./design_themes/calendly/calendly.md) — Calendly.com stil referansı (açık, sakin, profesyonel takvim teması).
- [`fancy-linkedin.md`](./design_themes/fancy_linkedin/fancy-linkedin.md) — "Fancy LinkedIn" içerik üretici dashboard tasarım sistemi (yüzen pencereler, mavi→mor vurgular).
- `notifications.md` — kanallar arası araç bildirimi tasarımı (mail / MS Teams / Webex); ilk not: **çift Outlook (yeni + klasik) HTML e-posta uyumluluğu** (tablo yerleşimi, satır içi CSS, `bgcolor`+style, VML butonlar, MSO DPI düzeltmesi). Herhangi bir e-posta/bildirim kurmadan önce oku.
- [`design_themes/spark_theme/spark_theme.md`](./design_themes/spark_theme/spark_theme.md) — "Afiniti Spark" enerjik dashboard teması (mavi→camgöbeği marka gradyanları, altın ödül vurguları, renk uyumlu parlama butonları, glassmorphism üst bar, oyunlaştırma widget'ları). Doğrudan bağlanabilir bir stil sayfası içerir: [`spark-theme.css`](./design_themes/spark_theme/spark-theme.css). Şirket içi araçlar, portallar ve oyunlaştırılmış/topluluk dashboard'ları için kullan.
- `formula_tooltip.md` — Formül tooltip kalıbı: Lucide Info simgesi, iki panelli popover ("How is this calculated?" + Technical Definition), React/Radix uygulaması, kopyala-yapıştır şablonu. Bir kolonun ya da metriğin formül açıklamasına ihtiyacı olduğunda kullan.
- `formula_tooltip.md` — Formül tooltip kalıbı: bir kolon/metrik "How is this calculated?" tooltip'i gerektirdiğinde bu kalıbı kullan.
- [`design-filters.md`](./design-filters.md) — Filtre ve sıralama kuralları; ayrıca **alandaki her filtre kuralının sahibi odur** — tablo kolonları *ve* görünüm geneli (bar) filtre kontrolleri: her kolon filtrelenebilir + sıralanabilir, adına tıklayınca her şey seçili başlayan ve **yalnızca Apply ile uygulanan** bir **popover** açılır (dışarı tıklama / `Esc` iptal eder), sayısal kolonlarda değer listesi yerine bir **aralık**, üst üste dizilmiş hücrelerde bir **alan seçici**, sayfayla birlikte **gönderilen ve aktif diğer her filtreye göre zincirlenen** seçenek listeleri (artı kaba-⊃-ince hiyerarşi tuzağı), sıralama simgesinin tablonun varsayılan sıralamasına dönen 3 tıklamalık döngüsü, aktif filtre göstergesi, **hover ile açılan** bar filtreleri, "All …" ilk seçeneği ve kapsam tanımlayan istisnası, ve grafiklerin çizilen kırılımına eşit ya da ondan ince bar zaman filtrelerinin yasağı. Herhangi bir filtre ya da sıralama kurmadan önce oku — `design-dashboard.md` bunu tekrarlamak yerine buraya devreder.
- [`design-form-validation.md`](./design-form-validation.md) — Zorunlu alan doğrulaması: gönderimde geçersiz zorunlu alanları **alanların kendi üstünde** işaretle (kırmızı kenarlık/halka), asla başka bir yerdeki mesajla değil; gönder butonunu tıklanabilir tut ve düzeltilene kadar gönderimi engelle. Zorunlu alanları olan herhangi bir form/modal kurmadan önce oku.

Formula Tooltips

When a metric column, KPI card, or dashboard value has a non-obvious formula, add a formula tooltip.

EN
## Formula Tooltips
When a metric column, KPI card, or dashboard value has a non-obvious formula, add a formula tooltip. See `formula_tooltip.md` for the full UX pattern, panel structure, content rules, and implementation code.
TR
## Formül Tooltip'leri
Bir metrik kolonunun, KPI kartının ya da dashboard değerinin formülü aşikâr değilse, bir formül tooltip'i ekle. Tam UX kalıbı, panel yapısı, içerik kuralları ve uygulama kodu için `formula_tooltip.md` dosyasına bak.

Numbers are digits, never words

Every number in an interface is written as a digit: headings, labels, buttons, badges, table cells, body copy.

EN
## Numbers are digits, never words
Every number in an interface is written as a digit: headings, labels, buttons, badges, table cells,
body copy. `3 tools, 9 stages`, not `three tools, nine stages`. There is no spell-out-under-ten
threshold. A digit is scannable, it aligns in a column, it survives translation without agreeing
with anything, and the reader does not have to parse a sentence to find a quantity.

The rule is about **counts and quantities**. `one` used to mean "single" or "a" is a word, not a
number — `one API surface`, `one source of truth`, `one transaction` stay as they are, and so do
ordinals like `first` and `second`. Applies to text shown on screen, not to chat, commit messages or
these documents.
TR
## Sayılar rakamla yazılır, asla kelimeyle
Arayüzdeki her sayı rakamla yazılır: başlıklar, etiketler, butonlar, badge'ler, tablo hücreleri,
gövde metni. `3 tools, 9 stages`, `three tools, nine stages` değil. Ona kadar yazıyla yazma diye
bir eşik yok. Rakam gözle taranabilir, kolonda hizalanır, hiçbir şeyle uyuşmak zorunda kalmadan
çeviriden sağ çıkar ve okuyan bir miktarı bulmak için cümle çözmek zorunda kalmaz.

Kural **adet ve miktarlar** hakkındadır. "tek" ya da "bir" anlamında kullanılan `one` bir sayı değil
kelimedir — `one API surface`, `one source of truth`, `one transaction` olduğu gibi kalır, `first`
ve `second` gibi sıra sayıları da öyle. Ekranda gösterilen metin için geçerlidir; sohbet, commit
mesajları ve bu dokümanlar için değil.

No Browser Autofill on Custom Inputs

Inputs that are NOT part of a normal login/profile form — tag/token entry boxes (email chips, search-tag inputs), inline…

EN
## No Browser Autofill on Custom Inputs
Inputs that are NOT part of a normal login/profile form — tag/token entry boxes (email chips, search-tag inputs), inline editors, filter fields — must suppress the browser's autofill/autocomplete dropdown and password-manager icons. They pollute a custom UI with unrelated saved emails/addresses. On every such `<input>` set: `autoComplete="off"`, `autoCorrect="off"`, `autoCapitalize="off"`, `spellCheck={false}`, plus a non-standard `name` (browsers ignore `autoComplete="off"` on fields named `email`/`name`/`address` — use something like `name="xyz-entry"`), and the password-manager opt-outs `data-1p-ignore`, `data-lpignore="true"`, `data-form-type="other"`. Genuine login/credential fields keep their proper `autoComplete` values (`username`, `current-password`) — this rule is only for custom non-credential inputs.
TR
## Özel Input'larda Tarayıcı Otomatik Doldurması Yok
Normal bir login/profil formunun parçası OLMAYAN input'lar — tag/token giriş kutuları (e-posta chip'leri, arama etiketi input'ları), satır içi editörler, filtre alanları — tarayıcının otomatik doldurma açılır listesini ve şifre yöneticisi simgelerini bastırmalı. Bunlar özel bir arayüzü alakasız kayıtlı e-posta ve adreslerle kirletir. Böyle her `<input>` üzerinde şunları ayarla: `autoComplete="off"`, `autoCorrect="off"`, `autoCapitalize="off"`, `spellCheck={false}`, ayrıca standart dışı bir `name` (tarayıcılar `email`/`name`/`address` adlı alanlarda `autoComplete="off"` değerini yok sayar — `name="xyz-entry"` gibi bir şey kullan) ve şifre yöneticisi devre dışı bırakmaları `data-1p-ignore`, `data-lpignore="true"`, `data-form-type="other"`. Gerçek login/kimlik alanları kendi doğru `autoComplete` değerlerini korur (`username`, `current-password`) — bu kural sadece kimlik taşımayan özel input'lar içindir.

Chip / Token Inputs (email & multi-value entry)

Any field that collects more than one value — recipient lists (To / Cc), tags, keywords — must be a chip/token input, not a…

EN
## Chip / Token Inputs (email & multi-value entry)

Any field that collects **more than one value** — recipient lists (To / Cc), tags, keywords — must be a
**chip/token input**, not a bare comma-separated text box. The chip is the confirmation: the user sees
each accepted value as a discrete box, so they never wonder whether their entry "took".

- **Validity is signalled by becoming a chip.** On commit (Enter, comma, or blur) a **valid** value
  turns into a removable chip; an **invalid** one stays as raw text in the box and the box border goes
  red — no chip, nothing lost. The user fixes it in place. Do not silently drop invalid input, and do
  not add an invalid value as a chip. For emails, validate against a real pattern
  (`/^[^\s@]+@[^\s@]+\.[^\s@]+$/`), not just "contains an @".
- **Chips are removable** — an `×` on each chip, and **Backspace on an empty input removes the last
  chip**. De-dupe on add (case-insensitive for emails).
- **Locked chips for non-negotiable values.** Values the user may not remove (e.g. system admins who are
  always CC'd) render as **locked chips** — shown first, visually distinct (muted, a lock glyph), no `×`.
  The server stays authoritative: it re-applies the locked floor and merges the user's additions
  (dedup), never trusting the client to have kept them.
- **Persistence stores the list**, not a joined string — e.g. `localStorage.setItem(key, JSON.stringify(list))`.
  When reading back, tolerate an older single-string format (split on `,`/`;`) so remembered values
  survive the upgrade.
- **Suppress browser autofill** on the entry `<input>` per *No Browser Autofill on Custom Inputs* above
  (a chip box is exactly the "tag/token entry" case that rule targets) — including a unique non-standard
  `name` per instance so two boxes in one modal (To + Cc) don't collide.
- **Required-field feedback follows `design-form-validation.md`.** On a submit attempt,
  a required chip field with **zero chips** gets the red ring on its container (not a message
  elsewhere); the submit stays clickable and is blocked until fixed.

Reference implementation: Reward Comp Tool → Data Quality → "Domain Review" report modal
(`ai_apps/Reward_Comp_Tool/app/data-quality/page.tsx`, `EmailTagInput` with `locked` / `invalid`
props) + its API (`app/api/data-quality/report-domain/route.ts`, which merges client Cc onto the
`SUPER_ADMINS` floor).
TR
## Chip / Token Input'ları (e-posta ve çok değerli giriş)

**Birden fazla değer** toplayan her alan — alıcı listeleri (To / Cc), etiketler, anahtar kelimeler —
virgülle ayrılmış yalın bir metin kutusu değil, **chip/token input'u** olmalı. Chip, onayın kendisidir:
kullanıcı kabul edilen her değeri ayrı bir kutu olarak görür, böylece girdisinin "tutup tutmadığını"
hiç merak etmez.

- **Geçerlilik, chip'e dönüşerek bildirilir.** Kesinleştiğinde (Enter, virgül veya odak kaybı) **geçerli**
  bir değer kaldırılabilir bir chip'e dönüşür; **geçersiz** olan kutuda ham metin olarak kalır ve kutunun
  kenarlığı kırmızıya döner — chip yok, kaybolan da yok. Kullanıcı onu yerinde düzeltir. Geçersiz girdiyi
  sessizce atma ve geçersiz bir değeri chip olarak ekleme. E-postalarda gerçek bir kalıpla doğrula
  (`/^[^\s@]+@[^\s@]+\.[^\s@]+$/`), sadece "@ içeriyor mu" ile değil.
- **Chip'ler kaldırılabilirdir** — her chip'te bir `×`, ve **boş input'ta Backspace son chip'i kaldırır**.
  Eklerken tekrarı ayıkla (e-postalarda büyük/küçük harf duyarsız).
- **Tartışmaya kapalı değerler için kilitli chip'ler.** Kullanıcının kaldıramayacağı değerler (ör. her zaman
  CC'lenen sistem yöneticileri) **kilitli chip** olarak çizilir — önce gösterilir, görsel olarak ayrışır
  (soluk, kilit simgesi), `×` yok. Sunucu son sözü söylemeye devam eder: kilitli tabanı yeniden uygular ve
  kullanıcının eklediklerini birleştirir (tekrarı ayıklayarak), istemcinin onları koruduğuna asla güvenmez.
- **Kalıcılık listeyi saklar**, birleştirilmiş bir metni değil — ör. `localStorage.setItem(key, JSON.stringify(list))`.
  Geri okurken eski tek metin biçimini de tolere et (`,`/`;` ile böl) ki hatırlanan değerler yükseltmeden
  sağ çıksın.
- Giriş `<input>`'unda yukarıdaki *Özel Input'larda Tarayıcı Otomatik Doldurması Yok* kuralına göre
  **otomatik doldurmayı bastır** (chip kutusu tam olarak o kuralın hedeflediği "tag/token girişi" durumudur)
  — her örnek için benzersiz, standart dışı bir `name` dahil, ki tek bir modal'daki iki kutu (To + Cc)
  çakışmasın.
- **Zorunlu alan geri bildirimi `design-form-validation.md`'yi izler.** Gönderme denemesinde, **sıfır chip'i**
  olan zorunlu bir chip alanı kırmızı halkayı container'ında alır (başka bir yerdeki mesajla değil);
  gönder butonu tıklanabilir kalır ve düzeltilene kadar engellenir.

Referans uygulama: Reward Comp Tool → Data Quality → "Domain Review" rapor modal'ı
(`ai_apps/Reward_Comp_Tool/app/data-quality/page.tsx`, `locked` / `invalid` prop'lu `EmailTagInput`)
ve API'si (`app/api/data-quality/report-domain/route.ts`, istemcinin Cc'sini `SUPER_ADMINS` tabanına
birleştirir).

Related elements must react to a click — filter DOWN, highlight UP

If two things on a screen describe the same data at different grains, clicking one must do something to the other.

EN
## Related elements must react to a click — filter DOWN, highlight UP

If two things on a screen describe the same data at different grains, clicking one **must** do
something to the other. A chart, its ranked table and the detail list beneath it are one instrument,
not three decorations; a click that changes nothing reads as a broken control.

**Which action depends on the direction of the relationship:**

| Click direction | Action | Why |
|---|---|---|
| **Big group → small group** (aggregate → detail) | **FILTER** | "Show me only these" — the detail list is long, narrowing it is the point. Clicking a year, a department or a bar filters the rows underneath to that slice. |
| **Small group → big group** (detail → aggregate) | **HIGHLIGHT** | "Where does this sit?" — filtering an aggregate down to one row destroys the comparison the aggregate exists to give. Clicking a person, a row or a single move highlights its bar/segment instead, leaving the rest visible for context. |

Rules that follow:

- **Never filter upwards.** Reducing a distribution chart to the single bar the clicked detail
  belongs to throws away the only thing that chart was for.
- **A filter is visibly active and reversible** — the filtered element shows a selected state, the
  target shows what it is filtered by, and clicking the same element again clears it. Never make the
  user reload to get back.
- **A highlight is transient and never hides data.** Emphasise the related element (colour, outline,
  de-emphasised siblings); do not remove rows.
- **Both clear on outside click**, per "Click-Outside Clears Selection" below.
- **State the pairing where it isn't obvious** — a one-line hint ("click a year to filter the moves
  below") is cheaper than a user never discovering the control.
TR
## İlişkili öğeler tıklamaya tepki vermeli — AŞAĞI filtrele, YUKARI vurgula

Bir ekranda iki şey aynı veriyi farklı kırılımlarda anlatıyorsa, birine tıklamak diğerine **mutlaka**
bir şey yapmalı. Bir grafik, onun sıralı tablosu ve altındaki detay listesi üç süs değil, tek bir
enstrümandır; hiçbir şeyi değiştirmeyen bir tıklama bozuk bir kontrol gibi okunur.

**Hangi eylemin uygulanacağı ilişkinin yönüne bağlıdır:**

| Tıklama yönü | Eylem | Neden |
|---|---|---|
| **Büyük grup → küçük grup** (toplam → detay) | **FİLTRELE** | "Sadece bunları göster" — detay listesi uzundur, onu daraltmak zaten amaçtır. Bir yıla, bir departmana ya da bir çubuğa tıklamak alttaki satırları o dilime filtreler. |
| **Küçük grup → büyük grup** (detay → toplam) | **VURGULA** | "Bu nerede duruyor?" — bir toplamı tek satıra filtrelemek, o toplamın var olma sebebi olan karşılaştırmayı yok eder. Bir kişiye, bir satıra ya da tek bir harekete tıklamak onun çubuğunu/dilimini vurgular ve gerisini bağlam için görünür bırakır. |

Bundan çıkan kurallar:

- **Asla yukarı doğru filtreleme.** Bir dağılım grafiğini, tıklanan detayın ait olduğu tek çubuğa
  indirgemek o grafiğin tek varlık sebebini atar.
- **Filtre görünür biçimde aktif ve geri alınabilirdir** — filtreleyen öğe seçili durumunu gösterir,
  hedef neye göre filtrelendiğini söyler ve aynı öğeye tekrar tıklamak temizler. Kullanıcıyı geri
  dönmek için sayfayı yenilemeye asla zorlama.
- **Vurgu geçicidir ve asla veri gizlemez.** İlişkili öğeyi öne çıkar (renk, çerçeve, kardeşlerini
  soluklaştırma); satır kaldırma.
- **İkisi de dışarı tıklamada temizlenir**, aşağıdaki "Dışarı Tıklamak Seçimi Temizler" kuralına göre.
- **Eşleşme aşikâr değilse söyle** — tek satırlık bir ipucu ("aşağıdaki hareketleri filtrelemek için
  bir yıla tıklayın") kullanıcının kontrolü hiç keşfetmemesinden ucuzdur.

A cross-filter names itself on EVERY element it touches

When selecting in one component filters others on the same screen, each affected component must say what it is filtered by —…

EN
## A cross-filter names itself on EVERY element it touches

When selecting in one component filters others on the same screen, each affected component must say
what it is filtered by — **immediately beside its own title, muted**, e.g. `Headcount by Country
· Department: Growth`.

**A banner at the top of the page does not count.** That is the failure this rule exists for: on a
screen of eight cards the reader scrolls, and by the time they are looking at the sixth chart the
banner is off-screen. What they see is one card showing 81 people where it showed 614 a moment ago,
with nothing on it explaining why — so the chart looks broken or, worse, believable.

- **Beside the title, not above the content.** The heading is the one thing a reader is guaranteed to
  have in view when they are looking at that card.
- **Muted, and secondary to the title.** It is state, not a name — normal weight, faint colour, never
  competing with the heading it qualifies.
- **Name the dimension AND the value** (`Department: Growth`, not `Growth`). Alone, a value is
  ambiguous the moment two dimensions share vocabulary.
- **The component you selected IN does not get one.** It keeps its full distribution so you can
  change selection; calling it "filtered by" would describe it wrongly. Its selected row/slice is
  already marked.
- **A component the filter does not reach must stay silent** — labelling it would imply a narrowing
  that did not happen.
- Keep the page-level banner as well if it carries the clear control; the two are not alternatives.
TR
## Çapraz filtre dokunduğu HER öğenin üstünde kendini söyler

Bir bileşendeki seçim aynı ekrandaki diğerlerini filtreliyorsa, etkilenen her bileşen neye göre
filtrelendiğini söylemeli — **kendi başlığının hemen yanında, soluk olarak**, ör. `Headcount by Country
· Department: Growth`.

**Sayfanın en üstündeki bir bant sayılmaz.** Bu kuralın var olma sebebi tam olarak o arıza: sekiz kartlık
bir ekranda okuyucu aşağı kayar ve altıncı grafiğe baktığında bant ekranın dışında kalmıştır. Gördüğü
şey, bir an önce 614 gösterirken şimdi 81 kişi gösteren bir karttır ve üstünde nedenini açıklayan hiçbir
şey yoktur — yani grafik bozuk görünür ya da daha kötüsü, inandırıcı görünür.

- **Başlığın yanında, içeriğin üstünde değil.** Okuyucunun o karta bakarken görüş alanında olduğu kesin
  olan tek şey başlıktır.
- **Soluk ve başlığa göre ikincil.** Bu bir durumdur, bir ad değil — normal kalınlık, soluk renk, nitelediği
  başlıkla asla yarışmaz.
- **Boyutu VE değeri birlikte söyle** (`Department: Growth`, `Growth` değil). Tek başına bir değer, iki
  boyut aynı kelimeyi paylaştığı anda belirsizleşir.
- **Seçim yaptığın bileşen bunu almaz.** O, seçimi değiştirebilmen için tam dağılımını korur; ona "şuna
  göre filtrelendi" demek onu yanlış anlatır. Seçili satırı/dilimi zaten işaretlidir.
- **Filtrenin ulaşmadığı bir bileşen sessiz kalmalı** — etiketlemek, gerçekleşmemiş bir daralmayı ima eder.
- Temizleme kontrolünü taşıyorsa sayfa seviyesindeki bandı da koru; ikisi alternatif değil.

Click-Outside Clears Selection

Any interactive selection state in a table or chart (highlighted row, column, cell, filter) must be cleared when the user…

EN
## Click-Outside Clears Selection
Any interactive selection state in a table or chart (highlighted row, column, cell, filter) must be cleared when the user clicks outside the component. Implement with a `mousedown`/`click` listener on `document` and a `ref` on the component container — if the click target is **not inside the ref**, reset the selection state.

- **Decide "outside" by testing the click target against the container ref — NEVER by "clear on every document click and rely on the selecting controls' `stopPropagation()` to be spared."** React's synthetic `stopPropagation()` does **not** reliably stop a native `document.addEventListener` handler (React delegates events at its root, not `document`), so a bare clear-on-any-click also fires on the very click that made the selection — the selection is set and then cleared in the same interaction, so it flashes and vanishes. The target-vs-ref test is the fix; keep `stopPropagation` only as harmless defence-in-depth.
- **A scrollbar click is NOT an outside click.** Dragging the scrollbar fires `mousedown` on the
  document with a target outside your container, so a naive test clears the selection the moment the
  user tries to scroll down to look at what they just selected — the single most infuriating version
  of this bug, because the act of examining the result destroys it. Bail out before the ref test:
  ```js
  // The scrollbar lives outside the client area, so its coordinates fall past these bounds.
  if (e.clientX > document.documentElement.clientWidth ||
      e.clientY > document.documentElement.clientHeight) return;
  ```
  Put this in ONE shared helper and use it in every click-outside handler — it is exactly the kind of
  rule that gets applied to three of four places and then reported as a bug on the fourth.
- **Include any portaled selection controls in the "inside" test.** If chips, a Reset button, or other selecting controls are rendered elsewhere via `createPortal` (e.g. into a top-bar slot), they live OUTSIDE the container ref's DOM subtree — the listener must treat clicks on those slot elements as "inside" too (test the ref **and** the portal target nodes), or clicking them wrongly clears everything.
TR
## Dışarı Tıklamak Seçimi Temizler
Bir tablodaki veya grafikteki her etkileşimli seçim durumu (vurgulu satır, kolon, hücre, filtre) kullanıcı bileşenin dışına tıkladığında temizlenmeli. `document` üzerinde bir `mousedown`/`click` dinleyicisi ve bileşen container'ında bir `ref` ile uygula — tıklama hedefi **ref'in içinde değilse** seçim durumunu sıfırla.

- **"Dışarısı"na, tıklama hedefini container ref'ine karşı sınayarak karar ver — ASLA "her document tıklamasında temizle ve seçim yapan kontrollerin `stopPropagation()` çağrısının kurtaracağına güven" diyerek değil.** React'in sentetik `stopPropagation()` çağrısı, yerel bir `document.addEventListener` işleyicisini güvenilir biçimde **durdurmaz** (React olayları `document` yerine kendi kökünde devreder), yani yalın bir her-tıklamada-temizle, seçimi yapan tıklamada da ateşlenir — seçim aynı etkileşim içinde hem kurulur hem temizlenir, bir an görünüp kaybolur. Çözüm hedef-ve-ref sınamasıdır; `stopPropagation`'ı sadece zararsız bir ikinci katman olarak tut.
- **Kaydırma çubuğuna tıklamak dışarı tıklama DEĞİLDİR.** Kaydırma çubuğunu sürüklemek, hedefi
  container'ının dışında olan bir `mousedown` olayını document üzerinde ateşler; yani saf bir sınama,
  kullanıcı az önce seçtiği şeye bakmak için aşağı kaydırmaya çalıştığı anda seçimi temizler — bu
  hatanın en sinir bozucu hali, çünkü sonucu inceleme eylemi sonucu yok eder. Ref sınamasından önce çık:
  ```js
  // The scrollbar lives outside the client area, so its coordinates fall past these bounds.
  if (e.clientX > document.documentElement.clientWidth ||
      e.clientY > document.documentElement.clientHeight) return;
  ```
  Bunu TEK bir ortak yardımcıya koy ve her dışarı-tıklama işleyicisinde kullan — tam olarak dört yerin
  üçüne uygulanıp dördüncüsünde hata olarak raporlanan kural cinsinden.
- **Portal'la taşınmış seçim kontrollerini "içerisi" sınamasına dahil et.** Chip'ler, bir Reset butonu ya da başka seçim kontrolleri `createPortal` ile başka bir yere çiziliyorsa (ör. üst bardaki bir yuvaya), container ref'inin DOM alt ağacının DIŞINDA yaşarlar — dinleyici o yuva öğelerine yapılan tıklamaları da "içerisi" saymalı (ref **ve** portal hedef düğümlerini sına), yoksa onlara tıklamak her şeyi yanlışlıkla temizler.

Reuse the app's existing control — never hand-roll a second copy of one that exists

Before writing a filter, dropdown, popover, table shell, or any other piece of shared chrome on a NEW screen, find what the…

EN
## Reuse the app's existing control — never hand-roll a second copy of one that exists
Before writing a filter, dropdown, popover, table shell, or any other piece of shared chrome on a NEW screen, find
what the app's existing screens use and import that. A hand-written `<input>`/`<select>` cannot satisfy the specs in
this file — the custom-dropdown rule, the caption-icon rule, the "All" first rule, hover-open, keyboard behaviour —
and every re-implementation is a second source of truth that drifts on the next change to either copy.

The tell that this went wrong: two screens in the same app whose filter bars look different. If a reviewer can see
the inconsistency in a screenshot, the cause is almost always a second implementation, not a missing style.

- **Look for the component first**, then the CSS class, then the doc. Grep the codebase for the control before
  writing one: a project with a `FilterBar` almost certainly has a `SingleSelect`/`MultiSelect` behind it.
- **Adding CSS for a control that already has styles is the smell.** If a new screen needs `.my-page-filter` rules to
  make a dropdown look right, the dropdown is the wrong dropdown.
- This applies inside panels and modals too — a raw select in a dialog is still a raw select.

(2026-08-06, Revenue Forecasting Tool: an Outlook screen shipped with bare `<select>` elements beside a Comparison
screen using the shared `SingleSelect`. The two bars were visibly different. Fixed by importing the component and
DELETING the bespoke CSS, not by restyling it.)
TR
## Uygulamanın mevcut kontrolünü yeniden kullan — var olanın ikinci bir kopyasını asla elle yazma
YENİ bir ekranda bir filtre, dropdown, popover, tablo iskeleti ya da başka bir ortak arayüz parçası yazmadan önce,
uygulamanın mevcut ekranlarının ne kullandığını bul ve onu içe aktar. Elle yazılmış bir `<input>`/`<select>` bu
dosyadaki şartları karşılayamaz — özel dropdown kuralı, başlık simgesi kuralı, "All" ilk sırada kuralı, hover ile
açılma, klavye davranışı — ve her yeniden uygulama, iki kopyadan birinde yapılacak ilk değişiklikte sapacak ikinci
bir doğruluk kaynağıdır.

Bunun yanlış gittiğinin işareti: aynı uygulamada filtre barları farklı görünen iki ekran. Bir gözden geçiren
tutarsızlığı bir ekran görüntüsünde görebiliyorsa, sebep neredeyse her zaman ikinci bir uygulamadır, eksik bir
stil değil.

- **Önce bileşeni ara**, sonra CSS sınıfını, sonra dokümanı. Bir tane yazmadan önce kod tabanında kontrolü grep'le:
  `FilterBar`'ı olan bir projenin arkasında neredeyse kesinlikle bir `SingleSelect`/`MultiSelect` vardır.
- **Zaten stilleri olan bir kontrol için CSS eklemek, kokunun kendisidir.** Yeni bir ekranın bir dropdown'ı doğru
  göstermek için `.my-page-filter` kurallarına ihtiyacı varsa, o dropdown yanlış dropdown'dır.
- Bu, panel ve modal'ların içinde de geçerlidir — bir diyalogdaki ham select hâlâ ham select'tir.

(2026-08-06, Revenue Forecasting Tool: bir Outlook ekranı, ortak `SingleSelect` kullanan bir Comparison ekranının
yanında yalın `<select>` öğeleriyle çıktı. İki bar gözle görülür biçimde farklıydı. Bileşen içe aktarılarak ve
özel CSS SİLİNEREK düzeltildi, yeniden stillendirilerek değil.)

An empty screen must name the filter that emptied it — silent fail-closed is still a failure

When a screen can render zero rows, it must distinguish "you are not permitted to see any of this" from "there is genuinely no…

EN
## An empty screen must name the filter that emptied it — silent fail-closed is still a failure
When a screen can render zero rows, it must distinguish **"you are not permitted to see any of this"** from
**"there is genuinely no data"**, and from **"your filters excluded everything"**. All three look identical as a
blank table, and only one of them is the user's to fix.

- **Permission-shaped emptiness names the remedy**: state that the viewer holds no role/scope for this screen, say
  what the scoping rule is, and name who grants it ("ask an administrator to assign you X for your clients").
- **Filter-shaped emptiness names the filters**: say how many rows were excluded and by which rule, so the user can
  reverse it. If a screen silently drops a fifth of its rows, it is lying by omission — report the count, split by
  reason, whether or not anyone asked.
- **Data-shaped emptiness says so plainly** rather than borrowing either of the above.

Learned the hard way (2026-08-06, Revenue Forecasting Tool): client-scoped authorisation was added to a screen whose
grant table had **never been populated**, so the correct answer — "no grants, therefore no rows" — rendered as an
empty table with no explanation, for every user including the app's owner. The authorisation logic was right; the
silence was the bug. Corollary in *Fail-closed authorisation: check the grant table is populated* in
`engineering.md`.
TR
## Boş bir ekran onu boşaltan filtreyi söylemeli — sessizce kapanmak da bir hatadır
Bir ekran sıfır satır gösterebiliyorsa, **"bunların hiçbirini görme yetkiniz yok"** ile **"gerçekten hiç veri yok"**
ve **"filtreleriniz her şeyi dışarıda bıraktı"** durumlarını ayırt etmelidir. Üçü de boş bir tablo olarak birebir aynı
görünür ve sadece birini kullanıcı düzeltebilir.

- **Yetki kaynaklı boşluk çareyi söyler**: bakan kişinin bu ekran için hiçbir rolü/kapsamı olmadığını belirt, kapsam
  kuralının ne olduğunu söyle ve bunu kimin verdiğini adlandır ("müşterileriniz için X yetkisini atamasını bir
  yöneticiden isteyin").
- **Filtre kaynaklı boşluk filtreleri söyler**: kaç satırın hangi kuralla dışarıda bırakıldığını söyle ki kullanıcı
  geri alabilsin. Bir ekran satırlarının beşte birini sessizce düşürüyorsa, eksiklikle yalan söylüyordur — kimse
  sormasa da sayıyı, sebebine göre ayırarak raporla.
- **Veri kaynaklı boşluk bunu düpedüz söyler**, yukarıdakilerden birini ödünç almak yerine.

Zor yoldan öğrenildi (2026-08-06, Revenue Forecasting Tool): yetki tablosu **hiç doldurulmamış** bir ekrana müşteri
kapsamlı yetkilendirme eklendi, böylece doğru cevap — "yetki yok, dolayısıyla satır yok" — uygulamanın sahibi dahil
her kullanıcıya açıklamasız boş bir tablo olarak göründü. Yetkilendirme mantığı doğruydu; hata sessizlikti. Karşılığı
`engineering.md` içindeki *Fail-closed authorisation: check the grant table is populated* kuralıdır.

Sortable Table Headers — consistent across every data column

In a data table, every data column must be sortable (show a sort icon and sort on click) — do not leave some columns sortable…

EN
## Sortable Table Headers — consistent across every data column
In a data table, **every data column must be sortable** (show a sort icon and sort on click) — do not
leave some columns sortable and others not. Only a pure **action / display** column (e.g. a per-row
formula-breakdown or a row-action column) may omit sort, because it has no orderable value. Computed
columns (USD conversions, taxes, derived totals) are sortable by their computed value.

**Icon vertical alignment:** the sort icon must always sit on the **first line of its column label**, at
the same height for every column — whether the header is one line or wraps to two/three lines. Achieve
this by aligning the icon to the top of the label (`items-start`) with a small optical nudge
(`mt-[2px]` for a 10px icon), NOT `items-center` (which drops the icon lower on multi-line headers and
makes the row's icons ragged). Right-aligned (numeric) headers put the icon to the **left** of the label;
left-aligned headers put it to the right.
TR
## Sıralanabilir Tablo Başlıkları — her veri kolonunda tutarlı
Bir veri tablosunda **her veri kolonu sıralanabilir olmalı** (sıralama simgesi göster ve tıklayınca sırala) — bazı
kolonları sıralanabilir bırakıp diğerlerini bırakmama. Sadece saf bir **aksiyon / gösterim** kolonu (ör. satır başına
formül kırılımı ya da satır aksiyonları kolonu) sıralamayı atlayabilir, çünkü sıralanabilir bir değeri yoktur. Hesaplanan
kolonlar (USD çevrimleri, vergiler, türetilmiş toplamlar) hesaplanmış değerlerine göre sıralanabilir.

**Simgenin dikey hizası:** sıralama simgesi her zaman **kolon etiketinin ilk satırında**, her kolon için aynı yükseklikte
durmalı — başlık tek satır olsun, iki ya da üç satıra sarsın fark etmez. Bunu simgeyi etiketin üstüne hizalayarak
(`items-start`) ve küçük bir optik kaydırmayla (10px simge için `mt-[2px]`) sağla, `items-center` ile DEĞİL (o, çok satırlı
başlıklarda simgeyi aşağı düşürür ve satırın simgelerini tırtıklı gösterir). Sağa hizalı (sayısal) başlıklar simgeyi
etiketin **soluna**, sola hizalı başlıklar sağına koyar.

Section Spacing

Adjacent dashboard sections (cards, tables, charts) must always have consistent vertical spacing that matches the rest of the…

EN
## Section Spacing
Adjacent dashboard sections (cards, tables, charts) must always have consistent vertical spacing that matches the rest of the page — use `mt-4` (or the page's standard gap) between sibling sections. Never let two bordered cards/tables sit flush against each other with no gap.
TR
## Bölüm Boşlukları
Yan yana duran dashboard bölümleri (kartlar, tablolar, grafikler) her zaman, sayfanın geri kalanıyla uyumlu ve tutarlı bir dikey boşluğa sahip olmalı — kardeş bölümler arasında `mt-4` (ya da sayfanın standart boşluğu) kullan. Kenarlıklı iki kartın/tablonun boşluksuz, birbirine yapışık durmasına asla izin verme.

Afiniti logos — two DISTINCT assets, never interchange them

Two different Afiniti logos live in the shared logos/ folder; each has its own place and they must never be swapped: - Afiniti…

EN
## Afiniti logos — two DISTINCT assets, never interchange them
Two different Afiniti logos live in the shared `logos/` folder; each has its own place and they must **never be swapped**:
- **Afiniti AICC logo ("afinitiaicc")** — canonical source **`logos/afinitiaicc_logo_transparent.png`** (not `afinitiaicc_logo.png`). The AICC team's own mark, and the **default on the sign-in screen** — shown centered above the login/landing card (per `login.md` §6) — as well as anchored top-right in a **dashboard top bar** (per [`design-dashboard.md`](./design-dashboard.md)). ⚠️ It exists **only at 166×44**: draw it at its native **44px** height on login, and never scale it past ~56px, where the edges visibly soften. There is no larger original; ask brand for a vector/2× rather than enlarging it.
- **Afiniti corporate wordmark (black-on-light)** — source `logos/Afiniti_Logo_black.png`, 2158×707 so it scales anywhere. The company logo, for a surface presented as a company-wide system rather than an AICC one. It was the sign-in default until 2026-08-20 and is now the documented **alternative** there — a project that wants it says so explicitly.

Copy whichever file the app needs into that app's own `public/` (self-contained — never hotlink the shared `logos/` folder), and keep the two straight: the corporate wordmark is **not** the afinitibi logo, and vice-versa.

**Excel export buttons use the Excel product logo** — `logos/excel_blue.png` (~18px, logo alone: no
text, no frame; muted/grayscale at rest, full color on hover, stays grey when disabled). The full
rule (every chart AND table card gets one, export content/format) lives in
[`design-dashboard.md`](./design-dashboard.md) → "Export to Excel button" — defined once there, this is the pointer.

**Top-bar logo aligns with the filter bar.** The dashboard top-bar logo (the AICC mark) is **sized to
the filter control row and vertically aligned with it**: constrain by height to roughly the control
row (e.g. `h-10` next to 36px controls — never larger than the filter block), and place its **bottom
edge on the filter block's bottom edge** (caption + control), nudged **up ~3px** for optical balance —
a logo's visual mass reads lower than a hard box edge. Compute `top = filterBlockBottom − logoHeight − 3`
on the absolutely-anchored logo; never pin it flush to the bar's top or let it hang below the
controls. (Full top-bar anchoring mechanics — absolute top-right corner, reserved bar padding —
in [`design-dashboard.md`](./design-dashboard.md) → "Dashboard top bar design".)
TR
## Afiniti logoları — iki AYRI görsel, asla birbirinin yerine kullanılmaz
Ortak `logos/` klasöründe iki farklı Afiniti logosu yaşar; her birinin kendi yeri vardır ve **asla yer değiştirmezler**:
- **Afiniti AICC logosu ("afinitiaicc")** — kanonik kaynak **`logos/afinitiaicc_logo_transparent.png`** (`afinitiaicc_logo.png` değil). AICC ekibinin kendi markası ve **giriş ekranındaki varsayılan** — login/landing kartının üstünde ortalanmış olarak (`login.md` §6'ya göre) ve ayrıca bir **dashboard üst barında** sağ üste sabitlenmiş olarak ([`design-dashboard.md`](./design-dashboard.md)'ye göre). ⚠️ **Sadece 166×44 boyutunda var**: girişte kendi doğal **44px** yüksekliğinde çiz ve kenarların gözle görülür biçimde yumuşadığı ~56px'i asla geçme. Daha büyük bir orijinali yok; büyütmek yerine markadan vektör/2× iste.
- **Afiniti kurumsal kelime markası (açık zemin üzerine siyah)** — kaynak `logos/Afiniti_Logo_black.png`, 2158×707, yani her yere ölçeklenir. Şirket logosudur; AICC'ye değil şirketin tamamına ait bir sistem olarak sunulan yüzeyler için. 2026-08-20'ye kadar giriş ekranının varsayılanıydı, artık orada belgeli **alternatif**; onu isteyen proje bunu açıkça söyler.

Uygulamanın ihtiyaç duyduğu dosyayı o uygulamanın kendi `public/` klasörüne kopyala (kendi kendine yeter olsun — ortak `logos/` klasörüne asla doğrudan bağlanma) ve ikisini karıştırma: kurumsal kelime markası afinitibi logosu **değildir**, tersi de öyle.

**Excel'e aktarma butonları Excel ürün logosunu kullanır** — `logos/excel_blue.png` (~18px, yalnız logo: metin yok,
çerçeve yok; beklemede soluk/gri tonlu, hover'da tam renkli, devre dışıyken gri kalır). Tam kural (her grafik VE tablo
kartı bir tane alır, aktarma içeriği/biçimi)
[`design-dashboard.md`](./design-dashboard.md) → "Export to Excel button" içinde yaşar — orada bir kez tanımlandı, bu onun işaretçisi.

**Üst bar logosu filtre barıyla hizalanır.** Dashboard üst bar logosu (AICC markası) **filtre kontrol satırına göre
boyutlanır ve onunla dikeyde hizalanır**: yüksekliği kabaca kontrol satırına sınırla (ör. 36px kontrollerin yanında
`h-10` — filtre bloğundan asla büyük değil) ve **alt kenarını filtre bloğunun alt kenarına** (başlık + kontrol) yerleştir,
optik denge için **~3px yukarı** kaydırarak — bir logonun görsel ağırlığı sert bir kutu kenarından daha aşağıda okunur.
Mutlak konumlandırılmış logoda `top = filterBlockBottom − logoHeight − 3` hesapla; onu asla barın üstüne yapıştırma ya da
kontrollerin altına sarkıtma. (Tam üst bar sabitleme mekaniği — mutlak sağ üst köşe, ayrılmış bar boşluğu —
[`design-dashboard.md`](./design-dashboard.md) → "Dashboard top bar design" içinde.)

KPI Card Row Alignment

KPI cards sitting side-by-side in the same grid row must keep their internal elements horizontally aligned across cards: title…

EN
## KPI Card Row Alignment
KPI cards sitting side-by-side in the same grid row must keep their internal elements horizontally aligned across cards: title labels at the same height, sub-labels at the same height, primary metrics at the same height, and footers at the same height. Use a 4-zone `flex flex-col` structure on every card: **Zone 1** title (`h-5 flex items-center`), **Zone 2** sub-label (`h-4 flex items-center`), **Zone 3** primary metric (`flex-1 flex items-end`), **Zone 4** footer (`mt-2`). Cards with no sub-label must still include an empty `h-4` spacer so the numbers align with cards that do have one.
TR
## KPI Kart Satırı Hizalaması
Aynı grid satırında yan yana duran KPI kartları, iç öğelerini kartlar arasında yatay olarak hizalı tutmalı: başlık etiketleri aynı yükseklikte, alt etiketler aynı yükseklikte, birincil metrikler aynı yükseklikte ve alt bilgiler aynı yükseklikte. Her kartta 4 bölgeli bir `flex flex-col` yapısı kullan: **Bölge 1** başlık (`h-5 flex items-center`), **Bölge 2** alt etiket (`h-4 flex items-center`), **Bölge 3** birincil metrik (`flex-1 flex items-end`), **Bölge 4** alt bilgi (`mt-2`). Alt etiketi olmayan kartlar bile boş bir `h-4` boşluk taşımalı ki sayılar, alt etiketi olan kartlarla hizalansın.

A heading and the items beneath it share ONE left edge

Within a panel — and across a vertical stack of panels/boxes in the same column — the section heading and every item under it…

EN
## A heading and the items beneath it share ONE left edge
Within a panel — and across a vertical stack of panels/boxes in the same column — the section
heading and **every item under it** (table cells, list rows, sub-boxes, derived rows, a summary bar)
must start at the **same left inset**. A heading indented further than, or less than, its own content
reads as broken, and a column of boxes whose contents each start at a different x looks like several
people built it.

- **Give the whole stack ONE horizontal inset** (pick a single value, e.g. `18px`, and apply it to
  every box's content padding — the heading wrapper, the framed sub-boxes, the cost/derive rows, the
  summary bar).
- **Table cells must use that same left padding**, not the table's default cell padding. A `<table>`
  dropped straight into a `padding:0` panel puts its first column at the table's own `td` inset
  (often ~11px) while the heading above sits at the panel inset (e.g. 21px) — the classic mismatch.
  Tag the table and set `th,td{padding-left:<inset>}` so the first column's text lines up under the
  heading.
- **One named exception: the sign-in screen.** The app name and its single sign-in line are
  **centered** under the centered logo (`login.md` §6) — an arrival surface with no
  content to align to. It is limited to those two lines; the fields below them are left-aligned with
  each other, and no other heading anywhere gets this treatment.
- **Verify the rendered text-left, not the box edges.** A cell's border-box left differs from its
  text-left by its padding; measure the heading's text-left and the first item's text-left (e.g. a
  `Range` over the text node) and require them **equal**, not merely "close".

(2026-08-17, AICC Executive Pack case detail: the "Efficiency gains" heading sat at 21px, its table
cells at 11px, the cost-avoided box at 18px and the ROI derive rows at 15px — four left edges in one
column. Unified to a single 18px inset; heading text and first cell text now land on the same x.)

### At page scale: one text left edge, and the page title is on it

The same rule, one level up. **Every piece of text on a page starts at the same x** — the page
title, plain paragraphs, section headings, and the text inside every card, callout and command box.

- **Align the text, not the boxes.** Lining up box edges is the trap: each box's own padding then
  pushes its text inward, so the page measures as aligned at the border-box and still reads as
  ragged. The shared line is the rendered text.
- **The page title sits on that line too.** In a sidebar-plus-content layout the title usually lives
  in its own top bar. Its horizontal padding and the content area's padding must come from the same
  decision, or the title drifts from everything beneath it. One wrapper owning the horizontal
  padding beats two equal values written in two places — equal values drift apart, a single source
  cannot.
- **Push the box out, keep the text still.** Since a box's padding indents its text, pull the
  container that holds boxes outward by exactly that padding plus its border. The box edge then sits
  outside the text column and the text lands on the shared line. This only works if every box's
  padding is normalised to one value first; with three different paddings, one offset cannot align
  three kinds of box.

(2026-09-24, firatada.com restructure. Two failures in sequence, and the rule above already covered
the second one. First the content wrapper was dropped while rewriting the shell: the title sat at
266px and every view at 236px — one missing element, seven pages wrong, no individual element at
fault. With the wrapper restored the box edges matched, but the text still did not: title 266, card
title 285, command-box text 298 — three lines in one column. Box paddings were normalised to 18px
and the containers pulled out 19px, after which title, lede, section headings, card titles and
excerpts all measured 266 on six pages. The lesson is not a new rule: "verify the rendered
text-left, not the box edges" was already written above, and was simply not applied.)
TR
## Bir başlık ve altındaki öğeler TEK bir sol kenarı paylaşır
Bir panelin içinde — ve aynı kolondaki panel/kutu yığınının tamamında — bölüm başlığı ve **altındaki
her öğe** (tablo hücreleri, liste satırları, alt kutular, türetilmiş satırlar, bir özet barı) **aynı sol
içeriden** başlamalı. Kendi içeriğinden daha içeride ya da daha dışarıda duran bir başlık bozuk okunur
ve içerikleri farklı x'lerden başlayan bir kutu kolonu, birkaç kişinin ayrı ayrı yaptığı gibi görünür.

- **Bütün yığına TEK bir yatay içeri ver** (tek bir değer seç, ör. `18px`, ve her kutunun içerik
  boşluğuna uygula — başlık sarmalayıcısı, çerçeveli alt kutular, maliyet/türetme satırları, özet barı).
- **Tablo hücreleri de aynı sol boşluğu kullanmalı**, tablonun varsayılan hücre boşluğunu değil.
  `padding:0` olan bir panele doğrudan bırakılan bir `<table>`, ilk kolonunu tablonun kendi `td` içerisine
  (çoğunlukla ~11px) koyar, oysa üstündeki başlık panel içerisinde (ör. 21px) durur — klasik uyumsuzluk.
  Tabloyu etiketle ve `th,td{padding-left:<inset>}` ayarla ki ilk kolonun metni başlığın altına otursun.
- **Adı konmuş tek istisna: giriş ekranı.** Uygulama adı ve tek satırlık giriş cümlesi, ortalanmış logonun
  altında **ortalanır** (`login.md` §6) — hizalanacak içeriği olmayan bir varış yüzeyi. Bu, o iki satırla
  sınırlıdır; altlarındaki alanlar birbiriyle sola hizalıdır ve başka hiçbir başlık bu muameleyi görmez.
- **Kutu kenarlarını değil, çizilen metnin solunu doğrula.** Bir hücrenin border-box solu, metin solundan
  kendi boşluğu kadar farklıdır; başlığın metin solunu ve ilk öğenin metin solunu ölç (ör. metin düğümü
  üzerinde bir `Range`) ve **eşit** olmalarını şart koş, "yakın" olmalarını değil.

(2026-08-17, AICC Executive Pack vaka detayı: "Efficiency gains" başlığı 21px'te, tablo hücreleri 11px'te,
kaçınılan maliyet kutusu 18px'te ve ROI türetme satırları 15px'te duruyordu — tek kolonda dört sol kenar.
Tek bir 18px içeride birleştirildi; başlık metni ve ilk hücre metni artık aynı x'e iniyor.)

### Sayfa ölçeğinde: tek bir metin sol kenarı, ve sayfa başlığı da onun üstünde

Aynı kural, bir seviye yukarıda. **Bir sayfadaki her metin parçası aynı x'ten başlar** — sayfa başlığı,
düz paragraflar, bölüm başlıkları ve her kartın, çağrı kutusunun ve komut kutusunun içindeki metin.

- **Kutuları değil metni hizala.** Kutu kenarlarını hizalamak tuzağın kendisidir: her kutunun kendi boşluğu
  metnini içeri iter, yani sayfa border-box'ta hizalı ölçülür ve yine de tırtıklı okunur. Paylaşılan çizgi,
  çizilen metindir.
- **Sayfa başlığı da o çizginin üstündedir.** Kenar çubuğu artı içerik düzeninde başlık genelde kendi üst
  barında yaşar. Onun yatay boşluğu ile içerik alanının boşluğu aynı karardan gelmeli, yoksa başlık altındaki
  her şeyden kayar. Yatay boşluğa sahip çıkan tek bir sarmalayıcı, iki yere yazılmış iki eşit değerden iyidir —
  eşit değerler birbirinden uzaklaşır, tek kaynak uzaklaşamaz.
- **Kutuyu dışarı it, metni yerinde tut.** Bir kutunun boşluğu metnini içeri çektiği için, kutuları taşıyan
  container'ı tam olarak o boşluk artı kenarlığı kadar dışarı çek. O zaman kutu kenarı metin kolonunun dışında
  kalır ve metin paylaşılan çizgiye iner. Bu ancak her kutunun boşluğu önce tek bir değere normalleştirilirse
  işe yarar; üç farklı boşlukla tek bir kaydırma üç tür kutuyu hizalayamaz.

(2026-09-24, firatada.com yeniden yapılandırması. Arka arkaya iki hata ve yukarıdaki kural ikincisini zaten
kapsıyordu. Önce kabuk yeniden yazılırken içerik sarmalayıcısı düştü: başlık 266px'te, her görünüm 236px'te —
tek bir eksik öğe, yedi sayfa yanlış, kusurlu tek bir öğe yok. Sarmalayıcı geri gelince kutu kenarları uydu ama
metin hâlâ uymadı: başlık 266, kart başlığı 285, komut kutusu metni 298 — tek kolonda üç çizgi. Kutu boşlukları
18px'e normalleştirildi ve container'lar 19px dışarı çekildi; ardından başlık, giriş metni, bölüm başlıkları, kart
başlıkları ve özetlerin hepsi altı sayfada 266 ölçtü. Ders yeni bir kural değil: "kutu kenarlarını değil çizilen
metnin solunu doğrula" zaten yukarıda yazılıydı ve sadece uygulanmamıştı.)

Multi-column text: the unbreakable unit is a section, not a block

In a multi-column text flow the thing that must never be split is a section: a heading together with the content under it.

EN
## Multi-column text: the unbreakable unit is a section, not a block
In a multi-column text flow the thing that must never be split is a **section**: a heading together
with the content under it. A list separated from its heading has lost what it is a list of.

Two settings look reasonable and both fail:
- **Every block unbreakable.** A long list counts as one indivisible block, jumps to the next
  column, and leaves the previous one nearly empty.
- **Every row unbreakable.** The list now flows and the columns fill, but the heading stays behind
  in one column while half its rows move to the next.

The fix is structural, not a property value: wrap each heading and its content in one container and
put the no-break rule on that container. Flat markup — a heading and its list as siblings — cannot
express the unit, so the wrapper has to exist before CSS can protect it.

(2026-09-24, firatada.com. The same card broke twice. First `break-inside: avoid` on every block
child pushed a ten-item list into column two and emptied column one. Then moving the rule down to
the rows filled both columns but stranded "Where your work is kept" in the left column with half its
items on the right. Wrapping each heading with its content in a `div.sec` and protecting that
settled it: five sections distributed across two columns, none of them cut.)
TR
## Çok kolonlu metin: bölünmez birim bir bölümdür, bir blok değil
Çok kolonlu bir metin akışında asla bölünmemesi gereken şey bir **bölümdür**: bir başlık ve altındaki
içerik birlikte. Başlığından koparılmış bir liste, neyin listesi olduğunu kaybetmiştir.

İki ayar makul görünür ve ikisi de başarısız olur:
- **Her blok bölünmez.** Uzun bir liste tek bir bölünemez blok sayılır, bir sonraki kolona atlar ve
  öncekini neredeyse boş bırakır.
- **Her satır bölünmez.** Liste artık akar ve kolonlar dolar, ama başlık bir kolonda kalırken satırlarının
  yarısı diğerine geçer.

Çözüm bir özellik değeri değil, yapısaldır: her başlığı ve içeriğini tek bir container'a sar ve bölünmezlik
kuralını o container'a koy. Düz işaretleme — başlık ve listesi kardeş olarak — birimi ifade edemez, yani CSS
onu koruyabilmeden önce sarmalayıcının var olması gerekir.

(2026-09-24, firatada.com. Aynı kart iki kez bozuldu. Önce her blok çocuğa konan `break-inside: avoid`, on
maddelik bir listeyi ikinci kolona itip birinci kolonu boşalttı. Sonra kuralı satır seviyesine indirmek iki
kolonu da doldurdu ama "Where your work is kept" başlığını sol kolonda, maddelerinin yarısını sağda bıraktı.
Her başlığı içeriğiyle birlikte bir `div.sec` içine sarıp onu korumak meseleyi kapattı: beş bölüm iki kolona
dağıldı, hiçbiri kesilmedi.)

Multi-Select Filter Dropdowns

MOVED to design-filters.md (2026-08-25, owner approval) — design-filters.md owns filter, search and lookup mechanics.

EN
## Multi-Select Filter Dropdowns
MOVED to [`design-filters.md`](./design-filters.md) (2026-08-25, owner approval) — `design-filters.md` owns filter,
search and lookup mechanics. See the section of the same name there.
TR
## Çoklu Seçimli Filtre Dropdown'ları
[`design-filters.md`](./design-filters.md) dosyasına TAŞINDI (2026-08-25, sahip onayı) — filtre, arama ve lookup
mekaniği `design-filters.md` dosyasına aittir. Orada aynı adlı bölüme bak.

Dropdowns Always On Top

Dropdowns, popovers, and any floating overlay must always render above all other page content — including sticky headers,…

EN
## Dropdowns Always On Top
Dropdowns, popovers, and any floating overlay must always render above all other page content — including sticky headers, scrollable table areas, and fixed sidebars. Use `z-[9999]` (not just `z-50`) on the floating panel, and ensure the trigger's parent container establishes a stacking context above any scrollable content beneath it (add `relative z-10` or `isolation: isolate` to the filter/toolbar row).
TR
## Dropdown'lar Her Zaman En Üstte
Dropdown'lar, popover'lar ve her türlü yüzen katman her zaman diğer bütün sayfa içeriğinin üstünde çizilmeli — sticky başlıklar, kaydırılabilir tablo alanları ve sabit kenar çubukları dahil. Yüzen panelde `z-[9999]` kullan (sadece `z-50` değil) ve tetikleyicinin üst container'ının, altındaki kaydırılabilir içeriğin üstünde bir yığın bağlamı kurduğundan emin ol (filtre/araç çubuğu satırına `relative z-10` ya da `isolation: isolate` ekle).

No Browser Popups

Never use native browser dialogs (alert(), confirm(), prompt()) anywhere in any project.

EN
## No Browser Popups
Never use native browser dialogs (`alert()`, `confirm()`, `prompt()`) anywhere in any project. Always use in-app **modals** (custom modal/dialog components) for confirmations, prompts, and alerts.
TR
## Tarayıcı Popup'ları Yok
Hiçbir projede, hiçbir yerde yerel tarayıcı diyalogları (`alert()`, `confirm()`, `prompt()`) kullanma. Onaylar, sorular ve uyarılar için her zaman uygulama içi **modal'lar** (özel modal/diyalog bileşenleri) kullan.

Skeleton Loading

Never use spinner/loader indicators for content loading states.

EN
## Skeleton Loading
Never use spinner/loader indicators for **content loading states**. Always use **skeleton loaders** (placeholder shapes matching the content layout, with a subtle shimmer/pulse) while data is being fetched.
- **Scope = the content area.** This rule governs the region whose data is loading (page body, card grid, table). That region gets a skeleton, never a spinner.
- **Navigation-pending affordance is the exception.** A small spinner **may** sit on the control the user just clicked (e.g. a nav/menu item, a submit button) to confirm the click registered while the next view loads — that is a per-control affordance, not a content-loading indicator. Both coexist: spinner on the clicked control, skeleton in the content area it's loading into.
TR
## Skeleton Loading
**İçerik yükleme durumlarında** asla spinner/loader göstergesi kullanma. Veri getirilirken her zaman **skeleton loader** kullan (içerik yerleşimiyle eşleşen yer tutucu şekiller, hafif bir parıltı/nabız efektiyle).
- **Kapsam = içerik alanı.** Bu kural, verisi yüklenen bölgeyi yönetir (sayfa gövdesi, kart ızgarası, tablo). O bölge skeleton alır, asla spinner değil.
- **Yönlendirme bekleme ipucu istisnadır.** Kullanıcının az önce tıkladığı kontrolün üzerinde küçük bir spinner **durabilir** (ör. bir menü öğesi, bir gönder butonu) ki bir sonraki görünüm yüklenirken tıklamanın kaydedildiği teyit edilsin — bu kontrol başına bir ipucudur, içerik yükleme göstergesi değil. İkisi bir arada yaşar: tıklanan kontrolde spinner, yüklendiği içerik alanında skeleton.

Never render a silently-degraded dataset — say it's wrong, don't draw it

An empty screen announces itself.

EN
## Never render a silently-degraded dataset — say it's wrong, don't draw it

An empty screen announces itself. **A screen drawn from partially-broken data does not** — it looks
deliberate, and a reader will act on it. If a pipeline regression can turn a required field into
all-NULL (a stale ETL deploy, a renamed source column, a failed join), the reading app must **detect
it and say so**, not render a plausible-looking wrong picture.

- **Assert the invariant the screen depends on**, not just "did rows come back". An org chart needs
  *exactly one root and almost no missing parent links*; a trend chart needs *dates that reach
  today*; a breakdown needs *its categories to be non-empty*. Rows arriving is not the same as data
  being usable.
- **On failure, show an explicit banner naming what's wrong** ("supervisor links are missing for 94%
  of people — the org chart cannot be trusted, the nightly sync may have failed") and either
  suppress the affected view or mark it. Never silently fall back to a shape that reads as intended:
  a tree whose parent links are all NULL renders as a *flat list*, which looks like a design choice.
- **Prefer stale-but-correct to fresh-but-broken.** A loader that cannot fill its columns should
  refuse and leave yesterday's data in place (`postgre.md` → "Rules for this doc" #7); the screen
  then shows a correct older snapshot with its date, which is far better than a confident wrong one.
- **Row counts are not a health check.** The failure that motivated this rule kept every row and
  every count intact — only the *values* in 25 columns went blank.
TR
## Sessizce bozulmuş bir veri setini asla çizme — yanlış olduğunu söyle, çizme

Boş bir ekran kendini duyurur. **Kısmen bozuk veriden çizilmiş bir ekran duyurmaz** — kasıtlı görünür ve
okuyan ona göre hareket eder. Bir pipeline gerilemesi zorunlu bir alanı tamamen NULL'a çevirebiliyorsa
(bayat bir ETL deploy'u, adı değişmiş bir kaynak kolonu, başarısız bir join), okuyan uygulama bunu
**tespit edip söylemeli**, makul görünen yanlış bir tablo çizmemeli.

- **Ekranın dayandığı değişmezi doğrula**, sadece "satır geldi mi" diye bakma. Bir organizasyon şeması
  *tam olarak bir kök ve neredeyse hiç eksik üst bağlantı olmamasını* ister; bir trend grafiği *bugüne
  ulaşan tarihler* ister; bir kırılım *kategorilerinin boş olmamasını* ister. Satırın gelmesi, verinin
  kullanılabilir olmasıyla aynı şey değildir.
- **Başarısızlıkta neyin bozuk olduğunu söyleyen açık bir bant göster** ("insanların %94'ü için yönetici
  bağlantıları eksik — organizasyon şemasına güvenilemez, gecelik senkronizasyon başarısız olmuş olabilir")
  ve etkilenen görünümü ya bastır ya işaretle. Amaçlanmış gibi okunan bir biçime asla sessizce düşme: üst
  bağlantılarının hepsi NULL olan bir ağaç *düz liste* olarak çizilir, ki bu bir tasarım tercihi gibi görünür.
- **Bayat-ama-doğruyu taze-ama-bozuğa tercih et.** Kolonlarını dolduramayan bir yükleyici reddetmeli ve dünün
  verisini yerinde bırakmalı (`postgre.md` → "Rules for this doc" #7); ekran o zaman tarihiyle birlikte doğru
  ve eski bir anlık görüntü gösterir, ki bu kendinden emin yanlış bir görüntüden çok daha iyidir.
- **Satır sayıları bir sağlık kontrolü değildir.** Bu kuralı doğuran arıza her satırı ve her sayımı olduğu gibi
  korudu — sadece 25 kolondaki *değerler* boşaldı.

Auth-dependent screens — resolve the session on the SERVER, never flash a wrong state

A page whose content depends on who is asking must never be painted from a "signed out / still loading" default.

EN
## Auth-dependent screens — resolve the session on the SERVER, never flash a wrong state
A page whose content depends on **who is asking** must never be painted from a "signed out / still
loading" default. Resolve the session, and the data that depends on it, **on the server, per
request**, so the first HTML the browser receives is already the final screen.

The failure mode, hit twice in a row in HR Offboarding: an already-signed-in user saw the **login
form** for a moment before it vanished. Hiding the form until the check finished only moved the
problem — they then saw a **spinner** instead. The flash was never the form or the spinner; it was
*rendering before the answer was known*. The route was statically prerendered, so every visitor got
HTML built from the component's initial state (`isLoggedIn = false`) and the truth arrived only after
hydration had fetched `/api/...`.

- **Server-render it.** Next.js App Router: make the route a **server component** with
  `export const dynamic = 'force-dynamic'`, read the session cookie there, fetch everything the
  screen needs **in parallel**, and hand it to the client component as one `initial` prop. The client
  component seeds its state from those props (and starts with its "checked" flag true, its "loading"
  flag false — there is nothing left to check).
- **No on-mount refetch of what the server already sent.** It flips the loading flag back on and
  reintroduces the very flash you just removed. Keep loaders for things that happen *after* render,
  not for startup.
- **Refresh after an in-page login.** Everything seeded from the server is stale the moment a user
  signs in without a reload — refetch **all** of it, not part. (Missing this kept the
  Dashboard/Settings buttons hidden from a freshly signed-in admin until they reloaded.)
- **Never gate UI on "not yet known".** `!isLoggedIn` also means "we haven't heard back yet". Gate on
  a *completed* check (`authChecked && !isLoggedIn`). If something genuinely must resolve in the
  browser, show a **skeleton** (see above) — never the signed-out variant of the screen.
- **Such a page must not be cached** — its body depends on the caller's cookie. Expect
  `Cache-Control: private, no-cache, no-store`. (A cached shell is "safe" only while it contains no
  user data at all, which is exactly why the bug went unnoticed.)

**Verify it objectively — don't eyeball the flash.** Request the page twice, with and without a valid
session cookie:
```bash
curl -sk -H "Cookie: auth-token=<token>" https://app/ > in.html
curl -sk                                 https://app/ > out.html
cmp in.html out.html            # must DIFFER
grep -c '>Password<'  in.html   # signed-in: 0 (no login markup)
grep -c 'animate-spin' in.html out.html   # both: 0 (no loading placeholder)
```
The two bodies **must differ** and each must already contain its final screen. Identical bodies mean
the page renders from a default state and **will** flash.
TR
## Oturuma bağlı ekranlar — session'ı SUNUCUDA çöz, asla yanlış bir durumu bir an gösterme
İçeriği **kimin sorduğuna** bağlı olan bir sayfa, asla "çıkış yapılmış / hâlâ yükleniyor" varsayılanından
çizilmemeli. Session'ı ve ona bağlı veriyi **sunucuda, istek başına** çöz ki tarayıcının aldığı ilk HTML
zaten nihai ekran olsun.

Arıza biçimi, HR Offboarding'de arka arkaya iki kez yaşandı: zaten giriş yapmış bir kullanıcı, kaybolmadan
önce bir an **login formunu** gördü. Kontrol bitene kadar formu gizlemek sorunu sadece taşıdı — bu kez
**spinner** gördüler. Parlayan şey hiçbir zaman form ya da spinner değildi; *cevap bilinmeden çizmekti*.
Rota statik olarak önceden çizilmişti, yani her ziyaretçi bileşenin başlangıç durumundan (`isLoggedIn = false`)
kurulmuş HTML alıyordu ve gerçek ancak hidrasyon `/api/...` çağrısını yaptıktan sonra geliyordu.

- **Sunucuda çiz.** Next.js App Router: rotayı `export const dynamic = 'force-dynamic'` ile bir **server
  component** yap, session çerezini orada oku, ekranın ihtiyaç duyduğu her şeyi **paralel** getir ve istemci
  bileşenine tek bir `initial` prop'u olarak ver. İstemci bileşeni durumunu bu prop'lardan tohumlar (ve
  "kontrol edildi" bayrağı true, "yükleniyor" bayrağı false olarak başlar — kontrol edilecek bir şey kalmamıştır).
- **Sunucunun zaten gönderdiğini mount'ta yeniden getirme.** Bu, yükleniyor bayrağını tekrar açar ve az önce
  kaldırdığın parlamayı geri getirir. Yükleyicileri çizimden *sonra* olan şeyler için sakla, başlangıç için değil.
- **Sayfa içi girişten sonra tazele.** Sunucudan tohumlanan her şey, bir kullanıcı sayfayı yenilemeden giriş
  yaptığı anda bayattır — **hepsini** yeniden getir, bir kısmını değil. (Bunu kaçırmak, yeni giriş yapmış bir
  yöneticiden Dashboard/Settings butonlarını sayfayı yenileyene kadar gizli tuttu.)
- **Arayüzü asla "henüz bilinmiyor" üzerine kapatma.** `!isLoggedIn` aynı zamanda "henüz cevap gelmedi" demektir.
  *Tamamlanmış* bir kontrole göre kapat (`authChecked && !isLoggedIn`). Bir şey gerçekten tarayıcıda çözülmek
  zorundaysa **skeleton** göster (yukarıya bak) — asla ekranın çıkış yapılmış hâlini değil.
- **Böyle bir sayfa önbelleğe alınmamalı** — gövdesi çağıranın çerezine bağlıdır. `Cache-Control: private, no-cache,
  no-store` bekle. (Önbelleğe alınmış bir kabuk ancak içinde hiç kullanıcı verisi yokken "güvenlidir"; hatanın
  fark edilmemesinin sebebi tam olarak budur.)

**Nesnel doğrula — parlamayı gözle arama.** Sayfayı geçerli bir session çereziyle ve onsuz olmak üzere iki kez iste:
```bash
curl -sk -H "Cookie: auth-token=<token>" https://app/ > in.html
curl -sk                                 https://app/ > out.html
cmp in.html out.html            # must DIFFER
grep -c '>Password<'  in.html   # signed-in: 0 (no login markup)
grep -c 'animate-spin' in.html out.html   # both: 0 (no loading placeholder)
```
İki gövde **farklı olmalı** ve her biri zaten kendi nihai ekranını içermeli. Aynı gövdeler, sayfanın bir varsayılan
durumdan çizildiği ve **parlayacağı** anlamına gelir.

Server-rendered dates — format in UTC, never the server's local zone

Once a screen renders on the server, every date must produce the same string on the server and in the browser.

EN
## Server-rendered dates — format in UTC, never the server's local zone
Once a screen renders on the server, every date must produce the **same string** on the server and in
the browser. If it doesn't, React rewrites the text during hydration — a second flash, and usually a
wrong value shown first.

- **Date-only columns (Postgres `DATE`) come back as midnight UTC.** Formatting that instant in a
  negative-offset zone rolls it back a day. The Linux box runs `America/New_York` while users sit in
  Istanbul, so a last working day of `2026-07-26` rendered as **25 July 2026** server-side and was
  silently corrected to **26 July 2026** by hydration.
- **Always pass an explicit `timeZone: 'UTC'`** to `toLocaleDateString` / `Intl.DateTimeFormat` for
  any stored date, from **one shared helper** rather than at each call site (HR Offboarding:
  `formatDateUTC()` in `components/offboarding/format.ts`; `lib/mail/templates.ts` already did this,
  which is why emails were right while the page was wrong).
- **Timestamps need it too** — `22:30 UTC` is a different calendar day in New York than in Istanbul.
- Locally-derived dates are fine **when parsed and formatted in the same zone**: parsing a
  `YYYY-MM-DD` value as **local noon** (append `T12:00:00`) and then formatting it locally always
  yields the intended day.
- **Test in the server's zone, not yours.** Set `process.env.TZ = 'America/New_York'` before any date
  work in the test file, and assert the trap itself (that local formatting gives the wrong day there)
  so a simulated zone can't silently stop applying and leave the test passing for the wrong reason.
TR
## Sunucuda çizilen tarihler — UTC ile biçimlendir, asla sunucunun yerel saat dilimiyle
Bir ekran sunucuda çizildiği andan itibaren, her tarih sunucuda ve tarayıcıda **aynı metni** üretmeli.
Üretmiyorsa React hidrasyon sırasında metni yeniden yazar — ikinci bir parlama ve genelde önce yanlış
bir değerin gösterilmesi.

- **Sadece tarih kolonları (Postgres `DATE`) UTC gece yarısı olarak döner.** O anı negatif ofsetli bir saat
  diliminde biçimlendirmek onu bir gün geri alır. Linux makinesi `America/New_York` ile çalışırken kullanıcılar
  İstanbul'daydı, yani `2026-07-26` olan son çalışma günü sunucuda **25 Temmuz 2026** olarak çizildi ve hidrasyonla
  sessizce **26 Temmuz 2026**'ya düzeltildi.
- **Saklanan her tarih için `toLocaleDateString` / `Intl.DateTimeFormat` çağrısına her zaman açık bir
  `timeZone: 'UTC'` geç**, her çağrı yerinde değil **tek bir ortak yardımcıdan** (HR Offboarding:
  `components/offboarding/format.ts` içindeki `formatDateUTC()`; `lib/mail/templates.ts` bunu zaten yapıyordu,
  e-postaların doğru, sayfanın yanlış olmasının sebebi buydu).
- **Zaman damgaları da buna ihtiyaç duyar** — `22:30 UTC`, New York'ta ve İstanbul'da farklı takvim günleridir.
- Yerel olarak türetilmiş tarihler, **aynı saat diliminde ayrıştırılıp biçimlendirildiğinde** sorunsuzdur: bir
  `YYYY-MM-DD` değerini **yerel öğlen** olarak ayrıştırmak (`T12:00:00` ekleyerek) ve sonra yerel olarak
  biçimlendirmek her zaman amaçlanan günü verir.
- **Kendi saat diliminde değil, sunucunun saat diliminde test et.** Test dosyasında herhangi bir tarih işleminden
  önce `process.env.TZ = 'America/New_York'` ayarla ve tuzağın kendisini doğrula (orada yerel biçimlendirmenin
  yanlış günü verdiğini) ki taklit edilen saat dilimi sessizce uygulanmayı bırakıp testi yanlış sebeple geçirmesin.

The rubber stamp — a verdict on a record, never a data field

A stamp is the treatment used for the status of a whole record on a document-like surface: rotated a few degrees, uppercase…

EN
## The rubber stamp — a verdict on a record, never a data field

A **stamp** is the treatment used for the status of a whole record on a document-like surface: rotated
a few degrees, uppercase letter-spaced mono, outlined in `currentColor`, and with **no background
fill**. It reads as something a person pressed onto the page. Reference implementation: `.stamp` in
`ai_apps/AICC Security Hub/app/globals.css`, on the finding cards of the InfoSec hub.

**The four things that make it a stamp.** Drop any one of them and it becomes a chip:

1. **A rotation of a few degrees** (`-7deg` there) — the whole point. It is what says "pressed by hand".
2. **A heavy outline in `currentColor`** (`2.5px`), so the frame takes its colour from the text and one
   class sets both.
3. **No background fill.** A filled version sits *among* the pills on the card instead of *on* the card.
   This is the property that gets lost first when somebody restyles it.
4. **Uppercase mono with real letter-spacing** (`0.1em`), at a small size.

### Use it only when all three of these hold

- **It is a verdict on the WHOLE record**, not a property of one field or row — "Remediated",
  "Question back", "Approved", "Superseded", "Retired". If the value belongs to a column, it is a chip.
- **The surface is document-like** — a paper-ish card, a report, a record someone reads top to bottom.
  On dashboard chrome it reads as costume; a dashboard uses [`design-dashboard.md`](./design-dashboard.md)'s own
  component classes.
- **There is exactly ONE per card**, at its edge. Two stamps on one thing is a costume, not a verdict.

### Do NOT use it for

- **Rows in a table or list.** Rotated elements repeating down a column cannot be scanned and read as
  broken rendering. Status in a table is a chip, aligned with its column.
- **A metric or KPI's state.** A number's status is data; nobody stamps a number.
- **Anything clickable.** A stamp is an assertion, not a control — and a control has to look like one
  (see *"Clickable affordance"* and *"Interactive cells look interactive"* below). If it filters,
  selects or navigates, it is not a stamp.
- **Transient state** — saving, loading, unsaved changes. A stamp implies finality; use the button's own
  acknowledgement instead.
- **Severity or priority that came from the data** (`Criticality: High`) — that is a chip, and on the
  hub's own cards it deliberately IS one, sitting below the title while the stamp sits above it.
- **A long label.** Uppercase mono with letter-spacing stops reading as a stamp past roughly three
  short words. If the verdict needs a sentence, it is not a stamp.

### Mechanics that are not optional

- **Reserve the space it overlaps.** It is absolutely positioned, so the title beside it needs padding
  or the two collide at the first awkward width.
- **Flatten it when the column gets narrow.** Under ~620px make it `position: static`, inline-block,
  no rotation, and give the reserved padding back. A rotated absolute element in a narrow column
  overlaps the text under it.
- **The label carries the meaning, not the angle or the colour.** Colour-blind and zoomed-in readers get
  the same word.
- **Write it as real CSS, not utilities.** A rotation, a `currentColor` border and a responsive
  flattening are values a utility framework can only express as unreadable arbitrary strings, and the
  exact values are the design.

⚠️ It survives restyling badly. On the InfoSec hub the page was rebuilt from its content and the stamp
came back as a filled rounded pill — every word correct, the whole character gone, and it took the
owner noticing to catch it. When you touch one, check the four properties above by name.
TR
## Kaşe — bir kaydın üstündeki hüküm, asla bir veri alanı değil

**Kaşe**, doküman benzeri bir yüzeyde bir kaydın tamamının durumu için kullanılan biçimdir: birkaç derece
döndürülmüş, harf aralığı açılmış büyük harf mono, `currentColor` ile çerçevelenmiş ve **arka plan dolgusu
olmayan**. Sayfaya biri tarafından basılmış gibi okunur. Referans uygulama: `ai_apps/AICC Security Hub/app/globals.css`
içindeki `.stamp`, InfoSec hub'ının bulgu kartlarında.

**Onu kaşe yapan dört şey.** Birini bile düşürürsen chip'e dönüşür:

1. **Birkaç derecelik döndürme** (orada `-7deg`) — bütün mesele bu. "Elle basıldı" diyen şey odur.
2. **`currentColor` ile kalın bir çerçeve** (`2.5px`), böylece çerçeve rengini metinden alır ve tek bir sınıf
   ikisini de ayarlar.
3. **Arka plan dolgusu yok.** Dolgulu bir sürüm kartın *üstünde* değil, karttaki hapların *arasında* durur.
   Biri yeniden stillendirdiğinde ilk kaybolan özellik budur.
4. **Gerçek harf aralığıyla büyük harf mono** (`0.1em`), küçük bir boyutta.

### Sadece şu üçü birden doğruyken kullan

- **KAYDIN TAMAMI hakkında bir hüküm olmalı**, bir alanın ya da satırın özelliği değil — "Remediated",
  "Question back", "Approved", "Superseded", "Retired". Değer bir kolona aitse, o bir chip'tir.
- **Yüzey doküman benzeri olmalı** — kâğıt hissi veren bir kart, bir rapor, birinin baştan sona okuduğu bir kayıt.
  Dashboard arayüzünde kostüm gibi okunur; bir dashboard [`design-dashboard.md`](./design-dashboard.md)'nin kendi
  bileşen sınıflarını kullanır.
- **Kart başına tam olarak BİR tane olmalı**, kenarında. Tek bir şeyde iki kaşe hüküm değil kostümdür.

### Şunlar için KULLANMA

- **Bir tablodaki ya da listedeki satırlar.** Bir kolonda tekrar eden döndürülmüş öğeler taranamaz ve bozuk
  çizim gibi okunur. Tablodaki durum bir chip'tir, kolonuyla hizalı.
- **Bir metriğin ya da KPI'ın durumu.** Bir sayının durumu veridir; kimse sayıya kaşe basmaz.
- **Tıklanabilir hiçbir şey.** Kaşe bir iddiadır, bir kontrol değil — ve bir kontrolün kontrol gibi görünmesi
  gerekir (aşağıdaki *"Tıklanabilirlik ipucu"* ve *"Etkileşimli hücreler etkileşimli görünür"* kurallarına bak).
  Filtreliyor, seçiyor ya da yönlendiriyorsa kaşe değildir.
- **Geçici durum** — kaydediliyor, yükleniyor, kaydedilmemiş değişiklikler. Kaşe kesinlik ima eder; onun yerine
  butonun kendi onayını kullan.
- **Veriden gelen önem ya da öncelik** (`Criticality: High`) — o bir chip'tir ve hub'ın kendi kartlarında bilerek
  chip'tir, başlığın altında durur, kaşe ise üstünde.
- **Uzun bir etiket.** Harf aralığı açılmış büyük harf mono, kabaca üç kısa kelimeden sonra kaşe gibi okunmayı
  bırakır. Hüküm bir cümle gerektiriyorsa kaşe değildir.

### İsteğe bağlı olmayan mekanikler

- **Üstüne bindiği yeri ayır.** Mutlak konumlandırılmıştır, yani yanındaki başlığın boşluğa ihtiyacı vardır,
  yoksa ilk tuhaf genişlikte çakışırlar.
- **Kolon daralınca düzleştir.** ~620px altında onu `position: static`, inline-block, döndürmesiz yap ve
  ayrılmış boşluğu geri ver. Dar bir kolondaki döndürülmüş mutlak öğe altındaki metinle çakışır.
- **Anlamı açı ya da renk değil etiket taşır.** Renk körü ve yakınlaştırılmış okuyan aynı kelimeyi alır.
- **Utility değil, gerçek CSS olarak yaz.** Bir döndürme, bir `currentColor` kenarlığı ve duyarlı bir düzleştirme,
  bir utility çerçevesinin ancak okunamaz keyfi dizgelerle ifade edebileceği değerlerdir ve tasarımın kendisi tam
  olarak o değerlerdir.

⚠️ Yeniden stillendirmeden kötü çıkar. InfoSec hub'ında sayfa içeriğinden yeniden kuruldu ve kaşe dolgulu, yuvarlak
bir hap olarak geri geldi — her kelime doğru, bütün karakter kayıp; ve bunu yakalamak için sahibinin fark etmesi
gerekti. Birine dokunduğunda yukarıdaki dört özelliği tek tek adıyla kontrol et.

Comment threads — see comments.md

A box where people write something onto a record that other people read — a question-back thread, a review note, an approval…

EN
## Comment threads — see `comments.md`
A box where people write something onto a record that other people read — a question-back thread, a
review note, an approval remark — has its own set of rules, and most of them are counter-intuitive
enough to have been learned by getting them wrong:
the typed text must never be nowhere while the post is in flight, the timestamp belongs to the
reader's clock and not the server's, and editing belongs to the author while deleting belongs to the
author or an admin. All of it, with the incidents that produced it, is in
`comments.md`. The rules in this file that it leans on — auto-growing textareas below,
"Every button acknowledges the click", "Side-by-side items share a size", and "Person identifiers —
title-case the dot-separated name" — stay defined here; `comments.md` says how they combine.
TR
## Yorum başlıkları — bkz. `comments.md`
İnsanların bir kaydın üstüne, başkalarının okuyacağı bir şey yazdığı kutu — geri soru başlığı, inceleme notu,
onay şerhi — kendi kural setine sahiptir ve çoğu, yanlış yapılarak öğrenilecek kadar sezgiye aykırıdır:
yazılan metin, gönderi yoldayken asla hiçbir yerde olmamalı, zaman damgası sunucunun değil okuyucunun saatine
aittir ve düzenleme yazara, silme ise yazara ya da bir yöneticiye aittir. Hepsi, onları doğuran olaylarla birlikte
`comments.md` içindedir. Bu dosyanın dayandığı kurallar — aşağıdaki kendiliğinden büyüyen textarea'lar,
"Her buton tıklamayı onaylar", "Yan yana duran öğeler aynı boyutu paylaşır" ve "Kişi tanımlayıcıları — noktayla
ayrılmış adı Title Case yap" — burada tanımlı kalır; `comments.md` bunların nasıl birleştiğini anlatır.

Auto-growing textareas

MOVED to inline-editing.md — it owns editing a value in place, including how the field sizes itself.

EN
## Auto-growing textareas
MOVED to [`inline-editing.md`](./inline-editing.md) — it owns editing a value in place, including how
the field sizes itself.
TR
## Kendiliğinden büyüyen textarea'lar
[`inline-editing.md`](./inline-editing.md) dosyasına TAŞINDI — bir değeri yerinde düzenlemek, alanın kendini
nasıl boyutlandırdığı dahil, o dosyaya aittir.

Inline validation — highlight the field, don't just message

Form validation must use inline, field-level validation, not a single generic error line detached from the inputs.

EN
## Inline validation — highlight the field, don't just message
Form validation must use **inline, field-level validation**, not a single generic error line detached
from the inputs. When a field is invalid: (1) show a **specific inline message** next to that field,
(2) put the field into a visible **error state** — red border / `aria-invalid="true"` — so the eye lands
on it, and (3) **focus (and scroll to) the first invalid field** on submit. Clear a field's error state
as soon as the user corrects it. Never rely on a lone toast or a detached "Please fill X" banner when the
offending field is on screen.
TR
## Satır içi doğrulama — alanı işaretle, sadece mesaj verme
Form doğrulaması, input'lardan kopuk tek bir genel hata satırı değil, **satır içi ve alan bazlı doğrulama**
kullanmalı. Bir alan geçersizse: (1) o alanın yanında **özel bir satır içi mesaj** göster, (2) alanı görünür bir
**hata durumuna** sok — kırmızı kenarlık / `aria-invalid="true"` — ki göz oraya insin, ve (3) gönderme anında
**ilk geçersiz alana odaklan (ve oraya kaydır)**. Kullanıcı düzelttiği anda o alanın hata durumunu temizle. Suçlu
alan ekrandayken tek başına bir toast'a ya da kopuk bir "Lütfen X'i doldurun" bandına asla güvenme.

Constrain inputs at the source — prevent invalid values, don't just validate after

When an input has a knowable valid range or set, constrain the control so an invalid value can't be entered in the first place…

EN
## Constrain inputs at the source — prevent invalid values, don't just validate after
When an input has a knowable valid range or set, **constrain the control so an invalid value can't be entered in the first place** — don't accept a bad value and then show a validation error. Date pickers set `min`/`max` (e.g. a "must be in the future" field sets `min` to **tomorrow** so today and past dates aren't even selectable in the calendar — never ship an after-the-fact "date must be in the future" message as the primary guard); number inputs set `min`/`max`/`step`; choice fields list only valid options. Compute date bounds in **local time** (a `min` built from `toISOString()` is UTC and can be off by a day). The inline validation above is only the **fallback** for values a user can still type manually, not the first line of defense.
TR
## Girdiyi kaynağında sınırla — geçersiz değeri sonradan doğrulama, en baştan engelle
Bir input'un bilinebilir bir geçerli aralığı veya kümesi varsa, **kontrolü öyle sınırla ki geçersiz bir değer en baştan girilemesin** — kötü bir değeri kabul edip sonra doğrulama hatası gösterme. Tarih seçiciler `min`/`max` ayarlar (ör. "gelecekte olmalı" diyen bir alan `min` değerini **yarına** ayarlar ki bugün ve geçmiş tarihler takvimde seçilebilir bile olmasın — birincil koruma olarak asla sonradan gelen bir "tarih gelecekte olmalı" mesajı çıkarma); sayı input'ları `min`/`max`/`step` ayarlar; seçim alanları sadece geçerli seçenekleri listeler. Tarih sınırlarını **yerel saatle** hesapla (`toISOString()` ile kurulmuş bir `min` UTC'dir ve bir gün kayabilir). Yukarıdaki satır içi doğrulama, kullanıcının hâlâ elle yazabildiği değerler için yalnızca **yedek**tir, ilk savunma hattı değil.

Action buttons must self-commit — no hidden second save

When a UI exposes an explicit action button (Add, Remove, Delete, Toggle, Reorder) on a list or collection, that action must…

EN
## Action buttons must self-commit — no hidden second save
When a UI exposes an explicit action button (Add, Remove, Delete, Toggle, Reorder) on a list or
collection, that action must **persist immediately**. Never require the user to press a separate
"Save" afterward to make an already-"committed" action stick — a user who clicks **Add** and sees the
item appear will reasonably assume it is saved and may leave the page, silently losing the change.
Prefer per-action autosave (Add/Remove persist on click; inline text edits persist on blur), with a
brief inline confirmation. If immediate persistence is truly impossible, the item must not appear
committed until saved (e.g. keep it in a pending "draft" affordance), never shown as done-but-unsaved.
TR
## Aksiyon butonları kendi kendini kaydetmeli — gizli ikinci bir kayıt olmaz
Bir arayüz, bir liste veya koleksiyon üzerinde açık bir aksiyon butonu (Ekle, Çıkar, Sil, Değiştir, Sırala)
sunuyorsa, o aksiyon **hemen kalıcı olmalı**. Zaten "kesinleşmiş" bir aksiyonun tutması için kullanıcıdan
sonradan ayrı bir "Kaydet"e basmasını asla isteme — **Ekle**'ye basıp öğenin belirdiğini gören bir kullanıcı
makul olarak kaydedildiğini varsayar, sayfadan ayrılabilir ve değişikliği sessizce kaybeder. Aksiyon başına
otomatik kaydı tercih et (Ekle/Çıkar tıklamada kalıcı olur; satır içi metin düzenlemeleri odak kaybında kalıcı
olur), kısa bir satır içi onayla birlikte. Anında kalıcılık gerçekten imkânsızsa, öğe kaydedilene kadar kesinleşmiş
görünmemeli (ör. bekleyen bir "taslak" ipucunda tut), asla olmuş-ama-kaydedilmemiş olarak gösterilmemeli.

Rich-text / content editors — insert at the caret

In any WYSIWYG or content editor, insert every element (table, image, block, etc.) at the current caret position, never at the…

EN
## Rich-text / content editors — insert at the caret
In any WYSIWYG or content editor, **insert every element (table, image, block, etc.) at the
current caret position**, never at the start or end by default. Preserve the user's selection when
a toolbar action or dialog steals focus (save the `Range`, restore it before inserting; use
`onMouseDown` `preventDefault` on toolbar buttons so clicking them doesn't move the caret).
When an inserted element needs a size or shape, **ask for it upfront in a single step** (e.g. a
table's dimensions as "rows × columns") and draw it to that spec — don't insert a default and make
the user fix it afterward.
TR
## Zengin metin / içerik editörleri — imlecin olduğu yere ekle
Her WYSIWYG veya içerik editöründe, **her öğeyi (tablo, görsel, blok vb.) mevcut imleç konumuna ekle**,
varsayılan olarak asla başa ya da sona değil. Bir araç çubuğu aksiyonu veya diyalog odağı çaldığında kullanıcının
seçimini koru (`Range`'i sakla, eklemeden önce geri yükle; araç çubuğu butonlarında `onMouseDown` üzerinde
`preventDefault` kullan ki onlara tıklamak imleci oynatmasın).
Eklenen bir öğenin boyuta ya da biçime ihtiyacı varsa, **bunu tek adımda baştan sor** (ör. bir tablonun boyutlarını
"satır × kolon" olarak) ve onu o şartnameye göre çiz — varsayılan bir şey ekleyip kullanıcıya sonradan düzelttirme.

Data Entry Tables — Lookup / Reference Screens

These rules apply to any admin screen that manages a lookup/reference table (e.g.

EN
## Data Entry Tables — Lookup / Reference Screens

These rules apply to any admin screen that manages a lookup/reference table (e.g. domain factors, pay ranges, FX rates, CL assignments).

### Inline row editing
MOVED to [`inline-editing.md`](./inline-editing.md) — the row-edit rules live there with the rest of
the in-place editing rules.

### Add new row
- A single **+ Add** button in the page header opens a new blank row at the top of the table (inline, not a modal). The same ✓ / ✗ button pattern applies.
- Auto-focus the first input in the new row on open.
- On successful save, append the new row to the table and reset the add form; on failure, show a toast with the API error message.

### Delete confirmation
- Delete must require a **two-step inline confirmation**: the first click replaces the row's action icons with the confirmation. Never use a modal or browser `confirm()` for row deletion.
- **The confirming button names the action** — "Yes, Delete", not a bare "Yes", which does not say what it agrees to when read on its own. Put the neutral way out (Cancel) first and leave it unstyled; the destructive button carries the danger colour.

### Numeric value constraints
- Define valid ranges for every numeric input at both the API layer and the frontend (`min`/`max` on the input, validation before submit).
- Display values as multipliers (e.g. `1.10x`), never as raw percentages (e.g. `110%`), unless the domain explicitly uses percentages.
- Clearly document the expected format in placeholder text and inline help copy (e.g. "e.g. 1.20 for 120%").

### Paste from Excel (bulk import)
Use this pattern whenever a table contains many rows that users may maintain in Excel:

1. **"Paste from Excel" button** in the page header (secondary style, `ClipboardPaste` icon). Opens a modal.
2. **Modal — paste screen:**
   - Header explains: only existing records will be updated (no new rows added), and the expected numeric format.
   - A `textarea` (monospace font, `autoFocus`) accepts raw tab-separated clipboard data from Excel.
   - As the user types/pastes, **parse in real-time** and show a live preview table below the textarea with columns: identifier, value columns, and a **Status** column.
3. **Status column per row (real-time validation):**
   - **Red** — hard error (unknown identifier, value out of range, parse failure): row will be skipped.
   - **Yellow** — fuzzy match warning (identifier looks like a known one but isn't exact, e.g. "Did you mean: X?"): row is blocked until corrected.
   - **Amber "Override"** — valid row, will update an existing record.
4. **Fuzzy match logic:** for each identifier in the paste, check against the existing list. If no exact match but a fuzzy match (≥ 80% character-sequence score or substring), show the suggestion and block import for that row.
5. **Footer summary:** "N valid row(s), M will be skipped" before the Import button. Disable Import if 0 valid rows.
6. **Import button** shows a spinner while in-flight. On success, switch modal to a **Result screen**:
   - Two summary cards: green "Imported" count, red "Skipped" count.
   - A table listing each skipped row with its reason.
   - A single "Done" button closes the modal.
7. Reload the table data after a successful import (without full page refresh).

### Skeleton loading
Show skeleton rows (matching table column layout) while the initial data fetch is in progress. Never show an empty table or a spinner in isolation.

### Header button layout
Page header: title + description on the left; action buttons (Paste from Excel, Add) stacked or grouped on the right.
TR
## Veri Giriş Tabloları — Lookup / Referans Ekranları

Bu kurallar, bir lookup/referans tablosunu yöneten her yönetim ekranı için geçerlidir (ör. alan katsayıları, ücret bantları, döviz kurları, CL atamaları).

### Satır içi satır düzenleme
[`inline-editing.md`](./inline-editing.md) dosyasına TAŞINDI — satır düzenleme kuralları, yerinde düzenlemeye dair
diğer kurallarla birlikte orada yaşar.

### Yeni satır ekleme
- Sayfa başlığındaki tek bir **+ Add** butonu, tablonun en üstünde yeni ve boş bir satır açar (satır içi, modal değil). Aynı ✓ / ✗ buton kalıbı geçerlidir.
- Açılışta yeni satırdaki ilk input'a otomatik odaklan.
- Başarılı kayıtta yeni satırı tabloya ekle ve ekleme formunu sıfırla; hatada API hata mesajıyla bir toast göster.

### Silme onayı
- Silme **iki adımlı satır içi onay** gerektirmeli: ilk tıklama satırın aksiyon simgelerini onayla değiştirir. Satır silmek için asla modal ya da tarayıcının `confirm()` çağrısını kullanma.
- **Onaylayan buton eylemi adlandırır** — tek başına okunduğunda neyi kabul ettiğini söylemeyen yalın bir "Evet" değil, "Yes, Delete". Nötr çıkışı (Cancel) öne koy ve stilsiz bırak; yıkıcı buton tehlike rengini taşır.

### Sayısal değer sınırları
- Her sayısal input için geçerli aralıkları hem API katmanında hem frontend'de tanımla (input'ta `min`/`max`, göndermeden önce doğrulama).
- Değerleri çarpan olarak göster (ör. `1.10x`), ham yüzde olarak değil (ör. `110%`), alan açıkça yüzde kullanmıyorsa.
- Beklenen biçimi placeholder metninde ve satır içi yardım metninde açıkça belgele (ör. "ör. %120 için 1.20").

### Excel'den yapıştırma (toplu içe aktarma)
Bu kalıbı, kullanıcıların Excel'de tutabileceği çok satırlı her tabloda kullan:

1. Sayfa başlığında **"Paste from Excel" butonu** (ikincil stil, `ClipboardPaste` simgesi). Bir modal açar.
2. **Modal — yapıştırma ekranı:**
   - Başlık şunu açıklar: sadece mevcut kayıtlar güncellenecek (yeni satır eklenmeyecek) ve beklenen sayı biçimi.
   - Bir `textarea` (mono font, `autoFocus`) Excel'den gelen ham, sekmeyle ayrılmış pano verisini kabul eder.
   - Kullanıcı yazdıkça/yapıştırdıkça **gerçek zamanlı ayrıştır** ve textarea'nın altında canlı bir önizleme tablosu göster; kolonlar: tanımlayıcı, değer kolonları ve bir **Status** kolonu.
3. **Satır başına Status kolonu (gerçek zamanlı doğrulama):**
   - **Kırmızı** — sert hata (bilinmeyen tanımlayıcı, aralık dışı değer, ayrıştırma hatası): satır atlanacak.
   - **Sarı** — bulanık eşleşme uyarısı (tanımlayıcı bilinen birine benziyor ama birebir değil, ör. "Did you mean: X?"): satır düzeltilene kadar engellenir.
   - **Kehribar "Override"** — geçerli satır, mevcut bir kaydı güncelleyecek.
4. **Bulanık eşleşme mantığı:** yapıştırmadaki her tanımlayıcıyı mevcut listeye karşı kontrol et. Birebir eşleşme yoksa ama bulanık eşleşme varsa (≥ %80 karakter dizisi skoru ya da alt dizge), öneriyi göster ve o satırın içe aktarımını engelle.
5. **Alt bilgi özeti:** Import butonundan önce "N geçerli satır, M atlanacak". 0 geçerli satır varsa Import'u devre dışı bırak.
6. **Import butonu** istek sürerken spinner gösterir. Başarıda modal'ı bir **Sonuç ekranına** çevir:
   - İki özet kartı: yeşil "Imported" sayısı, kırmızı "Skipped" sayısı.
   - Atlanan her satırı sebebiyle listeleyen bir tablo.
   - Modal'ı kapatan tek bir "Done" butonu.
7. Başarılı bir içe aktarmadan sonra tablo verisini yeniden yükle (sayfayı tümden yenilemeden).

### Skeleton loading
İlk veri getirme sürerken skeleton satırları göster (tablo kolon yerleşimiyle eşleşen). Asla boş bir tablo ya da tek başına bir spinner gösterme.

### Başlık buton yerleşimi
Sayfa başlığı: solda başlık + açıklama; sağda aksiyon butonları (Paste from Excel, Add) üst üste ya da gruplanmış.

Percentage Inputs — Whole-Number Entry Only

Any input that accepts a percentage value (tax rates, growth rates, discount rates, etc.) must: 1.

EN
## Percentage Inputs — Whole-Number Entry Only
Any input that accepts a percentage value (tax rates, growth rates, discount rates, etc.) must:
1. **Accept whole numbers only** — e.g. enter `17` for 17%, never `0.17`.
2. **Detect decimal entry** — if the user types a value `> 0 && < 1`, immediately show a red border on the input and an inline warning: _"Enter as whole number (e.g. 17, not 0.17)"_.
3. **Block save** while any percentage field is in decimal-entry state — disable or no-op the save/confirm action.
4. **Store internally as decimals** — divide by 100 before writing to the API/database; multiply by 100 when displaying back to the user.
5. **Show the `%` unit** in the display value (e.g. `17.0%`) so the expected format is unambiguous.
TR
## Yüzde Input'ları — Sadece Tam Sayı Girişi
Yüzde değeri kabul eden her input (vergi oranı, büyüme oranı, iskonto oranı vb.) şunları yapmalı:
1. **Sadece tam sayı kabul et** — ör. %17 için `17` gir, asla `0.17`.
2. **Ondalık girişi yakala** — kullanıcı `> 0 && < 1` bir değer yazarsa, input'ta hemen kırmızı kenarlık ve satır içi bir uyarı göster: _"Tam sayı olarak girin (ör. 17, 0.17 değil)"_.
3. Herhangi bir yüzde alanı ondalık giriş durumundayken **kaydı engelle** — kaydet/onayla eylemini devre dışı bırak ya da hiçbir şey yaptırma.
4. **İçeride ondalık olarak sakla** — API/veritabanına yazmadan önce 100'e böl; kullanıcıya geri gösterirken 100 ile çarp.
5. Gösterilen değerde **`%` birimini göster** (ör. `17.0%`) ki beklenen biçim belirsiz kalmasın.

Save buttons — confirm, then reset on new edits

A save/submit button must confirm success on the button itself: after a successful save, switch it to a distinct "Saved" state…

EN
## Save buttons — confirm, then reset on new edits
A save/submit button must **confirm success on the button itself**: after a successful save, switch
it to a distinct "Saved" state (e.g. a checkmark + "Saved", success styling, non-interactive) so
the user sees the action landed — don't rely on a toast alone. As soon as the user changes the
underlying content again, **revert the button to its default actionable state** ("Save …"). Keep
the loading state (spinner) during the in-flight request as a third, separate state.
TR
## Kaydet butonları — önce onayla, yeni düzenlemede sıfırla
Bir kaydet/gönder butonu başarıyı **butonun kendisinde teyit etmeli**: başarılı bir kayıttan sonra onu ayrı
bir "Kaydedildi" durumuna geçir (ör. bir onay işareti + "Saved", başarı stili, etkileşimsiz) ki kullanıcı
eylemin tuttuğunu görsün — tek başına bir toast'a güvenme. Kullanıcı alttaki içeriği tekrar değiştirdiği anda
**butonu varsayılan, eyleme hazır durumuna döndür** ("Save …"). İstek sürerken gösterilen yükleme durumunu
(spinner) üçüncü ve ayrı bir durum olarak koru.

Email Sending

Package: nodemailer

EN
## Email Sending

**Package:** `nodemailer`

Create a single reusable helper at `lib/mail/sendMail.ts` — never inline transporter setup in API routes:

```ts
import nodemailer from 'nodemailer';

export async function sendMail(opts: { to: string[]; subject: string; bodyHtml: string }) {
  const host = process.env.SMTP_HOST;
  const port = parseInt(process.env.SMTP_PORT ?? '25');
  const user = process.env.SMTP_USER;
  const pass = process.env.SMTP_PASS;
  const from = process.env.SMTP_FROM;

  if (!host) throw new Error('SMTP not configured (SMTP_HOST missing)');

  const transporter = nodemailer.createTransport({
    host, port, secure: false,
    tls: { rejectUnauthorized: false },
    ...(user && pass ? { auth: { user, pass } } : {}),
  } as Parameters<typeof nodemailer.createTransport>[0]);

  await transporter.sendMail({ from, to: opts.to.join(', '), subject: opts.subject, html: opts.bodyHtml });
}
```

- SMTP credentials always come from env vars (`SMTP_HOST`, `SMTP_PORT`, `SMTP_FROM`) — see `~/.env` for shared Afiniti relay values.
- `secure: false` + `rejectUnauthorized: false` — Afiniti internal relay does not use TLS.
- No-auth mode: if `SMTP_USER`/`SMTP_PASS` are absent, auth block is omitted entirely (relay accepts unauthenticated).
- API routes that send email must call `sendMail()` — never duplicate transporter setup inline.
- For admin CC: pass `SMTP_ADMIN_EMAIL` env var as an additional recipient in the `to` array or as a separate `cc` field.
TR
## E-posta Gönderimi

**Paket:** `nodemailer`

`lib/mail/sendMail.ts` içinde tek ve yeniden kullanılabilir bir yardımcı oluştur — transporter kurulumunu asla API rotalarının içine yazma:

```ts
import nodemailer from 'nodemailer';

export async function sendMail(opts: { to: string[]; subject: string; bodyHtml: string }) {
  const host = process.env.SMTP_HOST;
  const port = parseInt(process.env.SMTP_PORT ?? '25');
  const user = process.env.SMTP_USER;
  const pass = process.env.SMTP_PASS;
  const from = process.env.SMTP_FROM;

  if (!host) throw new Error('SMTP not configured (SMTP_HOST missing)');

  const transporter = nodemailer.createTransport({
    host, port, secure: false,
    tls: { rejectUnauthorized: false },
    ...(user && pass ? { auth: { user, pass } } : {}),
  } as Parameters<typeof nodemailer.createTransport>[0]);

  await transporter.sendMail({ from, to: opts.to.join(', '), subject: opts.subject, html: opts.bodyHtml });
}
```

- SMTP kimlik bilgileri her zaman ortam değişkenlerinden gelir (`SMTP_HOST`, `SMTP_PORT`, `SMTP_FROM`) — ortak Afiniti relay değerleri için `~/.env` dosyasına bak.
- `secure: false` + `rejectUnauthorized: false` — Afiniti iç relay'i TLS kullanmıyor.
- Kimlik doğrulamasız mod: `SMTP_USER`/`SMTP_PASS` yoksa auth bloğu tamamen çıkarılır (relay kimlik doğrulamasız kabul eder).
- E-posta gönderen API rotaları `sendMail()` çağırmalı — transporter kurulumunu asla satır içinde tekrarlama.
- Yönetici CC'si için: `SMTP_ADMIN_EMAIL` ortam değişkenini `to` dizisine ek alıcı olarak ya da ayrı bir `cc` alanı olarak geç.

Excel Export

Package: exceljs (always dynamic import on the client — never bundle it statically)

EN
## Excel Export

**Package:** `exceljs` (always dynamic import on the client — never bundle it statically)

Standard client-side export pattern used across all data tables:

```ts
const exportExcel = async () => {
  setExporting(true);
  const { default: ExcelJS } = await import('exceljs');
  const wb = new ExcelJS.Workbook();
  const ws = wb.addWorksheet('Sheet Name');

  // Header row — bold + filled background
  ws.addRow(['Col A', 'Col B', 'Col C']);
  ws.getRow(1).font = { bold: true };
  ws.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE5E7EB' } };

  // Data rows
  for (const row of data) {
    ws.addRow([row.a, row.b, row.c]);
  }

  // Trigger download
  const buf = await wb.xlsx.writeBuffer();
  const blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url; a.download = `Export_${dateStr}.xlsx`; a.click();
  URL.revokeObjectURL(url);
  setExporting(false);
};
```

- **Dynamic import** (`await import('exceljs')`) — mandatory to keep it out of the client bundle.
- **Filename convention:** `<ScreenName>_<etlDate>.xlsx` — always include the date so users can distinguish files.
- **Export button:** secondary style, `Download` icon (lucide), spinner (`Loader2 animate-spin`) while in-flight, disabled during export. Label: "Export" idle → "Exporting…" in-flight.
- **Header row styling:** bold font + light grey fill (`FFE5E7EB`) — consistent across all exports.
- **Number columns:** set `ws.getColumn(n).numFmt` for currency/percentage cells so Excel renders them correctly.
- Export operates on the **currently filtered/sorted** dataset, not the raw full list.
TR
## Excel'e Aktarma

**Paket:** `exceljs` (istemcide her zaman dinamik import — asla statik olarak paketleme)

Bütün veri tablolarında kullanılan standart istemci tarafı aktarma kalıbı:

```ts
const exportExcel = async () => {
  setExporting(true);
  const { default: ExcelJS } = await import('exceljs');
  const wb = new ExcelJS.Workbook();
  const ws = wb.addWorksheet('Sheet Name');

  // Header row — bold + filled background
  ws.addRow(['Col A', 'Col B', 'Col C']);
  ws.getRow(1).font = { bold: true };
  ws.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE5E7EB' } };

  // Data rows
  for (const row of data) {
    ws.addRow([row.a, row.b, row.c]);
  }

  // Trigger download
  const buf = await wb.xlsx.writeBuffer();
  const blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url; a.download = `Export_${dateStr}.xlsx`; a.click();
  URL.revokeObjectURL(url);
  setExporting(false);
};
```

- **Dinamik import** (`await import('exceljs')`) — istemci paketinin dışında tutmak için zorunlu.
- **Dosya adı kuralı:** `<ScreenName>_<etlDate>.xlsx` — kullanıcılar dosyaları ayırt edebilsin diye tarihi her zaman ekle.
- **Aktarma butonu:** ikincil stil, `Download` simgesi (lucide), istek sürerken spinner (`Loader2 animate-spin`), aktarma sırasında devre dışı. Etiket: beklemede "Export" → istek sürerken "Exporting…".
- **Başlık satırı stili:** kalın font + açık gri dolgu (`FFE5E7EB`) — bütün aktarmalarda tutarlı.
- **Sayı kolonları:** para/yüzde hücreleri için `ws.getColumn(n).numFmt` ayarla ki Excel onları doğru göstersin.
- Aktarma **o an filtrelenmiş/sıralanmış** veri kümesi üzerinde çalışır, ham tam liste üzerinde değil.

Downloads that open in an external app — show a transient "open with" hint

When a download button serves a file whose real destination is another APPLICATION (a .msg for Outlook, an .ics for the…

EN
## Downloads that open in an external app — show a transient "open with" hint
When a download button serves a file whose real destination is another APPLICATION (a `.msg` for
Outlook, an `.ics` for the calendar, a `.pbix` for Power BI — anything the browser itself cannot
render), the click must also show a **short-lived, generic hint** telling the user what to open it
with — because the browser may interrupt with an "open with / save" question the app never sees,
and a user who picks wrong blames the feature.

Mechanics (reference impl: Security Hub `MailList.tsx` "Download .msg"):
- A small toast, **bottom-center, auto-dismissing after ~6 s** (re-click resets the timer); dark
  ground, white text, one sentence: *"Downloading — if your browser asks how to open the file,
  choose **Outlook**."* Name the app in bold; keep the wording generic (it must stay true whether
  or not the browser actually asks).
- The hint is fired from the anchor's `onClick` **without preventing navigation** — the download is
  the anchor's own job and must not depend on any JS succeeding.
- `role="status"` so a screen reader announces it; never a blocking dialog and never a browser
  popup (see *No Browser Popups*).
- The top navigation bar must not duplicate links that already appear as cards/sections
  on their destination page (e.g. a Home screen that lists the process steps as cards).
  Keep the top nav minimal — primary landmarks only (e.g. Home, a reference/docs entry,
  Admin) — and let the in-page cards be the entry points for everything else.
- Provide **one global Back button in the top bar** (navigates through history), shown on
  every page except the home/root. Do not scatter per-page "Back" links in page bodies —
  the single bar Back is the only back affordance.
TR
## Harici bir uygulamada açılan indirmeler — geçici bir "şununla aç" ipucu göster
Bir indirme butonu, gerçek varış noktası başka bir UYGULAMA olan bir dosya veriyorsa (Outlook için `.msg`,
takvim için `.ics`, Power BI için `.pbix` — tarayıcının kendisinin çizemediği her şey), tıklama aynı zamanda
kullanıcıya onu neyle açacağını söyleyen **kısa ömürlü, genel bir ipucu** göstermeli — çünkü tarayıcı,
uygulamanın hiç göremediği bir "şununla aç / kaydet" sorusuyla araya girebilir ve yanlış seçen kullanıcı
suçu özelliğe atar.

Mekanik (referans uygulama: Security Hub `MailList.tsx` "Download .msg"):
- Küçük bir toast, **altta ortada, ~6 sn sonra kendiliğinden kapanan** (tekrar tıklamak süreyi sıfırlar); koyu
  zemin, beyaz metin, tek cümle: *"İndiriliyor — tarayıcınız dosyayı nasıl açacağınızı sorarsa **Outlook**'u
  seçin."* Uygulama adını kalın yaz; ifadeyi genel tut (tarayıcı sorsa da sormasa da doğru kalmalı).
- İpucu, bağlantının `onClick` olayından **yönlendirmeyi engellemeden** tetiklenir — indirme bağlantının kendi
  işidir ve herhangi bir JS'in başarılı olmasına bağlı olmamalıdır.
- Ekran okuyucunun duyurması için `role="status"`; asla engelleyen bir diyalog ve asla bir tarayıcı popup'ı
  olmamalı (bkz. *Tarayıcı Popup'ları Yok*).
- Üst gezinme barı, varış sayfalarında zaten kart/bölüm olarak görünen linkleri tekrarlamamalı
  (ör. süreç adımlarını kart olarak listeleyen bir Ana Sayfa). Üst gezinmeyi asgaride tut — sadece
  ana işaretler (ör. Ana Sayfa, bir referans/doküman girişi, Admin) — ve gerisinin giriş noktası
  sayfa içindeki kartlar olsun.
- Üst barda **tek bir global Geri butonu** bulundur (geçmişte gezinir), ana sayfa/kök dışında her sayfada
  görünsün. Sayfa gövdelerine sayfa başına "Geri" linkleri serpiştirme — tek bar Geri'si tek geri ipucudur.

Accordion / expandable cards — the whole card is the click target

When a card/box expands and collapses on click, the toggle must fire from every point inside the card's full bounding box,…

EN
## Accordion / expandable cards — the whole card is the click target
When a card/box expands and collapses on click, the toggle must fire from **every point inside
the card's full bounding box, without exception** — padding, edges, and corners included.

Two traps make naive implementations fail:
1. **Handler on an inner element.** Attaching the click handler to only an inner header/title
   element excludes the card's own padding — clicks near the borders silently do nothing.
2. **Rounded corners.** With `border-radius`, browser hit-testing clips the rounded corner
   area: a click 1–2px inside a corner lands on the *parent* element, not the card. Even a
   handler on the card container itself misses these corner clicks.

**Required implementation — coordinate-based document handler:**
```js
document.addEventListener("click", function (e) {
  // don't toggle when interacting with expanded content (links, buttons, text)
  if (e.target.closest && e.target.closest(".card-body")) return;
  // direct hit on the card (or anything inside it)
  var card = e.target.closest ? e.target.closest(".card") : null;
  // corner/edge fallback: any click inside a card's bounding rect counts
  if (!card) {
    document.querySelectorAll(".card").forEach(function (c) {
      var r = c.getBoundingClientRect();
      if (e.clientX >= r.left && e.clientX <= r.right &&
          e.clientY >= r.top  && e.clientY <= r.bottom) card = c;
    });
  }
  if (card) card.classList.toggle("open");
});
```
Rules:
- One **document-level** listener; resolve the card via `closest()` first, then fall back to
  a `getBoundingClientRect()` containment check so rounded corners still toggle.
- Clicks originating inside the **expanded content area** must never toggle — exempt it
  explicitly (e.g. `.card-body`), so links, copy buttons, and text selection work normally.
- Interactive controls that happen to sit on the card surface (e.g. a bookmark icon) must
  `stopPropagation()` or be exempted the same way.
- **Verify with a hit-test sweep** before delivery: for every card, programmatically click the
  4 corners (1px inside), 4 edge midpoints, and the center — all 9 must toggle. Corner clicks
  are the regression to watch for.
TR
## Akordeon / açılır kartlar — tıklama hedefi kartın tamamıdır
Bir kart/kutu tıklamayla açılıp kapanıyorsa, bu geçiş **kartın tam sınırlayıcı kutusu içindeki her noktadan,
istisnasız** tetiklenmeli — boşluklar, kenarlar ve köşeler dahil.

Saf uygulamaları iki tuzak düşürür:
1. **İşleyicinin iç bir öğede olması.** Tıklama işleyicisini yalnızca iç bir başlık/başlık öğesine bağlamak
   kartın kendi boşluğunu dışarıda bırakır — kenarlara yakın tıklamalar sessizce hiçbir şey yapmaz.
2. **Yuvarlak köşeler.** `border-radius` ile tarayıcının isabet testi yuvarlak köşe alanını kırpar: köşenin
   1–2px içine yapılan bir tıklama karta değil *üst* öğeye iner. Kart container'ının kendisindeki bir işleyici
   bile bu köşe tıklamalarını kaçırır.

**Gereken uygulama — koordinat tabanlı document işleyicisi:**
```js
document.addEventListener("click", function (e) {
  // don't toggle when interacting with expanded content (links, buttons, text)
  if (e.target.closest && e.target.closest(".card-body")) return;
  // direct hit on the card (or anything inside it)
  var card = e.target.closest ? e.target.closest(".card") : null;
  // corner/edge fallback: any click inside a card's bounding rect counts
  if (!card) {
    document.querySelectorAll(".card").forEach(function (c) {
      var r = c.getBoundingClientRect();
      if (e.clientX >= r.left && e.clientX <= r.right &&
          e.clientY >= r.top  && e.clientY <= r.bottom) card = c;
    });
  }
  if (card) card.classList.toggle("open");
});
```
Kurallar:
- Tek bir **document seviyesinde** dinleyici; kartı önce `closest()` ile çöz, sonra bir
  `getBoundingClientRect()` kapsama sınamasına düş ki yuvarlak köşeler de açıp kapasın.
- **Açılmış içerik alanının** içinden gelen tıklamalar asla açıp kapamamalı — onu açıkça muaf tut
  (ör. `.card-body`), böylece linkler, kopyala butonları ve metin seçimi normal çalışır.
- Kart yüzeyinde duran etkileşimli kontroller (ör. bir yer imi simgesi) `stopPropagation()` çağırmalı ya da
  aynı şekilde muaf tutulmalı.
- Teslimden önce **bir isabet testi taramasıyla doğrula**: her kart için 4 köşeye (1px içeriden), 4 kenar
  ortasına ve merkeze programatik olarak tıkla — dokuzunun da açıp kapaması gerekir. Dikkat edilecek gerileme
  köşe tıklamalarıdır.

Clickable affordance (discoverability)

Any element that navigates or triggers an action on click must look clickable — a user should never have to guess.

EN
## Clickable affordance (discoverability)
Any element that navigates or triggers an action on click **must look clickable** — a user
should never have to guess. Provide at least these cues on interactive rows/cards/tiles:
- `cursor-pointer`,
- a **hover state** (background/border/color change, with a short `transition-colors`), and
- for elements that navigate, a **directional cue** — e.g. a chevron (`ChevronRight`) at the
  trailing edge. It may be revealed on hover (`opacity-0 → group-hover:opacity-100`) rather
  than shown permanently, but the row itself must still have the pointer + hover cues so the
  affordance is discoverable before hover.
Never ship a clickable element whose only affordance is the pointer cursor.
TR
## Tıklanabilirlik ipucu (keşfedilebilirlik)
Tıklandığında yönlendiren veya bir aksiyon tetikleyen her öğe **tıklanabilir görünmeli** — kullanıcı
asla tahmin etmek zorunda kalmamalı. Etkileşimli satır/kart/döşemelerde en azından şu ipuçlarını ver:
- `cursor-pointer`,
- bir **hover durumu** (arka plan/kenarlık/renk değişimi, kısa bir `transition-colors` ile) ve
- yönlendiren öğeler için bir **yön ipucu** — ör. sondaki kenarda bir şevron (`ChevronRight`). Bu,
  kalıcı gösterilmek yerine hover'da ortaya çıkabilir (`opacity-0 → group-hover:opacity-100`), ama
  satırın kendisi yine de işaretçi ve hover ipuçlarını taşımalı ki ipucu hover'dan önce keşfedilebilsin.
Tek ipucu işaretçi imleci olan tıklanabilir bir öğeyi asla çıkarma.

Copyable command blocks — one paste per box

A box with a copy button holds exactly one thing to paste, and the button copies exactly that.

EN
## Copyable command blocks — one paste per box
A box with a copy button holds exactly **one thing to paste**, and the button copies exactly that.
- **Sequential commands are separate boxes, numbered.** Examples and alternatives are separate boxes too, unnumbered. Never two commands in one box: they get pasted one at a time anyway.
- **Context lives outside the box**, as a caption above it ("in your normal terminal (PowerShell)", "inside Claude Code", "or just ask…"), even when the box holds a single command. Nothing inside a box is a comment or explanation: a trailing `→ note` becomes a caption beside the box, `a / b / c` alternatives become separate boxes.
- **A single prompt is one line**, soft-wrapped on screen, never hard-wrapped with line breaks that would paste as newlines.
- **Exceptions:** a template meant to be pasted whole (a CLAUDE.md rule snippet, a config file) stays one multi-line box and copies whole. A box that is prose or illustration, not something to paste, gets no copy button.

(2026-09-14, firatada.com: `npm install -g ui-ux-pro-max-cli` and `uipro init --ai claude --global` shared one box under a `#` comment line; copying took both commands plus the comment.)
TR
## Kopyalanabilir komut blokları — kutu başına tek yapıştırma
Kopyala düğmesi olan bir kutu tam olarak **tek bir yapıştırılacak şey** taşır ve düğme tam olarak onu kopyalar.
- **Ardışık komutlar ayrı kutulardır, numaralı.** Örnekler ve alternatifler de ayrı kutulardır, numarasız. Bir kutuda asla iki komut olmaz: zaten teker teker yapıştırılırlar.
- **Bağlam kutunun dışında yaşar**, üstünde bir alt yazı olarak ("normal terminalinizde (PowerShell)", "Claude Code içinde", "ya da düz dille isteyin…"), kutu tek bir komut taşısa bile. Kutunun içindeki hiçbir şey yorum ya da açıklama değildir: sondaki bir `→ not` kutunun yanında bir alt yazıya dönüşür, `a / b / c` alternatifleri ayrı kutulara dönüşür.
- **Tek bir prompt tek satırdır**, ekranda yumuşak sarmalı, asla yapıştırınca satır sonu olacak sert satır sonlarıyla bölünmüş değil.
- **İstisnalar:** bütün olarak yapıştırılması amaçlanan bir şablon (bir CLAUDE.md kural parçası, bir konfigürasyon dosyası) tek ve çok satırlı bir kutu olarak kalır ve bütün kopyalanır. Yapıştırılacak bir şey değil, düz metin ya da örnek olan bir kutu kopyala düğmesi almaz.

(2026-09-14, firatada.com: `npm install -g ui-ux-pro-max-cli` ve `uipro init --ai claude --global` tek bir kutuyu bir `#` yorum satırının altında paylaşıyordu; kopyalamak iki komutu ve yorumu birden aldı.)

Interactive cells look interactive (not just the row)

When a table/list row is clickable, the primary status/label cell inside it must itself carry an interactive appearance —…

EN
## Interactive cells look interactive (not just the row)
When a table/list row is clickable, the **primary status/label cell inside it must itself
carry an interactive appearance** — style it as a button/chip (border + surface + subtle
shadow) or a link, and give it a hover reaction (e.g. brand-tinted border/background on
`group-hover`). Plain body text inside a clickable row reads as static even if the row
navigates; the user should be able to tell the cell is actionable without hovering. Pair
this with the row-level cues (pointer, hover background, trailing chevron).
TR
## Etkileşimli hücreler etkileşimli görünür (sadece satır değil)
Bir tablo/liste satırı tıklanabilir olduğunda, **içindeki ana durum/etiket hücresi de kendi başına
etkileşimli bir görünüm taşımalı** — onu bir buton/chip (kenarlık + yüzey + hafif gölge) ya da bir link
gibi biçimlendir ve bir hover tepkisi ver (ör. `group-hover` üzerinde marka rengine çalan kenarlık/arka plan).
Tıklanabilir bir satırın içindeki düz gövde metni, satır yönlendirse bile durağan okunur; kullanıcı hücrenin
eyleme açık olduğunu hover yapmadan anlayabilmeli. Bunu satır seviyesindeki ipuçlarıyla (işaretçi, hover arka
planı, sondaki şevron) birlikte kullan.

No ambiguous directional icons

Do not place a lone directional icon (e.g.

EN
## No ambiguous directional icons
Do not place a lone directional icon (e.g. a trailing chevron) as a click target when it is
unclear where it leads. Either remove it, or pair it with a clear label / make the real
target obviously interactive. An arrow that could mean several destinations is misleading.
TR
## Belirsiz yön simgeleri yok
Nereye götürdüğü belirsizken tek başına bir yön simgesini (ör. sondaki bir şevron) tıklama hedefi olarak
koyma. Ya onu kaldır, ya net bir etiketle eşle, ya da gerçek hedefi açıkça etkileşimli yap. Birkaç varış
noktası anlamına gelebilecek bir ok yanıltıcıdır.

Side-by-side items share a size

Items sitting next to each other (chips, pills, buttons, badges) must be visually proportional — the same height, and…

EN
## Side-by-side items share a size
Items sitting next to each other (chips, pills, buttons, badges) must be visually
proportional — the same height, and harmonized padding/font-size. Never place a large item
beside a small one of the same kind; pick one size and apply it to the whole group.
TR
## Yan yana duran öğeler aynı boyutu paylaşır
Yan yana duran öğeler (chip'ler, hap butonlar, butonlar, badge'ler) görsel olarak orantılı olmalı — aynı
yükseklik ve uyumlu boşluk/font boyutu. Aynı türden küçük bir öğenin yanına asla büyük bir öğe koyma; bir
boyut seç ve onu bütün gruba uygula.

A top bar has one vertical centre

When two headings of different sizes sit on the same row, the smaller one aligns to the vertical middle of the larger one,…

EN
## A top bar has one vertical centre
When two headings of different sizes sit on the same row, the smaller one aligns to the
vertical middle of the larger one, never to a shared baseline — a baseline drops the smaller
one and the row reads as broken. Inside a top bar or header bar, **every** item shares one
vertical centre: headings, wordmark, buttons, switches, badges, no exceptions. Do it by
centring the row's container (`align-items: center`), never by nudging items one by one.
TR
## Bir üst barın tek bir dikey merkezi vardır
Farklı boyutta iki başlık aynı satırda duruyorsa, küçük olan büyüğün dikey ortasına hizalanır, asla ortak
bir taban çizgisine değil — taban çizgisi küçüğü aşağı düşürür ve satır bozuk okunur. Bir üst barın ya da
başlık barının içinde **her** öğe tek bir dikey merkezi paylaşır: başlıklar, kelime markası, butonlar,
anahtarlar, badge'ler, istisnasız. Bunu satırın container'ını ortalayarak yap (`align-items: center`), asla
öğeleri tek tek kaydırarak değil.

Table cells — wrap instead of truncate when content doesn't fit

If a table cell's content is wider than its column and truncating loses meaningful information, remove truncate and allow…

EN
## Table cells — wrap instead of truncate when content doesn't fit
If a table cell's content is wider than its column and truncating loses meaningful information,
remove `truncate` and allow wrapping: use `whitespace-normal break-words leading-[1.3]`.
Never silently hide data with ellipsis when wrapping to two lines is readable and the row
height is already variable (e.g. other cells in the same row already wrap).
Reserve `truncate` only for columns where the full value is truly secondary (e.g. long URLs,
raw IDs) and a tooltip provides the full text on hover.
TR
## Tablo hücreleri — içerik sığmıyorsa kırpma, alta sar
Bir tablo hücresinin içeriği kolonundan genişse ve kırpmak anlamlı bilgiyi yok ediyorsa, `truncate`'i kaldır
ve alta sarmaya izin ver: `whitespace-normal break-words leading-[1.3]` kullan.
İki satıra sarmak okunabilirken ve satır yüksekliği zaten değişkenken (ör. aynı satırdaki diğer hücreler zaten
sarıyorken) veriyi üç noktayla sessizce gizleme.
`truncate`'i yalnızca tam değerin gerçekten ikincil olduğu kolonlar için sakla (ör. uzun URL'ler, ham ID'ler)
ve hover'da tam metni veren bir tooltip sun.

Table column headers — always top-aligned

When any column header in a table wraps to two or more lines, all header cells in that row must be vertically top-aligned…

EN
## Table column headers — always top-aligned
When any column header in a table wraps to two or more lines, **all** header cells in that
row must be vertically top-aligned (`vertical-align: top` / Tailwind `align-top` on the `<tr>`).
Never leave some headers centered vertically while others wrap — the baseline mismatch looks
broken. Apply `align-top` to the thead `<tr>` unconditionally whenever multiline headers exist.
TR
## Tablo kolon başlıkları — her zaman üste hizalı
Bir tablodaki herhangi bir kolon başlığı iki veya daha fazla satıra sarıyorsa, o satırdaki **bütün** başlık
hücreleri dikeyde üste hizalanmalı (`<tr>` üzerinde `vertical-align: top` / Tailwind `align-top`). Bazı
başlıkları dikeyde ortalanmış bırakıp diğerlerinin sarmasına izin verme — taban çizgisi uyumsuzluğu bozuk
görünür. Çok satırlı başlık olan her durumda thead `<tr>` üzerine koşulsuz `align-top` uygula.

Table column headers — dimension columns filter, measure columns sort

MOVED to design-filters.md (2026-08-25, owner approval) — design-filters.md owns filter, search and lookup mechanics.

EN
## Table column headers — dimension columns filter, measure columns sort
MOVED to [`design-filters.md`](./design-filters.md) (2026-08-25, owner approval) — `design-filters.md` owns filter,
search and lookup mechanics. See the section of the same name there.
TR
## Tablo kolon başlıkları — boyut kolonları filtreler, ölçü kolonları sıralar
[`design-filters.md`](./design-filters.md) dosyasına TAŞINDI (2026-08-25, sahip onayı) — filtre, arama ve lookup
mekaniği `design-filters.md` dosyasına aittir. Orada aynı adlı bölüme bak.

Number format control — Compact / Full

A dashboard whose money (or other large-number) surfaces can render both abbreviated and exact carries ONE "number format"…

EN
## Number format control — Compact / Full

A dashboard whose money (or other large-number) surfaces can render both abbreviated and exact
carries ONE **"number format"** control in the top bar: a **SEGMENTED two-button control** (never a
switch, never a dropdown) reading `Compact | Full`, wearing the bar's caption-over-control pair
with the lowercase caption **"number format"** and a small `#` (lucide `Hash`) icon.

- **Compact — the DEFAULT**: abbreviated, max one decimal — `$1.2M`, `$45.7K` (the chart-label
  rounding rule applied to page surfaces).
- **Full**: the exact figure with thousands separators — `$1,234,567`. No abbreviation.
- **Anatomy**: container at the bar's shared control height, `p-0.5 rounded-lg border
  border-gray-200 bg-gray-100/70`; the ACTIVE side is a white `rounded-md` button with accent-blue
  semibold text, a soft shadow and a light accent border; the inactive side is muted gray text on
  no fill. Each side carries `aria-pressed` and a title stating its example output.
- **A display preference, not a filter**: it changes no query and no scope, never summons the
  Reset button, and lives in the URL (`?nf=full`) so a shared link keeps its formatting.
- **The choice is ALWAYS remembered** (owner rule, 2026-08-26): every click also writes a
  preference cookie, and a URL naming NO format restores it (server-side redirect, so the URL
  stays the single source of truth the control reads). A URL that names a format wins over the
  cookie; nothing remembered means Compact. A fresh visit, a nav-tab click and a Reset therefore
  all keep the user's format.
- **Scope**: every KPI, table and summary figure on the view follows it. Chart data labels and
  axis ticks keep the chart rules (always compact); an exact reconciliation figure quoted in
  prose keeps its exact form regardless.

Reference implementations, kept in agreement:
`ai_apps/AAI Perfornance Summary/src/components/summary/SummaryPage.tsx` (the original) and
`ai_apps/Payroll_Manager/components/NumberFormatToggle.tsx`.
TR
## Sayı biçimi kontrolü — Compact / Full

Para (ya da başka büyük sayı) yüzeylerini hem kısaltılmış hem tam gösterebilen bir dashboard, üst barda TEK bir
**"number format"** kontrolü taşır: `Compact | Full` yazan **SEGMENTLİ iki butonlu bir kontrol** (asla bir anahtar,
asla bir dropdown), barın alt yazı-üstü-kontrol ikilisini giyerek, küçük harfli **"number format"** alt yazısı ve
küçük bir `#` (lucide `Hash`) simgesiyle.

- **Compact — VARSAYILAN**: kısaltılmış, en fazla bir ondalık — `$1.2M`, `$45.7K` (grafik etiketi yuvarlama
  kuralının sayfa yüzeylerine uygulanmış hâli).
- **Full**: binlik ayırıcılarla tam rakam — `$1,234,567`. Kısaltma yok.
- **Anatomi**: barın ortak kontrol yüksekliğinde bir container, `p-0.5 rounded-lg border border-gray-200
  bg-gray-100/70`; AKTİF taraf, vurgu mavisi yarı kalın metni, yumuşak gölgesi ve açık vurgu kenarlığı olan beyaz
  bir `rounded-md` butondur; pasif taraf dolgusuz, soluk gri metindir. Her taraf `aria-pressed` ve örnek çıktısını
  söyleyen bir title taşır.
- **Bir gösterim tercihi, filtre değil**: hiçbir sorguyu ve kapsamı değiştirmez, Reset butonunu asla çağırmaz ve
  URL'de yaşar (`?nf=full`) ki paylaşılan bir link biçimini korusun.
- **Seçim HER ZAMAN hatırlanır** (sahip kuralı, 2026-08-26): her tıklama aynı zamanda bir tercih çerezi yazar ve
  HİÇBİR biçim belirtmeyen bir URL onu geri yükler (sunucu tarafı yönlendirme, böylece kontrolün okuduğu tek
  doğruluk kaynağı URL olarak kalır). Bir biçim belirten URL çerezi yener; hatırlanan bir şey yoksa Compact geçerlidir.
  Yani yeni bir ziyaret, bir menü sekmesi tıklaması ve bir Reset, kullanıcının biçimini korur.
- **Kapsam**: görünümdeki her KPI, tablo ve özet rakamı onu izler. Grafik veri etiketleri ve eksen işaretleri grafik
  kurallarını korur (her zaman kısaltılmış); düz metin içinde alıntılanan tam bir mutabakat rakamı ne olursa olsun
  tam hâlini korur.

Anlaşmalı tutulan referans uygulamalar:
`ai_apps/AAI Perfornance Summary/src/components/summary/SummaryPage.tsx` (orijinali) ve
`ai_apps/Payroll_Manager/components/NumberFormatToggle.tsx`.

Excel Export — Full column header names, no abbreviations

Excel export column headers must use the longest, most descriptive name for each field.

EN
## Excel Export — Full column header names, no abbreviations
Excel export column headers must use the **longest, most descriptive name** for each field.
The exported file is consumed outside the app and must be self-explanatory without UI context.
- Write out every word: "Annual Salary (Local Currency)" not "Salary LC", "Career Level" not "CL"
- Include the unit in the header: "(USD)", "(Local Currency)", "%", "(Months)"
- Expand all acronyms: "Position in Range (PIR)" not "PIR", "Variable Compensation % (VC%)" not "VC%"
- Set column widths wide enough to show the full header without truncation in Excel

**A full NAME, never a definition.** The rule above is about abbreviation, and it is easy to
over-apply into a data dictionary glued to the header row:
`Career Level (Paycom pay grade; blank = not recorded)` — that is provenance and a null convention,
not the column's name, and it makes every column three times wider than its own data. The header is
what the field is called plus its unit; where the value came from and what a blank means belong in a
notes sheet or the app, read once, not carried down every filter the reader builds.
`Career Level` and `Tenure (Months)` are correct. Both halves of this matter — do not swing back to
`CL` or `Tnr`.
TR
## Excel'e Aktarma — Tam kolon başlığı adları, kısaltma yok
Excel aktarımındaki kolon başlıkları her alan için **en uzun, en açıklayıcı adı** kullanmalı.
Aktarılan dosya uygulamanın dışında tüketilir ve arayüz bağlamı olmadan kendini anlatmalıdır.
- Her kelimeyi açık yaz: "Salary LC" değil "Annual Salary (Local Currency)", "CL" değil "Career Level"
- Birimi başlığa koy: "(USD)", "(Local Currency)", "%", "(Months)"
- Bütün kısaltmaları aç: "PIR" değil "Position in Range (PIR)", "VC%" değil "Variable Compensation % (VC%)"
- Kolon genişliklerini, Excel'de başlığın kırpılmadan görüneceği kadar geniş ayarla

**Tam bir AD, asla bir tanım.** Yukarıdaki kural kısaltma hakkındadır ve başlık satırına yapıştırılmış bir veri
sözlüğüne dönüşecek kadar aşırı uygulanması kolaydır:
`Career Level (Paycom pay grade; blank = not recorded)` — bu, kolonun adı değil, kaynağı ve boş değer kuralıdır;
üstelik her kolonu kendi verisinden üç kat geniş yapar. Başlık, alanın adı artı birimidir; değerin nereden geldiği
ve boşun ne anlama geldiği bir not sayfasına ya da uygulamaya aittir, bir kez okunur, okuyucunun kurduğu her
filtrede taşınmaz.
`Career Level` ve `Tenure (Months)` doğrudur. Bunun iki yarısı da önemlidir — `CL` ya da `Tnr`'ye geri savrulma.

A label names its scope, in the fewest words that do it

Every label says what the control touches; the reader must never have to guess.

EN
## A label names its scope, in the fewest words that do it

Every label says what the control touches; the reader must never have to guess. "Search" becomes
**"search all transcripts"** — one word tells them the search is not limited to what they are reading.

- **Add the word that removes the ambiguity, and stop there.** Clarity is not length: any word that
  does not narrow what the control does comes out. A sentence of explanation belongs in the hint text
  under the control, never in its label.
- **One concept, one word, everywhere** — the same action is not "remove" on one screen and "delete"
  on the next.
- **Every button carries a tooltip saying what it does**, not only the icon-only ones. The label is
  the shortest name for the action; the tooltip is the sentence that does not fit in it, and it says
  what will happen when it is pressed ("Speed of playback" on a `1x` button, "Stops now. The lines
  already written stay, and you can continue later."). A button whose outcome is not obvious from its
  label and has no tooltip is unfinished.
TR
## Bir etiket kapsamını, bunu yapan en az kelimeyle adlandırır

Her etiket kontrolün neye dokunduğunu söyler; okuyan asla tahmin etmek zorunda kalmamalı. "Search",
**"search all transcripts"**e dönüşür — tek kelime, aramanın okuduğu şeyle sınırlı olmadığını söyler.

- **Belirsizliği kaldıran kelimeyi ekle ve orada dur.** Netlik uzunluk değildir: kontrolün ne yaptığını
  daraltmayan her kelime çıkar. Bir cümlelik açıklama, kontrolün altındaki ipucu metnine aittir, asla etiketine.
- **Tek kavram, tek kelime, her yerde** — aynı eylem bir ekranda "remove", diğerinde "delete" olmaz.
- **Her buton ne yaptığını söyleyen bir tooltip taşır**, sadece yalnız simgeli olanlar değil. Etiket, eylemin en
  kısa adıdır; tooltip ise ona sığmayan cümledir ve basıldığında ne olacağını söyler (`1x` butonunda "Oynatma hızı",
  "Şimdi durur. Zaten yazılmış satırlar kalır, sonra devam edebilirsiniz."). Sonucu etiketinden anlaşılmayan ve
  tooltip'i olmayan bir buton yarım kalmıştır.

Headings are Title Case — never Sentence case

If a heading's first word is capitalised, every significant word in it is.

EN
## Headings are Title Case — never Sentence case

If a heading's first word is capitalised, **every significant word in it is**. "Career Moves", not
"Career moves"; "Managers & Spans", not "Managers & spans"; "Full Hierarchy", not "Full hierarchy".
Applies to anything that names a thing rather than saying something: tab names, card and panel
titles, table column headers, modal titles, button labels.

Mixed casing is the actual failure mode — a tab row reading `Full hierarchy · Person Card ·
Career moves` looks like three different people built it, and that is exactly how it happens: each
label is written on the day its screen is, with nobody comparing them side by side. Pick the case
once, at the row, and the labels stay consistent as screens get added.

- **Short function words stay lowercase in the middle**: `of`, `by`, `in`, `to`, `for`, `and`, `the`,
  `a`, `or`. So "Headcount by Department", "Span of Control", "Where the Organisation Sits" — not
  "Headcount By Department". Capitalise them only as the first or last word.
- **Hyphenated compounds capitalise both halves**: "Career-Level Moves by Year".
- **Acronyms and product names keep their own casing**: `PDF`, `EUC`, `SRE`, `PIR`, `Paycom`.
- **Sentences are not headings.** Helper text, hints and empty-state copy are prose and stay in
  sentence case with a full stop. The line under a heading is prose; the heading is not.
- **Data is never re-cased.** A department called "Global Delivery - Operations and PMO" prints as it
  is stored — the rule governs labels the app writes, never values it read.
TR
## Başlıklar Title Case'dir — asla Sentence case değil

Bir başlığın ilk kelimesi büyük harfle başlıyorsa, **içindeki her anlamlı kelime de öyledir**. "Career moves"
değil "Career Moves"; "Managers & spans" değil "Managers & Spans"; "Full hierarchy" değil "Full Hierarchy".
Bir şey söylemek yerine bir şeyi adlandıran her yerde geçerlidir: sekme adları, kart ve panel başlıkları, tablo
kolon başlıkları, modal başlıkları, buton etiketleri.

Asıl arıza karışık harflemedir — `Full hierarchy · Person Card · Career moves` diye okunan bir sekme satırı, üç
farklı kişinin yaptığı gibi görünür ve tam olarak böyle olur: her etiket kendi ekranının yazıldığı gün yazılır ve
kimse onları yan yana karşılaştırmaz. Harflemeyi bir kez, satırın kendisinde seç; ekranlar eklendikçe etiketler
tutarlı kalır.

- **Kısa işlev kelimeleri ortada küçük kalır**: `of`, `by`, `in`, `to`, `for`, `and`, `the`, `a`, `or`. Yani
  "Headcount by Department", "Span of Control", "Where the Organisation Sits" — "Headcount By Department" değil.
  Onları yalnızca ilk ya da son kelimeyken büyüt.
- **Tireli bileşikler her iki yarıyı da büyütür**: "Career-Level Moves by Year".
- **Kısaltmalar ve ürün adları kendi harflemelerini korur**: `PDF`, `EUC`, `SRE`, `PIR`, `Paycom`.
- **Cümleler başlık değildir.** Yardım metni, ipuçları ve boş durum metinleri düz yazıdır; cümle harflemesinde ve
  noktayla biter. Bir başlığın altındaki satır düz yazıdır; başlık değildir.
- **Veri asla yeniden harflenmez.** "Global Delivery - Operations and PMO" adlı bir departman saklandığı gibi basılır —
  kural, uygulamanın yazdığı etiketleri yönetir, okuduğu değerleri asla.

File-type export icons — the product's own mark, greyscale until hovered

An export action is named by the file it produces, so it is drawn as that file type's own icon rather than the word "Export"…

EN
## File-type export icons — the product's own mark, greyscale until hovered

An export action is named by the **file it produces**, so it is drawn as that file type's own icon
rather than the word "Export" or a generic download arrow. People scan a toolbar for the green Excel
sheet and the red PDF badge faster than they read any label, and one download arrow cannot say which
of the two it is.

- **Icons live in `logos/`** — `excel_blue.png`, `PDF_*.png`. **Copy the file into the app's own
  `public/`; never hotlink** (same rule as the company wordmark in `login.md` §6).
- **Order: PDF on the LEFT, Excel on the RIGHT.** Fixed, so muscle memory carries between apps.
- **Greyscale at rest, full colour on hover.** `grayscale opacity-60` →
  `hover:grayscale-0 hover:opacity-100`. Product logos are saturated brand colours designed to win
  attention; at full strength a toolbar of them out-shouts the page's own data, which is the thing
  the reader actually came for. Colour on hover both calms the resting state and confirms the
  target.
- **Icon-only needs a `title` AND an `aria-label`.** Dropping the visible word is only acceptable if
  the accessible name survives; the tooltip should also say what will happen
  (*"Export this page to PDF — opens your print dialog"*).
- **Show progress in place.** A build that takes a moment swaps the icon for a spinner in the same
  button rather than moving or resizing it — a toolbar that reflows under the cursor loses the click.

### PDF export = `window.print()` plus a print stylesheet

For "export what is on screen", print the page. Do **not** reach for html2canvas/jsPDF: they
re-render into a bitmap, mangle SVG and canvas, and return a blurry screenshot instead of selectable
text — and they are two more dependencies behind the corporate proxy.

The print stylesheet is then the actual export format, so treat it as one:
- **Hide anything you could only click** — buttons, tab bars, filter chrome. A control in a PDF is a
  dead pixel. Give them a `print:hidden` class; keep branding and every piece of state (a focus chip,
  an active filter) because that is what tells a reader what they are looking at.
- **Force background printing** (`print-color-adjust: exact`) or every chart fill vanishes — the
  browser strips backgrounds by default and the data goes with them.
- **`break-inside: avoid` on cards**, or a chart lands half on each page.
- Drop shadows and tinted panels; white page, plain borders.
TR
## Dosya türü aktarma simgeleri — ürünün kendi markası, hover'a kadar gri tonlu

Bir aktarma aksiyonu **ürettiği dosyayla** adlandırılır, bu yüzden "Export" kelimesi ya da genel bir indirme oku
yerine o dosya türünün kendi simgesiyle çizilir. İnsanlar bir araç çubuğunda yeşil Excel sayfasını ve kırmızı PDF
rozetini, herhangi bir etiketi okuduklarından daha hızlı tarar ve tek bir indirme oku bunlardan hangisi olduğunu
söyleyemez.

- **Simgeler `logos/` klasöründe yaşar** — `excel_blue.png`, `PDF_*.png`. **Dosyayı uygulamanın kendi `public/`
  klasörüne kopyala; asla doğrudan bağlanma** (`login.md` §6'daki şirket kelime markasıyla aynı kural).
- **Sıra: SOLDA PDF, SAĞDA Excel.** Sabit, böylece kas hafızası uygulamalar arasında taşınır.
- **Beklemede gri tonlu, hover'da tam renkli.** `grayscale opacity-60` →
  `hover:grayscale-0 hover:opacity-100`. Ürün logoları dikkat kazanmak için tasarlanmış doygun marka renkleridir;
  tam güçteyken bunlardan oluşan bir araç çubuğu, okuyucunun asıl geldiği şey olan sayfanın kendi verisini bastırır.
  Hover'da renk, hem bekleme durumunu sakinleştirir hem hedefi teyit eder.
- **Yalnız simge, bir `title` VE bir `aria-label` ister.** Görünür kelimeyi düşürmek ancak erişilebilir ad ayakta
  kalırsa kabul edilebilir; tooltip ayrıca ne olacağını söylemeli
  (*"Bu sayfayı PDF'e aktar — yazdırma diyalogunuzu açar"*).
- **İlerlemeyi yerinde göster.** Bir an süren bir üretim, butonu oynatmak ya da yeniden boyutlandırmak yerine
  aynı butonun içinde simgeyi bir spinner ile değiştirir — imlecin altında yeniden akan bir araç çubuğu tıklamayı
  kaybeder.

### PDF aktarımı = `window.print()` artı bir yazdırma stil sayfası

"Ekranda olanı aktar" için sayfayı yazdır. html2canvas/jsPDF'e **uzanma**: bunlar bir bitmap'e yeniden çizer,
SVG ve canvas'ı bozar ve seçilebilir metin yerine bulanık bir ekran görüntüsü döner — üstelik kurumsal vekil
sunucunun arkasında iki bağımlılık daha demektir.

O zaman yazdırma stil sayfası aktarma biçiminin kendisidir, öyle davran:
- **Yalnızca tıklayabileceğin her şeyi gizle** — butonlar, sekme barları, filtre arayüzü. Bir PDF'teki kontrol ölü
  pikseldir. Onlara bir `print:hidden` sınıfı ver; markalamayı ve her durum parçasını (odak chip'i, aktif bir
  filtre) koru, çünkü okuyucuya neye baktığını söyleyen şey odur.
- **Arka plan basımını zorla** (`print-color-adjust: exact`), yoksa her grafik dolgusu kaybolur — tarayıcı
  varsayılan olarak arka planları söker ve veri de onunla gider.
- **Kartlarda `break-inside: avoid`**, yoksa bir grafik yarısı bir sayfada yarısı diğerinde kalır.
- Gölgeleri ve renk tonlu panelleri düşür; beyaz sayfa, düz kenarlıklar.

Design user flows for zero effort ("armut piş ağzıma düş")

Bring every required action to where the user already is, in the fewest steps, prefilled and automatic wherever we can.

EN
## Design user flows for zero effort ("armut piş ağzıma düş")
Bring every required action to where the user already is, in the fewest steps, prefilled and automatic wherever we can. Never make them hunt for a page, leave the product, copy-paste, or press a button we could press for them.

Count the steps the person takes, not the steps the system takes. A flow can be correct and still push work onto the reader: fetching a value, opening a second tab, copying something across. Each of those is a step the product could have taken instead.

(The global `CLAUDE.md` → UI rules carries the one-line trigger; this is its home.)
TR
## Kullanıcı akışlarını sıfır çaba için tasarla ("armut piş ağzıma düş")
Gereken her eylemi kullanıcının zaten olduğu yere, en az adımda, mümkün olan her yerde önceden doldurulmuş ve
otomatik olarak getir. Onu asla bir sayfa aramaya, üründen çıkmaya, kopyala-yapıştır yapmaya ya da onun yerine
basabileceğimiz bir butona basmaya zorlama.

Kişinin attığı adımları say, sistemin attıklarını değil. Bir akış doğru olabilir ve yine de işi okuyucuya yıkabilir:
bir değeri getirmek, ikinci bir sekme açmak, bir şeyi oradan oraya kopyalamak. Bunların her biri, ürünün onun yerine
atabileceği bir adımdır.

(Global `CLAUDE.md` → UI rules tek satırlık tetikleyiciyi taşır; burası onun evi.)

Do not make the user assemble your pipeline

Two models, two passes, two endpoints — that is your architecture, not the user's task.

EN
## Do not make the user assemble your pipeline

Two models, two passes, two endpoints — that is your architecture, not the user's task. The main
action runs the whole job and asks for everything it needs in **one panel**; the person clicking it
does not know, and must not need to know, that transcription and speaker labelling are separate
things.

- **Keep the expert control, demote it.** The button that re-runs only the second step stays for the
  case where someone wants exactly that, clearly labelled, beside the thing it affects. It is the
  shortcut, never the normal path.
- **One button, one result.** If finishing the job takes a second click somewhere else, the first
  button lied about being finished.
TR
## Kullanıcıya senin pipeline'ını kurdurma

İki model, iki geçiş, iki uç nokta — bu senin mimarin, kullanıcının görevi değil. Ana aksiyon işin tamamını
çalıştırır ve ihtiyaç duyduğu her şeyi **tek bir panelde** ister; ona tıklayan kişi, transkripsiyon ile konuşmacı
etiketlemenin ayrı şeyler olduğunu bilmez ve bilmek zorunda olmamalıdır.

- **Uzman kontrolünü koru, ama geri plana at.** Sadece ikinci adımı yeniden çalıştıran buton, tam olarak bunu
  isteyen biri için kalır; açıkça etiketlenmiş ve etkilediği şeyin yanında. O kısayoldur, asla normal yol değil.
- **Tek buton, tek sonuç.** İşi bitirmek başka bir yerde ikinci bir tıklama gerektiriyorsa, ilk buton bittiği
  konusunda yalan söylemiştir.

Let the container decide the columns

Lay items out with repeat(auto-fit, minmax(<readable width>, 1fr)) and let the container answer how many fit: side by side…

EN
## Let the container decide the columns

Lay items out with `repeat(auto-fit, minmax(<readable width>, 1fr))` and let the container answer how
many fit: side by side when there is room, stacked when there is not. Picking the column count by
hand means picking it wrong at every width you did not test.

- **No hard minimum on a block inside a flexible parent.** A `min-width` that the parent cannot honour
  does not shrink the block, it widens the page: a horizontal scrollbar on the whole document.
- **No arbitrary maximum either.** A cap chosen for a narrow case squeezes the content into a strip
  when the same component is later used in a wide one.
- **Judge it at both ends** — the narrowest container the component appears in, and the widest.

(2026-09, Voice to Text: the same quality picker first lived under a `min-width: 17rem` that pinned it
to the left half of a wide panel, then under a `minmax(20rem, 1fr)` that pushed the page into
horizontal scroll. `auto-fit` with no hard bounds solved both.)
TR
## Kolon sayısına container karar versin

Öğeleri `repeat(auto-fit, minmax(<okunabilir genişlik>, 1fr))` ile yerleştir ve kaçının sığdığını container
cevaplasın: yer varken yan yana, yokken alt alta. Kolon sayısını elle seçmek, test etmediğin her genişlikte
yanlış seçmek demektir.

- **Esnek bir ebeveynin içindeki bir bloğa sert alt sınır koyma.** Ebeveynin karşılayamadığı bir `min-width`
  bloğu küçültmez, sayfayı genişletir: bütün dokümanda yatay kaydırma çubuğu.
- **Keyfi bir üst sınır da koyma.** Dar bir durum için seçilmiş bir tavan, aynı bileşen sonradan geniş bir yerde
  kullanıldığında içeriği bir şeride sıkıştırır.
- **İki uçtan da değerlendir** — bileşenin göründüğü en dar container ve en geniş olanı.

(2026-09, Voice to Text: aynı kalite seçicisi önce onu geniş bir panelin sol yarısına çivileyen bir
`min-width: 17rem` altında yaşadı, sonra sayfayı yatay kaydırmaya iten bir `minmax(20rem, 1fr)` altında.
Sert sınırsız `auto-fit` ikisini de çözdü.)

A panel opens where the control that opened it is

An inline panel raised by a button must appear next to that button, not at the bottom of the container it belongs to, and it…

EN
## A panel opens where the control that opened it is

An inline panel raised by a button must appear **next to that button**, not at the bottom of the
container it belongs to, and it scrolls itself into view when it opens. Rendered under two thousand
lines of content, the panel is off screen, the click looks like nothing happened, and the button gets
reported as broken — it was working the whole time.

- **Judge it at the worst length**, not on the short test record: a panel placed after the content is
  fine with ten rows and invisible with two thousand.
- The reverse move is just as wrong: a panel that opens far **above** the click steals the reader's
  place. Adjacent means adjacent.

### The pointer never travels for the next step

The rule above is one case of a general one: **whatever a click raises appears where that click
happened.** A panel, a confirmation, a menu, a follow-up question, an error — the second step of an
interaction belongs beside the control that started it, so the hand that is already there can finish
without crossing the screen and coming back.

- **A confirmation is not exempt because it is modal.** Modal means the rest of the page waits, not
  that the question has to sit in the middle of the screen. Anchor the card to the control that
  raised it, keep the dimmed backdrop if you want the click-outside and the waiting, and the reader
  answers within a few pixels of where they pressed. A centred confirm makes every destructive
  action a round trip across the window.
- **Anchor it, then clamp it to the viewport.** Prefer just below the control with its near edge
  aligned to the control's; flip above when the page runs out underneath. A card pushed off screen
  by the anchoring is worse than a centred one.
- **Measure again when the page moves.** The position was taken against the page as it stood, so a
  scroll or a resize either re-measures it or closes it. Leaving it where it was points the reader
  at the wrong thing.
- **Watch the self-closing trap.** Focusing the button inside the raised card scrolls it into view,
  which fires the same scroll handler that repositions or closes the card — it shuts in the frame
  it opened. Take focus with `preventScroll`.
TR
## Bir panel, onu açan kontrolün olduğu yerde açılır

Bir butonun açtığı satır içi panel, ait olduğu container'ın en altında değil **o butonun yanında** görünmeli ve
açıldığında kendini görüş alanına kaydırmalı. İki bin satırlık içeriğin altına çizildiğinde panel ekran dışında
kalır, tıklama hiçbir şey olmamış gibi görünür ve buton bozuk diye raporlanır — oysa baştan beri çalışıyordu.

- **En kötü uzunlukta değerlendir**, kısa test kaydında değil: içerikten sonra yerleştirilmiş bir panel on satırla
  sorunsuz, iki bin satırla görünmezdir.
- Ters hamle de aynı ölçüde yanlıştır: tıklamanın çok **üstünde** açılan bir panel okuyucunun yerini çalar.
  Bitişik, bitişik demektir.

### İmleç sonraki adım için yolculuk yapmaz

Yukarıdaki kural, genel bir kuralın tek bir hâlidir: **bir tıklamanın açtığı her şey, o tıklamanın olduğu yerde
belirir.** Bir panel, bir onay, bir menü, bir takip sorusu, bir hata — bir etkileşimin ikinci adımı, onu başlatan
kontrolün yanına aittir ki zaten orada olan el, ekranı geçip geri dönmeden bitirebilsin.

- **Bir onay, modal olduğu için muaf değildir.** Modal, sayfanın geri kalanının beklediği anlamına gelir; sorunun
  ekranın ortasında durması gerektiği anlamına değil. Kartı onu açan kontrole sabitle, dışarı tıklamayı ve beklemeyi
  istiyorsan karartılmış zemini koru; okuyan, bastığı yerden birkaç piksel ötede cevap verir. Ortalanmış bir onay,
  her yıkıcı aksiyonu pencere boyunca gidiş-dönüşe çevirir.
- **Sabitle, sonra görüş alanına kıstır.** Tercihen kontrolün hemen altında, yakın kenarı kontrolünkiyle hizalı;
  altta yer kalmadığında üste çevir. Sabitleme yüzünden ekran dışına itilmiş bir kart, ortalanmış olandan kötüdür.
- **Sayfa oynadığında yeniden ölç.** Konum, sayfanın o anki hâline göre alındı; yani bir kaydırma ya da yeniden
  boyutlandırma ya yeniden ölçer ya kapatır. Onu olduğu yerde bırakmak okuyucuyu yanlış şeye işaret eder.
- **Kendi kendini kapatma tuzağına dikkat et.** Açılan kartın içindeki butona odaklanmak onu görüş alanına kaydırır,
  bu da kartı yeniden konumlandıran ya da kapatan aynı kaydırma işleyicisini ateşler — kart açıldığı karede kapanır.
  Odağı `preventScroll` ile al.

Reserve the scrollbar's place

Give the root scrollbar-gutter: stable.

EN
## Reserve the scrollbar's place

Give the root `scrollbar-gutter: stable`. The bar's width stays reserved whether or not it is showing,
so the page never jumps sideways when the content's height crosses the viewport: opening a section,
collapsing a list, filtering rows, showing a panel. Without it every one of those shifts the whole
layout by the bar's width, twice — once out, once back.

- **Not `html { overflow-y: scroll }`.** The old trick shows a dead track on pages that do not scroll.
- **Not a `100vw` width.** `100vw` includes the scrollbar, so it produces the opposite bug: a
  horizontal overflow of exactly the bar's width.
- **The same applies to an inner scroll box** whose content grows and shrinks; give it the same
  property rather than letting its own bar push the content around.
- Where scrollbars overlay the content (macOS, touch), the rule costs nothing: there is no gutter to
  reserve.
TR
## Kaydırma çubuğunun yerini ayır

Köke `scrollbar-gutter: stable` ver. Çubuğun genişliği görünsün görünmesin ayrılmış kalır, böylece içeriğin
yüksekliği görüş alanını geçtiğinde sayfa asla yana zıplamaz: bir bölüm açmak, bir listeyi kapatmak, satır
filtrelemek, bir panel göstermek. Onsuz bunların her biri bütün yerleşimi çubuğun genişliği kadar, iki kez
kaydırır — bir gidiş, bir dönüş.

- **`html { overflow-y: scroll }` değil.** Eski numara, kaydırmayan sayfalarda ölü bir ray gösterir.
- **`100vw` genişlik de değil.** `100vw` kaydırma çubuğunu içerir, yani tam tersi hatayı üretir: tam olarak
  çubuğun genişliği kadar yatay taşma.
- **Aynısı, içeriği büyüyüp küçülen bir iç kaydırma kutusu için de geçerlidir**; kendi çubuğunun içeriği itmesine
  izin vermek yerine ona da aynı özelliği ver.
- Kaydırma çubuklarının içeriğin üstüne bindiği yerlerde (macOS, dokunmatik) kural hiçbir şeye mal olmaz:
  ayrılacak bir oluk yoktur.

One row, one shape

Every interactive element on a screen shares one height and one corner radius.

EN
## One row, one shape

Every interactive element on a screen shares **one height and one corner radius**. A badge, a chip, an
icon-only button, a search box and a labelled button standing in the same row are all the same box:
different heights and mixed roundings in one line read as a broken layout, not as a hierarchy.

- **The height comes from `min-height`, never a fixed height**, so a long label can still wrap (see
  *Never give a button a fixed height*).
- **The radius is a token, not a per-element choice.** Do not mix a 999 px pill with a rounded
  rectangle in the same row.
- **A non-clickable badge takes the same box too.** What separates it from a button is its colour and
  its cursor, not a smaller size.
- **One box model, or the rule cannot hold.** Set `*, *::before, *::after { box-sizing: border-box }`.
  Without it a `<button>` is border-box while an `<a>` or a `<span>` is content-box, so the *same*
  `min-height` produces different boxes. (2026-09, Voice to Text: one `min-height: 36px` gave a
  47.6 px Export link, 38 px chips and 36 px buttons, side by side in one row.)
- **Compliance is measured, never read.** Collect every control's *rendered* height and corner radius
  and check that the set has exactly one member. Reading the CSS is not evidence: all three heights
  above came from a single, correct-looking declaration.
- **It applies to controls**, not to surfaces: buttons, icon buttons, chips, badges, input fields.
  A card, a drop zone or a panel carries its own size, but its radius still comes from the same tokens.
- **An exception is written down, or it is a violation.** A control that genuinely cannot take the
  shape — a native element wrapped in your own bar, for instance — says so in the doc, with the
  reason. An exception decided silently reads as an oversight to everyone who meets it later,
  including the person who decided it.
- The value itself is per project; what the rule fixes is that there is exactly one of it.

(2026-09, Voice to Text: a 26 px pill status chip and 29 px speaker chips sat beside 36 px buttons
with a 14 px radius, and the action row looked misaligned at every width. A later audit of the same
screen found two more: the audio player bar, left as a 999 px pill because it wraps the browser's own
control panel — an exception nobody had written down — and the 26 px icon buttons.)
TR
## Tek satır, tek biçim

Bir ekrandaki her etkileşimli öğe **tek bir yüksekliği ve tek bir köşe yarıçapını** paylaşır. Aynı satırda duran
bir badge, bir chip, yalnız simgeli bir buton, bir arama kutusu ve etiketli bir buton aynı kutudur: tek satırda
farklı yükseklikler ve karışık yuvarlamalar hiyerarşi değil, bozuk yerleşim olarak okunur.

- **Yükseklik `min-height`'tan gelir, asla sabit bir yükseklikten**, ki uzun bir etiket hâlâ alta sarabilsin
  (bkz. *Bir butona asla sabit yükseklik verme*).
- **Yarıçap bir token'dır, öğe başına bir tercih değil.** Aynı satırda 999 px'lik bir hapla yuvarlatılmış bir
  dikdörtgeni karıştırma.
- **Tıklanamayan bir badge de aynı kutuyu alır.** Onu bir butondan ayıran şey rengi ve imlecidir, daha küçük bir
  boyut değil.
- **Tek kutu modeli, yoksa kural tutmaz.** `*, *::before, *::after { box-sizing: border-box }` ayarla. Onsuz bir
  `<button>` border-box iken bir `<a>` ya da `<span>` content-box'tır, yani *aynı* `min-height` farklı kutular üretir.
  (2026-09, Voice to Text: tek bir `min-height: 36px`, yan yana tek satırda 47,6 px'lik bir Export linki, 38 px
  chip'ler ve 36 px butonlar verdi.)
- **Uyum okunmaz, ölçülür.** Her kontrolün *çizilmiş* yüksekliğini ve köşe yarıçapını topla ve kümenin tam olarak
  bir üyesi olduğunu doğrula. CSS'i okumak kanıt değildir: yukarıdaki üç yükseklik de tek ve doğru görünen bir
  bildirimden geliyordu.
- **Kontroller için geçerlidir**, yüzeyler için değil: butonlar, simge butonları, chip'ler, badge'ler, input alanları.
  Bir kart, bir bırakma alanı ya da bir panel kendi boyutunu taşır ama yarıçapı yine aynı token'lardan gelir.
- **İstisna yazılıdır, yoksa ihlaldir.** Biçimi gerçekten alamayan bir kontrol — örneğin kendi barına sarılmış yerel
  bir öğe — bunu dokümanda, gerekçesiyle söyler. Sessizce verilmiş bir istisna, sonradan karşılaşan herkese, onu
  veren kişi dahil, bir gözden kaçma olarak okunur.
- Değerin kendisi projeye göredir; kuralın sabitlediği şey, ondan tam olarak bir tane olmasıdır.

(2026-09, Voice to Text: 26 px'lik hap biçiminde bir durum chip'i ve 29 px'lik konuşmacı chip'leri, 14 px yarıçaplı
36 px butonların yanında duruyordu ve aksiyon satırı her genişlikte hizasız görünüyordu. Aynı ekranın sonraki bir
denetimi iki tane daha buldu: tarayıcının kendi kontrol panelini sardığı için 999 px hap olarak bırakılmış ses
oynatıcı barı — kimsenin yazmadığı bir istisna — ve 26 px'lik simge butonları.)

Never give a button a fixed height

Use min-height and let a long label wrap inside the button.

EN
## Never give a button a fixed height

Use `min-height` and let a long label wrap inside the button. A fixed height plus a label that grew
(translation, a longer state name) spills text outside the shape, and it looks like a bug because it
is one.

- **Keep the label short instead of stretching the button.** A requirement the user has to satisfy
  ("pick a number first") goes in the hint line next to the control, not inside the label — the
  button stays "Start" and looks unavailable (see *A blocked action must point at what is blocking
  it* for why it must still answer a press).
TR
## Bir butona asla sabit yükseklik verme

`min-height` kullan ve uzun bir etiketin butonun içinde alta sarmasına izin ver. Sabit bir yükseklik artı büyümüş
bir etiket (çeviri, daha uzun bir durum adı) metni biçimin dışına taşırır ve hata gibi görünür, çünkü hatadır.

- **Butonu esnetmek yerine etiketi kısa tut.** Kullanıcının karşılaması gereken bir şart ("önce bir sayı seçin")
  etiketin içine değil, kontrolün yanındaki ipucu satırına girer — buton "Başlat" olarak kalır ve kullanılamaz
  görünür (basıldığında neden yine de cevap vermesi gerektiği için bkz. *Engellenmiş bir aksiyon, onu neyin
  engellediğini göstermeli*).

A blocked action must point at what is blocking it

A greyed-out button says "not yet" and nothing more.

EN
## A blocked action must point at what is blocking it

A greyed-out button says "not yet" and nothing more. The reader is left hunting for the answer they
skipped, and on a long panel they may never find it. So a button blocked by a missing answer is
**never `disabled`**: a disabled button swallows the click, gives no feedback at all, and screen
readers skip past it. It looks unavailable, carries `aria-disabled`, and stays clickable — pressing
it is how the reader finds out what is missing.

Pressing it does all of this at once:

- **Bring the unanswered control into view** and give it the keyboard.
- **Say it is unanswered, twice over**: one short movement (a single brief shake) to catch the eye,
  plus a warning outline that *stays* until the answer is given. Motion alone is missed by whoever
  looked away; a static mark alone is missed on a long page.
- **Announce the reason** in the hint next to that control, as a live region (`role="alert"`), so it
  reaches a reader who cannot see the outline.

Two boundaries keep it from turning into nagging:

- **Nothing is marked before the first press.** A panel that opens already scolding the reader for
  not having answered yet is wrong.
- **The mark clears the moment the answer arrives**, and with several missing answers all are
  marked, while the scroll and the focus go to the first.

Under `prefers-reduced-motion: reduce` the shake is replaced by a single highlight, never kept as
motion. (2026-09, Voice to Text: Start sat greyed out under a speaker question the reader had not
noticed.)
TR
## Engellenmiş bir aksiyon, onu neyin engellediğini göstermeli

Soluklaştırılmış bir buton "henüz değil" der, fazlasını değil. Okuyan, atladığı cevabı aramaya bırakılır ve uzun
bir panelde onu hiç bulamayabilir. Bu yüzden eksik bir cevapla engellenmiş bir buton **asla `disabled` olmaz**:
devre dışı bir buton tıklamayı yutar, hiçbir geri bildirim vermez ve ekran okuyucular onu atlar. Kullanılamaz
görünür, `aria-disabled` taşır ve tıklanabilir kalır — ona basmak, okuyanın neyin eksik olduğunu öğrenme yoludur.

Ona basmak şunların hepsini aynı anda yapar:

- **Cevaplanmamış kontrolü görüş alanına getirir** ve klavyeyi ona verir.
- **Cevaplanmadığını iki kez söyler**: gözü yakalamak için kısa bir hareket (tek ve kısa bir sallanma) ve cevap
  verilene kadar *kalan* bir uyarı çerçevesi. Tek başına hareketi, başka yere bakan kaçırır; tek başına durağan bir
  işareti, uzun bir sayfada kaçıran olur.
- **Sebebi duyurur**, o kontrolün yanındaki ipucunda, canlı bölge olarak (`role="alert"`), böylece çerçeveyi
  göremeyen okuyucuya da ulaşır.

İki sınır, bunun dırdıra dönüşmesini engeller:

- **İlk basıştan önce hiçbir şey işaretlenmez.** Daha cevaplamadığı için okuyucuyu baştan azarlayarak açılan bir
  panel yanlıştır.
- **Cevap gelir gelmez işaret kalkar** ve birden fazla eksik cevap varsa hepsi işaretlenir, kaydırma ve odak ise
  ilkine gider.

`prefers-reduced-motion: reduce` altında sallanma tek bir vurguyla değiştirilir, asla hareket olarak korunmaz.
(2026-09, Voice to Text: Başlat butonu, okuyucunun fark etmediği bir konuşmacı sorusunun altında soluk duruyordu.)

A partial result says what it is missing

A half-finished result looks exactly like a finished one.

EN
## A partial result says what it is missing

A half-finished result looks exactly like a finished one. Nothing in a transcript that stops at
minute 12, or in a table whose last column was never computed, says so — the reader takes what is
there as the whole thing, and the silence is what misleads them.

- **Name the missing part where the result is shown**, in words, not as an absence: "the first 12:59
  of 1:01:53 is kept", "speaker names were not detected for it".
- **Every export carries the same sentence.** A file leaves the screen that explained it; mark it
  partial in its own header, with the point it reached.
- **Put the action that completes it next to the sentence**, already filled in with what the user
  chose earlier. Telling someone what is missing without offering to finish it is only half the job.
- **Only a gap at the END counts as missing.** Gaps in the middle or at the start are often normal
  output (a line no speaker turn overlapped), and reporting them as failures teaches the reader to
  ignore the notice.

(2026-09, Voice to Text: a transcript stopped early carried no speaker labels at all, and nothing on
the card said so — it read as "this recording has one unnamed speaker".)
TR
## Kısmi bir sonuç neyin eksik olduğunu söyler

Yarım kalmış bir sonuç, tamamlanmış olandan birebir aynı görünür. 12. dakikada duran bir transkriptte ya da son
kolonu hiç hesaplanmamış bir tabloda bunu söyleyen hiçbir şey yoktur — okuyan orada olanı bütün sanır ve onu
yanıltan şey sessizliktir.

- **Eksik parçayı sonucun gösterildiği yerde adlandır**, yokluk olarak değil kelimelerle: "1:01:53'ün ilk 12:59'u
  tutuldu", "onun için konuşmacı adları tespit edilemedi".
- **Her aktarma aynı cümleyi taşır.** Bir dosya, onu açıklayan ekrandan ayrılır; kendi başlığında, ulaştığı noktayla
  birlikte kısmi olduğunu işaretle.
- **Onu tamamlayan aksiyonu cümlenin yanına koy**, kullanıcının daha önce seçtikleriyle önceden doldurulmuş olarak.
  Birine neyin eksik olduğunu söyleyip bitirmeyi önermemek işin sadece yarısıdır.
- **Sadece SONDAKİ boşluk eksik sayılır.** Ortadaki ya da baştaki boşluklar çoğu zaman normal çıktıdır (hiçbir
  konuşmacı sırasının çakışmadığı bir satır) ve onları hata diye raporlamak okuyucuya bildirimi yok saymayı öğretir.

(2026-09, Voice to Text: erken duran bir transkript hiç konuşmacı etiketi taşımıyordu ve kartta bunu söyleyen bir
şey yoktu — "bu kaydın adsız tek bir konuşmacısı var" gibi okundu.)

Name what the user sees by its outcome, and show its real cost

Technical names leak the implementation and leave the user guessing: "Small / Medium / Large v3 Turbo" tells a person nothing…

EN
## Name what the user sees by its outcome, and show its real cost

Technical names leak the implementation and leave the user guessing: "Small / Medium / Large v3
Turbo" tells a person nothing about which one their recording needs. Name each option for the
**situation it is for**, and put the price beside it.

- **Outcome, not mechanism.** This covers **every name the user sees** — options, status badges,
  progress lines, toasts, empty states — not only the items in a picker. Name what the person is
  living through, never the step the code is on: *Quick draft — one clear voice in a quiet room* ·
  *Best quality — noisy room, accents, technical words*; a job that is warming up reads **Working on
  It**, not *Loading Model*. A status label is never a one-to-one translation of an internal state
  (`loading`, `processing`): those names stay in the code and never reach the screen.
- **Quantify the cost from measurement, in the unit the user feels** — minutes for this file, not a
  parameter count, and computed for the actual input when it is known.
- **Drop the option that is worse on every axis.** If one choice is both slower and less accurate, it
  is not a trade-off, it is a wrong answer kept for old data.
- **The same input gives the same number everywhere.** When a factor in the estimate has not been
  chosen yet, assume the path that will actually run, not zero: an unanswered question is not a free
  step. (2026-09: one panel quoted 46 minutes and another 53 for the same file, because the first had
  not yet been told how many speakers there were and silently priced the second pass at nothing.)
TR
## Kullanıcının gördüğü şeyi sonucuyla adlandır ve gerçek maliyetini göster

Teknik adlar uygulamayı sızdırır ve kullanıcıyı tahmine bırakır: "Small / Medium / Large v3 Turbo" bir insana
kaydının hangisine ihtiyacı olduğu konusunda hiçbir şey söylemez. Her seçeneği **ne için olduğu duruma göre**
adlandır ve yanına fiyatını koy.

- **Mekanizma değil sonuç.** Bu, yalnızca bir seçicideki maddeleri değil **kullanıcının gördüğü her adı** kapsar —
  seçenekler, durum badge'leri, ilerleme satırları, toast'lar, boş durumlar. Kodun hangi adımda olduğunu değil,
  kişinin neyi yaşadığını adlandır: *Hızlı taslak — sessiz bir odada tek net ses* · *En iyi kalite — gürültülü oda,
  aksanlar, teknik kelimeler*; ısınmakta olan bir iş *Model Yükleniyor* değil **Üzerinde Çalışıyoruz** diye okunur.
  Bir durum etiketi, dahilî bir durumun birebir çevirisi asla değildir (`loading`, `processing`): o adlar kodda kalır
  ve ekrana hiç çıkmaz.
- **Maliyeti ölçümden, kullanıcının hissettiği birimle nicelendir** — parametre sayısı değil, bu dosya için dakika;
  ve bilindiğinde gerçek girdi için hesaplanmış olarak.
- **Her eksende daha kötü olan seçeneği kaldır.** Bir seçim hem yavaş hem daha az doğruysa, o bir denge değil, eski
  veri için tutulmuş yanlış bir cevaptır.
- **Aynı girdi her yerde aynı sayıyı verir.** Tahmindeki bir etken henüz seçilmemişse, sıfırı değil gerçekten
  çalışacak yolu varsay: cevaplanmamış bir soru bedava bir adım değildir. (2026-09: aynı dosya için bir panel 46,
  diğeri 53 dakika söylüyordu, çünkü ilkine kaç konuşmacı olduğu henüz söylenmemişti ve ikinci geçişi sessizce
  sıfır fiyatlamıştı.)

A warning that is not checked against reality becomes a lie

"Downloads 1.6 GB the first time" is correct once and wrong forever after.

EN
## A warning that is not checked against reality becomes a lie

"Downloads 1.6 GB the first time" is correct once and wrong forever after. Any warning about a
one-off cost has to be **conditional on the current state** — check whether the download already
happened and say nothing when it did. A UI that keeps warning about things that cannot happen trains
the reader to skip every warning, including the ones that matter.
TR
## Gerçeğe karşı kontrol edilmeyen bir uyarı yalana dönüşür

"İlk seferde 1,6 GB indirir" bir kez doğrudur ve sonsuza kadar yanlış. Tek seferlik bir maliyetle ilgili her uyarı
**mevcut duruma bağlı** olmalı — indirmenin zaten olup olmadığını kontrol et ve olduysa hiçbir şey söyleme.
Olamayacak şeyler hakkında uyarmaya devam eden bir arayüz, okuyucuya önemli olanlar dahil her uyarıyı atlamayı
öğretir.

Two instances of the same control share one shell

The same job in two places — search inside this record, search across all of them — is one component with two scopes, not two…

EN
## Two instances of the same control share one shell

The same job in two places — search inside this record, search across all of them — is one component
with two scopes, not two designs. Same height, same radius, same border, same shadow; only the label
names the difference. When the two drift apart, a reader sees two different things and stops
believing the smaller one does what the bigger one does.
TR
## Aynı kontrolün iki örneği tek bir kabuğu paylaşır

Aynı iş iki yerde — bu kaydın içinde ara, hepsinin içinde ara — iki tasarım değil, iki kapsamı olan tek bir
bileşendir. Aynı yükseklik, aynı yarıçap, aynı kenarlık, aynı gölge; farkı yalnızca etiket adlandırır. İkisi
birbirinden uzaklaştığında okuyan iki farklı şey görür ve küçük olanın büyük olanın yaptığını yaptığına inanmayı
bırakır.

The control that starts playback stops it

MOVED to audio-player.md — it owns every player rule.

EN
## The control that starts playback stops it
MOVED to [`audio-player.md`](./audio-player.md) — it owns every player rule. See *A transcript beside a
player is one instrument* there.
TR
## Oynatmayı başlatan kontrol onu durdurur
[`audio-player.md`](./audio-player.md) dosyasına TAŞINDI — bütün oynatıcı kuralları o dosyaya aittir. Orada
*A transcript beside a player is one instrument* bölümüne bak.

A translucent surface invalidates measured contrast

Contrast is a property of the composite, not of the two tokens you chose.

EN
## A translucent surface invalidates measured contrast

Contrast is a property of the **composite**, not of the two tokens you chose. Over glass, blur or any
semi-transparent fill, text sits on whatever shows through, so a colour that passes on the nominal
background can fail on the real one. Measure the rendered result; if you cannot, treat the effective
background as the worst case the surface can produce.

(2026-09: a 12px timestamp colour that measured 4.5:1 on white measured **4.2:1** over a 58%-opaque
card, under the floor. Fixed by darkening the token, not by removing the effect.)
TR
## Yarı saydam bir yüzey ölçülmüş kontrastı geçersiz kılar

Kontrast, seçtiğin iki token'ın değil, **bileşiğin** bir özelliğidir. Cam, bulanıklık ya da yarı saydam herhangi
bir dolgunun üstünde metin, arkasından görünen şeyin üstüne oturur; yani nominal arka planda geçen bir renk gerçek
olanda kalabilir. Çizilen sonucu ölç; ölçemiyorsan, etkin arka planı yüzeyin üretebileceği en kötü durum olarak kabul et.

(2026-09: beyaz üzerinde 4,5:1 ölçen 12px'lik bir zaman damgası rengi, %58 opak bir kartın üstünde **4,2:1** ölçtü,
yani tabanın altında. Efekt kaldırılarak değil, token koyulaştırılarak düzeltildi.)

A theme is a token swap, not a rewrite

If every colour, radius, shadow and surface decision lives in one token block, changing the whole visual language is editing…

EN
## A theme is a token swap, not a rewrite

If every colour, radius, shadow and surface decision lives in one token block, changing the whole
visual language is editing that block plus a handful of surface rules. If changing the theme means
touching components, the tokens were never doing their job.

(2026-09, Voice to Text: glassmorphism to claymorphism was one token block, one surface section and
the two fonts — the layout, the components and the behaviour were untouched.)
TR
## Tema bir token takasıdır, yeniden yazım değil

Her renk, yarıçap, gölge ve yüzey kararı tek bir token bloğunda yaşıyorsa, görsel dilin tamamını değiştirmek o bloğu
artı bir avuç yüzey kuralını düzenlemektir. Temayı değiştirmek bileşenlere dokunmak demekse, token'lar zaten işini
hiç yapmıyordu.

(2026-09, Voice to Text: glassmorphism'den claymorphism'e geçiş tek bir token bloğu, tek bir yüzey bölümü ve iki
fonttu — yerleşim, bileşenler ve davranış hiç ellenmedi.)

A range selection draws its own outline

When a user drags to select a range of items — rows, lines, cells, days on a calendar — draw a rounded rectangle from the…

EN
## A range selection draws its own outline

When a user drags to select a range of items — rows, lines, cells, days on a calendar — draw a
**rounded rectangle from the point they pressed, following the pointer**, and light up the items it
covers. Without it they are guessing where the range starts and ends, and they only find out what
they picked after the destructive action.

- **The rectangle starts where the pointer went down and tracks it in every direction.** It is the
  user's own gesture drawn back to them, so it must not be snapped to item boundaries or redrawn
  somewhere tidier; the moment of the press is when it has to appear.
- **It may start anywhere**, including empty space beside the list. Requiring the drag to begin on an
  item makes the gesture fail exactly when the user aims at the gap between two of them.
- **Highlight the covered items live, not just the box.** The rectangle says where the gesture is;
  the highlight says what it takes, and an item counts as soon as the box reaches any part of it.
- **Translucent fill, no pointer events.** It frames the content, it must not hide it or swallow
  clicks on what it covers.
- **Both stay while the action is offered.** If releasing the drag raises a control ("3 selected ·
  Hide"), the outline and the highlight remain until the selection is cleared, so the control and
  what is marked always describe the same set. `Esc`, a click outside, or anything that moves the
  content under a viewport-anchored box clears all of it together.
- **Where dragging already means something — selecting text — put the box behind a modifier**
  (`Shift` + drag) rather than taking the plain drag away. Losing text selection to gain a marquee is
  a bad trade; both fit if one of them is qualified.
TR
## Aralık seçimi kendi çerçevesini çizer

Kullanıcı bir öğe aralığını sürükleyerek seçtiğinde — satırlar, çizgiler, hücreler, takvimdeki günler —
**bastığı noktadan başlayan, imleci takip eden yuvarlak köşeli bir dikdörtgen** çiz ve kapsadığı öğeleri yak.
Onsuz, aralığın nerede başlayıp nerede bittiğini tahmin ederler ve ne seçtiklerini ancak yıkıcı aksiyondan sonra
öğrenirler.

- **Dikdörtgen imlecin indiği yerde başlar ve onu her yöne takip eder.** Bu, kullanıcının kendi hareketinin ona geri
  çizilmiş hâlidir, yani öğe sınırlarına kilitlenmemeli ya da daha derli toplu bir yere yeniden çizilmemeli;
  görünmesi gereken an, basma anıdır.
- **Her yerden başlayabilir**, listenin yanındaki boşluk dahil. Sürüklemenin bir öğenin üstünde başlamasını şart
  koşmak, kullanıcı tam da ikisinin arasındaki boşluğu hedeflediğinde hareketi bozar.
- **Kapsanan öğeleri canlı vurgula, sadece kutuyu değil.** Dikdörtgen hareketin nerede olduğunu söyler; vurgu neyi
  aldığını söyler ve kutu bir öğenin herhangi bir yerine değdiği anda o öğe sayılır.
- **Yarı saydam dolgu, imleç olayı yok.** İçeriği çerçeveler; onu gizlememeli ya da kapsadığı şeye yapılan
  tıklamaları yutmamalı.
- **Aksiyon sunulduğu sürece ikisi de kalır.** Sürüklemeyi bırakmak bir kontrol açıyorsa ("3 seçili · Gizle"),
  seçim temizlenene kadar çerçeve ve vurgu kalır, böylece kontrol ile işaretli olan her zaman aynı kümeyi anlatır.
  `Esc`, dışarı bir tıklama ya da görüş alanına sabitlenmiş bir kutunun altındaki içeriği oynatan her şey hepsini
  birlikte temizler.
- **Sürüklemenin zaten bir anlamı olduğu yerde — metin seçmek — kutuyu bir değiştiriciye bağla** (`Shift` + sürükle),
  düz sürüklemeyi elinden almak yerine. Bir seçim dikdörtgeni kazanmak için metin seçimini kaybetmek kötü bir
  takastır; biri şarta bağlanırsa ikisi de sığar.

A notice about one row belongs on that row

When an action applies to a single row, card or cell, its result — and the undo that goes with it — renders inside or…

EN
## A notice about one row belongs on that row

When an action applies to a single row, card or cell, its result — and the undo that goes with it —
renders **inside or immediately beside that element**, not in the screen-corner toast. The user's eyes
are on the thing they just changed; a message at the bottom of the window is outside their attention
and gets missed, and the undo inside it is missed with it.

- **The bottom toast keeps the page-level messages** — saved, deleted, exported, copied, a rename that
  affects the whole record. Both coexist; they are not alternatives.
- **The notice carries its action**, and while it does it stays longer than a plain confirmation
  (an undo nobody can reach is not an undo), and it always has an explicit dismiss control.
- **One at a time.** A second notice replaces the first, so the page never accumulates stale bubbles.
- **Float it over the content, anchored to the row; never insert it into the flow.** A notice pushed
  in between rows shifts everything below it, so the list jumps twice — once when it appears and again
  when it goes — and the reader loses the line they were looking at. Render it as a popover just under
  the element with a caret pointing at it, over the neighbouring rows. Position it inside the scrolling
  content so it travels with its row, and outside any clipped container so it is never cut off.
- **Dismiss it like any popover:** a click anywhere outside it, or `Esc`. Clicks inside it must keep
  working, or the undo it exists for becomes unreachable. Anything that changes the layout it was
  measured against (expanding, collapsing, a reflow) closes it too, since its position is now a lie.
TR
## Tek bir satırla ilgili bildirim o satırın üstüne aittir

Bir aksiyon tek bir satıra, karta ya da hücreye uygulandığında, sonucu — ve yanındaki geri alma — ekranın
köşesindeki toast'ta değil, **o öğenin içinde ya da hemen yanında** çizilir. Kullanıcının gözü az önce
değiştirdiği şeyin üstündedir; pencerenin altındaki bir mesaj onun dikkatinin dışındadır ve kaçırılır, içindeki
geri alma da onunla birlikte kaçırılır.

- **Alttaki toast, sayfa seviyesindeki mesajları tutar** — kaydedildi, silindi, aktarıldı, kopyalandı, kaydın
  tamamını etkileyen bir yeniden adlandırma. İkisi bir arada yaşar; alternatif değildirler.
- **Bildirim kendi aksiyonunu taşır** ve taşıdığı sürece düz bir onaydan uzun kalır (kimsenin ulaşamadığı bir geri
  alma, geri alma değildir) ve her zaman açık bir kapatma kontrolü bulunur.
- **Aynı anda bir tane.** İkinci bir bildirim birincinin yerini alır, böylece sayfa asla bayat balonlar biriktirmez.
- **İçeriğin üstünde, satıra sabitlenmiş olarak yüzdür; akışa asla sokma.** Satırların arasına sokulan bir bildirim
  altındaki her şeyi kaydırır, yani liste iki kez zıplar — belirdiğinde ve gittiğinde — ve okuyan baktığı satırı
  kaybeder. Onu öğenin hemen altında, ona işaret eden bir okla, komşu satırların üstünde bir popover olarak çiz.
  Satırıyla birlikte hareket etsin diye kaydırılan içeriğin içine, asla kırpılmasın diye kırpılmış her container'ın
  dışına konumlandır.
- **Her popover gibi kapat:** dışarıda herhangi bir yere tıklama ya da `Esc`. İçine yapılan tıklamalar çalışmaya
  devam etmeli, yoksa var olma sebebi olan geri alma ulaşılamaz hâle gelir. Ölçüldüğü yerleşimi değiştiren her şey
  (açılma, kapanma, yeniden akış) onu da kapatır, çünkü konumu artık bir yalandır.

Delete is the trash can, not the word

A control that deletes a record, a row or a card is drawn as the trash icon alone — no "Delete" label beside it.

EN
## Delete is the trash can, not the word

A control that deletes a record, a row or a card is drawn as the **trash icon alone** — no "Delete"
label beside it. The icon is the one destructive mark every user already knows, and a worded button
sitting among labelled actions (Export, Copy, Re-run) reads as just another one of them.

- **Icon only, and it must still have a name**: `title` **and** `aria-label` saying what is deleted
  ("Delete recording"), per the icon-only rule in *File-type export icons* above.
- **Carry the danger in the colour**, not in extra words: the icon takes the error colour, and its
  hover state tints the button.
- **The confirmation keeps its words.** The second step spells out what is about to be lost
  ("Delete this recording and its transcript?") with a worded **Yes, Delete** — that is the moment a
  reader must be told, and an icon cannot say it. The two-step inline confirm itself (never a modal,
  never `confirm()`) is specified in *Data Entry Tables → Delete confirmation*.
- This is about the **control**, not the menu: a text item inside an overflow/context menu still
  reads "Delete".
TR
## Silme çöp kutusudur, kelime değil

Bir kaydı, satırı ya da kartı silen kontrol **sadece çöp kutusu simgesiyle** çizilir — yanında "Sil" etiketi
olmaz. Simge, her kullanıcının zaten bildiği tek yıkıcı işarettir; etiketli aksiyonların (Export, Copy, Re-run)
arasında duran kelimeli bir buton ise onlardan biri gibi okunur.

- **Yalnız simge, ama yine de bir adı olmalı**: neyin silindiğini söyleyen `title` **ve** `aria-label`
  ("Kaydı sil"), yukarıdaki *Dosya türü aktarma simgeleri* bölümündeki yalnız-simge kuralına göre.
- **Tehlikeyi renkte taşı**, fazladan kelimede değil: simge hata rengini alır ve hover durumu butonu renklendirir.
- **Onay kelimelerini korur.** İkinci adım neyin kaybedilmek üzere olduğunu açık açık yazar ("Bu kaydı ve
  transkriptini sil?") ve kelimeli bir **Yes, Delete** taşır — okuyana söylenmesi gereken an odur ve bir simge bunu
  söyleyemez. İki adımlı satır içi onayın kendisi (asla modal, asla `confirm()`) *Veri Giriş Tabloları → Silme onayı*
  bölümünde tanımlıdır.
- Bu, **kontrol** hakkındadır, menü hakkında değil: bir taşma/bağlam menüsündeki metin öğesi yine "Delete" diye okunur.

A record's own actions ride in its frozen header

On a screen built around one long record — a transcript, a document, a log — that record's actions (export, copy, re-run,…

EN
## A record's own actions ride in its frozen header

On a screen built around one long record — a transcript, a document, a log — that record's actions
(export, copy, re-run, delete) belong in the **header that freezes under the top bar**, beside its
title and status. Actions parked at the bottom of the card are reachable only by scrolling past the
whole body, which is exactly what the frozen header exists to prevent.

- **The frozen header holds what you need while reading**: title and identity line, the record's own
  controls (a player, a search box), and its actions. Everything else stays in the body.
- **Keep it short.** It is a header, not a panel: roughly three rows. If it needs a fourth, something
  in it belongs to the body.
- **The title never gets squeezed to fit the buttons.** Let the row wrap so the actions drop to their
  own line (give the title block a flex-basis of about 20rem); a title breaking one letter per line
  is the tell that the actions took the space.
- **Opaque background, and the separator appears only once stuck** — a transparent sticky header lets
  the body bleed through it. Detect the stuck state with a 1px sentinel above the header plus an
  `IntersectionObserver`, then add the border/shadow.
- **That opaque header also covers the card's own side lines, so it draws them itself**
  (`inset 1px 0 0 <border>` and `inset -1px 0 0 <border>`), **in both states, stuck and not.** The
  rule is general: any opaque layer spanning a container's full width covers that container's edge
  and has to continue it. It is unavoidable when the card's outline is an inset shadow rather than a
  real border, which is how a pinned header gets to own the rounded top corners. Check it by
  following the card's left edge from the top of the card to the bottom, once at rest and once
  scrolled: it is one unbroken line or it is wrong.
- Data tables freeze only their column header instead; see *Data-table header freezes on page scroll*.
TR
## Bir kaydın kendi aksiyonları donmuş başlığında durur

Tek bir uzun kaydın etrafında kurulmuş bir ekranda — bir transkript, bir doküman, bir log — o kaydın aksiyonları
(aktar, kopyala, yeniden çalıştır, sil) **üst barın altında donan başlığa**, başlığının ve durumunun yanına aittir.
Kartın altına park edilmiş aksiyonlara ancak bütün gövdeyi geçerek kaydırıp ulaşılır, ki donmuş başlığın var olma
sebebi tam olarak bunu engellemektir.

- **Donmuş başlık, okurken ihtiyacın olanı tutar**: başlık ve kimlik satırı, kaydın kendi kontrolleri (bir oynatıcı,
  bir arama kutusu) ve aksiyonları. Diğer her şey gövdede kalır.
- **Kısa tut.** O bir başlıktır, panel değil: kabaca üç satır. Dördüncüye ihtiyaç duyuyorsa, içindeki bir şey gövdeye
  aittir.
- **Başlık, butonlara yer açmak için asla sıkıştırılmaz.** Satırın sarmasına izin ver ki aksiyonlar kendi satırlarına
  insin (başlık bloğuna yaklaşık 20rem'lik bir flex-basis ver); satır başına bir harf düşecek şekilde kırılan bir
  başlık, aksiyonların yeri aldığının işaretidir.
- **Opak arka plan, ve ayırıcı ancak yapıştığında belirir** — saydam bir sticky başlık gövdenin içinden geçmesine
  izin verir. Yapışmış durumu başlığın üstündeki 1px'lik bir gözcü ve bir `IntersectionObserver` ile tespit et,
  sonra kenarlığı/gölgeyi ekle.
- **O opak başlık kartın kendi yan çizgilerini de kapatır, bu yüzden onları kendisi çizer**
  (`inset 1px 0 0 <border>` ve `inset -1px 0 0 <border>`), **her iki durumda da, yapışmışken ve değilken.** Kural
  geneldir: bir container'ın tam genişliğini kaplayan her opak katman o container'ın kenarını kapatır ve onu
  sürdürmek zorundadır. Kartın çerçevesi gerçek bir kenarlık yerine bir iç gölgeyse bu kaçınılmazdır, ki sabitlenmiş
  bir başlığın yuvarlak üst köşelere sahip olma yolu da budur. Kartın sol kenarını kartın üstünden altına kadar,
  biri beklemede biri kaydırılmış olmak üzere iki kez takip ederek kontrol et: ya tek ve kesintisiz bir çizgidir ya
  da yanlıştır.
- Veri tabloları bunun yerine sadece kolon başlığını dondurur; bkz. *Veri tablosu başlığı sayfa kaydırılırken donar*.

Date columns sort DESCENDING — newest first, always

Any table with a date/time column (a date, a year, a month, a period) shows the newest row first.

EN
## Date columns sort DESCENDING — newest first, always

Any table with a date/time column (a date, a year, a month, a period) shows the **newest row first**.
Nobody opens a table of dates to read the oldest one; the current period is what the reader came for,
and an ascending list buries it at the bottom, off-screen on a long table.

- **Default sort:** if a date column is the table's natural ordering, it is the default sort and it
  is **descending**. This outranks the "sort by the primary measure descending" default — a date
  column wins over a measure when both are present and the table is a chronology.
- **First click on a date header sorts descending too** (then ascending on the second click), like a
  measure and unlike a text column, which starts A–Z.
- **Year and month columns count as dates.** `2026, 2025, 2024…`, not `2022, 2023, 2024…`.
- **The paired CHART stays chronological** (oldest → newest, left to right). A time series read
  right-to-left is unreadable, so the table and its chart deliberately run in opposite directions —
  that is correct, not an inconsistency to "fix".
- Exception: a table that is explicitly a forward-looking schedule (upcoming deadlines, planned
  releases) sorts **ascending**, because there the nearest future date is the one that matters.
TR
## Tarih kolonları AZALAN sıralanır — her zaman en yeni önce

Tarih/saat kolonu olan her tablo **en yeni satırı en üstte** gösterir. Kimse bir tarih tablosunu en eskisini
okumak için açmaz; okuyanın geldiği şey mevcut dönemdir ve artan bir liste onu en dibe gömer, uzun bir tabloda
ekranın dışına.

- **Varsayılan sıralama:** bir tarih kolonu tablonun doğal sıralamasıysa, varsayılan sıralama odur ve **azalandır**.
  Bu, "birincil ölçüye göre azalan sırala" varsayılanını yener — ikisi de varsa ve tablo bir kronolojiyse tarih
  kolonu ölçüyü yener.
- **Bir tarih başlığına ilk tıklama da azalan sıralar** (ikinci tıklamada artan), bir ölçü gibi ve A–Z ile başlayan
  bir metin kolonunun aksine.
- **Yıl ve ay kolonları tarih sayılır.** `2026, 2025, 2024…`, `2022, 2023, 2024…` değil.
- **Eşlenik GRAFİK kronolojik kalır** (eskiden yeniye, soldan sağa). Sağdan sola okunan bir zaman serisi okunamaz,
  bu yüzden tablo ve grafiği bilerek ters yönlerde akar — bu doğrudur, "düzeltilecek" bir tutarsızlık değil.
- İstisna: açıkça ileriye dönük bir takvim olan bir tablo (yaklaşan son tarihler, planlanmış sürümler) **artan**
  sıralanır, çünkü orada önemli olan en yakın gelecek tarihtir.

Numeric table columns — always right-aligned

Any table column whose content is a number (currency, percentage, count, ratio, score, etc.) must be right-aligned in both the…

EN
## Numeric table columns — always right-aligned
Any table column whose content is a number (currency, percentage, count, ratio, score, etc.)
must be **right-aligned in both the header and every data cell**. Never use `text-center` for
a numeric column header while the cells are `text-right` — header and data must match.

- Header: `text-right` (e.g. `<th className="... text-right">` or `align="right"` on SortTh)
- Cell: `text-right font-mono` (monospace keeps digit widths consistent for scanning)
- Left-aligned columns: text labels only (names, descriptions, categories)
- Exception: a lone icon or badge that isn't a scannable number may be centered
TR
## Sayısal tablo kolonları — her zaman sağa hizalı
İçeriği sayı olan her tablo kolonu (para, yüzde, adet, oran, skor vb.) **hem başlıkta hem her veri hücresinde
sağa hizalanmalı**. Hücreler `text-right` iken sayısal bir kolon başlığında asla `text-center` kullanma —
başlık ve veri uyuşmalı.

- Başlık: `text-right` (ör. `<th className="... text-right">` ya da SortTh üzerinde `align="right"`)
- Hücre: `text-right font-mono` (mono, taramak için rakam genişliklerini tutarlı tutar)
- Sola hizalı kolonlar: sadece metin etiketleri (adlar, açıklamalar, kategoriler)
- İstisna: taranabilir bir sayı olmayan tek bir simge ya da badge ortalanabilir

Stacked multi-field table cells

When a table column contains multiple related fields for the same entity, stack them vertically inside a single <td> instead…

EN
## Stacked multi-field table cells
When a table column contains multiple related fields for the same entity, stack them
vertically inside a single `<td>` instead of creating a separate column per field.
This frees horizontal space for more important data columns.

**When to apply:**
- Person/employee columns: name + email + title + code + country in one cell
- Org-unit columns: department + sub-department + team in one cell
- Any set of ≥ 2 tightly-related fields that belong to the same entity

**Implementation pattern (Tailwind):**
```tsx
<td className="px-2 py-1.5">
  <div className="flex flex-col gap-px">
    {/* Primary field */}
    <span className="text-[11px] font-medium text-text-primary truncate leading-[1.3]">{primary}</span>
    {/* Secondary fields */}
    <span className="text-[9px] text-text-muted font-mono truncate leading-[1.3]">{secondary}</span>
    {/* Inline grouped items on the same row (e.g. code + country) */}
    <div className="flex items-center gap-1.5 leading-[1.3]">
      <span className="text-[9px] text-text-muted font-mono">{code}</span>
      <span className="text-[9px] text-text-muted font-mono">{country}</span>
    </div>
  </div>
</td>
```

**Typography scale within a stacked cell:**
- Primary field: `text-[11px] font-medium text-text-primary`
- Secondary fields: `text-[9px] text-text-muted` (add `font-mono` for codes/IDs)
- Line-height: `leading-[1.3]` throughout to keep rows compact
- Spacing between lines: `gap-px` in the flex column container

**Sticky + opaque background:** if the stacked cell is `position: sticky`, its background
must be fully opaque (`bg-bg-surface group-hover:bg-bg-elevated`, no `/opacity` suffix)
to prevent scrolled content from bleeding through.
TR
## Çok alanlı, üst üste tablo hücreleri
Bir tablo kolonu aynı varlığa ait birden fazla ilişkili alan içeriyorsa, alan başına ayrı bir kolon açmak yerine
onları tek bir `<td>` içinde dikey olarak üst üste diz. Bu, daha önemli veri kolonlarına yatay yer açar.

**Ne zaman uygulanır:**
- Kişi/çalışan kolonları: tek hücrede ad + e-posta + unvan + kod + ülke
- Organizasyon birimi kolonları: tek hücrede departman + alt departman + ekip
- Aynı varlığa ait, sıkıca ilişkili ≥ 2 alandan oluşan her küme

**Uygulama kalıbı (Tailwind):**
```tsx
<td className="px-2 py-1.5">
  <div className="flex flex-col gap-px">
    {/* Primary field */}
    <span className="text-[11px] font-medium text-text-primary truncate leading-[1.3]">{primary}</span>
    {/* Secondary fields */}
    <span className="text-[9px] text-text-muted font-mono truncate leading-[1.3]">{secondary}</span>
    {/* Inline grouped items on the same row (e.g. code + country) */}
    <div className="flex items-center gap-1.5 leading-[1.3]">
      <span className="text-[9px] text-text-muted font-mono">{code}</span>
      <span className="text-[9px] text-text-muted font-mono">{country}</span>
    </div>
  </div>
</td>
```

**Üst üste hücre içindeki tipografi ölçeği:**
- Birincil alan: `text-[11px] font-medium text-text-primary`
- İkincil alanlar: `text-[9px] text-text-muted` (kod/ID'ler için `font-mono` ekle)
- Satır yüksekliği: satırları sıkı tutmak için baştan sona `leading-[1.3]`
- Satır arası boşluk: flex kolon container'ında `gap-px`

**Sticky + opak arka plan:** üst üste dizilmiş hücre `position: sticky` ise, arka planı tamamen opak olmalı
(`bg-bg-surface group-hover:bg-bg-elevated`, `/opacity` eki yok) ki kaydırılan içerik içinden geçmesin.

Employee/people search boxes (typeahead)

MOVED to design-filters.md (2026-08-25, owner approval) — design-filters.md owns filter, search and lookup mechanics.

EN
## Employee/people search boxes (typeahead)
MOVED to [`design-filters.md`](./design-filters.md) (2026-08-25, owner approval) — `design-filters.md` owns filter,
search and lookup mechanics. See the section of the same name there.
TR
## Çalışan/kişi arama kutuları (typeahead)
[`design-filters.md`](./design-filters.md) dosyasına TAŞINDI (2026-08-25, sahip onayı) — filtre, arama ve lookup
mekaniği `design-filters.md` dosyasına aittir. Orada aynı adlı bölüme bak.

Person identifiers — title-case the dot-separated name

When a person is shown by a dot-separated identifier — a full email, a username, or a dotted name — title-case only the name…

EN
## Person identifiers — title-case the dot-separated name
When a person is shown by a dot-separated identifier — a full **email**, a **username**, or a dotted
**name** — title-case **only the name part** (the local part before `@`), capitalizing the first
letter of each dot-separated segment and **keeping the dots and the whole `@domain` intact**:
`cevat.erkibas@afiniti.com` → `Cevat.Erkibas@afiniti.com`, `mehmet.ali.yilmaz@afiniti.com` →
`Mehmet.Ali.Yilmaz@afiniti.com`, a bare `qa.bot` → `Qa.Bot`. It generalizes to any number of
segments — title-case the first letter of every token split on `.` in the name part.
- **Do NOT strip the `@domain`** — show the full email; only the name before the `@` is
  title-cased, the domain is left exactly as-is.
- **Display-only.** Never mutate the underlying value used as a key, filter value, join key, or
  export cell — those keep the raw lowercased identifier (the Excel-export raw-value rule still
  wins: exports carry the whole raw email/username).
- Apply wherever a person's identity is surfaced from such an identifier: table cells, filter
  option lists, chart labels/legends, chips, tooltips.
- Only title-case; do not otherwise rewrite the token (don't replace dots with spaces, don't
  expand/abbreviate) unless a specific screen's rule says so.
TR
## Kişi tanımlayıcıları — noktayla ayrılmış adı Title Case yap
Bir kişi noktayla ayrılmış bir tanımlayıcıyla gösterildiğinde — tam bir **e-posta**, bir **kullanıcı adı** ya da
noktalı bir **ad** — **sadece ad kısmını** (`@` öncesindeki yerel kısmı) Title Case yap; noktayla ayrılmış her
parçanın ilk harfini büyüt ve **noktaları ve bütün `@domain` kısmını olduğu gibi koru**:
`cevat.erkibas@afiniti.com` → `Cevat.Erkibas@afiniti.com`, `mehmet.ali.yilmaz@afiniti.com` →
`Mehmet.Ali.Yilmaz@afiniti.com`, yalın bir `qa.bot` → `Qa.Bot`. Kaç parça olursa olsun genelleşir — ad kısmında
`.` ile bölünmüş her parçanın ilk harfini büyüt.
- **`@domain` kısmını SÖKME** — tam e-postayı göster; sadece `@` öncesindeki ad Title Case yapılır, alan adı
  tam olarak olduğu gibi bırakılır.
- **Sadece gösterim için.** Anahtar, filtre değeri, birleştirme anahtarı ya da aktarma hücresi olarak kullanılan
  asıl değeri asla değiştirme — onlar ham, küçük harfli tanımlayıcıyı korur (Excel aktarımının ham değer kuralı
  hâlâ kazanır: aktarmalar tam ham e-postayı/kullanıcı adını taşır).
- Bir kişinin kimliğinin böyle bir tanımlayıcıdan yüzeye çıktığı her yerde uygula: tablo hücreleri, filtre seçenek
  listeleri, grafik etiketleri/açıklamaları, chip'ler, tooltip'ler.
- Sadece Title Case yap; belirli bir ekranın kuralı öyle demiyorsa token'ı başka türlü yeniden yazma (noktaları
  boşlukla değiştirme, açma/kısaltma yapma).

Design for the CSS viewport, not the monitor

Desktop density only; phone and touch width belong to mobile-design.md.

EN
## Design for the CSS viewport, not the monitor

*Desktop density only; phone and touch width belong to [`mobile-design.md`](./mobile-design.md).*

**Windows display scaling at 125% is the OEM default on most laptops, and it turns a 1920px monitor
into a 1536px CSS viewport** (`devicePixelRatio` 1.25). A layout sized for ~1900 then fills that
viewport edge to edge with everything drawn 25% larger — it reads as a magnified desktop rather than
a page designed for the screen. 1366 and 1440px laptops at 100% land in the same place, so this is
most of our users, not an edge case.

**Step density down at `width <= 1600px`. Do not reach for `zoom`.** The two are not
interchangeable, and picking the wrong one is the usual mistake:

| Tool | What it gives back | Use it when |
|---|---|---|
| Root `font-size` | **Density** — every rem-based size steps down; the viewport stays 1536 | The layout is fluid and may keep margins at the edges. **Almost everything we build.** |
| `zoom` on the root | **Viewport width** — 0.8 makes the page lay out as if it had 1920 CSS px again | The layout has a fixed width that MUST fit the screen horizontally. Dashboards only — see [`design-dashboard.md`](./design-dashboard.md) → "Windows-scaling normalizer". |

Density is what a fluid layout is short of; width is what a fixed one is short of. Reaching for
`zoom` in a fluid app also drags the 10–13px labels below the readability floor, which the density
step deliberately leaves alone.

**Whichever you use, never touch `dpr >= 2.0`.** A 4K/Retina user runs 150–200% precisely so the UI is
readable, and their CSS viewport is already wide — shrinking them is pure loss.

**Size the step to the gap, and settle it by looking, not by arithmetic.** 125% needs `0.8` to match
the 100% layout exactly. A timid step is worse than none: it costs a media query and the page still
reads as magnified. Write the ratio you land on into the stylesheet, or the next person re-tunes it
blind.

Expect to close nearly the whole gap. The instinct to hold a few percent back "because the reader
chose a larger UI" does not survive a side-by-side — they chose a larger *rendering*, which they
still get; the layout was never the thing they asked to be bigger. (2026-08-11, Organisation
Explorer: 16 → 15px = `0.94` moved almost nothing and left every px display size behind; 16 → 13.5px
= `0.84` was better but still read loose beside the 100% plate; **16 → 13px = `0.81`** is what
shipped.)

- **Drive it from ONE place — the root `font-size`, and nothing else.** Tailwind's whole spacing
  scale is rem-based, so one declaration steps every gap and padding at once. The media query should
  end up a single line; if it is growing a list of per-element overrides, those elements are in px
  and belong in the next bullet instead.
- **Convert EVERY px value the layout depends on to rem — starting with the shell's `max-width`.**
  Not a list of the usual suspects: grep the project for `px` and decide each one, because whatever
  you skip silently opts out of the whole system. The **page shell's max-width is the single most
  important one** and the easiest to forget, since it is written once in an app-shell file rather
  than in the components you are looking at. Then the display sizes (page titles, KPI numbers, chart
  heights) and any layout `min-width`.
  **Why the shell outranks the type:** a fixed `1500px` shell is 78% of a 1920 viewport and reads as
  designed; the same 1500px is **98%** of the 1536 viewport a 125% display reports, so the content
  runs to both edges. Every font and gap inside it can shrink correctly and the page still reads as
  too big — worse, cards get *wider*, because the same column count divides a proportionally wider
  shell. What a reader perceives is the share of the screen the content takes, not its type scale.
  (2026-08-11, Organisation Explorer: shipped exactly this. The type was converted, the shell was
  not, and the user's verdict was "still too big and it fills the screen" — correct, against a
  passing test suite.)
- **Do not shrink small text.** 10–13px labels and body copy are already at the readability floor —
  leave them in px deliberately. The excess is in whitespace and display sizes, not in body copy, and
  at 125% an 11px label is still rendering at ~14 physical px.
- **Assert the RATIO, not the knob.** The test that matters measures the content shell's width as a
  **share of the viewport** and requires the same share at both scales (~0.78 either way, a band of
  roughly 0.72–0.86). That is the only assertion no px value can satisfy by accident, and it is the
  thing the reader actually perceives.
  Asserting the computed root `font-size` is worth keeping — every rem on the page hangs off it — but
  understand what it proves: that the knob was **turned**, not that anything downstream **listened**.
  A suite asserting only the root font-size and no horizontal overflow passes green against a layout
  a user can see is wrong, which is exactly how the shell-width bug above shipped.
- **Emulate with BOTH numbers.** 1920×1080 @1 and 1536×864 @1.25 — `deviceScaleFactor` alone changes
  nothing CSS can see, and the viewport alone misses the DPI. Screenshots on top, for the judgement a
  test can't make.
TR
## Monitöre değil, CSS viewport'una göre tasarla

*Sadece masaüstü yoğunluğu; telefon ve dokunma genişliği [`mobile-design.md`](./mobile-design.md) dosyasına aittir.*

**Windows'ta %125 ekran ölçeklemesi çoğu dizüstünde OEM varsayılanıdır ve 1920px'lik bir monitörü 1536px'lik bir
CSS viewport'una çevirir** (`devicePixelRatio` 1,25). ~1900'e göre boyutlanmış bir yerleşim o viewport'u kenardan
kenara doldurur ve her şey %25 büyük çizilir — ekran için tasarlanmış bir sayfa değil, büyütülmüş bir masaüstü gibi
okunur. %100'deki 1366 ve 1440px'lik dizüstüler de aynı yere düşer, yani bu uç bir durum değil, kullanıcılarımızın
çoğu.

**`width <= 1600px` noktasında yoğunluğu bir kademe düşür. `zoom`'a uzanma.** İkisi birbirinin yerine geçmez ve
yanlışını seçmek alışılmış hatadır:

| Araç | Ne geri verir | Ne zaman kullanılır |
|---|---|---|
| Kök `font-size` | **Yoğunluk** — rem tabanlı her boyut bir kademe iner; viewport 1536 kalır | Yerleşim akışkansa ve kenarlarda boşluk kalabiliyorsa. **Kurduğumuz neredeyse her şey.** |
| Kökte `zoom` | **Viewport genişliği** — 0,8, sayfayı yine 1920 CSS px'i varmış gibi yerleştirir | Yerleşimin, ekrana yatay olarak sığMASI ZORUNLU sabit bir genişliği varsa. Sadece dashboard'lar — bkz. [`design-dashboard.md`](./design-dashboard.md) → "Windows-scaling normalizer". |

Akışkan bir yerleşimin eksiği yoğunluktur; sabit olanınki genişliktir. Akışkan bir uygulamada `zoom`'a uzanmak ayrıca
10–13px'lik etiketleri okunabilirlik tabanının altına çeker, ki yoğunluk kademesi onlara bilerek dokunmaz.

**Hangisini kullanırsan kullan, `dpr >= 2.0`'a asla dokunma.** Bir 4K/Retina kullanıcısı arayüz okunabilir olsun diye
tam da %150–200 çalıştırır ve CSS viewport'u zaten geniştir — onları küçültmek saf kayıptır.

**Kademeyi farka göre boyutlandır ve aritmetikle değil bakarak karara bağla.** %125, %100 yerleşimine birebir uymak
için `0,8` ister. Çekingen bir kademe, hiç olmamasından kötüdür: bir medya sorgusuna mal olur ve sayfa yine
büyütülmüş okunur. Vardığın oranı stil sayfasına yaz, yoksa bir sonraki kişi onu körlemesine yeniden ayarlar.

Farkın neredeyse tamamını kapatmayı bekle. "Okuyucu daha büyük bir arayüz seçti" diye birkaç puan geride tutma
içgüdüsü yan yana karşılaştırmadan sağ çıkmaz — onlar daha büyük bir *çizim* seçtiler ve onu almaya devam ediyorlar;
büyütülmesini istedikleri şey yerleşim hiç değildi. (2026-08-11, Organisation Explorer: 16 → 15px = `0,94` neredeyse
hiçbir şeyi oynatmadı ve her px gösterim boyutunu geride bıraktı; 16 → 13,5px = `0,84` daha iyiydi ama %100'lük
plakanın yanında hâlâ gevşek okunuyordu; çıkan sürüm **16 → 13px = `0,81`** oldu.)

- **Bunu TEK bir yerden sür — kök `font-size`, başka hiçbir şey.** Tailwind'in bütün boşluk ölçeği rem tabanlıdır,
  yani tek bir bildirim her boşluğu ve dolguyu aynı anda bir kademe indirir. Medya sorgusu tek satırda bitmeli;
  öğe başına geçersiz kılmalardan oluşan bir listeye dönüşüyorsa, o öğeler px cinsindendir ve sıradaki maddeye aittir.
- **Yerleşimin dayandığı HER px değerini rem'e çevir — kabuğun `max-width` değerinden başlayarak.** Alışılmış
  şüphelilerin listesi değil: projede `px` ara ve her birine ayrı karar ver, çünkü atladığın her ne varsa sistemin
  tamamından sessizce çıkar. **Sayfa kabuğunun max-width değeri bunların en önemlisidir** ve unutulması en kolayıdır,
  çünkü baktığın bileşenlerde değil, bir uygulama kabuğu dosyasında bir kez yazılıdır. Sonra gösterim boyutları
  (sayfa başlıkları, KPI sayıları, grafik yükseklikleri) ve yerleşimdeki her `min-width`.
  **Kabuğun tipografiyi neden yendiği:** sabit bir `1500px` kabuk, 1920'lik bir viewport'un %78'idir ve tasarlanmış
  gibi okunur; aynı 1500px, %125'lik bir ekranın bildirdiği 1536'lık viewport'un **%98'idir**, yani içerik iki kenara
  da dayanır. İçindeki her font ve boşluk doğru küçülebilir ve sayfa yine de fazla büyük okunur — daha kötüsü,
  kartlar *genişler*, çünkü aynı kolon sayısı orantılı olarak daha geniş bir kabuğu böler. Okuyanın algıladığı şey,
  içeriğin kapladığı ekran payıdır, tipografi ölçeği değil.
  (2026-08-11, Organisation Explorer: tam olarak bu çıktı. Tipografi çevrildi, kabuk çevrilmedi ve kullanıcının
  hükmü "hâlâ çok büyük ve ekranı dolduruyor" oldu — geçen bir test paketine rağmen doğru.)
- **Küçük metni küçültme.** 10–13px'lik etiketler ve gövde metni zaten okunabilirlik tabanındadır — onları bilerek
  px cinsinde bırak. Fazlalık gövde metninde değil, boşlukta ve gösterim boyutlarındadır; üstelik %125'te 11px'lik
  bir etiket hâlâ ~14 fiziksel px çizilir.
- **Düğmeyi değil, ORANI doğrula.** Önemli olan test, içerik kabuğunun genişliğini **viewport'un bir payı olarak**
  ölçer ve her iki ölçekte de aynı payı şart koşar (iki yönde de ~0,78, kabaca 0,72–0,86 bandı). Hiçbir px değerinin
  kazara karşılayamayacağı tek iddia budur ve okuyanın gerçekten algıladığı şey de odur.
  Hesaplanmış kök `font-size` değerini doğrulamak korunmaya değer — sayfadaki her rem ona asılıdır — ama neyi
  kanıtladığını anla: düğmenin **çevrildiğini**, aşağıdaki hiçbir şeyin **dinlediğini** değil. Sadece kök font
  boyutunu ve yatay taşma olmadığını doğrulayan bir paket, kullanıcının yanlış olduğunu görebildiği bir yerleşime
  karşı yeşil geçer; yukarıdaki kabuk genişliği hatası tam olarak böyle çıktı.
- **HER İKİ sayıyla da emüle et.** 1920×1080 @1 ve 1536×864 @1,25 — tek başına `deviceScaleFactor` CSS'in görebileceği
  hiçbir şeyi değiştirmez, tek başına viewport ise DPI'ı kaçırır. Üstüne ekran görüntüleri, bir testin veremeyeceği
  hüküm için.

Consistent page width across a project

Every top-level page in a project shares the same content width — the app shell defines one max-width container (e.g.

EN
## Consistent page width across a project
Every top-level page in a project shares the **same content width** — the app shell defines
one max-width container (e.g. `max-w-[1400px]` centered) and pages render into it at full
width. Do NOT narrow individual pages with their own `max-w-2xl` / `max-w-3xl` wrappers; a
page that opens noticeably narrower than the others reads as broken. Keep list, form, detail,
and result pages the same width unless the user explicitly asks for a narrower reading column
on a specific page. (Exception: true overlays/modals may cap their own width — that is a
dialog, not a page.) If a form looks lost at full width, constrain individual fields/cards,
not the whole page.
TR
## Proje genelinde tutarlı sayfa genişliği
Bir projedeki her üst seviye sayfa **aynı içerik genişliğini** paylaşır — uygulama kabuğu tek bir max-width
container tanımlar (ör. ortalanmış `max-w-[1400px]`) ve sayfalar onun içine tam genişlikte çizilir. Sayfaları
kendi `max-w-2xl` / `max-w-3xl` sarmalayıcılarıyla tek tek daraltma; diğerlerinden gözle görülür biçimde dar
açılan bir sayfa bozuk okunur. Kullanıcı belirli bir sayfada açıkça daha dar bir okuma kolonu istemedikçe liste,
form, detay ve sonuç sayfalarını aynı genişlikte tut. (İstisna: gerçek katmanlar/modal'lar kendi genişliklerini
sınırlayabilir — o bir diyalogdur, sayfa değil.) Bir form tam genişlikte kaybolmuş görünüyorsa, bütün sayfayı
değil tek tek alanları/kartları sınırla.

Hover dropdowns: no dead-zone, forgiving close

Any menu/panel that opens on hover (dropdowns, flyouts, mega-menus) must never close while the user travels from the trigger…

EN
## Hover dropdowns: no dead-zone, forgiving close
Any menu/panel that opens on hover (dropdowns, flyouts, mega-menus) must never close while the
user travels from the trigger to the panel:
- **No hover dead-zone**: if there is a visual gap between trigger and panel, bridge it with an
  invisible hover area — but the bridge MUST live on an element that stays in the DOM, i.e. the
  **trigger or its wrapper**, never a `::before`/`::after` on the panel itself. A panel that is
  `display:none` when closed takes its own pseudo-element with it, so a bridge placed on the panel
  vanishes exactly when the cursor is in the gap (the classic "menu disappears mid-travel" bug).
  Reliable pattern: put `padding-bottom:Npx` on the trigger's wrapper (a positioned ancestor of both
  trigger and panel) and anchor the panel at `top:100%` — the padding is always present, hoverable,
  and keeps `:hover` alive across the gap. The visual gap stays; the hover target is continuous.
- **Close grace period (~200ms)**: closing gets a short delay (`transition-delay` on the hidden
  state, `0s` on the open state) so brief diagonal cursor excursions don't snap the menu shut;
  opening stays instant.
Losing the menu in that gap is one of the most user-hostile UI bugs — never ship a hover menu
without both protections.
TR
## Hover dropdown'ları: ölü bölge yok, bağışlayıcı kapanma
Hover ile açılan her menü/panel (dropdown, flyout, mega menü), kullanıcı tetikleyiciden panele giderken asla
kapanmamalı:
- **Hover ölü bölgesi yok**: tetikleyici ile panel arasında görsel bir boşluk varsa, onu görünmez bir hover
  alanıyla köprüle — ama köprü, DOM'da kalan bir öğede, yani **tetikleyicide ya da sarmalayıcısında** yaşamalı,
  asla panelin kendisindeki bir `::before`/`::after` üzerinde değil. Kapalıyken `display:none` olan bir panel
  kendi sözde öğesini de yanında götürür, yani panele konmuş bir köprü tam olarak imleç boşluktayken kaybolur
  (klasik "menü yol ortasında kayboluyor" hatası). Güvenilir kalıp: tetikleyicinin sarmalayıcısına (hem
  tetikleyicinin hem panelin konumlandırılmış atası) `padding-bottom:Npx` koy ve paneli `top:100%` konumuna
  sabitle — boşluk her zaman vardır, hover edilebilir ve `:hover` durumunu aradaki boşluk boyunca canlı tutar.
  Görsel boşluk yerinde kalır; hover hedefi kesintisizdir.
- **Kapanma müsamaha süresi (~200ms)**: kapanmaya kısa bir gecikme verilir (gizli durumda `transition-delay`,
  açık durumda `0s`) ki imlecin kısa çapraz sapmaları menüyü aniden kapatmasın; açılma anlık kalır.
Menüyü o boşlukta kaybetmek en kullanıcı düşmanı arayüz hatalarından biridir — her iki koruma olmadan bir hover
menüsünü asla çıkarma.

Card hover-lift: never let the lift un-hover itself

A card/tile "lift on hover" (transform: translateY(-Npx) on :hover) is a sibling instability to the dropdown dead-zone above:…

EN
## Card hover-lift: never let the lift un-hover itself
A card/tile "lift on hover" (`transform: translateY(-Npx)` on `:hover`) is a sibling instability to
the dropdown dead-zone above: if the cursor is resting in the card's bottom few pixels, the lift
moves the card away from the cursor, `:hover` drops, the card falls back under the cursor,
`:hover` re-fires — a visible, continuous jitter for as long as the pointer sits there (found on
an AI First dashboard: any card whose bottom edge the mouse happened to rest on shook indefinitely).
- **The move itself must not gate on live `:hover`.** Keep `:hover` for shadow/border feedback
  (those don't change geometry, so they're safe), but drive the transform from a separate class
  (e.g. `.lifted`) toggled by JS with **hysteresis**: once lifted, stay lifted while the pointer is
  anywhere inside the card's LIFTED bounding box or the strip it just vacated (a few px margin
  below/around); only drop `.lifted` when the pointer truly leaves that region. One document-level
  `mouseover`/`mousemove` pair tracking a single "currently lifted" element is enough — no per-card
  listeners needed.
- Removing the lift entirely (shadow/border-only feedback) is an acceptable fallback if hysteresis
  isn't worth the code, but a genuinely "dead" hover with no lift on hover-capable devices reads as
  less polished — try hysteresis first before giving up the motion.
TR
## Kart hover kalkışı: kalkış kendi hover'ını asla bozmasın
Bir kart/döşeme "hover'da kalkışı" (`:hover` üzerinde `transform: translateY(-Npx)`), yukarıdaki dropdown ölü
bölgesinin kardeş kararsızlığıdır: imleç kartın alt birkaç pikselinde duruyorsa, kalkış kartı imleçten uzaklaştırır,
`:hover` düşer, kart imlecin altına geri iner, `:hover` yeniden ateşlenir — işaretçi orada durduğu sürece görünür ve
kesintisiz bir titreme (bir AI First dashboard'unda bulundu: farenin alt kenarında durduğu her kart sonsuza kadar
titriyordu).
- **Hareketin kendisi canlı `:hover` durumuna bağlanmamalı.** `:hover`'ı gölge/kenarlık geri bildirimi için koru
  (bunlar geometriyi değiştirmez, yani güvenlidir), ama dönüşümü JS ile **histerezisli** olarak açılıp kapanan ayrı
  bir sınıftan (ör. `.lifted`) sür: bir kez kalktıysa, işaretçi kartın KALKMIŞ sınırlayıcı kutusunun ya da az önce
  boşalttığı şeridin herhangi bir yerindeyken kalkmış kalsın (altında/etrafında birkaç px pay); `.lifted` ancak
  işaretçi o bölgeden gerçekten çıktığında düşsün. Tek bir "şu an kalkmış" öğeyi izleyen, document seviyesinde tek
  bir `mouseover`/`mousemove` çifti yeter — kart başına dinleyici gerekmez.
- Histerezis koda değmiyorsa kalkışı tamamen kaldırmak (sadece gölge/kenarlık geri bildirimi) kabul edilebilir bir
  yedektir, ama hover yapabilen cihazlarda kalkışsız, gerçekten "ölü" bir hover daha az özenli okunur — hareketten
  vazgeçmeden önce histerezisi dene.

Popover / tooltip content — fully visible, no inner scroll

A hover/click popover or tooltip must show its content in full, with no scrollbar of its own — on neither axis.

EN
## Popover / tooltip content — fully visible, no inner scroll
A hover/click popover or tooltip must show its content **in full, with no scrollbar of its own** — on
neither axis. Apply both fixes together:
- **No vertical clip:** do not cap it with a fixed `max-height` + `overflow:auto` that hides content
  behind an inner scrollbar. Let the panel grow to its content (size it by widening the panel, not by
  clipping its height). Fall back to an inner scroll only for genuinely unbounded content.
- **No horizontal scroll:** long unbreakable strings (file paths, URLs) must wrap, not trigger a
  sideways scrollbar. Use `white-space:pre-wrap` (keeps intentional line breaks, wraps the rest) plus
  `overflow-wrap:anywhere` so a long token can break mid-string. Widen the panel a little to reduce
  ugly wraps before relying on the break.
The reader should take in the whole panel at a glance; an inner scrollbar inside a small floating
panel is the anti-pattern to avoid.
TR
## Popover / tooltip içeriği — tamamen görünür, iç kaydırma yok
Hover/tıklama ile açılan bir popover veya tooltip içeriğini **tam olarak, kendi kaydırma çubuğu olmadan**
göstermeli — hiçbir eksende. İki düzeltmeyi birlikte uygula:
- **Dikey kırpma yok:** onu, içeriği bir iç kaydırma çubuğunun arkasına gizleyen sabit bir `max-height` +
  `overflow:auto` ile sınırlama. Panelin içeriğine göre büyümesine izin ver (yüksekliğini kırparak değil, paneli
  genişleterek boyutlandır). İç kaydırmaya ancak gerçekten sınırsız içerik için düş.
- **Yatay kaydırma yok:** uzun ve bölünemez dizgeler (dosya yolları, URL'ler) yana kaydırma çubuğu tetiklemek yerine
  alta sarmalı. `white-space:pre-wrap` (kasıtlı satır sonlarını korur, gerisini sarar) artı `overflow-wrap:anywhere`
  kullan ki uzun bir token dizge ortasından bölünebilsin. Bölmeye güvenmeden önce çirkin sarmaları azaltmak için
  paneli biraz genişlet.
Okuyan bütün paneli bir bakışta almalı; küçük ve yüzen bir panelin içindeki kaydırma çubuğu kaçınılacak anti-kalıptır.

Time Range dropdown — group ordering

MOVED to design-filters.md (2026-08-25, owner approval) — design-filters.md owns filter, search and lookup mechanics.

EN
## Time Range dropdown — group ordering
MOVED to [`design-filters.md`](./design-filters.md) (2026-08-25, owner approval) — `design-filters.md` owns filter,
search and lookup mechanics. See the section of the same name there.
TR
## Zaman Aralığı dropdown'ı — grup sıralaması
[`design-filters.md`](./design-filters.md) dosyasına TAŞINDI (2026-08-25, sahip onayı) — filtre, arama ve lookup
mekaniği `design-filters.md` dosyasına aittir. Orada aynı adlı bölüme bak.

Column value-filter popover (funnel on the header)

MOVED to design-filters.md (2026-08-25, owner approval) — design-filters.md owns filter, search and lookup mechanics.

EN
## Column value-filter popover (funnel on the header)
MOVED to [`design-filters.md`](./design-filters.md) (2026-08-25, owner approval) — `design-filters.md` owns filter,
search and lookup mechanics. See the section of the same name there.
TR
## Kolon değer filtresi popover'ı (başlıktaki huni)
[`design-filters.md`](./design-filters.md) dosyasına TAŞINDI (2026-08-25, sahip onayı) — filtre, arama ve lookup
mekaniği `design-filters.md` dosyasına aittir. Orada aynı adlı bölüme bak.

Line charts — data-point labels

Every line chart shows its data-point value labels (small, bold, coloured to the line, aligned above the point; auto-hide…

EN
## Line charts — data-point labels
Every line chart shows its data-point value labels (small, bold, coloured to the line, aligned above
the point; auto-hide overlapping labels on dense series). Reserve a little top padding so the topmost
label isn't clipped. Bar charts keep their own value/percentage labels; composition doughnuts keep
theirs — the rule is specifically about **line** charts.
TR
## Çizgi grafikler — veri noktası etiketleri
Her çizgi grafik veri noktası değer etiketlerini gösterir (küçük, kalın, çizginin rengiyle, noktanın üstüne hizalı;
yoğun serilerde çakışan etiketleri otomatik gizle). En üstteki etiket kırpılmasın diye biraz üst boşluk ayır. Çubuk
grafikler kendi değer/yüzde etiketlerini korur; bileşim halkaları da kendilerininkini — kural özellikle **çizgi**
grafikler hakkındadır.

Weekly vs monthly chart height parity

When a dashboard shows both weekly-trend mini charts and monthly charts, size them to the same rendered height so the panels…

EN
## Weekly vs monthly chart height parity
When a dashboard shows both weekly-trend mini charts and monthly charts, size them to the **same
rendered height** so the panels line up. If the monthly chart height changes, update the weekly minis
to match.
TR
## Haftalık ve aylık grafik yüksekliği eşitliği
Bir dashboard hem haftalık trend mini grafiklerini hem aylık grafikleri gösteriyorsa, ikisini **aynı çizilmiş
yüksekliğe** getir ki paneller hizalansın. Aylık grafiğin yüksekliği değişirse, haftalık minileri de ona göre güncelle.

Data-table header freezes on page scroll

Every data table's column header stays frozen (sticky) while the PAGE scrolls — the user must never lose the headers while…

EN
## Data-table header freezes on page scroll
Every data table's column header stays **frozen (sticky) while the PAGE scrolls** — the user must
never lose the headers while reading rows further down. Do NOT achieve this with a fixed-height
inner scroll box around the table: that creates a second scrollbar inside the page (double-scroll),
which reads as broken. The table scrolls with the page; only the header pins.

Implementation (verified on AAI Performance Summary):
- `th { position: sticky; top: 0 }` on **every header cell**, each with its own **opaque background**
  (e.g. `bg-gray-50`) — row content bleeds through a transparent sticky header while scrolling.
- Draw the header's bottom border as an **inset box-shadow** on the `th`
  (`shadow-[inset_0_-1px_0_#e5e7eb]`) — a normal `border-bottom` does not travel with sticky cells.
- **Gotcha — sticky cannot escape an `overflow-x: auto` ancestor** (the wrapper becomes the scroll
  root and the header only sticks inside it, i.e. never against the page). The wide-table wrapper
  must use **`overflow-x: clip`** while the table fits its container (`clip` does not create a
  scroll container, so page-stickiness keeps working), and fall back to `overflow-x: auto` only when
  the table genuinely overflows (a `ResizeObserver` fits-check toggling the style) — in that fallback
  the freeze is off, since the two can't coexist. Corollary: keep the table fitting the viewport
  width (dashboard.md fit rule — trim paddings/min-widths first) so the frozen state is the normal one.
TR
## Veri tablosu başlığı sayfa kaydırılırken donar
Her veri tablosunun kolon başlığı **SAYFA kaydırılırken donmuş (sticky) kalır** — kullanıcı aşağıdaki satırları
okurken başlıkları asla kaybetmemeli. Bunu tablonun etrafında sabit yükseklikli bir iç kaydırma kutusuyla YAPMA:
o, sayfanın içinde ikinci bir kaydırma çubuğu yaratır (çift kaydırma) ve bozuk okunur. Tablo sayfayla birlikte
kayar; sadece başlık sabitlenir.

Uygulama (AAI Performance Summary'de doğrulandı):
- **Her başlık hücresinde** `th { position: sticky; top: 0 }`, her birinin kendi **opak arka planıyla**
  (ör. `bg-gray-50`) — kaydırma sırasında satır içeriği saydam bir sticky başlığın içinden geçer.
- Başlığın alt kenarlığını `th` üzerinde bir **iç box-shadow** olarak çiz
  (`shadow-[inset_0_-1px_0_#e5e7eb]`) — normal bir `border-bottom` sticky hücrelerle birlikte hareket etmez.
- **Tuzak — sticky, bir `overflow-x: auto` atasından kaçamaz** (sarmalayıcı kaydırma kökü olur ve başlık yalnızca
  onun içinde sabitlenir, yani sayfaya karşı hiç). Geniş tablo sarmalayıcısı, tablo container'ına sığdığı sürece
  **`overflow-x: clip`** kullanmalı (`clip` bir kaydırma container'ı yaratmaz, yani sayfa sabitlemesi çalışmaya
  devam eder) ve yalnızca tablo gerçekten taştığında `overflow-x: auto` yedeğine düşmeli (stili değiştiren bir
  `ResizeObserver` sığma kontrolü) — o yedekte dondurma kapalıdır, çünkü ikisi bir arada olamaz. Sonuç olarak:
  tabloyu görüş alanı genişliğine sığar tut (dashboard.md sığdırma kuralı — önce boşlukları/min genişlikleri kıs)
  ki donmuş durum normal durum olsun.

All page parameters load at once with defaults selected — never a visible cascade

MOVED to design-filters.md (2026-08-25, owner approval) — design-filters.md owns filter, search and lookup mechanics.

EN
## All page parameters load at once with defaults selected — never a visible cascade
MOVED to [`design-filters.md`](./design-filters.md) (2026-08-25, owner approval) — `design-filters.md` owns filter,
search and lookup mechanics. See the section of the same name there.
TR
## Bütün sayfa parametreleri varsayılanları seçili olarak tek seferde yüklenir — asla görünür bir zincir değil
[`design-filters.md`](./design-filters.md) dosyasına TAŞINDI (2026-08-25, sahip onayı) — filtre, arama ve lookup
mekaniği `design-filters.md` dosyasına aittir. Orada aynı adlı bölüme bak.

Data tables with more than ~10 rows get a search option

MOVED to design-filters.md (2026-08-25, owner approval) — design-filters.md owns filter, search and lookup mechanics.

EN
## Data tables with more than ~10 rows get a search option
MOVED to [`design-filters.md`](./design-filters.md) (2026-08-25, owner approval) — `design-filters.md` owns filter,
search and lookup mechanics. See the section of the same name there.
TR
## ~10 satırdan fazla veri tabloları bir arama seçeneği alır
[`design-filters.md`](./design-filters.md) dosyasına TAŞINDI (2026-08-25, sahip onayı) — filtre, arama ve lookup
mekaniği `design-filters.md` dosyasına aittir. Orada aynı adlı bölüme bak.

Filter labels — see design-filters.md

Filter caption/label rules (including the owner's 2026-08-25 decision that captions carry NO icon) live in design-filters.md →…

EN
## Filter labels — see `design-filters.md`
Filter caption/label rules (including the owner's 2026-08-25 decision that captions carry **NO
icon**) live in [`design-filters.md`](./design-filters.md) → *Bar (view-wide) filter controls*. The earlier rule in
this spot mandated a ~12px blue icon per caption; the owner reversed it and the rule MOVED.
TR
## Filtre etiketleri — bkz. `design-filters.md`
Filtre başlığı/etiketi kuralları (sahibin 2026-08-25 tarihli, başlıkların **HİÇBİR simge** taşımadığı kararı dahil)
[`design-filters.md`](./design-filters.md) → *Bar (view-wide) filter controls* içinde yaşar. Bu noktadaki önceki
kural, başlık başına ~12px mavi bir simge şart koşuyordu; sahip bunu tersine çevirdi ve kural TAŞINDI.

Scope

Owns: the index of every knowledge doc in the global knowledge mds, and the harvest log that records which conversation…

EN
## Scope
**Owns:** the index of every knowledge doc in the global knowledge mds, and the harvest log that records which conversation transcripts have already been processed.
**Does NOT own:** any knowledge itself. Every fact lives in exactly one child doc below; this file holds pointers, never content. Behavior rules (how Claude should work) live in the global CLAUDE.md, not here.

**The boundary against project folders.** A fact belongs in the global knowledge mds only if it stays true when the code changes: why a decision was made, rejected alternatives, platform limits, product logic, lessons. Anything that moves with the code — ports, commands, file paths, current state, data model, what is built so far — belongs in that project's own `memory.md` and is never copied here, because a copy starts going stale the moment it is written. Reusable technique notes are the mirror rule: they live here in a topic doc, never inside a project's docs, so the second project that hits the same trap finds it in exactly one place.

**Docs first, code second.** Read the relevant branch before making a change AND before answering a question about how something works. The code is only the implementation of these notes.

---
TR
## Kapsam
**Sahiplendiği:** global knowledge md'lerdeki her bilgi dokümanının dizini ve hangi konuşma transkriptlerinin zaten işlendiğini kaydeden harvest günlüğü.
**Sahiplenmediği:** bilginin kendisi. Her bilgi aşağıdaki çocuk dokümanlardan tam olarak birinde yaşar; bu dosya işaretçi tutar, asla içerik. Davranış kuralları (Claude'un nasıl çalışması gerektiği) burada değil global CLAUDE.md'de yaşar.

**Proje klasörlerine karşı sınır.** Bir bilgi, global knowledge md'lere ancak kod değiştiğinde doğru kalıyorsa aittir: bir kararın neden verildiği, reddedilen alternatifler, platform sınırları, ürün mantığı, dersler. Kodla birlikte hareket eden her şey — port'lar, komutlar, dosya yolları, mevcut durum, veri modeli, şu ana kadar ne yapıldığı — o projenin kendi `memory.md` dosyasına aittir ve buraya asla kopyalanmaz, çünkü bir kopya yazıldığı anda bayatlamaya başlar. Yeniden kullanılabilir teknik notlar bunun ayna kuralıdır: burada bir konu dokümanında yaşarlar, asla bir projenin dokümanlarının içinde değil; böylece aynı tuzağa düşen ikinci proje onu tam olarak tek bir yerde bulur.

**Önce doküman, sonra kod.** Bir değişiklik yapmadan VE bir şeyin nasıl çalıştığına dair bir soruyu cevaplamadan önce ilgili dalı oku. Kod, bu notların yalnızca uygulamasıdır.

---

Reference Docs

Projects - sawabswipe.md — Android "conscious scrolling" dhikr app: product identity, product rules, distribution strategy,…

EN
## Reference Docs

**Projects**
- [`sawabswipe.md`](./sawabswipe.md) — Android "conscious scrolling" dhikr app: product identity, product rules, distribution strategy, roadmap.
- [`mindcopy.md`](./mindcopy.md) — MindCopy, one searchable library for saved items on X, YouTube and Instagram: scope, data model, design direction, the resolved realtime question.
- [`loan-origination.md`](./loan-origination.md) — the BKT Kosovo credit platform engagement: client situation, positioning beside Pega, architecture decisions and rejected alternatives.
- [`jarvis.md`](./jarvis.md) — planned voice assistant on Claude Code + an Obsidian vault + local speech: architecture, vault design, install order, two blocking decisions.
- [`career.md`](./career.md) — durable career facts (roles, employers, achievements, education) and the CV's writing standard.
- [`dubai-property.md`](./dubai-property.md) — the Dubai off-plan purchase, its 71-instalment schedule, and the monthly Turkish capital-export paperwork.
- [`market-research.md`](./market-research.md) — consumer-app benchmarks, growth-channel mechanics, and the app-research recipe.

**Android**
- [`android-accessibility-overlay.md`](./android-accessibility-overlay.md) — counting gestures inside other apps and drawing a floating overlay: burst counting, overlay geometry, permission gating, the flag that unifies every show/hide path.
- [`android-emulator.md`](./android-emulator.md) — what the emulator and adb can and cannot verify: GPU mode trade-off, uiautomator dump, input-injection limits, the reinstall loop.
- [`android-toolchain.md`](./android-toolchain.md) — installing JDK, SDK and Gradle on a bare Windows machine without Android Studio, including the license-hash trick.
- [`compose-ui.md`](./compose-ui.md) — Jetpack Compose: keyboard insets under `adjustResize`, cross-cutting layout fixes, drag-to-reorder gestures.

**Audio, jobs and media**
- [`speech-to-text.md`](./speech-to-text.md) — local transcription and speaker labelling: why Whisper under CPU/offline/Turkish constraints, measured speeds, the sherpa-onnx diarization stack, and why the speaker count matters more than the model.
- [`background-jobs.md`](./background-jobs.md) — long jobs on the user's machine: resuming after an interruption instead of restarting, honest time-left, one worker and one queue.
- [`browser-media.md`](./browser-media.md) — serving audio from a local server and the Chrome behaviour that makes playback look broken: cache locks, hidden-tab deferral, ranged responses instead of streamed file handles.

**Backend and services**
- [`architecture.md`](./architecture.md) — **how to choose a backend architecture for any project.** Starts with a seven-trait checklist, then routes: no traits → Shape A (self-owned app on a BaaS, RLS primary); on-prem or an external system of record → Shape B (orchestration system: on-prem-shaped hosting, modular monolith, write-through, append-only decision evidence, outbox). Every Shape B rule names the trait it needs.
- [`supabase.md`](./supabase.md) — schema and RLS patterns, advisor hardening and which warnings are intentional, `supabase-kt` traps, anonymous-first auth.
- [`oauth-and-email.md`](./oauth-and-email.md) — Google OAuth setup for a Supabase app (and the redirect URL everyone forgets), Resend's key-versus-domain model.
- [`platform-apis.md`](./platform-apis.md) — what X, YouTube and Instagram let an app read about saved content, what it costs, and the design consequences.

**Design**
- [`design.md`](./design.md) — **the top of the design tree.** Cross-project UI and interaction rules for application screens, plus pointers to its three dashboard children and to the opt-in visual themes. Read it before any design, planning or implementation work on a UI, and before answering any question about how a screen should behave.
  - [`design-dashboard.md`](./design-dashboard.md) — dashboard page structure: tokens, component classes, page skeleton, top bar, modals, tooltips, micro-interactions.
  - [`design-charts.md`](./design-charts.md) — charts and chart cards, the clustered-bar-plus-ranked-table pairing, selection, drill-down, linked time-highlighting.
  - [`design-metrics.md`](./design-metrics.md) — reporting semantics: filters versus detail tables, export scope, additive versus snapshot metrics.
  - [`design-filters.md`](./design-filters.md) — every filter, sort, search and lookup rule: the popover, numeric ranges, typeahead, cascading option lists, bar filter controls, what goes in the URL.
  - [`design-form-validation.md`](./design-form-validation.md) — required-field errors: highlight on the field, submit stays clickable, trigger on the submit attempt.
  - [`design-comments.md`](./design-comments.md) — margin comments: gutter placement per audience, anchoring, leader-line routing, stacking, the comment box and its actions.
  - [`language.md`](./language.md) — bilingual screens: terms and headings stay in the source language and only explanations translate, plus how to build the switch without breaking a finished page.
  - [`tooltip.md`](./tooltip.md) — the tooltip: where it sits, how long it lives, and what may never be said only there.
  - [`mobile-design.md`](./mobile-design.md) — a web interface at phone width: no keyboard-shortcut hints on touch, labels give way to icons before rows wrap, and horizontal scroll is treated as a bug.
  - [`ui-verification.md`](./ui-verification.md) — verifying a UI change in the browser: measuring the text's own box, the traps that make a measurement lie, in-page test fixtures, and cleaning up.
  - [`inline-editing.md`](./inline-editing.md) — editing a value in place: the field takes the text's box without moving it, saves on Enter or blur, keeps the original it replaced.
  - [`audio-player.md`](./audio-player.md) — the audio player: the native element plus the actions Chrome hides, and how a transcript and a player drive each other.
  - [`web-design-guidelines.md`](./web-design-guidelines.md) — imported Vercel Web Interface Guidelines as a per-file review checklist: accessibility, focus, forms, motion, performance, i18n, hydration, plus the conflicts with the house rules.
  - `design_themes/` — complete alternative visual languages (`calendly`, `fancy_linkedin`, `spark_theme`), opt-in only, used when named.

**Frontend and build**
- [`expo-react-native.md`](./expo-react-native.md) — Expo SDK 54 traps, share-intent integration, proving Metro actually bundles shared code.
- [`web-build-typescript.md`](./web-build-typescript.md) — `tsc` traps in a shared-folder repo, Vite port pinning, porting a Claude Design prototype.
- [`repo-architecture.md`](./repo-architecture.md) — the standing layout: one repo, plain `web/ mobile/ shared/ supabase/` folders, no npm workspaces, and why.

**Banking domain**
- [`credit-lifecycle-domain.md`](./credit-lifecycle-domain.md) — retail credit lifecycle domain knowledge: origination stages, decisioning vocabulary, policy rules, collateral, limits, collections, accounting.
- [`lending-vendor-landscape.md`](./lending-vendor-landscape.md) — how Pega, FICO, Provenir, Azentio and LendingWise position themselves, and how to argue against an incumbent without losing credibility.

**Environment and infrastructure**
- [`windows-env.md`](./windows-env.md) — this machine's toolchain inventory and every Windows/PowerShell trap that has cost time, including Office automation and PDF surgery.
- [`windows-file-associations.md`](./windows-file-associations.md) — why Windows keeps re-asking how to open a file, why no script can set a default app on this build, and the remedies that actually work.
- [`system.md`](./system.md) — operating the machine itself: Modern Standby and what really triggers it, reading a kernel dump, removing software with no uninstall entry, the shell conditions Claude Code needs, where its config lives, and the Meet transcript limits.
- [`hosting-dns.md`](./hosting-dns.md) — Netlify versus Vercel for commercial side products, subfolder deploys, diagnosing a half-migrated domain.
- [`mydomains.md`](./mydomains.md) — the domain portfolio and the registrar side of it: at-cost pricing, the Cloudflare nameserver lock that decides where a name can live, transfer and 60-day lock mechanics, TLD traits.
- [`going-live.md`](./going-live.md) — **read before pointing a domain at a site, before any launch, and to audit one already live.** Answers: is this site set up correctly, is it secure, why is it slow on repeat visits, are the HTTPS/SSL/TLS settings right, do we need HSTS or CSP or security headers, should `www` and the apex both answer, do we need DNSSEC or SPF or DMARC, and who can take the whole thing over. Carries the verified target configuration and the two measurement traps that report "secure" when it is not.
- [`i18n.md`](./i18n.md) — generating multilingual content from an existing source, runtime language switching, binding services to the app language.
- [`prayer-times.md`](./prayer-times.md) — offline prayer-time computation, why it differs from official calendars, and why reminders need no server.

**Claude Code and this system**
- [`claude-code.md`](./claude-code.md) — where Claude Code stores transcripts, how session identity works, tool limits, skill anatomy.
- [`knowledge-system.md`](./knowledge-system.md) — why the global knowledge mds is shaped the way it is, the rejected alternatives, and what a harvest measurably misses.
TR
## Referans Dokümanları

**Projeler**
- [`sawabswipe.md`](./sawabswipe.md) — Android "bilinçli kaydırma" zikir uygulaması: ürün kimliği, ürün kuralları, dağıtım stratejisi, yol haritası.
- [`mindcopy.md`](./mindcopy.md) — MindCopy, X, YouTube ve Instagram'da kaydedilenler için tek ve aranabilir bir kütüphane: kapsam, veri modeli, tasarım yönü, çözülmüş gerçek zaman sorusu.
- [`loan-origination.md`](./loan-origination.md) — BKT Kosova kredi platformu işi: müşteri durumu, Pega'nın yanındaki konumlanma, mimari kararlar ve reddedilen alternatifler.
- [`jarvis.md`](./jarvis.md) — Claude Code + bir Obsidian kasası + yerel konuşma üzerine planlanan sesli asistan: mimari, kasa tasarımı, kurulum sırası, iki tıkayıcı karar.
- [`career.md`](./career.md) — kalıcı kariyer bilgileri (roller, işverenler, başarılar, eğitim) ve CV'nin yazım standardı.
- [`dubai-property.md`](./dubai-property.md) — Dubai'deki projeden satın alma, 71 taksitlik ödeme planı ve aylık Türkiye sermaye çıkışı evrakları.
- [`market-research.md`](./market-research.md) — tüketici uygulaması kıyasları, büyüme kanalı mekaniği ve uygulama araştırması tarifi.

**Android**
- [`android-accessibility-overlay.md`](./android-accessibility-overlay.md) — başka uygulamaların içinde hareket saymak ve üstlerine yüzen bir katman çizmek: seri sayma, katman geometrisi, izin kapıları, her göster/gizle yolunu birleştiren bayrak.
- [`android-emulator.md`](./android-emulator.md) — emülatörün ve adb'nin neyi doğrulayıp neyi doğrulayamadığı: GPU modu ödünleşimi, uiautomator dump, girdi enjeksiyonu sınırları, yeniden kurulum döngüsü.
- [`android-toolchain.md`](./android-toolchain.md) — Android Studio olmadan çıplak bir Windows makinesine JDK, SDK ve Gradle kurmak, lisans hash numarası dahil.
- [`compose-ui.md`](./compose-ui.md) — Jetpack Compose: `adjustResize` altında klavye insets'leri, kesişen yerleşim düzeltmeleri, sürükleyerek sıralama hareketleri.

**Ses, işler ve medya**
- [`speech-to-text.md`](./speech-to-text.md) — yerel transkripsiyon ve konuşmacı etiketleme: CPU/çevrimdışı/Türkçe kısıtları altında neden Whisper, ölçülmüş hızlar, sherpa-onnx diarizasyon yığını ve konuşmacı sayısının neden modelden önemli olduğu.
- [`background-jobs.md`](./background-jobs.md) — kullanıcının makinesindeki uzun işler: kesintiden sonra baştan başlamak yerine devam etmek, dürüst kalan süre, tek worker ve tek kuyruk.
- [`browser-media.md`](./browser-media.md) — yerel bir sunucudan ses servis etmek ve oynatmayı bozuk gösteren Chrome davranışı: cache kilitleri, gizli sekme erteleme, akıtılan dosya tutamakları yerine aralıklı cevaplar.

**Backend ve servisler**
- [`architecture.md`](./architecture.md) — **herhangi bir proje için backend mimarisi nasıl seçilir.** Yedi nitelikli bir kontrol listesiyle başlar, sonra yönlendirir: nitelik yok → Biçim A (BaaS üzerinde kendi kendine yeten uygulama, birincil RLS); on-prem ya da harici bir kayıt sistemi → Biçim B (orkestrasyon sistemi: on-prem biçiminde barındırma, modüler monolit, write-through, yalnızca eklenen karar kanıtı, outbox). Her Biçim B kuralı ihtiyaç duyduğu niteliği adıyla söyler.
- [`supabase.md`](./supabase.md) — şema ve RLS kalıpları, advisor sıkılaştırması ve hangi uyarıların kasıtlı olduğu, `supabase-kt` tuzakları, önce anonim auth.
- [`oauth-and-email.md`](./oauth-and-email.md) — bir Supabase uygulaması için Google OAuth kurulumu (ve herkesin unuttuğu yönlendirme URL'si), Resend'in anahtar-mı-alan-mı modeli.
- [`platform-apis.md`](./platform-apis.md) — X, YouTube ve Instagram'ın bir uygulamaya kaydedilen içerik hakkında neyi okutturduğu, bunun maliyeti ve tasarım sonuçları.

**Tasarım**
- [`design.md`](./design.md) — **tasarım ağacının tepesi.** Uygulama ekranları için projeler arası arayüz ve etkileşim kuralları, artı üç dashboard çocuğuna ve tercihe bağlı görsel temalara işaretçiler. Bir arayüzde herhangi bir tasarım, planlama ya da uygulama işinden önce ve bir ekranın nasıl davranması gerektiğine dair herhangi bir soruyu cevaplamadan önce oku.
  - [`design-dashboard.md`](./design-dashboard.md) — dashboard sayfa yapısı: token'lar, bileşen sınıfları, sayfa iskeleti, üst bar, modal'lar, tooltip'ler, mikro etkileşimler.
  - [`design-charts.md`](./design-charts.md) — grafikler ve grafik kartları, kümelenmiş çubuk artı sıralı tablo eşleşmesi, seçim, detaya iniş, bağlı zaman vurgulama.
  - [`design-metrics.md`](./design-metrics.md) — raporlama semantiği: filtrelere karşı detay tabloları, aktarma kapsamı, toplanabilir metriklere karşı anlık görüntü metrikleri.
  - [`design-filters.md`](./design-filters.md) — her filtre, sıralama, arama ve lookup kuralı: popover, sayısal aralıklar, typeahead, zincirlenen seçenek listeleri, bar filtre kontrolleri, URL'ye ne girdiği.
  - [`design-form-validation.md`](./design-form-validation.md) — zorunlu alan hataları: alanın üstünde işaretle, gönder tıklanabilir kalır, gönderme denemesinde tetiklenir.
  - [`design-comments.md`](./design-comments.md) — kenar yorumları: okuyucuya göre kenar yerleşimi, sabitleme, bağlantı çizgisi yönlendirme, üst üste dizme, yorum kutusu ve aksiyonları.
  - [`language.md`](./language.md) — iki dilli ekranlar: terimler ve başlıklar kaynak dilde kalır, yalnızca açıklamalar çevrilir; artı bitmiş bir sayfayı bozmadan dil değiştiricinin nasıl kurulacağı.
  - [`tooltip.md`](./tooltip.md) — tooltip: nerede durur, ne kadar yaşar ve ne asla sadece orada söylenemez.
  - [`mobile-design.md`](./mobile-design.md) — telefon genişliğinde bir web arayüzü: dokunmatikte klavye kısayolu ipuçları yok, etiketler satırlar sarmadan önce simgelere yer bırakır ve yatay kaydırma bir hata sayılır.
  - [`ui-verification.md`](./ui-verification.md) — bir arayüz değişikliğini tarayıcıda doğrulamak: metnin kendi kutusunu ölçmek, ölçümü yalancı yapan tuzaklar, sayfa içi test düzenekleri ve temizlik.
  - [`inline-editing.md`](./inline-editing.md) — bir değeri yerinde düzenlemek: alan, metni oynatmadan onun kutusunu alır, Enter'da ya da odak kaybında kaydeder, yerine geçtiği orijinali korur.
  - [`audio-player.md`](./audio-player.md) — ses oynatıcı: yerel öğe artı Chrome'un gizlediği aksiyonlar ve bir transkript ile bir oynatıcının birbirini nasıl sürdüğü.
  - [`web-design-guidelines.md`](./web-design-guidelines.md) — dosya başına inceleme kontrol listesi olarak içe aktarılmış Vercel Web Interface Guidelines: erişilebilirlik, odak, formlar, hareket, performans, i18n, hidrasyon, artı ev kurallarıyla çelişkiler.
  - `design_themes/` — eksiksiz alternatif görsel diller (`calendly`, `fancy_linkedin`, `spark_theme`), yalnızca tercihe bağlı, adı anıldığında kullanılır.

**Frontend ve derleme**
- [`expo-react-native.md`](./expo-react-native.md) — Expo SDK 54 tuzakları, paylaşım niyeti entegrasyonu, Metro'nun paylaşılan kodu gerçekten paketlediğini kanıtlamak.
- [`web-build-typescript.md`](./web-build-typescript.md) — paylaşılan klasörlü bir repoda `tsc` tuzakları, Vite port sabitleme, bir Claude Design prototipini taşımak.
- [`repo-architecture.md`](./repo-architecture.md) — yerleşik düzen: tek repo, sade `web/ mobile/ shared/ supabase/` klasörleri, npm workspaces yok, ve nedeni.

**Bankacılık alanı**
- [`credit-lifecycle-domain.md`](./credit-lifecycle-domain.md) — bireysel kredi yaşam döngüsü alan bilgisi: başvuru aşamaları, karar sözlüğü, politika kuralları, teminat, limitler, tahsilat, muhasebe.
- [`lending-vendor-landscape.md`](./lending-vendor-landscape.md) — Pega, FICO, Provenir, Azentio ve LendingWise kendilerini nasıl konumlandırıyor ve mevcut tedarikçiye karşı güvenilirliği kaybetmeden nasıl argüman kurulur.

**Ortam ve altyapı**
- [`windows-env.md`](./windows-env.md) — bu makinenin araç envanteri ve zaman kaybettiren her Windows/PowerShell tuzağı, Office otomasyonu ve PDF cerrahisi dahil.
- [`windows-file-associations.md`](./windows-file-associations.md) — Windows bir dosyanın nasıl açılacağını neden tekrar tekrar soruyor, bu sürümde neden hiçbir script varsayılan uygulamayı ayarlayamıyor ve gerçekten işe yarayan çareler.
- [`system.md`](./system.md) — makinenin kendisini işletmek: Modern Standby ve onu gerçekten ne tetikliyor, bir kernel dump okumak, kaldırma kaydı olmayan yazılımı silmek, Claude Code'un ihtiyaç duyduğu shell koşulları, konfigürasyonunun nerede durduğu ve Meet transkript sınırları.
- [`hosting-dns.md`](./hosting-dns.md) — ticari yan ürünler için Netlify'a karşı Vercel, alt klasör deploy'ları, yarım göç etmiş bir alan adını teşhis etmek.
- [`going-live.md`](./going-live.md) — **bir alan adını bir siteye yönlendirmeden önce, herhangi bir lansmandan önce ve zaten canlı olan bir siteyi denetlemek için oku.** Şunları cevaplar: bu site doğru kurulmuş mu, güvenli mi, tekrar ziyaretlerde neden yavaş, HTTPS/SSL/TLS ayarları doğru mu, HSTS ya da CSP ya da güvenlik başlıkları gerekli mi, `www` ve kök adres ikisi birden cevap vermeli mi, DNSSEC ya da SPF ya da DMARC gerekli mi ve bütün bunları kim devralabilir. Doğrulanmış hedef yapılandırmayı ve güvenli olmadığı hâlde "güvenli" raporlayan iki ölçüm tuzağını taşır.
- [`i18n.md`](./i18n.md) — mevcut bir kaynaktan çok dilli içerik üretmek, çalışma anında dil değiştirme, servisleri uygulamanın diline bağlamak.
- [`prayer-times.md`](./prayer-times.md) — çevrimdışı namaz vakti hesabı, resmî takvimlerden neden farklı olduğu ve hatırlatıcıların neden sunucuya ihtiyaç duymadığı.

**Claude Code ve bu sistem**
- [`claude-code.md`](./claude-code.md) — Claude Code transkriptleri nerede saklar, session kimliği nasıl çalışır, araç sınırları, skill anatomisi.
- [`knowledge-system.md`](./knowledge-system.md) — global knowledge md'lerin neden bu biçimde olduğu, reddedilen alternatifler ve bir harvest'in ölçülebilir biçimde neyi kaçırdığı.

---

Tools

tools/distill.js — the transcript distiller both harvest commands run: node distill.js <projectFolder> <outFile>.

EN
## Tools
- `tools/distill.js` — the transcript distiller both harvest commands run: `node distill.js <projectFolder> <outFile>`. Orders sessions, walks the `parentUuid` chain with an orphan sweep, strips plumbing, and deliberately **keeps** file-writing tool bodies and marks lost images. Its header comment records why each cap is set where it is.
- `tools/branch-index.py` — the branch list the `SessionStart` hook prints. Reads only the first line of every md here and emits `name — teaser`, so the index can never be stale or incomplete. Exits silently if the drive is not mounted.

---
TR
## Araçlar
- `tools/distill.js` — her iki harvest komutunun da çalıştırdığı transkript damıtıcısı: `node distill.js <projectFolder> <outFile>`. Session'ları sıralar, `parentUuid` zincirini öksüz taramasıyla birlikte yürür, tesisatı ayıklar ve dosyaya yazan araç gövdelerini bilerek **korur**, kaybolan görselleri işaretler. Başlık yorumu, her sınırın neden o değerde olduğunu kaydeder.
- `tools/branch-index.py` — `SessionStart` hook'unun bastığı dal listesi. Buradaki her md'nin yalnızca ilk satırını okur ve `ad — tanıtım` basar, böylece dizin asla bayat ya da eksik olamaz. Sürücü bağlı değilse sessizce çıkar.

---

Scope

Owns: the architecture choice for every project, the trait checklist that drives it, and the decisions and reasons behind each…

EN
## Scope
**Owns:** the architecture choice for every project, the trait checklist that drives it, and the decisions and reasons behind each shape.
**Does NOT own:** Supabase mechanics and RLS patterns ([`supabase.md`](./supabase.md)), repository layout ([`repo-architecture.md`](./repo-architecture.md)), static hosting and DNS ([`hosting-dns.md`](./hosting-dns.md)), one engagement's specifics ([`loan-origination.md`](./loan-origination.md)), or the banking domain ([`credit-lifecycle-domain.md`](./credit-lifecycle-domain.md)).

---
TR
## Kapsam
**Sahiplendiği:** her proje için mimari seçimi, bunu yönlendiren nitelik kontrol listesi ve her biçimin arkasındaki kararlar ve gerekçeler.
**Sahiplenmediği:** Supabase mekaniği ve RLS kalıpları ([`supabase.md`](./supabase.md)), repo yerleşimi ([`repo-architecture.md`](./repo-architecture.md)), statik hosting ve DNS ([`hosting-dns.md`](./hosting-dns.md)), tek bir işin özellikleri ([`loan-origination.md`](./loan-origination.md)) ya da bankacılık alanı ([`credit-lifecycle-domain.md`](./credit-lifecycle-domain.md)).

---

Start here: classify the project

Before recommending any architecture, check the project against these traits.

EN
## Start here: classify the project

**Before recommending any architecture, check the project against these traits.** The traits decide the shape; never propose a shape from habit.

| Trait | The project has it when |
|---|---|
| **On-prem** | production is installed in the customer's own infrastructure |
| **External record** | another system (a core banking system, an ERP, a vendor product) owns the master or financial data, and this system orchestrates around it |
| **Background work** | there are long-running jobs, retries, timers, batch runs, or guaranteed delivery to another system |
| **Atomic change** | a single business action must change several business objects consistently |
| **Decision authorization** | who may act depends on amount, stage, authority, committee or dual control, not only on who owns a row |
| **Audited decisions** | a decision must be reproducible later for a regulator, an auditor or a dispute |
| **Configurable process** | the process itself is a product feature that business users change |

**Then pick the shape:**

* **No traits** → [Shape A](#shape-a-self-owned-app-on-a-backend-as-a-service). The app owns its data, users act on their own rows, nothing runs in the background, and it is hosted by us.
* **On-prem or External record present** → [Shape B](#shape-b-orchestration-system-around-an-external-record). These two change where code runs and who owns the data, which Shape A cannot absorb.
* **Only some of the other traits** → start from Shape A and add **only** the Shape B sections whose trait is present. Every Shape B section names the trait it needs.

State the classification out loud when recommending, so a wrong trait can be corrected before it drives the design.

---
TR
## Buradan başla: projeyi sınıflandır

**Herhangi bir mimari önermeden önce projeyi bu niteliklere karşı kontrol et.** Biçime nitelikler karar verir; asla alışkanlıktan bir biçim önerme.

| Nitelik | Proje buna sahiptir, eğer |
|---|---|
| **On-prem** | production müşterinin kendi altyapısına kuruluyorsa |
| **Harici kayıt** | ana veya finansal veriye başka bir sistem (bir çekirdek bankacılık sistemi, bir ERP, bir tedarikçi ürünü) sahipse ve bu sistem onun etrafında orkestrasyon yapıyorsa |
| **Arka plan işi** | uzun süren işler, yeniden denemeler, zamanlayıcılar, toplu çalışmalar ya da başka bir sisteme garantili teslim varsa |
| **Atomik değişiklik** | tek bir iş eyleminin birden fazla iş nesnesini tutarlı biçimde değiştirmesi gerekiyorsa |
| **Karar yetkilendirmesi** | kimin işlem yapabileceği yalnızca bir satırın sahibine değil; tutara, aşamaya, yetkiye, komiteye ya da çift kontrole bağlıysa |
| **Denetlenen kararlar** | bir kararın sonradan bir düzenleyici, bir denetçi ya da bir uyuşmazlık için yeniden üretilebilir olması gerekiyorsa |
| **Yapılandırılabilir süreç** | sürecin kendisi, iş kullanıcılarının değiştirdiği bir ürün özelliğiyse |

**Sonra biçimi seç:**

* **Hiçbir nitelik yok** → [Biçim A](#shape-a-self-owned-app-on-a-backend-as-a-service). Uygulama kendi verisine sahiptir, kullanıcılar kendi satırları üzerinde işlem yapar, arka planda hiçbir şey çalışmaz ve barındırma bizdedir.
* **On-prem ya da Harici kayıt var** → [Biçim B](#shape-b-orchestration-system-around-an-external-record). Bu ikisi kodun nerede çalıştığını ve veriye kimin sahip olduğunu değiştirir, ki Biçim A bunu soğuramaz.
* **Diğer niteliklerden sadece bazıları** → Biçim A'dan başla ve **sadece** niteliği mevcut olan Biçim B bölümlerini ekle. Her Biçim B bölümü ihtiyaç duyduğu niteliği adıyla söyler.

Öneri yaparken sınıflandırmayı açıkça söyle ki yanlış bir nitelik, tasarımı yönlendirmeden önce düzeltilebilsin.

---

Shape A: self-owned app on a backend-as-a-service

Source projects: sawabswipe, mindcopy.

EN
## Shape A: self-owned app on a backend-as-a-service

*Source projects: sawabswipe, mindcopy.*

The backend-as-a-service **is** the backend: Supabase provides Postgres, auth and storage, and **RLS is the primary authorization**, which is correct when each user acts on rows they own. Business rules that must not be bypassed go into RPC functions. The web front end is a static or SPA build on a static host.

Why this shape here: nothing needs a long-running process, there is no system of record other than our own database, and there is no customer infrastructure to be portable to, so every extra layer would be cost without a buyer.

Details live elsewhere and are not repeated here: schema, RLS and RPC patterns in [`supabase.md`](./supabase.md), the `web/ mobile/ shared/ supabase/` layout in [`repo-architecture.md`](./repo-architecture.md), static hosting choice in [`hosting-dns.md`](./hosting-dns.md).

---
TR
## Biçim A: backend-as-a-service üzerinde kendi kendine yeten uygulama

*Kaynak projeler: sawabswipe, mindcopy.*

Backend-as-a-service'in kendisi **backend'dir**: Supabase Postgres, auth ve storage sağlar ve **birincil yetkilendirme RLS'tir**, ki her kullanıcı kendi sahip olduğu satırlar üzerinde işlem yaptığında bu doğrudur. Atlanmaması gereken iş kuralları RPC fonksiyonlarına girer. Web ön yüzü, statik bir barındırıcıda statik ya da SPA bir derlemedir.

Burada bu biçimin sebebi: uzun süren bir sürece ihtiyaç duyan hiçbir şey yok, kendi veritabanımız dışında bir kayıt sistemi yok ve taşınılacak bir müşteri altyapısı yok; yani her ek katman, alıcısı olmayan bir maliyet olurdu.

Ayrıntılar başka yerde yaşar ve burada tekrarlanmaz: şema, RLS ve RPC kalıpları [`supabase.md`](./supabase.md) içinde, `web/ mobile/ shared/ supabase/` yerleşimi [`repo-architecture.md`](./repo-architecture.md) içinde, statik hosting seçimi [`hosting-dns.md`](./hosting-dns.md) içinde.

---

Shape B: orchestration system around an external record

Source project: loan origination, BKT Kosovo (2026-09).

EN
## Shape B: orchestration system around an external record

*Source project: loan origination, BKT Kosovo (2026-09).*

### Hosting

**Run on-prem-shaped from day one.** *Needs: On-prem.*
The same `docker-compose.yml` (proxy, web, api, worker, Postgres, S3-compatible store) runs the demo on one small VM and production on the customer's metal. The install becomes a redeploy rather than a migration, and portability is tested every day instead of promised.
*Why not a managed backend now and port later:* coupling leaks in silently (auth schema references, platform roles, extensions) and surfaces on port day, the most expensive moment.

**A managed backend earns its place only when you use what is unique to it.** *Needs: On-prem.*
If the design rules out its generated API, its row-level authorization and its functions, what remains is hosted Postgres, which any VM also gives without the coupling. Supabase self-hosting exists and is licence-free ([Self-Hosting](https://supabase.com/docs/guides/self-hosting)), but it hands the operator the whole service set, backups and point-in-time recovery included.

**A backend-as-a-service is not an app host.** *Needs: Background work.*
Supabase provides Postgres, auth and storage; it does not run an API server or a worker. The Supabase route is then always two places, Supabase plus a server.

**Serverless and edge platforms cannot host a worker or a database.** *Needs: Background work.*
Vercel, Netlify and Cloudflare run code per request, scale idle instances away, and do not keep container disks:
* Vercel runs Docker images as Functions that scale down after 5 minutes without traffic, and its Compose guide says *"You can't run your own Postgres or Redis container with local disk"* ([Container Images](https://vercel.com/docs/functions/container-images), [Compose mapping](https://vercel.com/kb/guide/docker-compose-concepts-on-vercel)).
* Netlify runs functions *"in an ephemeral runtime environment"* ([Functions](https://docs.netlify.com/build/functions/overview/)).
* Cloudflare Containers sleep after idle (default 10 minutes) and *"All disk is ephemeral"* ([Architecture](https://developers.cloudflare.com/containers/platform-details/architecture/)).

They remain right for static pages and request-driven APIs. A long-running worker would have to be rewritten against the platform's own queue product, and that code cannot follow the system on-prem.

### Service decomposition

**Modular monolith, not microservices.** *Needs: Atomic change.*
When one approval changes case, limit, collateral and facility together, microservices turn that into a distributed transaction exactly where atomicity is required.
Keep the split possible: one Postgres schema per domain module, **no cross-module JOINs**, access only through a module's public interface, and every external system reached through **one** integration module.

**A domain module never calls another domain module.** *Needs: Atomic change.*
The process layer is the only caller. It asks one domain, takes the answer, and passes it into the next domain's request. **If two domains genuinely need to call each other, the boundary between them is drawn in the wrong place and they should be one domain.** Treat the urge to add a domain-to-domain call as a design smell, not a convenience.

Two things it buys. The call graph stays a single table — every call is one row of `stage → domain`, so nobody has to hold a second, invisible graph in their head. And cycles become impossible by construction rather than by discipline, which matters because the first cycle in a domain layer is never noticed until something deadlocks or double-writes.

It does mean the process layer carries values between domains. That is not logic in the process layer: passing a number through is orchestration, computing one is not. The test from **Process modelling** still holds — an `if` on a business threshold in process code means a domain call is missing.

### API and authorization surface

**One API surface.** *Needs: Decision authorization.*
A generated table API (PostgREST) is useful for list and filter screens, but if some screens read through it and others through the application API, authorization lives in two places and they drift.

**Row-level security for visibility, application code for actions.** *Needs: Decision authorization.*
"A branch user sees only their branch's rows" is a row filter, and RLS does it well. "This user may approve this amount at this stage under their approval authority" is a decision, and RLS cannot express it. RLS also binds to the database session's identity, which forces identity into every pooled connection and bypass paths for background jobs; the guarantee disappears through those bypasses. Keep RLS as a second layer at most. This is the opposite of Shape A, where RLS is primary, and both are right for their shape.
Mechanics: tables created in raw SQL or migrations get no RLS, only dashboard-created tables do, and enabling RLS with no policy denies everything ([Supabase RLS](https://supabase.com/docs/guides/database/postgres/row-level-security)).

### System of record

**One owner per field, never two-way sync.** *Needs: External record.*
Write the data the external system owns into it and **always read it back from it, even if you wrote it** (write-through, not write-back). A local copy becomes a second truth the moment another channel changes the original.

**Three buckets, never mixed.** *Needs: External record.*

| Bucket | Owner | Read from |
|---|---|---|
| Financial and master data | the external system | the external system, every time |
| Process state (stage, assignee, policy version, votes, document status) | this system | its own store |
| Decision evidence (what a decision saw) | this system | its own store, as evidence only |

This system's store holds **no financial figure without a counterpart in the external system**. That rule keeps it from becoming a second system of record.

**Decision evidence is an append-only snapshot, not a cache.** *Needs: Audited decisions.*
"Can we act now?" needs the live value; "what was this approved on?" needs the value as it was, and the external system has overwritten it. Store the raw response, never update it, one row per enquiry. At each gate read the live value, compare it with the snapshot, and surface any change instead of proceeding on a different number from the one approved.

### Writing to another system

**Outbox plus idempotency key, never inside the business transaction.** *Needs: External record and Background work.*
A slow external system must not lock a case, and a retry must not execute twice.

**Three states for anything sent.** *Needs: External record.*
Not sent, sent with an uncertain result (held or awaiting authorization), accepted. Core systems often hold or leave a record unauthorized depending on configuration; treating "sent" as "done" is how a held record gets acted on.

**Never write into a vendor product's database schema.** *Needs: External record.*
When business logic is compiled into the database (as in Oracle FLEXCUBE's PL/SQL), a direct insert skips validation, accounting and audit, and breaks at the next upgrade. Writes go through the vendor's service layer. A vendor-documented read path, such as a connector schema of views, is acceptable for reads.

### Process modelling

**The process definition is data, not code.** *Needs: Configurable process.*
A recursive node table as source of truth, in-step logic as registered code handlers, the product's own model canonical and BPMN only as an export format. The reasoning and the rejected alternatives are in [`loan-origination.md`](./loan-origination.md).
TR
## Biçim B: harici bir kaydın etrafındaki orkestrasyon sistemi

*Kaynak proje: loan origination, BKT Kosova (2026-09).*

### Barındırma

**İlk günden on-prem biçiminde çalıştır.** *Gerektirir: On-prem.*
Aynı `docker-compose.yml` (proxy, web, api, worker, Postgres, S3 uyumlu depo) demoyu küçük bir VM'de, production'ı müşterinin kendi donanımında çalıştırır. Kurulum bir göç değil bir yeniden deploy hâline gelir ve taşınabilirlik vaat edilmek yerine her gün test edilir.
*Neden şimdi yönetilen bir backend kullanıp sonra taşımıyoruz:* bağımlılık sessizce sızar (auth şeması referansları, platform rolleri, uzantılar) ve en pahalı anda, taşıma gününde ortaya çıkar.

**Yönetilen bir backend, ancak ona özgü olanı kullanıyorsan yerini hak eder.** *Gerektirir: On-prem.*
Tasarım onun üretilmiş API'sini, satır seviyesi yetkilendirmesini ve fonksiyonlarını devre dışı bırakıyorsa geriye barındırılan Postgres kalır, ki onu herhangi bir VM de bağımlılık olmadan verir. Supabase self-hosting mevcuttur ve lisans ücretsizdir ([Self-Hosting](https://supabase.com/docs/guides/self-hosting)), ama işletmeciye yedekler ve zaman noktası kurtarma dahil bütün servis setini yükler.

**Bir backend-as-a-service uygulama barındırıcısı değildir.** *Gerektirir: Arka plan işi.*
Supabase Postgres, auth ve storage sağlar; bir API sunucusu ya da bir worker çalıştırmaz. Supabase yolu o zaman her zaman iki yerdir, Supabase artı bir sunucu.

**Sunucusuz ve edge platformları bir worker'ı ya da bir veritabanını barındıramaz.** *Gerektirir: Arka plan işi.*
Vercel, Netlify ve Cloudflare kodu istek başına çalıştırır, boştaki örnekleri kapatır ve container disklerini saklamaz:
* Vercel, Docker imajlarını trafik olmadan 5 dakika sonra kapanan Function'lar olarak çalıştırır ve Compose rehberi şöyle der: *"You can't run your own Postgres or Redis container with local disk"* ([Container Images](https://vercel.com/docs/functions/container-images), [Compose mapping](https://vercel.com/kb/guide/docker-compose-concepts-on-vercel)).
* Netlify fonksiyonları *"in an ephemeral runtime environment"* çalıştırır ([Functions](https://docs.netlify.com/build/functions/overview/)).
* Cloudflare Containers boşta kaldıktan sonra uyur (varsayılan 10 dakika) ve *"All disk is ephemeral"* ([Architecture](https://developers.cloudflare.com/containers/platform-details/architecture/)).

Statik sayfalar ve istek güdümlü API'ler için doğru olmaya devam ederler. Uzun süren bir worker, platformun kendi kuyruk ürününe göre yeniden yazılmak zorunda kalırdı ve o kod sistemi on-prem'e takip edemez.

### Servis ayrıştırması

**Mikroservis değil, modüler monolit.** *Gerektirir: Atomik değişiklik.*
Tek bir onay; vakayı, limiti, teminatı ve krediyi birlikte değiştirdiğinde, mikroservisler bunu tam da atomikliğin gerektiği yerde dağıtık bir işleme çevirir.
Bölünmeyi mümkün tut: alan modülü başına bir Postgres şeması, **modüller arası JOIN yok**, erişim yalnızca bir modülün açık arayüzü üzerinden ve her harici sisteme **tek** bir entegrasyon modülüyle ulaşılır.

**Bir alan modülü asla başka bir alan modülünü çağırmaz.** *Gerektirir: Atomik değişiklik.*
Tek çağıran süreç katmanıdır. Bir alana sorar, cevabı alır ve onu bir sonraki alanın isteğine geçirir. **İki alanın gerçekten birbirini çağırması gerekiyorsa, aralarındaki sınır yanlış yere çizilmiştir ve tek bir alan olmalıdırlar.** Alandan alana çağrı ekleme dürtüsünü bir kolaylık değil, bir tasarım kokusu say.

İki şey kazandırır. Çağrı grafiği tek bir tablo olarak kalır — her çağrı bir `aşama → alan` satırıdır, yani kimse kafasında ikinci ve görünmez bir grafik tutmak zorunda kalmaz. Ve döngüler disiplinle değil, kuruluş gereği imkânsız hâle gelir; bu önemlidir çünkü bir alan katmanındaki ilk döngü, bir şey kilitlenene ya da iki kez yazana kadar fark edilmez.

Bu, süreç katmanının alanlar arasında değer taşıması anlamına gelir. Bu, süreç katmanında mantık değildir: bir sayıyı geçirmek orkestrasyondur, bir sayıyı hesaplamak değildir. **Süreç modelleme** bölümündeki test hâlâ geçerlidir — süreç kodunda bir iş eşiği üzerine bir `if`, eksik bir alan çağrısı demektir.

### API ve yetkilendirme yüzeyi

**Tek API yüzeyi.** *Gerektirir: Karar yetkilendirmesi.*
Üretilmiş bir tablo API'si (PostgREST) liste ve filtre ekranları için faydalıdır, ama bazı ekranlar ondan, bazıları uygulama API'sinden okuyorsa yetkilendirme iki yerde yaşar ve birbirinden uzaklaşırlar.

**Görünürlük için satır seviyesi güvenlik, eylemler için uygulama kodu.** *Gerektirir: Karar yetkilendirmesi.*
"Bir şube kullanıcısı yalnızca kendi şubesinin satırlarını görür" bir satır filtresidir ve RLS bunu iyi yapar. "Bu kullanıcı, bu aşamada, kendi onay yetkisi altında bu tutarı onaylayabilir" bir karardır ve RLS bunu ifade edemez. RLS ayrıca veritabanı session'ının kimliğine bağlanır, ki bu kimliği her havuzlanmış bağlantıya ve arka plan işleri için atlatma yollarına zorlar; garanti o atlatmalarla birlikte kaybolur. RLS'i en fazla ikinci bir katman olarak tut. Bu, RLS'in birincil olduğu Biçim A'nın tam tersidir ve her ikisi de kendi biçimi için doğrudur.
Mekanik: ham SQL ya da migration ile oluşturulan tablolar RLS almaz, yalnızca panelden oluşturulanlar alır; ve politikasız RLS açmak her şeyi reddeder ([Supabase RLS](https://supabase.com/docs/guides/database/postgres/row-level-security)).

### Kayıt sistemi

**Alan başına tek sahip, asla çift yönlü senkronizasyon.** *Gerektirir: Harici kayıt.*
Harici sistemin sahip olduğu veriyi ona yaz ve **sen yazmış olsan bile her zaman ondan geri oku** (write-back değil, write-through). Yerel bir kopya, başka bir kanal orijinali değiştirdiği anda ikinci bir gerçeğe dönüşür.

**Üç kova, asla karışmaz.** *Gerektirir: Harici kayıt.*

| Kova | Sahip | Nereden okunur |
|---|---|---|
| Finansal ve ana veri | harici sistem | her seferinde harici sistemden |
| Süreç durumu (aşama, atanan kişi, politika sürümü, oylar, doküman durumu) | bu sistem | kendi deposundan |
| Karar kanıtı (bir kararın neyi gördüğü) | bu sistem | kendi deposundan, yalnızca kanıt olarak |

Bu sistemin deposu **harici sistemde karşılığı olmayan hiçbir finansal rakam tutmaz**. Bu kural onun ikinci bir kayıt sistemine dönüşmesini engeller.

**Karar kanıtı bir cache değil, yalnızca eklenen bir anlık görüntüdür.** *Gerektirir: Denetlenen kararlar.*
"Şimdi işlem yapabilir miyiz?" canlı değeri ister; "bu neye göre onaylandı?" değerin o günkü hâlini ister ve harici sistem onu üzerine yazmıştır. Ham cevabı sakla, onu asla güncelleme, sorgu başına bir satır. Her kapıda canlı değeri oku, anlık görüntüyle karşılaştır ve onaylanandan farklı bir sayıyla ilerlemek yerine değişimi yüzeye çıkar.

### Başka bir sisteme yazmak

**İş işleminin içinde değil, outbox artı idempotency anahtarı.** *Gerektirir: Harici kayıt ve Arka plan işi.*
Yavaş bir harici sistem bir vakayı kilitlememeli ve bir yeniden deneme iki kez çalışmamalı.

**Gönderilen her şey için üç durum.** *Gerektirir: Harici kayıt.*
Gönderilmedi, sonucu belirsiz gönderildi (beklemede ya da yetkilendirme bekliyor), kabul edildi. Çekirdek sistemler yapılandırmaya göre bir kaydı sık sık bekletir ya da yetkilendirilmemiş bırakır; "gönderildi"yi "bitti" saymak, bekleyen bir kayıt üzerinde işlem yapılmasının yoludur.

**Bir tedarikçi ürününün veritabanı şemasına asla yazma.** *Gerektirir: Harici kayıt.*
İş mantığı veritabanına derlenmişse (Oracle FLEXCUBE'un PL/SQL'inde olduğu gibi), doğrudan bir insert doğrulamayı, muhasebeyi ve denetimi atlar ve bir sonraki yükseltmede kırılır. Yazmalar tedarikçinin servis katmanından geçer. Tedarikçinin belgelediği bir okuma yolu, ör. görünümlerden oluşan bir connector şeması, okumalar için kabul edilebilir.

### Süreç modelleme

**Süreç tanımı koddur değil, veridir.** *Gerektirir: Yapılandırılabilir süreç.*
Doğruluk kaynağı olarak özyinelemeli bir düğüm tablosu, adım içi mantık kayıtlı kod işleyicileri olarak, ürünün kendi modeli kanonik ve BPMN yalnızca bir dışa aktarma biçimi olarak. Gerekçe ve reddedilen alternatifler [`loan-origination.md`](./loan-origination.md) içindedir.

How an individual living in Turkey can sell app and web subscriptions without opening a company: one bank account, a final 15 % tax, and no VAT. Every claim below names the article it rests on.

The Short Answer: No Company, 15 % Final Tax

Income from a mobile app sold through the app stores, and from services sold over the internet, can be taxed at a final 15 % under GVK mükerrer 20/B.

Income from a mobile app sold through the app stores, and from services sold over the internet, can be taxed at a final 15 % under the Turkish Income Tax Law, article mükerrer 20/B. The bank withholds the tax as the money arrives, and that is the end of it.

  • No company. The exemption is for individuals only; a limited or joint-stock company, or a company abroad, cannot use it.
  • No books, no invoices, no annual return while yearly earnings stay under the limit: 5,300,000 TL for 2026.
  • No VAT. The VAT Law exempts the services this income comes from.
  • One condition above all: every lira of the activity must arrive through one dedicated bank account in Turkey.

A company abroad (Estonia, the UK, a US LLC) does not lower the tax for someone living in Istanbul: a Turkish resident is taxed on worldwide income, and a company run from Istanbul can be treated as Turkish. It would also rule out this exemption.

This page records research done in October 2026. It is not tax advice; confirm your case with a certified accountant (mali müşavir).

What the Law Covers

Article 20/B covers three groups: content creators, services given over the internet, and mobile apps sold through app stores.

Article 20/B covers three groups:

İnternet ve benzeri elektronik ortamlar üzerinden metin, görüntü, ses, video gibi içerikler paylaşan sosyal içerik üreticilerinin bu faaliyetlerinden elde ettikleri kazançlar ve bu ortamlar üzerinden verilen bireysel kurs, eğitim, veri işleme ve geliştirme, ürün tanıtımı gibi hizmetlerden sağlanan kazançlar ile akıllı telefon veya tablet gibi mobil cihazlar için uygulama geliştirenlerin elektronik uygulama paylaşım ve satış platformları üzerinden elde ettikleri kazançlar gelir vergisinden müstesnadır.

Source: GVK mükerrer 20/B, paragraph 1 (full text in Tebliğ 325)

  • Mobile apps are covered only for income through the app stores: paid apps, in-app purchases, subscriptions, ads (Tebliğ 318, article 4/17). Selling the app to a company is not covered.
  • Web subscriptions fall under the second group, services given over the internet such as data processing. The list is open ("gibi"), and the communiqué says these services are covered "niteliğine bakılmaksızın", whatever their nature (Tebliğ 318, article 6/2).
  • The boundary: software sold as a product outside the app stores, such as a desktop program, is not covered (Tebliğ 318, example 20). A web app that processes the user's data on its own servers is a service, not a product.
  • Payment platforms are fine. Income collected through a third-party provider still qualifies (Tebliğ 318, article 4/19), so a merchant of record such as Lemon Squeezy can collect the web subscriptions.

Setting It Up, Step by Step

A certificate from the tax office, one bank account, a notice within a month, and every payout pointed at that account.

  1. Tax office. Apply to the tax office of your home address for the exemption certificate (Mükerrer 20/B İstisna Belgesi). Without an existing registration, the office first registers the activity, then issues the certificate (Tebliğ 318, article 4/4).
  2. Bank account. Open the account by showing the certificate, or dedicate an existing one. App stores and payment platforms pay in dollars, so ask for a linked foreign-currency account (Tebliğ 318, article 4/5).
  3. Notify the tax office within one month: bank, branch and IBAN, in writing. Missing this loses the exemption (Tebliğ 318, article 4/6).
  4. Mobile: open individual developer accounts on the App Store and Google Play, and set this account as the payout account.
  5. Web: on Lemon Squeezy (or a similar merchant of record), choose bank payout to this account. PayPal does not operate in Turkey.

Rules That Keep the Exemption

All income through the one account, a yearly limit, and other businesses are allowed beside it.

  • All income of the activity goes through the account. One payment collected elsewhere, even in cash, loses the exemption for the whole year (article 20/B, paragraph 5; Tebliğ 318, example 13).
  • Other businesses do not block it. The law says so outright; the exemption sits beside a sole proprietorship as long as the two are tracked separately (article 20/B, paragraph 4; Tebliğ 318, article 6/4).
  • Only individuals. Corporate taxpayers cannot use it (Tebliğ 318, article 4/16).
  • Yearly limit: the fourth income-tax bracket, 5,300,000 TL for 2026. What happens above it is on the next card.
Mükelleflerin birinci fıkra kapsamı dışında başka faaliyetlerinden kaynaklanan kazanç ya da iratlarının bulunması istisnadan faydalanmalarına engel değildir.

Source: GVK mükerrer 20/B, paragraph 4 (full text in Tebliğ 325)

Above the Limit, and What Breaks It

Passing 5,300,000 TL means an annual return for that year, but VAT stays exempt. Breaking the rules brings VAT, penalty and interest.

Passing the Limit

  • Income tax: the whole year's 20/B income goes on an annual return at the normal rates, and the 15 % already withheld is credited (article 20/B, paragraph 5; Tebliğ 318, article 4/9). Documented expenses can be deducted, so keep the receipts.
  • Books and invoices are still not required (Tebliğ 318, article 6/3).
  • VAT stays exempt. The VAT communiqué says so in as many words (KDV Genel Uygulama Tebliği II/F-4.26, Resmî Gazete 21.12.2021, no. 31696):
Gelir Vergisi Kanununun mükerrer 20/B maddesinin birinci fıkrası kapsamındaki kazançların toplamının aynı Kanunun 103 üncü maddesinde yazılı tarifenin dördüncü gelir diliminde yer alan tutarı aşmasının ve bu kazançların mezkur Kanun uyarınca yıllık beyanname ile beyan edilecek olmasının, istisna uygulamasına bir etkisi bulunmamaktadır.

Source: KDV Genel Uygulama Tebliği II/F-4.26 (Resmî Gazete 21.12.2021, no. 31696)

  • The next year starts fresh: meeting the conditions again restores the exemption (Tebliğ 318, article 4/9).

Breaking the Rules

Collecting income outside the account, or income found not to qualify, is different: the missing income tax and the VAT are both claimed, with penalty and interest (article 20/B, paragraph 6; KDV Genel Uygulama Tebliği II/F-4.26).

Söz konusu işlemlerin Gelir Vergisi Kanununun mükerrer 20/B maddesindeki şartları baştan taşımadığı ya da şartların daha sonra ihlal edildiğinin tespiti halinde, ziyaa uğratılan vergi ile buna bağlı ceza, faiz ve zamlar işlemleri yapanlardan aranır.

Source: KDV Genel Uygulama Tebliği II/F-4.26 (Resmî Gazete 21.12.2021, no. 31696)

Legal Basis and Open Points

The law, the two communiqués and a 2026 ruling this page rests on, and what is still unconfirmed.

Sources

  • Gelir Vergisi Kanunu (193), mükerrer 20/B, as rewritten by Law 7491 from 1 January 2024.
  • Gelir Vergisi Genel Tebliği 318, articles 4 and 6, as changed by Tebliğ 325 (Resmî Gazete 26.09.2024, no. 32674).
  • Katma Değer Vergisi Kanunu (3065), article 17/4-a, and KDV Genel Uygulama Tebliği II/F-4.26 (Seri No 38, Resmî Gazete 21.12.2021, no. 31696).
  • Revenue Administration ruling E-38418978-120[Mük 20-2025/1]-25669 (14.01.2026): a taxpayer already registered for commercial income may use 20/B for content and promotion income from YouTube and their own web site.

Open Points

  • No ruling yet names a web app subscription as a 20/B internet service. A personal ruling (özelge) from the Revenue Administration settles it, and acting on a favourable one protects against penalty and interest.
  • The VAT communiqué text read here dates from 2021 and names content creators and mobile apps; its update for the internet services added in 2024 was seen only in secondary sources.

Background

Forward focused and diligent professional with over 18 years of combined experience in Technology field (Data, Business Intelligence, AI and Finance), currently serving as the Global AI & Data Director at Afiniti, a world leading AI company headquartered in USA Silicon Valley with operations across 20 countries and +750 employees.

Education

  • Boğaziçi University — MSc, Finance Engineering
  • Yeditepe University — BA, Business Administration (full national scholarship)

Career 18+ years

2021 — present

Afiniti

Global Director, AI & Data

Own the DWH, BI, data quality and data governance domains. Built the enterprise data warehouse from the ground up; delivered solutions across finance, revenue, HR and sales; built open-source LLM services.

2019 — 2021

Credit Bureau of Turkey (KKB)

Data Science Manager - Consultant

Created the first open source, no-cost machine-learning Findeks citizenship score in the bureau's history — used by 70 banks and 60M+ banking customers.

2017 — 2019

Vodafone

Data Science Executive, Risk Analytics

Built financial risk data science models; innovated and patented the world's first telco dealer risk model.

2011 — 2017

Turk Telekom

Credit Risk Analytics Team Leader

Led the analytics team designing and implementing ML risk scorecards — applicational and behavioral models, credit bureau integration.

Highlights

  • Created National Banking Sector — KKB Findeks Score, used by 60+ million citizens. Led the Data Science Team at the National Credit Bureau, pioneering a machine learning model used by over 60 million citizens and 70 banks as the National Credit Score (Version G6, still in use in the country).
  • Innovated a Data Science Patent. Developed a patent of "Banking/Telco Dealer Credit Risk Machine Learning Model — Decreasing False Positives in Risk Models", currently in the process of being patented (invention owner: Fırat Ada). Link to patent
  • Establishment of DWH, Big Data & BI platforms. Founded and scaled a greenfield Raw & Master data architecture of DWH, Big Data, BI and Data Governance for a 750-person, 20-country organization, converting raw operational data into the intelligence layer behind executive and AI decisioning. Link to Medium article
  • Global AI operations & agentic engineering at scale. Run the company's AI Centre as Global AI Director, operating 500+ production AI agents and 500+ Claude Code, Codex and OpenCode engineers across 20 countries, and architected the Centralized AI Context Warehouse (CWH) — the governed context and markdown memory layer that feeds every agent with trusted, permission-aware enterprise context.
  • In-house LLM platforms and chat with your data. Built an open source in-house LLM centre that keeps sensitive business data inside company boundaries while powering chat-with-your-data services globally.
  • Recipient of the Vodafone World Technology Data Monetization Award. Won the award for data monetization in the field of "Dealer Credit & Fraud Risk" across 30 Vodafone countries. Link to award reference
  • Instructor in data science. Delivered 60-hour lectures as Data Science Instructor (Python and R) and Prediction Models Instructor at Turk Telekom Academy, Vodafone Red Academy and the Credit Bureau of Turkey. Link to reference
  • Credit Risk Monitoring & Validation System. Developed and implemented a robust credit risk monitoring and validation system, with strong expertise in risk and fraud models, at Turk Telekom and Vodafone.
  • Dealer Scorecard & Monitoring System. Designed and implemented a dealer scorecard and monitoring system for Vodafone and Turk Telekom, enabling fraud and credit risk prediction for over 5,000 dealers and branches.

Three ways to show what you built: a video written in code, a launch video made from your project, and a screen recording you polish yourself.

Remotion — Videos Written in Code

remotion-dev/remotion docs

You describe the video, Claude writes the scenes in React, you watch it in a browser preview and ask for changes, then it renders an MP4. Not an AI video model: nothing is generated from a prompt. It animates what you give it (text, screenshots, images, clips) with camera moves, transitions and overlays.

what it gives you
  • studio a live preview in the browser that updates while Claude edits
  • scenes text, images and screenshots with zooms, pans, fades and slides
  • render MP4 at any size: 1920×1080, vertical 1080×1920, square
  • skills Remotion's own skills teach Claude how to build each part
install
Claude Code prompt
Install the Remotion skills from remotion-dev/skills, globally.

Claude runs npx skills add remotion-dev/skills. Needs Node.js. Once only: for each video Claude creates the project and opens the preview itself.

use

Give Claude your site's address: it visits the pages, takes the screenshots itself and writes the scenes. Ask for the scene list first:

Claude Code prompt
Use Remotion to make a 30-second promo video of my site https://example.com. Visit it, take the screenshots you need and show me the scene list first.

Approve the scenes, watch the video in the preview and ask for changes. When it looks right:

Claude Code prompt
Render the Remotion video as an MP4.
licence

Free, commercial use included, for individuals, companies of up to 3 employees and non-profits. Larger companies need a company licence.

limits

No music is included: bring a royalty-free track. A voiceover needs a text-to-speech service such as ElevenLabs and its API key.

Creates video presentations of your work — show what you built instead of describing it.

Creates video presentations of your work — show what you built instead of describing it.

install
Claude Code prompt
1
/plugin marketplace add latent-spaces/brag
2
/plugin install brag@brag

Then /reload-plugins. Needs Node.js 22 or later and FFmpeg; ask Claude to install them if you do not have them.

use

Inside the project you want to show:

Claude Code prompt
let's /brag

The video lands in brag-output/brag.mp4. Add --voice for a voiceover.

▶ See a demo on X

Recordly — Screen Recorder & Editor

webadderallorg/recordly releases

An open-source desktop app that records a whole display or a single window and edits the result in the same place. It is the free answer to Screen Studio: the polish that usually takes a video editor is built into the timeline. Not a Claude Code skill — a standalone application you install and drive yourself.

what it gives you
  • auto-zoom follows the action instead of leaving the viewer to find it
  • cursor polish smoothing, motion blur and a click bounce, so the pointer reads as deliberate
  • styled frames wallpaper, solid or gradient background, padding, rounded corners, blur, shadow
  • webcam bubble a moving overlay of you, on top of the recording
  • timeline editing built for demos rather than for film
  • mp4 and gif gif with its own frame rate and loop controls
  • .recordly project reopens with the editor state intact
install

Download Recordly-windows-x64.exe (177 MB, v1.4.0) from the releases page and run it. Needs Windows 10 build 19041 or newer. Building from source instead needs Visual Studio 2022 with the C++ workload and CMake.

use

Open the app, pick a display or a window, choose microphone and system audio, record, then edit and export.

Claude does not paint pictures, but it draws by writing code: logos and icons in SVG, 3D scenes in three.js. It checks its own work by opening the page and looking at a screenshot. The dashboard below was drawn this way, with example data. Drag it to turn it. All you need to do is say “draw it in 3D using three.js”.

three.js — 3D in the Browser

mrdoob/three.js docs

A JavaScript library that draws 3D scenes in the browser. Nothing is installed: the page loads it, and each visitor's browser draws the scene. Claude writes the scene as code, then opens it and checks a screenshot before you see it.

what it gives you
  • models buildings, products and rooms built from simple shapes, or a 3D file you already have
  • labels live figures pinned to parts of the scene, like the dashboard above
  • motion turning views, moving cameras, animated parts
  • interaction drag to turn, click a part to see its details
install

Nothing for you. A page loads it with one line; inside an app it is npm install three.

use

Describe the scene and what it should show:

Claude Code prompt — example
Draw a 3D view of my warehouse using three.js, with the stock level on each zone.

Claude makes a first draft; refine it together from screenshots.

licence

It is completely free! MIT licence.

limits

It draws, it does not generate: a photo or an illustration from a prompt needs an image model (see Codex below). Large scenes run slowly on older phones.

Codex — Images from Your ChatGPT Plan

openai/codex-plugin-cc

Claude cannot generate an image itself, but it can ask Codex, which uses OpenAI's image model. Codex signs in with your ChatGPT account, so you need no API key: the images come out of your ChatGPT plan.

use
Claude Code prompt
Ask Codex to generate an image: a flat logo for my site, blue on white. Save it as img/logo.png.

Say what to draw and where to save it. Claude can then put the image on your page and check how it looks.

install

Needs the Codex plugin, once: see Codex under Add-ons.

quota

Images count toward your Codex usage limits and use them 3 to 5 times faster than a text request. ChatGPT's daily image limit does not apply here. Not on the Free plan: images need Plus or higher.

limits

Each change is a new request: you cannot refine an image in a chat as you do in ChatGPT. Every image is also kept in ~/.codex/generated_images; on Windows Codex may not be allowed to save into your folder, and then Claude copies it from there.

Tools I built for my own Claude Code setup. Each one is public and free, and installs with one prompt.

claude-plus — Filling the Gaps in Claude Code

github.com/firatada-ctrl/claude-plus

My add-on pack for Claude Code. Once it is installed, everything below happens automatically, in this order.

status line

Always on screen: model, context, 5-hour and weekly usage, your last prompt. Bars turn yellow at 50 % and red at 80 %.

Status line: Opus 5.5, ctx 45%, 5h 11% 02:50, week 8% Tue 07:00, and the last prompt below
1 · context at 90 %

Claude automatically stops once and writes everything down: commits, memory.md, handoff.md.

Context window is at 91 %. Before anything else, write everything down, because auto-compaction is close and only a summary survives it: …
2 · after compaction

Claude automatically reads them back, then carries on.

claude-plus: after compaction Claude reads C:\Projects\my-app\handoff.md, C:\Projects\my-app\memory.md
3 · new session

If the project has a handoff.md, Claude automatically reads it first.

claude-plus: this project has a handoff.md (last written 2026-10-24 09:40 (today)); Claude reads C:\Projects\my-app\handoff.md, C:\Projects\my-app\memory.md first
4 · usage limit

A dialog offers to continue the same session on GLM. It is completely free! Yes opens it in a new window. Your code then goes to z.ai. Without a z.ai key the dialog never appears.

Dialog: Claude's usage limit has been reached in my-app. Continue this session with GLM (z.ai) in a new window? Yes, No
install
Claude Code prompt
Install claude-plus from github.com/firatada-ctrl/claude-plus.

Then restart open sessions.

z.ai key

It is completely free!

  1. Sign up at z.ai/model-api.
  2. Open API Keys, create a key and copy it.
  3. Ask Claude to open the key file:
Claude Code prompt
Open the claude-plus z.ai key file ~/.claudex/profiles/glm/.env in Notepad.

Paste the key in place of PASTE_YOUR_KEY_HERE and save. Never paste it into the chat.

limits

Windows only. Usage figures need a claude.ai plan.

Models that work with recordings of people talking. Diarization answers who spoke when; a speech-to-text model then writes down what each of them said.

Nemotron 3 Diarization — Who Spoke When

nvidia/Nemotron-3-Diarization

NVIDIA's open model that splits a recording by speaker and marks who spoke when, for up to 8 people. It does not write down the words: pair it with a speech-to-text model to get a transcript with a name on every line. It works on a whole file, or live while the audio streams in.

what it gives you
  • 8 speakers numbered in the order they first speak
  • live speaker labels with as little as 0.32 s delay
  • whole file the most accurate setting, for recordings you already have
  • formats wav, flac, opus or mp3
install
Claude Code prompt
Install NVIDIA's Nemotron-3-Diarization model from Hugging Face.

Claude installs NVIDIA NeMo (pip install "nemo-toolkit[asr]") and downloads the model, about 100 million parameters. Needs an NVIDIA GPU and Python 3.12 or later. Linux is preferred; on Windows, use WSL.

use
Claude Code prompt — example
Use Nemotron-3-Diarization to find who spoke when in meeting.wav, and list each speaker turn with its start and end time.

The model takes 16 kHz mono audio; Claude converts other recordings first.

accuracy

On standard meeting benchmarks it gets 6 to 13 % of the speech time wrong (diarization error rate, whole-file setting), clearly better than NVIDIA's earlier 4-speaker model. Live settings cost a little accuracy. Crowded talks are harder: with 5 to 9 speakers the error rises to about 28 %.

licence

It is completely free! OpenMDW 1.1 licence, commercial use allowed.

limits

It can miscount speakers, so check the result on your own recordings first. A recording of people's voices is personal data: get their consent before you process it.